Core functionality

All referred files are located at GitHub (https://github.com/dausi/zipgallery).

ZIP image gallery how-to

To retrieve a single image file from the server the client (browser) loads the ZIP archive with attached image file name. Having the ZIP archive available at

http://my.site/some/path/image-gallery.zip

then the image file some-image.jpg can be retrieved from the ZIP image file utilising the URL

http://my.site/some/path/image-gallery.zip/some-image.jpg

To extract an image files from the ZIP archive, the server backend uses PHP. To enable the PHP code, an Apache-Rewrite-Regel is utilised:

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

On following these rewrite rules, the URL from above is converted (rewritten) to

http://my.site/external/zipgallery/galleries.php?zip=/some/path/image-gallery.zip&file=some-image.jpg

File galleries.php

This file represents the interface to the functionality coded in PHP classes.

Lines [8 - 14] do include the PHP files utilised for the PHP classes ZipGallery and ZipGalleryCache.

On the first readout of the ZIP archive the table of contents including all EXIF and IPTC data is extracted. A JSON file with this data is written to a cache to accelerate access to the images. The cache is defined by parameters in function getZipGalleryCacheConfig() [16 - 22].

[24] explodes the query string, [25] instantiates the ZipGallery (se file zip_gallery.php), the path name to the ZIP archive is contained in query string variable zip. Next the query string is evaluated:

  • Does the query string contain the variable info the JSON formatted info file is returned [26 - 39]. On query variable tnw or tnh set (thumbnail width / height, [28 - 35]) each image is written base64 coded into the JSON formatted info file.
  • The query string variable file qualifies the requested image file from the ZIP archive.
  • Having a variable thumb a thumbnail image is returned. The variables tnh (ThumbNailHeight) and tnw (ThumbNailWidth) do specify the thumbnail size (standard: 50 x 50) [43 -48].
  • Otherwise the requested image file is returned [51].

Only JPEG formatted image files contained in the ZIP archive are paid respect to.

File: galleries.php (55 lines)
<?php /** * galleries.php * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner (aka dausi) */ 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']); } } ?>

File zip_gallery.php

This file contains a class ZipGallery extending PHP standard class ZipArchive [9]. On creation of an instance (function __construct()) the ZIP archive is opened [45 - 54]. On opening OK the table of contents is extracted from the ZIP archive  [53 - 54] and the cache installed [55]. Cache entries names are prefixed by the ZIP archive path name [56]. To avoid deletion of any ZIP archive information files, a cache ignore pattern is defined [57].

[71 - 80] defines method getFromZip() to read an image file from the ZIP archive. Reading ZIP archive is performed in the corresponding cache class [77].

private method getFromCache() [87 - 96] reads a file from cache. To check cache expiration the modification time of the ZIP archive is supplied.

The bit more voluminous method getInfo() [100 - 181] reads a ZIP archive info file info.json from cache [106], if available. If not present the table of contents is extracted (for-loop) [111]. Image files of file type jpg or jpeg only are considered. [124] reads EXIF information from each image file including IPTC information, if present [138 - 147]. Finally [156, 157] the information is JSON formatted and written to cache.

If the ZIP archive info file info.json is available from cache and the parameter $tnSize (thumbnail size) set the JSON data is reverted to the media information array $this->media [161 - 164]. Media information is enriched by the corresponding thumbnail images [172 - 176] and converted to JSON [177] and returned.

The final method getThumb() [190- 239] creates a thumbnail image file or reads it from cache, if present. Name of a cache file is extended by thumbnail size (tnw x tnh). By this any image file from a ZIP archive can populate different dimensioned thumbnail image files in cache. The thumbnail creation algorithm generates a snippet oriented at the image center according to the thumbnail dimensions [200- 235]. A thumbnail image file is created as a jpg file and written to cache on delivery [229 - 235].

File: zip_gallery.php (240 lines)
<?php /** * Class ZipGallery * * A representation of an image gallery from a ZIP archive * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner (aka dausi) */ 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; } }

File zip_gallery_cache.php

Class ZipGalleryCache coded in this file implements a file system based cache. A database cache is implemented in the solution for concrete5.

On instantiation (function __construct()) the configuration is retrieved (function getZipCacheConfig(), see above file galleries.php)  and the cache directory created, if not present [33 - 36].

Method setIgnorePattern() [44 - 47] sets the cache ignore pattern used by method setEntry().

Method getEntry() [55 - 75] checks the presence of a cache entry. In advance all slashes (/)  in the cache file name are replaced by number signs (#). If the cache file entry exists the date is checked against the supplied expiration time [66]. On not expired the cache file content is returned [61]. On expired the cache file is unlinked [71]. Thus the cache file contents are always up-to-date.

Method setEntry() [80 - 125] writes new cache entry. Again slashes (/) in the cache file name are replaced. If the cache file name matches the cache ignore pattern (ignorePattern) the cache file is instantly written to cache. On no match the number of cache entries is calculated [93 - 100] and compared to the maximum number of cache entries allowed [102]. If the number of cache entries exceeds the oldest cache entry is determined [106 - 112] and unlinked [114 - 121]. Eventually the cache file is written.

File: zip_gallery_cache.php (127 lines)
<?php /** * Class ZipGalleryCache * * implemantation of a simple cache. * * Copyright 2016, 2017 - TDSystem Beratung & Training - Thomas Dausner (aka dausi) * * 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; } }

List of applications

ZIP image gallery how-to.
The general swiper based application is suited for basic web sites possibly not managed by a CMS.
Enhancing the common ZIP image gallery solution a version is implemented for the CMS concrete5.