diff --git a/.github/workflows/phpTestLinux.yml b/.github/workflows/phpTestLinux.yml
new file mode 100644
index 0000000..c0c6bdc
--- /dev/null
+++ b/.github/workflows/phpTestLinux.yml
@@ -0,0 +1,52 @@
+name: PHP Tests on Linux
+
+on: [push, pull_request]
+
+jobs:
+ testLinux:
+ name: PHP ${{ matrix.php-versions }} DokuWiki ${{ matrix.dokuwiki-branch }}
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
+
+ strategy:
+ matrix:
+ php-versions: ['7.2', '7.3', '7.4', '8.0']
+ dokuwiki-branch: [ 'master', 'stable']
+ exclude:
+ - dokuwiki-branch: 'stable'
+ php-versions: '8.0'
+ fail-fast: false
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring, intl, PDO, pdo_sqlite, bz2
+
+ - name: Setup problem matchers
+ run: |
+ echo ::add-matcher::${{ runner.tool_cache }}/php.json
+ echo ::add-matcher::${{ runner.tool_cache }}/phpunit.json
+
+ - name: Download DokuWiki Test-setup
+ run: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+
+ - name: Run DokuWiki Test-setup
+ env:
+ CI_SERVER: 1
+ DOKUWIKI: ${{ matrix.dokuwiki-branch }}
+ run: sh travis.sh
+
+ - name: Setup PHPUnit
+ run: |
+ php _test/fetchphpunit.php
+ cd _test
+
+ - name: Run PHPUnit
+ run: |
+ cd _test
+ php phpunit.phar --verbose --stderr --group plugin_gallery
diff --git a/_test/FeedGalleryTest.php b/_test/FeedGalleryTest.php
new file mode 100644
index 0000000..ccf265a
--- /dev/null
+++ b/_test/FeedGalleryTest.php
@@ -0,0 +1,28 @@
+getImages();
+ $this->assertIsArray($images);
+ $this->assertCount(3, $images);
+ }
+}
diff --git a/_test/GeneralTest.php b/_test/GeneralTest.php
new file mode 100644
index 0000000..638dd10
--- /dev/null
+++ b/_test/GeneralTest.php
@@ -0,0 +1,86 @@
+assertFileExists($file);
+
+ $info = confToHash($file);
+
+ $this->assertArrayHasKey('base', $info);
+ $this->assertArrayHasKey('author', $info);
+ $this->assertArrayHasKey('email', $info);
+ $this->assertArrayHasKey('date', $info);
+ $this->assertArrayHasKey('name', $info);
+ $this->assertArrayHasKey('desc', $info);
+ $this->assertArrayHasKey('url', $info);
+
+ $this->assertEquals('gallery', $info['base']);
+ $this->assertRegExp('/^https?:\/\//', $info['url']);
+ $this->assertTrue(mail_isvalid($info['email']));
+ $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+ $this->assertTrue(false !== strtotime($info['date']));
+ }
+
+ /**
+ * Test to ensure that every conf['...'] entry in conf/default.php has a corresponding meta['...'] entry in
+ * conf/metadata.php.
+ */
+ public function testPluginConf(): void
+ {
+ $conf_file = __DIR__ . '/../conf/default.php';
+ $meta_file = __DIR__ . '/../conf/metadata.php';
+
+ if (!file_exists($conf_file) && !file_exists($meta_file)) {
+ self::markTestSkipped('No config files exist -> skipping test');
+ }
+
+ if (file_exists($conf_file)) {
+ include($conf_file);
+ }
+ if (file_exists($meta_file)) {
+ include($meta_file);
+ }
+
+ $this->assertEquals(
+ gettype($conf),
+ gettype($meta),
+ 'Both ' . DOKU_PLUGIN . 'gallery/conf/default.php and ' . DOKU_PLUGIN . 'gallery/conf/metadata.php have to exist and contain the same keys.'
+ );
+
+ if ($conf !== null && $meta !== null) {
+ foreach ($conf as $key => $value) {
+ $this->assertArrayHasKey(
+ $key,
+ $meta,
+ 'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'gallery/conf/metadata.php'
+ );
+ }
+
+ foreach ($meta as $key => $value) {
+ $this->assertArrayHasKey(
+ $key,
+ $conf,
+ 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'gallery/conf/default.php'
+ );
+ }
+ }
+
+ }
+}
diff --git a/_test/NamespaceGalleryTest.php b/_test/NamespaceGalleryTest.php
new file mode 100644
index 0000000..e844e71
--- /dev/null
+++ b/_test/NamespaceGalleryTest.php
@@ -0,0 +1,43 @@
+getImages();
+ $this->assertIsArray($images);
+ $this->assertCount(3, $images);
+ }
+}
diff --git a/_test/data/media/gallery/img_0153.jpg b/_test/data/media/gallery/img_0153.jpg
new file mode 100644
index 0000000..3ce67b9
Binary files /dev/null and b/_test/data/media/gallery/img_0153.jpg differ
diff --git a/_test/data/media/gallery/img_0373.jpg b/_test/data/media/gallery/img_0373.jpg
new file mode 100644
index 0000000..3f2b707
Binary files /dev/null and b/_test/data/media/gallery/img_0373.jpg differ
diff --git a/_test/data/media/gallery/img_1378.jpg b/_test/data/media/gallery/img_1378.jpg
new file mode 100644
index 0000000..30734e7
Binary files /dev/null and b/_test/data/media/gallery/img_1378.jpg differ
diff --git a/classes/AbstractGallery.php b/classes/AbstractGallery.php
new file mode 100644
index 0000000..dfd8354
--- /dev/null
+++ b/classes/AbstractGallery.php
@@ -0,0 +1,84 @@
+options = $options;
+ }
+
+ /**
+ * Simple heuristic if something is an image
+ *
+ * @param string $src
+ * @return bool
+ */
+ public function hasImageExtension($src)
+ {
+ return (bool)preg_match(Image::IMG_REGEX, $src);
+ }
+
+ /**
+ * Get the images of this gallery
+ *
+ * The result will be sorted, reversed and limited according to the options
+ *
+ * @return Image[]
+ */
+ public function getImages()
+ {
+ $images = $this->images; // create a copy of the array
+
+ switch ($this->options->sort) {
+ case Options::SORT_FILE:
+ usort($images, function ($a, $b) {
+ return strcmp($a->getFilename(), $b->getFilename());
+ });
+ break;
+ case Options::SORT_CTIME:
+ usort($images, function ($a, $b) {
+ return $a->getCreated() - $b->getCreated();
+ });
+ break;
+ case Options::SORT_MTIME:
+ usort($images, function ($a, $b) {
+ return $a->getModified() - $b->getModified();
+ });
+ break;
+ case Options::SORT_TITLE:
+ usort($images, function ($a, $b) {
+ return strcmp($a->getTitle(), $b->getTitle());
+ });
+ break;
+ case Options::SORT_RANDOM:
+ shuffle($images);
+ break;
+ }
+ if ($this->options->reverse) {
+ $images = array_reverse($images);
+ }
+ if ($this->options->offset) {
+ $images = array_slice($images, $this->options->offset);
+ }
+ if ($this->options->limit) {
+ $images = array_slice($images, 0, $this->options->limit);
+ }
+
+ return $images;
+ }
+}
diff --git a/classes/FeedGallery.php b/classes/FeedGallery.php
new file mode 100644
index 0000000..361eb73
--- /dev/null
+++ b/classes/FeedGallery.php
@@ -0,0 +1,90 @@
+initBaseUrl($url);
+ $this->parseFeed($url);
+ }
+
+ /**
+ * Parses the given feed and adds all images to the gallery
+ *
+ * @param string $url
+ * @return void
+ * @throws \Exception
+ */
+ protected function parseFeed($url) {
+ $feed = new FeedParser();
+ $feed->set_feed_url($url);
+ $ok = $feed->init();
+ if (!$ok) throw new \Exception($feed->error());
+
+ foreach ($feed->get_items() as $item) {
+ $enclosure = $item->get_enclosure();
+ if (!$enclosure) continue;
+
+ // skip non-image enclosures
+ if ($enclosure->get_type() && substr($enclosure->get_type(), 0, 5) != 'image') {
+ continue;
+ } elseif (!$this->hasImageExtension($enclosure->get_link())) {
+ continue;
+ }
+
+ $enclosureLink = $this->makeAbsoluteUrl($enclosure->get_link());
+ $detailLink = $this->makeAbsoluteUrl($item->get_link());
+
+ $image = new Image($enclosureLink);
+ $image->setDetaillink($detailLink);
+ $image->setTitle(htmlspecialchars_decode($enclosure->get_title() ?? '', ENT_COMPAT));
+ $image->setDescription(strip_tags(htmlspecialchars_decode($enclosure->get_description() ?? '', ENT_COMPAT)));
+ $image->setCreated($item->get_date('U'));
+ $image->setModified($item->get_date('U'));
+ $image->setWidth($enclosure->get_width());
+ $image->setHeight($enclosure->get_height());
+
+ $this->images[] = $image;
+ }
+ }
+
+ /**
+ * Make the given URL absolute using feed's URL as base
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function makeAbsoluteUrl($url)
+ {
+
+ if (!preg_match('/^https?:\/\//i', $url)) {
+ if ($url[0] == '/') {
+ $url = $this->feedHost . $url;
+ } else {
+ $url = $this->feedHost . $this->feedPath . $url;
+ }
+ }
+ return $url;
+ }
+
+ /**
+ * Initialize base url to use for broken feeds with non-absolute links
+ * @param string $url The feed URL
+ * @return void
+ */
+ protected function initBaseUrl($url)
+ {
+ $main = parse_url($url);
+ $this->feedHost = $main['scheme'] . '://' . $main['host'] . (!empty($main['port']) ? ':' . $main['port'] : '');
+ $this->feedPath = dirname($main['path']) . '/';
+ }
+}
diff --git a/classes/Formatter.php b/classes/Formatter.php
new file mode 100644
index 0000000..e4fe2ed
--- /dev/null
+++ b/classes/Formatter.php
@@ -0,0 +1,225 @@
+options = $options;
+ }
+
+ // region Main Formatters
+
+ /**
+ * @param AbstractGallery $gallery
+ * @return string
+ */
+ public function format(AbstractGallery $gallery)
+ {
+ $html = '
';
+
+ $images = $gallery->getImages();
+ $pages = $this->paginate($images);
+ foreach ($pages as $page => $images) {
+ $html .= $this->formatPage($images, $page);
+ }
+
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * Create an array of pages for the given images
+ *
+ * @param Image[] $images
+ * @return Image[][]
+ */
+ protected function paginate($images)
+ {
+ if ($this->options->paginate) {
+ $pages = array_chunk($images, $this->options->paginate);
+ } else {
+ $pages = [$images];
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Format the given images into a gallery page
+ *
+ * @param Image[] $images
+ * @param int $page The page number
+ * @return string
+ */
+ protected function formatPage($images, int $page)
+ {
+ $html = '';
+ foreach ($images as $image) {
+ $html .= $this->formatImage($image);
+ }
+ $html .= '
';
+ return $html;
+ }
+
+ protected function formatImage(Image $image)
+ {
+ global $ID;
+
+ // thumbnail image properties
+ list($w, $h) = $this->getThumbnailSize($image);
+ $img = [];
+ $img['width'] = $w;
+ $img['height'] = $h;
+ $img['src'] = ml($image->getSrc(), ['w' => $w, 'h' => $h], true, '&');
+ $img['alt'] = $image->getFilename();
+ $img['loading'] = 'lazy';
+
+ // link properties
+ $a = [];
+ $a['href'] = $this->getDetailLink($image);
+ $a['title'] = $image->getTitle();
+ $a['data-caption'] = $image->getDescription();
+ if ($this->options->lightbox) {
+ $a['class'] = "lightbox JSnocheck";
+ $a['rel'] = 'lightbox[gal-' . substr(md5($ID), 4) . ']'; //unique ID for the gallery
+ $a['data-url'] = $this->getLightboxLink($image);
+ }
+
+ // figure properties
+ $fig = [];
+ $fig['class'] = 'gallery-image';
+ $fig['style'] = 'width: ' . $w . 'px;';
+
+ # differentiate between the URL for the lightbox and the URL for details
+ # using a data-attribute
+ # needs slight adjustment in the swipebox script
+ # fall back to href when no data-attribute is set
+ # lightbox url should have width/height limit -> adjust from old defaults to 1600x1200?
+ # use detail URLs for thumbnail, title and filename
+ # when direct is set it should link to full size image
+
+
+ $html = '';
+ $html .= '';
+ $html .= '
';
+ $html .= '';
+
+ if ($this->options->showtitle || $this->options->showname) {
+ $html .= '';
+ if ($this->options->showname) {
+ $a = [
+ 'href' => $this->getDetailLink($image),
+ 'class' => 'gallery-filename',
+ 'title' => $image->getFilename(),
+ ];
+ $html .= '' . hsc($image->getFilename()) . '';
+ }
+ if ($this->options->showtitle) {
+ $a = [
+ 'href' => $this->getDetailLink($image),
+ 'class' => 'gallery-title',
+ 'title' => $image->getTitle(),
+ ];
+ $html .= '' . hsc($image->getTitle()) . '';
+ }
+ $html .= '';
+ }
+
+ $html .= '';
+ return $html;
+ }
+
+ // endregion
+
+ // region Utilities
+
+ protected function getDetailLink(Image $image)
+ {
+ if ($image->getDetaillink()) {
+ // external image
+ return $image->getDetaillink();
+ } else {
+ return ml($image->getSrc(), '', $this->options->direct, '&');
+ }
+ }
+
+ /**
+ * Get the direct link to the image but limit it to a certain size
+ *
+ * @param Image $image
+ * @return string
+ */
+ protected function getLightboxLink(Image $image)
+ {
+ // use original image if no size is available
+ if (!$image->getWidth() || !$image->getHeight()) {
+ return ml($image->getSrc(), '', true, '&');
+ }
+
+ // fit into bounding box
+ list($width, $height) = $this->fitBoundingBox(
+ $image->getWidth(),
+ $image->getHeight(),
+ $this->options->lightboxWidth,
+ $this->options->lightboxHeight
+ );
+
+ // no upscaling
+ if ($width > $image->getWidth() || $height > $image->getHeight()) {
+ return ml($image->getSrc(), '', true, '&');
+ }
+
+ return ml($image->getSrc(), ['w' => $width, 'h' => $height], true, '&');
+ }
+
+ /** Calculate the thumbnail size */
+ protected function getThumbnailSize(Image $image)
+ {
+ $crop = $this->options->crop;
+ if (!$image->getWidth() || !$image->getHeight()) {
+ $crop = true;
+ }
+ if (!$crop) {
+ list($thumbWidth, $thumbHeight) = $this->fitBoundingBox(
+ $image->getWidth(),
+ $image->getHeight(),
+ $this->options->thumbnailWidth,
+ $this->options->thumbnailHeight
+ );
+ } else {
+ $thumbWidth = $this->options->thumbnailWidth;
+ $thumbHeight = $this->options->thumbnailHeight;
+ }
+ return [$thumbWidth, $thumbHeight];
+ }
+
+
+ /**
+ * Calculate the size of a thumbnail to fit into a bounding box
+ *
+ * @param int $imgWidth
+ * @param int $imgHeight
+ * @param int $bBoxWidth
+ * @param int $bBoxHeight
+ * @return int[]
+ */
+ protected function fitBoundingBox($imgWidth, $imgHeight, $bBoxWidth, $bBoxHeight)
+ {
+ $scale = min($bBoxWidth / $imgWidth, $bBoxHeight / $imgHeight);
+
+ $width = round($imgWidth * $scale);
+ $height = round($imgHeight * $scale);
+
+ return [$width, $height];
+ }
+
+ // endregion
+}
diff --git a/classes/Image.php b/classes/Image.php
new file mode 100644
index 0000000..dd29916
--- /dev/null
+++ b/classes/Image.php
@@ -0,0 +1,191 @@
+src = $src;
+ if (preg_match('/^https:\/\//i', $src)) {
+ $this->isExternal = true;
+ $path = parse_url($src, PHP_URL_PATH);
+ $this->filename = basename($path);
+ } else {
+ $this->localfile = mediaFN($src);
+ if (!file_exists($this->localfile)) throw new \Exception('File not found: ' . $this->localfile);
+ $this->filename = basename($this->localfile);
+ $this->modified = filemtime($this->localfile);
+
+ $jpegMeta = new \JpegMeta($this->localfile);
+ $this->title = $jpegMeta->getField('Simple.Title');
+ $this->description = $jpegMeta->getField('Iptc.Caption');
+ $this->created = $jpegMeta->getField('Date.EarliestTime');
+ $this->width = $jpegMeta->getField('File.Width');
+ $this->height = $jpegMeta->getField('File.Height');
+ }
+ }
+
+ public function isExternal()
+ {
+ return $this->isExternal;
+ }
+
+ public function getSrc()
+ {
+ return $this->src;
+ }
+
+ public function getFilename()
+ {
+ return $this->filename;
+ }
+
+ public function setFilename(string $filename)
+ {
+ $this->filename = $filename;
+ }
+
+ public function getLocalfile()
+ {
+ return $this->localfile;
+ }
+
+ public function setLocalfile(string $localfile)
+ {
+ $this->localfile = $localfile;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ if (empty($this->title) || $this->title == $this->filename) {
+ $title = str_replace('_', ' ', $this->filename);
+ $title = preg_replace(self::IMG_REGEX, '', $title);
+ $title = PhpString::ucwords($title);
+ return $title;
+ }
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return trim(str_replace("\n", ' ', $this->description));
+ }
+
+ /**
+ * @param string $description
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * @return int
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * @param int $width
+ */
+ public function setWidth($width)
+ {
+ $this->width = $width;
+ }
+
+ /**
+ * @return int
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * @param int $height
+ */
+ public function setHeight($height)
+ {
+ $this->height = $height;
+ }
+
+ /**
+ * @return int
+ */
+ public function getCreated()
+ {
+ return $this->created;
+ }
+
+ /**
+ * @param int $created
+ */
+ public function setCreated($created)
+ {
+ $this->created = $created;
+ }
+
+ /**
+ * @return int
+ */
+ public function getModified()
+ {
+ return $this->modified;
+ }
+
+ /**
+ * @param int $modified
+ */
+ public function setModified($modified)
+ {
+ $this->modified = $modified;
+ }
+
+ public function getDetaillink()
+ {
+ return $this->detaillink;
+ }
+
+ public function setDetaillink($detaillink)
+ {
+ $this->detaillink = $detaillink;
+ }
+}
diff --git a/classes/NamespaceGallery.php b/classes/NamespaceGallery.php
new file mode 100644
index 0000000..de13606
--- /dev/null
+++ b/classes/NamespaceGallery.php
@@ -0,0 +1,73 @@
+searchNamespace($ns, $options->recursive, $options->filter);
+ }
+
+ /**
+ * Find the images
+ *
+ * @param string $ns
+ * @param bool $recursive search recursively?
+ * @param string $filter regular expresion to filter image IDs against (without namespace)
+ * @throws \Exception
+ */
+ protected function searchNamespace($ns, $recursive, $filter)
+ {
+ global $conf;
+
+ if (media_exists($ns) && !is_dir(mediaFN($ns))) {
+ // this is a single file, not a namespace
+ if ($this->hasImageExtension($ns)) {
+ $this->images[] = new Image($ns);
+ }
+ } else {
+ search(
+ $this->images,
+ $conf['mediadir'],
+ [$this, 'searchCallback'],
+ [
+ 'depth' => $recursive ? 0 : 1,
+ 'filter' => $filter
+ ],
+ utf8_encodeFN(str_replace(':', '/', $ns))
+ );
+ }
+ }
+
+ /**
+ * Callback for search() to find images
+ */
+ public function searchCallback(&$data, $base, $file, $type, $lvl, $opts)
+ {
+ if ($type == 'd') {
+ if (empty($opts['depth'])) return true; // recurse forever
+ $depth = substr_count($file, '/'); // we can't use level because we start deeper
+ if ($depth >= $opts['depth']) return false; // depth reached
+ return true;
+ }
+
+ $id = pathID($file, true);
+
+ // skip non-valid files
+ if ($id != cleanID($id)) return false;
+ //check ACL for namespace (we have no ACL for mediafiles)
+ if (auth_quickaclcheck(getNS($id) . ':*') < AUTH_READ) return false;
+ // skip non-images
+ if (!$this->hasImageExtension($file)) return false;
+ // skip filtered images
+ if ($opts['filter'] && !preg_match($opts['filter'], noNS($id))) return false;
+
+ // still here, add to result
+ $data[] = new Image($id);
+ return false;
+ }
+}
diff --git a/classes/Options.php b/classes/Options.php
new file mode 100644
index 0000000..7e5d117
--- /dev/null
+++ b/classes/Options.php
@@ -0,0 +1,109 @@
+thumbnailWidth = $plugin->getConf('thumbnail_width');
+ $this->thumbnailHeight = $plugin->getConf('thumbnail_height');
+ $this->lightboxWidth = $plugin->getConf('image_width');
+ $this->lightboxHeight = $plugin->getConf('image_height');
+ $this->columns = $plugin->getConf('cols');
+ $this->sort = $plugin->getConf('sort');
+ $this->parseParameters($plugin->getConf('options'));
+ }
+
+ /**
+ * Simple option strings parser
+ *
+ * @param string $params
+ * @return void
+ */
+ public function parseParameters($params)
+ {
+ $params = preg_replace('/[,&?]+/', ' ', $params);
+ $params = explode(' ', $params);
+ foreach ($params as $param) {
+ if ($param === '') continue;
+ if ($param == 'titlesort') {
+ $this->sort = self::SORT_TITLE;
+ } elseif ($param == 'datesort') {
+ $this->sort = self::SORT_CTIME;
+ } elseif ($param == 'modsort') {
+ $this->sort = self::SORT_MTIME;
+ } elseif (preg_match('/^=(\d+)$/', $param, $match)) {
+ $this->limit = (int)$match[1];
+ } elseif (preg_match('/^\+(\d+)$/', $param, $match)) {
+ $this->offset = (int)$match[1];
+ } elseif (is_numeric($param)) {
+ $this->columns = (int)$param;
+ } elseif (preg_match('/^~(\d+)$/', $param, $match)) {
+ $this->paginate = (int)$match[1];
+ } elseif (preg_match('/^(\d+)([xX])(\d+)$/', $param, $match)) {
+ if ($match[2] == 'X') {
+ $this->lightboxWidth = (int)$match[1];
+ $this->lightboxHeight = (int)$match[3];
+ } else {
+ $this->thumbnailWidth = (int)$match[1];
+ $this->thumbnailHeight = (int)$match[3];
+ }
+ } elseif (strpos($param, '*') !== false) {
+ $param = preg_quote($param, '/');
+ $param = '/^' . str_replace('\\*', '.*?', $param) . '$/';
+ $this->filter = $param;
+ } else {
+ if (substr($param, 0, 2) == 'no') {
+ $opt = substr($param, 2);
+ $set = false;
+ } else {
+ $opt = $param;
+ $set = true;
+ }
+ if (!property_exists($this, $opt) || !is_bool($this->$opt)) continue;
+ $this->$opt = $set;
+ }
+ }
+ }
+
+}
diff --git a/conf/default.php b/conf/default.php
index 1ea08ff..01a0d0f 100644
--- a/conf/default.php
+++ b/conf/default.php
@@ -7,8 +7,8 @@
$conf['thumbnail_width'] = 120;
$conf['thumbnail_height'] = 120;
-$conf['image_width'] = 800;
-$conf['image_height'] = 600;
+$conf['image_width'] = 1600;
+$conf['image_height'] = 1200;
$conf['cols'] = 5;
$conf['sort'] = 'file';
diff --git a/conf/metadata.php b/conf/metadata.php
index f43fe28..d544037 100644
--- a/conf/metadata.php
+++ b/conf/metadata.php
@@ -5,12 +5,23 @@
* @author Dmitry Baikov
*/
+use dokuwiki\plugin\gallery\classes\Options;
+
$meta['thumbnail_width'] = array('numeric');
$meta['thumbnail_height'] = array('numeric');
$meta['image_width'] = array('numeric');
$meta['image_height'] = array('numeric');
$meta['cols'] = array('numeric');
-$meta['sort'] = array('multichoice', '_choices' => array('file', 'mod', 'date', 'title'));
-$meta['options'] = array('multicheckbox', '_choices' => array('cache', 'crop', 'direct', 'lightbox', 'random', 'reverse', 'showname', 'showtitle'));
+$meta['sort'] = array(
+ 'multichoice',
+ '_choices' => array(
+ Options::SORT_FILE,
+ Options::SORT_CTIME,
+ Options::SORT_MTIME,
+ Options::SORT_TITLE,
+ Options::SORT_RANDOM,
+ )
+);
+$meta['options'] = array('multicheckbox', '_choices' => array('cache', 'crop', 'direct', 'lightbox', 'reverse', 'showname', 'showtitle'));
diff --git a/syntax.php b/syntax.php
index 86677bf..f87678b 100644
--- a/syntax.php
+++ b/syntax.php
@@ -1,6 +1,10 @@
galleryID = substr(md5($match), 0, 4);
// alignment
- $data['align'] = 0;
- if (substr($match, 0, 1) == ' ') $data['align'] += 1;
- if (substr($match, -1, 1) == ' ') $data['align'] += 2;
+ if (substr($match, 0, 1) == ' ') $options->align += Options::ALIGN_LEFT;
+ if (substr($match, -1, 1) == ' ') $options->align += Options::ALIGN_RIGHT;
- // extract params
- list($ns, $params) = explode('?', $match, 2);
- $ns = trim($ns);
+ // extract src and params
+ list($src, $params) = sexplode('?', $match, 2);
+ $src = trim($src);
- // namespace (including resolving relatives)
- if (!preg_match('/^https?:\/\//i', $ns)) {
+ // resolve relative namespace
+ if (!preg_match('/^https?:\/\//i', $src)) {
$pageResolver = new PageResolver($ID);
- $data['ns'] = $pageResolver->resolveId($ns);
- } else {
- $data['ns'] = $ns;
+ $src = $pageResolver->resolveId($src);
}
- $data = array_merge(
- $data,
- $this->getDataFromParams($this->getConf('options') . ',' . $params)
- );
+ // parse parameters
+ $options->parseParameters($params);
- return $data;
- }
-
- /**
- * Extract options from the provided parameter string
- *
- * @param string $params
- *
- * @return array associative array of the options defined in the string
- */
- public function getDataFromParams($params)
- {
- // set the defaults
- $data = [];
- $data['tw'] = $this->getConf('thumbnail_width');
- $data['th'] = $this->getConf('thumbnail_height');
- $data['iw'] = $this->getConf('image_width');
- $data['ih'] = $this->getConf('image_height');
- $data['cols'] = $this->getConf('cols');
- $data['filter'] = '';
- $data['lightbox'] = false;
- $data['direct'] = false;
- $data['showname'] = false;
- $data['showtitle'] = false;
- $data['reverse'] = false;
- $data['random'] = false;
- $data['cache'] = true;
- $data['crop'] = false;
- $data['recursive'] = true;
- $data['sort'] = $this->getConf('sort');
- $data['limit'] = 0;
- $data['offset'] = 0;
- $data['paginate'] = 0;
-
- // parse additional options
- $params = preg_replace('/[,&?]+/', ' ', $params);
- $params = explode(' ', $params);
- foreach ($params as $param) {
- if ($param === '') continue;
- if ($param == 'titlesort') {
- $data['sort'] = 'title';
- } elseif ($param == 'datesort') {
- $data['sort'] = 'date';
- } elseif ($param == 'modsort') {
- $data['sort'] = 'mod';
- } elseif (preg_match('/^=(\d+)$/', $param, $match)) {
- $data['limit'] = $match[1];
- } elseif (preg_match('/^\+(\d+)$/', $param, $match)) {
- $data['offset'] = $match[1];
- } elseif (is_numeric($param)) {
- $data['cols'] = (int)$param;
- } elseif (preg_match('/^~(\d+)$/', $param, $match)) {
- $data['paginate'] = $match[1];
- } elseif (preg_match('/^(\d+)([xX])(\d+)$/', $param, $match)) {
- if ($match[2] == 'X') {
- $data['iw'] = $match[1];
- $data['ih'] = $match[3];
- } else {
- $data['tw'] = $match[1];
- $data['th'] = $match[3];
- }
- } elseif (strpos($param, '*') !== false) {
- $param = preg_quote($param, '/');
- $param = '/^' . str_replace('\\*', '.*?', $param) . '$/';
- $data['filter'] = $param;
- } else {
- if (substr($param, 0, 2) == 'no') {
- $data[substr($param, 2)] = false;
- } else {
- $data[$param] = true;
- }
- }
- }
-
- // implicit direct linking?
- if ($data['lightbox']) $data['direct'] = true;
-
- return $data;
+ return [
+ $src, $options
+ ];
}
/** @inheritdoc */
public function render($mode, Doku_Renderer $R, $data)
{
global $ID;
+
+ [$src, $options] = $data;
+
+ if (preg_match('/^https?:\/\//i', $src)) {
+ $gallery = new FeedGallery($src, $options);
+ } else {
+ $gallery = new NamespaceGallery($src, $options);
+ }
+
+
if ($mode == 'xhtml') {
- $R->info['cache'] &= $data['cache'];
- $R->doc .= $this->formatGallery($data);
+ $R->info['cache'] = $options->cache;
+ $formatter = new Formatter($options);
+ $R->doc .= $formatter->format($gallery);
+
+ // FIXME next steps:
+ // * implement minimal standard renderer for all renderers (just inline thumbnails with links)
+ // * maybe implement PDF renderer separately from XHTML
+ // * adjust lightbox script
+ // * redo CSS
+ // * add more unit tests
+
+
return true;
} elseif ($mode == 'metadata') {
- $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_CACHE);
- $img = $rel['firstimage'];
- if (empty($img)) {
- $files = $this->findimages($data);
- if (count($files)) $R->internalmedia($files[0]['id']);
+ // render the first image of the gallery, to ensure it will be used as first image if needed
+ $images = $gallery->getImages();
+ if (count($images)) {
+ if ($images[0]->isExternal()) {
+ $R->externalmedia($images[0]->getSrc());
+ } else {
+ $R->internalmedia($images[0]->getSrc());
+ }
}
return true;
}
return false;
}
-
- /**
- * Loads images from a MediaRSS or ATOM feed
- */
- protected function loadRSS($url)
- {
- $feed = new FeedParser();
- $feed->set_feed_url($url);
- $feed->init();
- $files = array();
-
- // base url to use for broken feeds with non-absolute links
- $main = parse_url($url);
- $host = $main['scheme'] . '://' .
- $main['host'] .
- (($main['port']) ? ':' . $main['port'] : '');
- $path = dirname($main['path']) . '/';
-
- foreach ($feed->get_items() as $item) {
- if ($enclosure = $item->get_enclosure()) {
- // skip non-image enclosures
- if ($enclosure->get_type()) {
- if (substr($enclosure->get_type(), 0, 5) != 'image') continue;
- } else {
- if (!preg_match('/\.(jpe?g|png|gif)(\?|$)/i',
- $enclosure->get_link())) continue;
- }
-
- // non absolute links
- $ilink = $enclosure->get_link();
- if (!preg_match('/^https?:\/\//i', $ilink)) {
- if ($ilink[0] == '/') {
- $ilink = $host . $ilink;
- } else {
- $ilink = $host . $path . $ilink;
- }
- }
- $link = $item->link;
- if (!preg_match('/^https?:\/\//i', $link)) {
- if ($link[0] == '/') {
- $link = $host . $link;
- } else {
- $link = $host . $path . $link;
- }
- }
-
- $files[] = array(
- 'id' => $ilink,
- 'isimg' => true,
- 'file' => basename($ilink),
- // decode to avoid later double encoding
- 'title' => htmlspecialchars_decode($enclosure->get_title(), ENT_COMPAT),
- 'desc' => strip_tags(htmlspecialchars_decode($enclosure->get_description(), ENT_COMPAT)),
- 'width' => $enclosure->get_width(),
- 'height' => $enclosure->get_height(),
- 'mtime' => $item->get_date('U'),
- 'ctime' => $item->get_date('U'),
- 'detail' => $link,
- );
- }
- }
- return $files;
- }
-
- /**
- * Gather all photos matching the given criteria
- */
- protected function findimages(&$data)
- {
- global $conf;
- $files = array();
-
- // http URLs are supposed to be media RSS feeds
- if (preg_match('/^https?:\/\//i', $data['ns'])) {
- $files = $this->loadRSS($data['ns']);
- $data['_single'] = false;
- } else {
- $dir = utf8_encodeFN(str_replace(':', '/', $data['ns']));
- // all possible images for the given namespace (or a single image)
- if (is_file($conf['mediadir'] . '/' . $dir)) {
- $files[] = array(
- 'id' => $data['ns'],
- 'isimg' => preg_match('/\.(jpe?g|gif|png)$/', $dir),
- 'file' => basename($dir),
- 'mtime' => filemtime($conf['mediadir'] . '/' . $dir),
- 'meta' => new JpegMeta($conf['mediadir'] . '/' . $dir)
- );
- $data['_single'] = true;
- } else {
- $depth = $data['recursive'] ? 0 : 1;
- search($files,
- $conf['mediadir'],
- 'search_media',
- array('depth' => $depth),
- $dir);
- $data['_single'] = false;
- }
- }
-
- // done, yet?
- $len = count($files);
- if (!$len) return $files;
- if (isset($data['single']) && $data['single']) return $files;
-
- // filter images
- for ($i = 0; $i < $len; $i++) {
- if (!$files[$i]['isimg']) {
- unset($files[$i]); // this is faster, because RE was done before
- } elseif ($data['filter']) {
- if (!preg_match($data['filter'], noNS($files[$i]['id']))) unset($files[$i]);
- }
- }
- if ($len < 1) return $files;
-
- // random?
- if ($data['random']) {
- shuffle($files);
- } else {
- // sort?
- if ($data['sort'] == 'date') {
- usort($files, array($this, 'datesort'));
- } elseif ($data['sort'] == 'mod') {
- usort($files, array($this, 'modsort'));
- } elseif ($data['sort'] == 'title') {
- usort($files, array($this, 'titlesort'));
- }
-
- // reverse?
- if ($data['reverse']) $files = array_reverse($files);
- }
-
- // limits and offsets?
- if ($data['offset']) $files = array_slice($files, $data['offset']);
- if ($data['limit']) $files = array_slice($files, 0, $data['limit']);
-
- return $files;
- }
-
- /**
- * usort callback to sort by file lastmodified time
- */
- protected function modsort($a, $b)
- {
- if ($a['mtime'] < $b['mtime']) return -1;
- if ($a['mtime'] > $b['mtime']) return 1;
- return strcmp($a['file'], $b['file']);
- }
-
- /**
- * usort callback to sort by EXIF date
- */
- protected function datesort($a, $b)
- {
- $da = $this->readMeta($a, 'cdate');
- $db = $this->readMeta($b, 'cdate');
- if ($da < $db) return -1;
- if ($da > $db) return 1;
- return strcmp($a['file'], $b['file']);
- }
-
- /**
- * usort callback to sort by EXIF title
- */
- protected function titlesort($a, $b)
- {
- $ta = $this->readMeta($a, 'title');
- $tb = $this->readMeta($b, 'title');
- return strcmp($ta, $tb);
- }
-
-
- /**
- * Does the gallery formatting
- */
- protected function formatGallery($data)
- {
- $ret = '';
-
- $files = $this->findimages($data);
-
- //anything found?
- if (!count($files)) {
- $ret .= '' . $this->getLang('nothingfound') . '
';
- return $ret;
- }
-
- // prepare alignment
- $align = '';
- $xalign = '';
- if ($data['align'] == 1) {
- $align = ' gallery_right';
- $xalign = ' align="right"';
- }
- if ($data['align'] == 2) {
- $align = ' gallery_left';
- $xalign = ' align="left"';
- }
- if ($data['align'] == 3) {
- $align = ' gallery_center';
- $xalign = ' align="center"';
- }
- if (!$data['_single']) {
- if (!$align) $align = ' gallery_center'; // center galleries on default
- if (!$xalign) $xalign = ' align="center"';
- }
-
- $page = 0;
-
- // build gallery
- if ($data['_single']) {
- $ret .= $this->formatThumbnail($files[0], $data);
- $ret .= $this->formatName($files[0], $data);
- $ret .= $this->formatTitle($files[0], $data);
- } elseif ($data['cols'] > 0) { // format as table
- $close_pg = false;
-
- $i = 0;
- foreach ($files as $img) {
-
- // new page?
- if ($data['paginate'] && ($i % $data['paginate'] == 0)) {
- $ret .= '';
- $close_pg = true;
- }
-
- // new table?
- if ($i == 0 || ($data['paginate'] && ($i % $data['paginate'] == 0))) {
- $ret .= '
';
-
- }
-
- // new row?
- if ($i % $data['cols'] == 0) {
- $ret .= '';
- }
-
- // an image cell
- $ret .= '';
- $ret .= $this->formatThumbnail($img, $data);
- $ret .= $this->formatName($img, $data);
- $ret .= $this->formatTitle($img, $data);
- $ret .= ' | ';
- $i++;
-
- // done with this row? close it
- $close_tr = true;
- if ($i % $data['cols'] == 0) {
- $ret .= '
';
- $close_tr = false;
- }
-
- // close current page and table
- if ($data['paginate'] && ($i % $data['paginate'] == 0)) {
- if ($close_tr) {
- // add remaining empty cells
- while ($i % $data['cols']) {
- $ret .= ' | ';
- $i++;
- }
- $ret .= '';
- }
- $ret .= '
';
- $ret .= '
';
- $close_pg = false;
- $i = 0; // reset counter to ensure next page is properly started
- }
-
- } // foreach
-
- if ($close_tr) {
- // add remaining empty cells
- while ($i % $data['cols']) {
- $ret .= ' | ';
- $i++;
- }
- $ret .= '';
- }
-
- if (!$data['paginate']) {
- $ret .= '';
- } elseif ($close_pg) {
- $ret .= '';
- $ret .= '';
- }
- } else { // format as div sequence
- $i = 0;
- $close_pg = false;
- foreach ($files as $img) {
-
- if ($data['paginate'] && ($i % $data['paginate'] == 0)) {
- $ret .= '';
- $close_pg = true;
- }
-
- $ret .= '
';
- $ret .= $this->formatThumbnail($img, $data);
- $ret .= $this->formatName($img, $data);
- $ret .= $this->formatTitle($img, $data);
- $ret .= '
';
-
- $i++;
-
- if ($data['paginate'] && ($i % $data['paginate'] == 0)) {
- $ret .= '
';
- $close_pg = false;
- }
- }
-
- if ($close_pg) $ret .= '';
-
- $ret .= '
';
- }
-
- // pagination links
- $pgret = '';
- if ($page) {
- $pgret .= '' . $this->getLang('pages') . ' ';
- for ($j = 1; $j <= $page; $j++) {
- $pgret .= '
' . $j . ' ';
- }
- $pgret .= '
';
- }
-
- return '';
- }
-
- /**
- * Defines how a thumbnail should look like
- */
- protected function formatThumbnail(&$img, $data)
- {
- global $ID;
-
- // calculate thumbnail size
- if (!$data['crop']) {
- $w = (int)$this->readMeta($img, 'width');
- $h = (int)$this->readMeta($img, 'height');
- if ($w && $h) {
- $dim = array();
- if ($w > $data['tw'] || $h > $data['th']) {
- $ratio = $this->calculateRatio($img, $data['tw'], $data['th']);
- $w = floor($w * $ratio);
- $h = floor($h * $ratio);
- $dim = array('w' => $w, 'h' => $h);
- }
- } else {
- $data['crop'] = true; // no size info -> always crop
- }
- }
- if ($data['crop']) {
- $w = $data['tw'];
- $h = $data['th'];
- $dim = array('w' => $w, 'h' => $h);
- }
-
- //prepare img attributes
- $i = array();
- $i['width'] = $w;
- $i['height'] = $h;
- $i['border'] = 0;
- $i['alt'] = $this->readMeta($img, 'title');
- $i['class'] = 'tn';
- $iatt = buildAttributes($i);
- $src = ml($img['id'], $dim);
-
- // prepare lightbox dimensions
- $w_lightbox = (int)$this->readMeta($img, 'width');
- $h_lightbox = (int)$this->readMeta($img, 'height');
- $dim_lightbox = array();
- if ($w_lightbox > $data['iw'] || $h_lightbox > $data['ih']) {
- $ratio = $this->calculateRatio($img, $data['iw'], $data['ih']);
- $w_lightbox = floor($w_lightbox * $ratio);
- $h_lightbox = floor($h_lightbox * $ratio);
- $dim_lightbox = array('w' => $w_lightbox, 'h' => $h_lightbox);
- }
-
- //prepare link attributes
- $a = array();
- $a['title'] = $this->readMeta($img, 'title');
- $a['data-caption'] = trim(str_replace("\n", ' ', $this->readMeta($img, 'desc')));
- if (!$a['data-caption']) unset($a['data-caption']);
- if ($data['lightbox']) {
- $href = ml($img['id'], $dim_lightbox);
- $a['class'] = "lightbox JSnocheck";
- $a['rel'] = 'lightbox[gal-' . substr(md5($ID), 4) . ']'; //unique ID for the gallery
- } elseif ($img['detail'] && !$data['direct']) {
- $href = $img['detail'];
- } else {
- $href = ml($img['id'], array('id' => $ID), $data['direct']);
- }
- $aatt = buildAttributes($a);
-
- // prepare output
- $ret = '';
- $ret .= '';
- $ret .= '
';
- $ret .= '';
- return $ret;
- }
-
-
- /**
- * Defines how a filename + link should look
- */
- protected function formatName($img, $data)
- {
- global $ID;
-
- if (!$data['showname']) {
- return '';
- }
-
- //prepare link
- $lnk = ml($img['id'], array('id' => $ID), false);
-
- // prepare output
- $ret = '';
- $ret .= '
';
- $ret .= hsc($img['file']);
- $ret .= '';
- return $ret;
- }
-
- /**
- * Defines how title + link should look
- */
- protected function formatTitle($img, $data)
- {
- global $ID;
-
- if (!$data['showtitle']) {
- return '';
- }
-
- //prepare link
- $lnk = ml($img['id'], array('id' => $ID), false);
-
- // prepare output
- $ret = '';
- $ret .= '
';
- $ret .= hsc($this->readMeta($img, 'title'));
- $ret .= '';
- return $ret;
- }
-
- /**
- * Return the metadata of an item
- *
- * Automatically checks if a JPEGMeta object is available or if all data is
- * supplied in array
- */
- protected function readMeta(&$img, $opt)
- {
- if ($img['meta']) {
- // map JPEGMeta calls to opt names
-
- switch ($opt) {
- case 'title':
- return $img['meta']->getField('Simple.Title');
- case 'desc':
- return $img['meta']->getField('Iptc.Caption');
- case 'cdate':
- return $img['meta']->getField('Date.EarliestTime');
- case 'width':
- return $img['meta']->getField('File.Width');
- case 'height':
- return $img['meta']->getField('File.Height');
-
-
- default:
- return '';
- }
-
- } else {
- // just return the array field
- return $img[$opt];
- }
- }
-
- /**
- * Calculates the multiplier needed to resize the image to the given
- * dimensions
- *
- * @author Andreas Gohr
- */
- protected function calculateRatio(&$img, $maxwidth, $maxheight = 0)
- {
- if (!$maxheight) $maxheight = $maxwidth;
-
- $w = $this->readMeta($img, 'width');
- $h = $this->readMeta($img, 'height');
-
- $ratio = 1;
- if ($w >= $h) {
- if ($w >= $maxwidth) {
- $ratio = $maxwidth / $w;
- } elseif ($h > $maxheight) {
- $ratio = $maxheight / $h;
- }
- } else {
- if ($h >= $maxheight) {
- $ratio = $maxheight / $h;
- } elseif ($w > $maxwidth) {
- $ratio = $maxwidth / $w;
- }
- }
- return $ratio;
- }
-
}