Beschreibung der Kernfunktion

Alle hier erwähnten Dateien sind zu finden unter GitHub (https://github.com/dausi/zipgallery).

Wie funktioniert die ZIP Bilder-Galerie?

Um ein einzelnes Bilde eines ZIP-Archives vom Client (Browser) aus ansprechen zu können, lädt der Client vom Server den Pfad des ZIP-Archives mit dem angehängtem Namen der gewünschten Bilddatei. Liegt das ZIP-Archiv unter

http://my.site/irgend/ein/pfad/bildergalerie.zip

so ist die Bilddatei ein-bild.jpg aus diesem Archiv zu erreichen unter

http://my.site/irgend/ein/pfad/bildergalerie.zip/ein-bild.jpg

Für die Extraktion der Bilder aus einem ZIP-Archiv wird serverseitig PHP-Code verwendet. Zum aktivieren dieses PHP-Codes wird eine Apache-Rewrite-Regel verwendet:

RewriteEngine On
RewriteCond %{REQUEST_URI} ^(.*)/(.+.zip)/(.+)$
RewriteCond %{DOCUMENT_ROOT}%1/%2 -f
RewriteRule . /external/zipgallery/galleries.php?zip=%1/%2&file=%3 [L,QSA]

Beim Auswerten dieser Regel wird der oben dargestellte Pfad umgewandelt in

http://my.site/external/zipgallery/galleries.php?zip=/irgend/ein/pfad/bildergalerie.zip&file=ein-bild.jpg

Datei galleries.php

Diese Datei stellt das Interface zum der als PHP-Klassen kodierten Funktionalität dar.

In den Zeilen [8 -14] im Listing unten werden die benötigten PHP-Dateien für die Klassen ZipGallery und ZipGalleryCache eingebunden.

Beim ersten Auslesen des ZIP-Archivs wird das Inhaltsverzeichnis sowie alle EXIF- und IPTC-Informationen der enthaltenen Bilder ausgelesen und in einer JSON-Datei in einem Cache abgelegt. Dadurch wird der Zugriff auf die Bilder beschleunigt. Der Cache wird durch die function getZipGalleryCacheConfig() definiert [16 - 22].

[24] zerlegt den Query-String, [25] initialisiert die ZipGallery (siehe Datei zip_gallery.php), der Pfad zum ZIP-Archiv ist in der Query-Variablen zip enthalten. Anschließend wird der Query-String ausgewertet:

  • Ist im Query-String die Variable info enthalten, wird die Info-Datei im JSON-Format zurück geliefert [26 - 39]. Ist zusätzlich die Query-Variable tnw oder tnh gesetzt (thumbnail width / height, [28 - 35]), wird für jedes Bild das Thumbnail base64-kodiert in der JSON-Info-Datei mit ausgegeben.
  • Die Variable file im Query-String qualifiziert die gewünschte Bilddatei im ZIP-Archiv.
  • Ist die Variable thumb belegt, wird statt der Bilddatei ein Thumbnail der Bilddatei geliefert. Dabei kann mittels der Variablen tnh (ThumbNailHeight) und tnw (ThumbNailWidth) die Größe des Thumbnail-Bildes definiert werden (Standard: 50 x 50) [43 - 48].
  • Sonst wird die Bilddatei zurückgegeben [51].

Das ZIP-Archiv kann derzeit nur Bilder im jpeg-Format beinhalten.

Datei: galleries.php (55 Zeilen)
<?php /** * galleries.php * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner */ spl_autoload_register(function($classname) { $classname = strtolower(trim(preg_replace('/([A-Z])/', '_$1', $classname), '_')); require dirname(__FILE__) . DIRECTORY_SEPARATOR . $classname . '.php'; }); spl_autoload_call('ZipGallery'); spl_autoload_call('ZipGalleryCache'); function getZipGalleryCacheConfig() { return [ 'cacheRoot' => dirname(dirname(dirname(__FILE__))) . '/application/files/zip_cache', 'cacheEntries' => 10000 ]; } parse_str($_SERVER['QUERY_STRING'], $query); $zip = new ZipGallery($query['zip']); if (isset($query['info'])) { $tnSize = null; if (isset($query['tnw']) || isset($query['tnh'])) { $tnSize = [ 'tnw' => isset($query['tnw']) ? $query['tnw'] : 50, 'tnh' => isset($query['tnh']) ? $query['tnh'] : 50 ]; } header('Content-Type: text/json'); // JSON info header('Access-Control-Allow-Origin: *'); echo $zip->getInfo($tnSize); } elseif (isset($query['file'])) { header('Content-Type: image/jpeg'); // JPG picture if (isset($query['thumb'])) { $tnw = isset($query['tnw']) ? $query['tnw'] : 50; $tnh = isset($query['tnh']) ? $query['tnh'] : 50; echo $zip->getThumb($query['file'], $tnw, $tnh); } else { echo $zip->getFromZip($query['file']); } } ?>

Datei zip_gallery.php

Die in dieser Datei enthaltene Klasse ZipGallery erweitert die PHP-Standard-Klasse ZipArchive [9]. Beim Erstellen der Instanz (function __construct()) wird das ZIP-Archiv geöffnet [45 - 50]. Im OK-Fall wird das Inhaltsverzeichnis des ZIP-Archives ausgelesen [53 - 54] und der Cache angelegt [55]. Für die Einträge im Cache wird der Pfad des ZIP-Archives als Dateinamens-Präfix definiert [56]. Damit die Info-Einträge im Cache dauerhaft erhalten bleiben, wird das Cache-Ignore-Pattern entsprechend definiert [57].

[71 - 80] enthält die Methode getFromZip(), um ein Bild aus dem ZIP-Archiv auszulesen. Das Auslesen selbst erfolgt in der Cache-Klasse [77].

Die private Methode getFromCache() [87 - 96] liest eine Datei aus dem Cache. Zur Überprüfung der Aktualität wird der Cache-Einträge wird die Modifikationszeit des ZIP-Archives mit gegeben.

Die etwas umfangreichere Methode getInfo() [100 - 181] liest aus dem Cache die Datei info.json aus [106], sofern diese vorhanden ist. Ist dieses nicht der Fall, wird das Inhaltsverzeichnis des ZIP-Archives ausgelesen (for-loop) [111], wobei nur Dateien des Typs jpg bzw jpeg berücksichtigt werden. In [124] wird jeweils die EXIF-Information ausgelesen. Ist die Information vorhanden, wird in [138 - 147] die IPTC-Information ausgelesen. Abschließend [156, 157] werden die die gesammelten Bild-Informationen in JSON umgewandelt und in den Cache geschrieben.

Wenn die ZIP-Info-Datei info.json im Cache verfügbar ist, und der Parameter $tnSize (thumbnail size) gesetzt ist, wird die JSON-Information in das Media-Info-Array $this->media [161 - 164] zurück gewandelt. Anschließend wird das Media-Info-Array jeweils um das Thumbnail-Bild angereichert [172 - 176] und nach JSON konvertiert [177] und zurückgegeben.

In der letzten Methode getThumb() [190- 239] werden die Thumbnail-Dateien generiert bzw. aus dem Cache ausgelesen. Der Name der Cache-Datei wird dabei um die Größe (tnw x tnh) erweitert. Dadurch können für jede Bilddatei eines ZIP-Archivs Thumbnails mit verschiedenen Größen gleichzeitig im Cache gehalten werden. Der Algorithmus zur Thumbnail-Generierung erzeugt einen an den Abmessungen des Thumbnails angepassten mittensymetrischen Ausschnitt der Bilddatei [200- 235]. Eine Thumbnail-Datei wird als JPG generiert und vor Auslieferung in den Cache geschrieben [229- 235].

Datei: zip_gallery.php (240 Zeilen)
<?php /** * Class ZipGallery * * A representation of an image gallery from a ZIP archive * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner */ class ZipGallery extends ZipArchive { protected $zipFilename; protected $zipStat; protected $cache; protected $cacheNamePrefix; protected $zip; protected $entries; protected $iptcFields = [ '2#005' => 'title', '2#010' => 'urgency', '2#015' => 'category', '2#020' => 'subcategories', '2#025' => 'subject', '2#040' => 'specialInstructions', '2#055' => 'cdate', '2#080' => 'authorByline', '2#085' => 'authorTitle', '2#090' => 'city', '2#095' => 'state', '2#101' => 'country', '2#103' => 'OTR', '2#105' => 'headline', '2#110' => 'source', '2#115' => 'photoSource', '2#116' => 'copyright', '2#120' => 'caption', '2#122' => 'captionWriter' ]; /** * Opens a ZIP file and scans it for contained files. * * @param string $zipFilename */ public function __construct($zipFilename) { $this->entries = 0; $this->zipFilename = $zipFilename; $pathToZip = $_SERVER['DOCUMENT_ROOT'] . '/' . $zipFilename; $this->zip = new ZipArchive; if ($this->zip->open($pathToZip) == true) { $this->zipStat = stat($pathToZip); $this->entries = $this->zip->numFiles; $this->cache = new ZipGalleryCache; $this->cacheNamePrefix = ltrim($this->zipFilename, '/') . '/'; $this->cache->setIgnorePattern('/\.json$/'); } } public function __destruct() { } /** * Get file identified by file name from ZIP archive. * Returns data or FALSE. * * @param string filename */ public function getFromZip($filename) { $data = FALSE; if ($this->entries > 0) { $data = $this->zip->getFromName($filename); } return $data; } /** * Get file identified by file name from cache. * Returns data or null. * * @param string filename */ private function getFromCache($filename) { $data = null; if ($this->entries > 0) { $data = $this->cache->getEntry($this->zipStat['mtime'], $this->cacheNamePrefix . $filename); } return $data; } /** * Get entries from ZIP archive as JSON array */ public function getInfo($tnSize) { $info = null; if ($this->entries > 0) { // ZIP file is open, look for cached info entry if (($info = $this->getFromCache('info.json')) === null) { // ZIP file info is not in cache, generate and set into cache $entryNum = 0; $finfo = new finfo(FILEINFO_NONE); for ($i = 0; $i < $this->zip->numFiles; $i++) { $stat = $this->zip->statIndex($i); $filename = $stat['name']; if (preg_match('/jpe?g$/i', $filename) === 1) { // ZIP entry is relevant file $data = $this->zip->getFromName($filename); // init decoded IPTC fields with pseudo 'filename' $iptcDecoded = [ 'filename' => $filename ]; if (($exif = @exif_read_data('data://image/jpeg;base64,'.base64_encode($data), null, true)) !== false) { $size = getimagesizefromstring($data, $imgInfo); if (isset($imgInfo['APP13'])) { if (($iptc = iptcparse($imgInfo['APP13'])) != null) foreach ($iptc as $key => $value) { $idx = isset($this->iptcFields[$key]) ? $this->iptcFields[$key] : $key; $iptcDecoded[$idx] = $value; } } } $exifData = []; foreach ($exif as $exKey => $exValue) { foreach ($exValue as $key => $value) { if (is_array($value) || $finfo->buffer($value) != 'data') { $exifData[$exKey][$key] = $value; } } } $this->media[$entryNum++] = [ 'name' => $filename, 'exif' => $exifData, 'iptc' => $iptcDecoded ]; } } $info = json_encode($this->media, JSON_PARTIAL_OUTPUT_ON_ERROR); $this->cache->setEntry($this->cacheNamePrefix . 'info.json', $info); } else { if ($tnSize !== null) { $this->media = json_decode($info, true); } } if ($tnSize !== null) { /* * enrich json by thumbs */ $tnw = $tnSize['tnw']; $tnh = $tnSize['tnh']; foreach($this->media as $idx => $value) { $filename = $this->media[$idx]['name']; $this->media[$idx]['thumbnail'] = base64_encode($this->getThumb($filename, $tnw, $tnh)); } $info = json_encode($this->media, JSON_PARTIAL_OUTPUT_ON_ERROR); } } return $info; } /** * Generate thumb from file identified by file name. * Outputs thumbnail and returns true or false in case of error. * * @param string filename * @param int new_width * @param int new_height */ public function getThumb($filename, $new_width, $new_height) { $tnFilename = $new_width . 'x' . $new_height . '/' . $filename; $data = $this->getFromCache($tnFilename); if ($data === null) { // not in cache, create $data = $this->getFromZip($filename); if ($data != null) { $im = imagecreatefromstring($data); list($width, $height) = getimagesizefromstring($data); if ($new_width < 0) { // // fixed height, flexible width // $new_width = intval($new_height * $width / $height); } $x = $y = 0; if ($new_width == $new_height) { // // square thumbnail // if ($width > $height) { $x = intval(($width - $height ) / 2); $width = $height; } else { $y = intval(($height - $width ) / 2); $height = $width; } } $tnail = imagecreatetruecolor($new_width, $new_height); imagecopyresampled($tnail, $im, 0, 0, $x, $y, $new_width, $new_height, $width, $height); ob_start(); if (imagejpeg($tnail, null)) { $data = ob_get_contents(); $this->cache->setEntry($this->cacheNamePrefix . $tnFilename, $data); } ob_end_clean(); } } return $data; } }

Datei zip_gallery_cache.php

Die hier kodierte Klasse ZipGalleryCache implementiert den Cache. Dieser liegt im Dateisystem. Ein Cache, der in einer Datenbank residiert, ist in der Anwendung für concrete5 implementiert.

Beim Erstellen der Instanz (function __construct()) wird die Konfiguration angefordert (function getZipCacheConfig(), siehe oben Datei galleries.php) und das Cache-Verzeichnis angelegt, falls es noch nicht existiert [33 - 36].

Die Methode setIgnorePattern() [44 - 47] setzt das Cache-Ignore-Pattern, das in der Methode setEntry() verwendet wird.

Die Methode getEntry() [55 - 75] prüft, ob der Cache-Eintrag existiert. Vorab werden im Namen der Cache-Datei alle Schrägstriche (/) gegen Number-Zeichen (#) ausgetauscht. Existiert die Datei, wird geprüft, ob diese neuer oder gleich alt ist, wie der als Argument übergebene Referenzzeitpunkt [63]. Ist dieses der Fall, wird der Inhalt der Cache-Datei zurückgegeben [66]. Ist dieses nicht der Fall, so wird die Cache-Datei gelöscht [71]. Dadurch wird gewährleistet, dass die Cache-Inhalte immer aktuell sind.

Die Methode setEntry() [80 - 125] stellt neue Einträge in den Cache ein. Auch hier werden im Namen der Cache-Datei alle Schrägstriche (/) ersetzt. Wenn der Dateiname der zu schreibenden Cache-Datei dem Ausschluss-Kriterium (ignorePattern) entspricht, wird die Datei direkt in den Cache geschrieben. Ist das nicht der Fall, wird die Anzahl der Cache-Einträge ermittelt [93 - 100] und geprüft, ob diese größer oder gleich der maximalen Anzahl der Cache-Einträge ist [102]. Ist das der Fall, so wird der älteste Eintrag ermittelt [106 - 112] und dieser gelöscht [114 - 121]. Abschließend wird die Datei in den Cache geschrieben.

Datei: zip_gallery_cache.php (127 Zeilen)
<?php /** * Class ZipGalleryCache * * implemantation of a simple cache. * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner * * Configuration data for cache: * [ * 'cacheRoot' => dirname(dirname(dirname(__FILE__))) . '/application/files/zip_cache', * 'cacheEntries' => 10000 * ]; * * All cache entries are kept in one folder. Cache entry file names are set up in caller. * * On running into $config['cacheEntries'] number of cache entries the oldest entry is discarded. * * Each cache entry file names consists of * - the full path to the zip file (leading '/' character stripped) * - attached the name of the file from the zip archive having all chars '/' replaces by '#'. */ class ZipGalleryCache { private $cacheFolder; private $maxEntries; private $ignorePattern = ''; public function __construct() { $config = getZipGalleryCacheConfig(); $this->cacheFolder = $config['cacheRoot']; if (!is_dir($this->cacheFolder)) { mkdir($this->cacheFolder, 0755, true); } $this->maxEntries = $config['cacheEntries']; } public function __destruct() { } public function setIgnorePattern($ignorePattern) { $this->ignorePattern = $ignorePattern; } /* * get entry from cache. * entry found but older than 'oldest' is umlinked. * * @return null or content */ public function getEntry($oldest, $cacheName) { $cacheEntry = $this->cacheFolder . '/' . str_replace('/', '#', $cacheName); $data = null; $cStat = @stat($cacheEntry); if (is_array($cStat)) { // cached file exists if ($oldest <= $cStat['mtime']) { // cached file is newer or same as $oldest $data = file_get_contents($cacheEntry); } else { // cached file is older than $oldest unlink($cacheEntry); } } return $data; } /* * set entry from cache */ public function setEntry($cacheName, $data) { $cacheEntry = $this->cacheFolder . '/' . str_replace('/', '#', $cacheName); if (preg_match($this->ignorePattern, $cacheEntry) === 0) { $dirEntries = scandir($this->cacheFolder); $entries = array(); if ($this->ignorePattern == '') { $entries = $dirEntries; } else { $idx = 0; for ($i = 0; $i < count($dirEntries); $i++) { if (preg_match($this->ignorePattern, $dirEntries[$i]) === 0) { $entries[$idx++] = $dirEntries[$i]; } } } if (count($entries) >= $this->maxEntries + 2) { // must unlink oldest // create array having entries mtime => filename $times = []; // first $entries are '.' and '..' for ($i = 2; $i < count($entries); $i++) { $times[stat($this->cacheFolder . '/' . $entries[$i])['mtime']] = $entries[$i]; } ksort($times); // first entry keeps oldest file foreach($times as $mtime => $filename) { if ($filename != $cacheName) { unlink($this->cacheFolder . '/' . $filename); break; } } } } return file_put_contents($cacheEntry, $data) !== false; } }

Anwendungen

Die allgemeine auf Swiper basierende Anwendung zielt auf einfache Webseiten ab, die ggf. auch ohne…
Wie funktioniert der Kern der ZIP Bilder-Galerie? Die Antwort liegt hier.