diff --git a/conf/default.php b/conf/default.php
index 378f166..4d10087 100644
--- a/conf/default.php
+++ b/conf/default.php
@@ -35,3 +35,7 @@ $conf['gallery_thumb_scale'] = 1;
// Local client service used by {{open>...}}.
$conf['open_service_url'] = 'http://127.0.0.1:8765';
+
+// Image syntax defaults
+$conf['default_image_width'] = 250;
+$conf['default_image_align'] = 'right'; // left|right|center
diff --git a/js/gallery-thumbnails.js b/js/gallery-thumbnails.js
index 6d2b3fa..1c9d1a1 100644
--- a/js/gallery-thumbnails.js
+++ b/js/gallery-thumbnails.js
@@ -28,7 +28,8 @@
}
function init() {
- var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src]');
+ // Handle both gallery and imagebox thumbnails
+ var imgs = document.querySelectorAll('div.luxtools-gallery img[data-thumb-src], div.luxtools-imagebox img[data-thumb-src]');
if (!imgs || !imgs.length) return;
if ('IntersectionObserver' in window) {
diff --git a/src/Output.php b/src/Output.php
index c443e08..fb4555a 100644
--- a/src/Output.php
+++ b/src/Output.php
@@ -74,41 +74,58 @@ class Output
$genThumbW = (int)max(1, (int)round($thumbW * $thumbScale));
$genThumbH = (int)max(1, (int)round($thumbH * $thumbScale));
- $placeholderUrl = DOKU_BASE . 'lib/images/blank.gif';
$placeholderId = $syntax ? trim((string)$syntax->getConf('thumb_placeholder')) : '';
- if ($placeholderId !== '' && function_exists('ml')) {
- // ml() builds a fetch.php URL for a MediaManager item
- $placeholderUrl = ml($placeholderId, ['w' => $genThumbW, 'h' => $genThumbH], true, '&');
- }
/** @var \Doku_Renderer_xhtml $renderer */
$renderer = $this->renderer;
$renderer->doc .= '
';
+ global $ID;
+ $pageId = isset($ID) ? (string)$ID : '';
+ if (function_exists('cleanID')) {
+ $pageId = (string)cleanID($pageId);
+ }
+
foreach ($this->files as $item) {
$url = $this->itemWebUrl($item, !empty($params['randlinks']));
- $thumbUrl = $this->withQueryParams($url, [
- 'thumb' => 1,
- 'w' => $genThumbW,
- 'h' => $genThumbH,
- // Keep quality explicit so cache file naming stays stable.
- 'q' => $thumbQ,
- ]);
$safeUrl = hsc($url);
- $safeThumbUrl = hsc($thumbUrl);
- $safePlaceholderUrl = hsc($placeholderUrl);
$label = hsc($item['name']);
$caption = hsc(basename((string)($item['name'] ?? '')));
if ($caption === '') {
$caption = $label;
}
- $initialSrc = $safePlaceholderUrl;
- $dataThumb = ' data-thumb-src="' . $safeThumbUrl . '"';
- $thumbCachePath = $this->thumbCachePathForItem($item, $genThumbW, $genThumbH, $thumbQ);
- if (is_string($thumbCachePath) && $thumbCachePath !== '' && @is_file($thumbCachePath)) {
- // Thumb already exists: start with it immediately (no JS swap needed)
- $initialSrc = $safeThumbUrl;
+ // Use ThumbnailHelper to get thumbnail info
+ $imagePath = isset($item['path']) && is_string($item['path']) ? $item['path'] : '';
+ if ($imagePath !== '' && is_file($imagePath)) {
+ // Extract root and local from full path for helper
+ $root = $this->basedir;
+ $local = '';
+ if (str_starts_with($imagePath, $root)) {
+ $local = substr($imagePath, strlen($root));
+ }
+
+ $thumb = ThumbnailHelper::getThumbnail(
+ $root,
+ $local,
+ $pageId,
+ $genThumbW,
+ $genThumbH,
+ $thumbQ,
+ $placeholderId !== '' ? $placeholderId : null
+ );
+
+ $initialSrc = hsc($thumb['url']);
+ $dataThumb = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"';
+ } else {
+ // Fallback: use URL directly
+ $thumbUrl = $this->withQueryParams($url, [
+ 'thumb' => 1,
+ 'w' => $genThumbW,
+ 'h' => $genThumbH,
+ 'q' => $thumbQ,
+ ]);
+ $initialSrc = hsc($thumbUrl);
$dataThumb = '';
}
@@ -161,44 +178,6 @@ class Output
return $url . $glue . http_build_query($params, '', '&', PHP_QUERY_RFC3986) . $fragment;
}
- /**
- * Compute the expected thumbnail cache path for an item.
- *
- * Mirrors the hashing scheme in file.php so we can detect whether a thumb
- * already exists and can be used immediately.
- *
- * @param array $item
- * @param int $w
- * @param int $h
- * @param int $q
- * @return string|null
- */
- protected function thumbCachePathForItem(array $item, int $w, int $h, int $q): ?string
- {
- if (!isset($item['path']) || !is_string($item['path']) || $item['path'] === '') return null;
- if (!isset($item['mtime'])) return null;
-
- $path = $item['path'];
- $mtime = (int)$item['mtime'];
-
- // Decide output format the same way file.php does.
- try {
- [, $mime,] = mimetype($path, false);
- } catch (\Throwable $e) {
- return null;
- }
- if (!is_string($mime) || !str_starts_with($mime, 'image/')) return null;
- $dstFormat = ($mime === 'image/png' || $mime === 'image/gif') ? 'png' : 'jpg';
-
- global $conf;
- if (!isset($conf['cachedir']) || !is_string($conf['cachedir']) || trim($conf['cachedir']) === '') return null;
-
- $hash = sha1($path . '|' . $mtime . '|w=' . $w . '|h=' . $h . '|q=' . $q . '|f=' . $dstFormat);
- $sub = substr($hash, 0, 2);
- $cacheDir = rtrim($conf['cachedir'], '/');
- return $cacheDir . '/luxtools/thumbs/' . $sub . '/' . $hash . '.' . $dstFormat;
- }
-
/**
* Renders the files as a table, including details if configured that way.
*
diff --git a/src/ThumbnailHelper.php b/src/ThumbnailHelper.php
new file mode 100644
index 0000000..b3a8567
--- /dev/null
+++ b/src/ThumbnailHelper.php
@@ -0,0 +1,189 @@
+ string, // Always usable URL (thumbnail or placeholder)
+ * 'isFinal' => bool, // true if thumbnail ready, false if placeholder
+ * 'thumbUrl' => string // Real thumbnail URL (for lazy loading)
+ * ]
+ */
+ public static function getThumbnail(
+ string $rootPath,
+ string $localPath,
+ string $pageId,
+ int $width,
+ int $height,
+ int $quality = 80,
+ ?string $placeholderId = null
+ ): array {
+ $fullPath = $rootPath . $localPath;
+ $thumbUrl = self::buildThumbnailUrl($rootPath, $localPath, $pageId, $width, $height, $quality);
+
+ // Check if cached
+ $cachePath = self::getCachePath($fullPath, $width, $height, $quality);
+ $isCached = $cachePath !== null && @is_file($cachePath);
+
+ if ($isCached) {
+ return [
+ 'url' => $thumbUrl,
+ 'isFinal' => true,
+ 'thumbUrl' => $thumbUrl,
+ ];
+ }
+
+ // Not cached: return placeholder
+ return [
+ 'url' => self::getPlaceholderUrl($width, $height, $placeholderId),
+ 'isFinal' => false,
+ 'thumbUrl' => $thumbUrl,
+ ];
+ }
+
+ /**
+ * Build the file.php URL for a thumbnail.
+ *
+ * @param string $rootPath Root filesystem path
+ * @param string $localPath Local path relative to root
+ * @param string $pageId Page ID for ACL check
+ * @param int $width Width
+ * @param int $height Height
+ * @param int $quality JPEG quality
+ * @return string Complete URL to file.php with thumbnail parameters
+ */
+ protected static function buildThumbnailUrl(
+ string $rootPath,
+ string $localPath,
+ string $pageId,
+ int $width,
+ int $height,
+ int $quality
+ ): string {
+ $params = [
+ 'root' => $rootPath,
+ 'file' => $localPath,
+ 'id' => $pageId,
+ 'thumb' => 1,
+ 'w' => $width,
+ 'h' => $height,
+ 'q' => $quality,
+ ];
+ return DOKU_BASE . 'lib/plugins/luxtools/file.php?' . http_build_query($params, '', '&');
+ }
+
+ /**
+ * Compute the expected thumbnail cache path.
+ *
+ * Mirrors the hashing scheme in file.php so we can detect whether a thumb
+ * already exists and can be used immediately.
+ *
+ * @param string $path Full filesystem path to the image
+ * @param int $w Width
+ * @param int $h Height
+ * @param int $q Quality (JPEG)
+ * @return string|null Path to cached thumbnail, or null if unavailable
+ */
+ public static function getCachePath(string $path, int $w, int $h, int $q = 80): ?string
+ {
+ if ($path === '' || !is_file($path)) return null;
+
+ $mtime = @filemtime($path);
+ if ($mtime === false) return null;
+
+ // Decide output format the same way file.php does
+ try {
+ [, $mime,] = mimetype($path, false);
+ } catch (\Throwable $e) {
+ return null;
+ }
+ if (!is_string($mime) || !str_starts_with($mime, 'image/')) return null;
+ $dstFormat = ($mime === 'image/png' || $mime === 'image/gif') ? 'png' : 'jpg';
+
+ global $conf;
+ if (!isset($conf['cachedir']) || !is_string($conf['cachedir']) || trim($conf['cachedir']) === '') return null;
+
+ $hash = sha1($path . '|' . $mtime . '|w=' . $w . '|h=' . $h . '|q=' . $q . '|f=' . $dstFormat);
+ $sub = substr($hash, 0, 2);
+ $cacheDir = rtrim($conf['cachedir'], '/');
+ return $cacheDir . '/luxtools/thumbs/' . $sub . '/' . $hash . '.' . $dstFormat;
+ }
+
+ /**
+ * Get placeholder image URL.
+ *
+ * @param int $width Desired width
+ * @param int $height Desired height
+ * @param string|null $placeholderId Optional MediaManager ID for custom placeholder
+ * @return string Placeholder URL
+ */
+ public static function getPlaceholderUrl(int $width, int $height, ?string $placeholderId = null): string
+ {
+ $placeholderUrl = DOKU_BASE . 'lib/images/blank.gif';
+
+ if ($placeholderId !== null && $placeholderId !== '' && function_exists('ml')) {
+ $placeholderUrl = ml($placeholderId, ['w' => $width, 'h' => $height], true, '&');
+ }
+
+ return $placeholderUrl;
+ }
+
+ /**
+ * Determine the initial image source and data attributes for lazy thumbnail loading.
+ *
+ * Returns placeholder info if thumbnail needs to be loaded, or the actual
+ * thumbnail URL if it's already cached.
+ *
+ * @param string $imagePath Full filesystem path to the image
+ * @param string $thumbUrl URL to the thumbnail
+ * @param int $width Width
+ * @param int $height Height
+ * @param int $quality Quality (JPEG)
+ * @param string|null $placeholderId Optional MediaManager ID for custom placeholder
+ * @return array ['src' => string, 'dataThumbAttr' => string]
+ */
+ public static function getDisplayInfo(
+ string $imagePath,
+ string $thumbUrl,
+ int $width,
+ int $height,
+ int $quality = 80,
+ ?string $placeholderId = null
+ ): array {
+ $thumbCachePath = self::getCachePath($imagePath, $width, $height, $quality);
+
+ if ($thumbCachePath !== null && @is_file($thumbCachePath)) {
+ // Thumbnail exists: display it immediately
+ return [
+ 'src' => $thumbUrl,
+ 'dataThumbAttr' => '',
+ ];
+ }
+
+ // Thumbnail doesn't exist: show placeholder and lazy-load
+ $placeholderUrl = self::getPlaceholderUrl($width, $height, $placeholderId);
+ return [
+ 'src' => $placeholderUrl,
+ 'dataThumbAttr' => ' data-thumb-src="' . hsc($thumbUrl) . '"',
+ ];
+ }
+}
diff --git a/syntax/image.php b/syntax/image.php
index b94f36d..793daf8 100644
--- a/syntax/image.php
+++ b/syntax/image.php
@@ -2,6 +2,7 @@
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Path;
+use dokuwiki\plugin\luxtools\ThumbnailHelper;
require_once(__DIR__ . '/../autoload.php');
@@ -58,7 +59,7 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
// ?200x150¢er - full options
$width = null;
$height = null;
- $align = 'right'; // default alignment
+ $align = null; // Will use default if not specified
if (strpos($pathPart, '?') !== false) {
[$pathPart, $paramStr] = explode('?', $pathPart, 2);
@@ -107,6 +108,18 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
return true;
}
+ // Apply default settings if not explicitly specified
+ if ($data['width'] === null) {
+ $data['width'] = (int)$this->getConf('default_image_width');
+ if ($data['width'] <= 0) $data['width'] = 250;
+ }
+ if ($data['align'] === null) {
+ $data['align'] = (string)$this->getConf('default_image_align');
+ if (!in_array($data['align'], ['left', 'right', 'center'], true)) {
+ $data['align'] = 'right';
+ }
+ }
+
try {
$pathHelper = new Path($this->getConf('paths'));
// Use addTrailingSlash=false since this is a file path, not a directory
@@ -135,14 +148,23 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
return true;
}
- // Build the image URL using the plugin's file.php endpoint
+ // Get thumbnail from helper - it handles everything
global $ID;
- $imageUrl = $this->buildFileUrl($pathInfo, $data['width'], $data['height']);
+ $placeholderId = trim((string)$this->getConf('thumb_placeholder'));
+ $thumb = ThumbnailHelper::getThumbnail(
+ $pathInfo['root'],
+ $pathInfo['local'],
+ $ID,
+ $data['width'] ?? 250,
+ $data['height'] ?? ($data['width'] ?? 250),
+ 80,
+ $placeholderId !== '' ? $placeholderId : null
+ );
// Build full-size URL for linking
- $fullUrl = $this->buildFileUrl($pathInfo, null, null);
+ $fullUrl = $this->buildFileUrl($pathInfo, null, null, false);
- $this->renderImageBox($renderer, $imageUrl, $fullUrl, $data);
+ $this->renderImageBox($renderer, $thumb, $fullUrl, $data);
return true;
}
@@ -153,9 +175,10 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
* @param array $pathInfo Path info from Path helper
* @param int|null $width Optional width
* @param int|null $height Optional height
+ * @param bool $thumbnail Whether to generate a thumbnail
* @return string
*/
- protected function buildFileUrl(array $pathInfo, ?int $width, ?int $height): string
+ protected function buildFileUrl(array $pathInfo, ?int $width, ?int $height, bool $thumbnail = false): string
{
global $ID;
@@ -165,6 +188,12 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
'id' => $ID,
];
+ if ($thumbnail && ($width !== null || $height !== null)) {
+ // Enable thumbnail mode (same as gallery logic)
+ $params['thumb'] = 1;
+ $params['q'] = 80; // JPEG quality
+ }
+
if ($width !== null) {
$params['w'] = $width;
}
@@ -179,11 +208,11 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
* Render the imagebox HTML.
*
* @param \Doku_Renderer $renderer
- * @param string $imageUrl URL for the displayed image
+ * @param array $thumb Thumbnail info from ThumbnailHelper::getThumbnail()
* @param string $fullUrl URL for the full-size image (on click)
* @param array $data Parsed data from handle()
*/
- protected function renderImageBox(\Doku_Renderer $renderer, string $imageUrl, string $fullUrl, array $data): void
+ protected function renderImageBox(\Doku_Renderer $renderer, array $thumb, string $fullUrl, array $data): void
{
$align = $data['align'] ?? 'right';
$caption = $data['caption'] ?? '';
@@ -205,8 +234,11 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
$outerStyle = ' style="width: ' . ($width + 10) . 'px;"';
}
+ // Use thumbnail metadata from helper
+ $dataThumbAttr = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"';
+
// Build image attributes
- $imgAttrs = 'class="media" loading="lazy" decoding="async"';
+ $imgAttrs = 'class="media luxtools-thumb" loading="lazy" decoding="async"';
if ($width !== null) {
$imgAttrs .= ' width="' . (int)$width . '"';
}
@@ -221,8 +253,8 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
// Image with link to full size
$renderer->doc .= '
';
- $renderer->doc .= '
';
- $renderer->doc .= '';
+ $renderer->doc .= '
 . ')
';
+ $renderer->doc .= '';;
// Caption
if ($caption !== '') {