General application

The general application is suited for basic web sites possibly not managed by a CMS. Example is the here explained version.

Initially the ZIP image gallery was fancybox based. The fancybox based version is still available at the Repository at GitHub but no longer enhanced or supported.

The current version utilises Swiper licensed under MIT. The example HTML page is swiper.html utilising ZIP image galleries gallery-1.zipgallery-2.zip and fat-gallery.zip:

File: swiper-excerpt.html (74 lines)
<!DOCTYPE HTML> <html lang="en"> <head> <!-- Add swiper main JS and CSS files --> <script type="text/javascript" src="swiper.3.4.2/js/swiper.jquery.js"></script> <link rel="stylesheet" type="text/css" href="swiper.3.4.2/css/swiper.min.css" media="screen" /> <!-- Add zipGallery JS and CSS files --> <script type="text/javascript" src="js/zipGallery-swiper.js"></script> <link rel="stylesheet" type="text/css" media="screen" href="css/zipGallery-swiper.css" /> <title>ZIP gallery for swiper</title> </head> <body> <div id="content"> <div id="sitetitle"> <h1>ZIP gallery for swiper</h1> </div> <div class="section"> <p> ZipGallery is based on PHP code to extract a ZIP archive containing images of a gallery in conjunction with an AJAX driven front end. The image gallery itself is based on the <a href="http://idangero.us/swiper/" target="_blank">swiper</a> jQuery plugin. </p> </div> <div class="section"> <p> Click at one of the gallery links to see how things work! </p> <ul> <li><a class="gallery" href="http://tdsystem.eu/external/zipgallery/galleries/gallery-1.zip">Test gallery one</a> try "gallery one" localised <span class="toggle">en</span></li> <li><a class="gallery" href="http://tdsystem.eu/external/zipgallery/galleries/gallery-2.zip" data-thumbsize="75x50" data-caption="{%title% }{<b class='dim'>©%copyright%</b>}">2nd test gallery</a></li> <li><a class="gallery" href="http://tdsystem.eu/external/zipgallery/galleries/fat-gallery.zip" data-caption="%localised% <b style='color: #ff8800;'>©%copyright%</b>">fat gallery</a></li> </ul> <p>Please note:</p> <ul> <li>The first link "Test gallery one" utilises standard caption and thumbnail size defined in JavaScript file <code><a href="js/zipGallery-swiper.js" target="_blank">zipGallery-swiper.js</a></code>:<br/> <code>   dfltThumbSize = [50, 50];<br/>   dfltCaption = '{%localised%&ensp;}{<b class="dim">&copy;%copyright%} - %filename%</b>'; </code> </li> <li>The second link has custom defined caption and thumbnail size:<br/> <code>   data-thumbsize="75x50"<br/>   data-caption="{%title%&ensp;}{<b class='dim'>&copy;%copyright%</b>}" </code> </li> <li>The "fat gallery" link sets a colour attribute to parts of the caption:<br/> <code>   data-caption="%localised% <b style='color: #ff8800;'>&copy;%copyright%</b>" </code> </li> </ul> <br /> <hr /> </div> <div class="section"> <a class="contact" href="mailto:info(at)tdsystem.eu">© 2016 TDSystem Beratung & Training</a> </div> </div> </body> </html>
  • The first link "Test gallery one" is based on the default settings for caption and thumbnail size from JavaScript file zipGallery-swiper.js:
      dfltCaption = '{%localised%&ensp;}{<b class="dim">&copy;%copyright%} - %filename%</b>';
      dfltThumbSize = [50, 50];

     
  • The second link is set up with distinct caption and thumbnail size:
      data-thumbsize="75x50"
      data-caption="{%title%&ensp;}{<b class='dim'>&copy;%copyright%</b>}"

     
  • The third link "fat gallery" sets  a colour attribute for some parts of the caption:
      data-caption="%localised% <b style='color: #ff8800;'>&copy;%copyright%</b>"

Thumbnail size is set up by "width x height". Quiet easy.

What's about the caption?

Let's have a look at file zipGallery-swiper.js:

File: zipGallery-swiper.js (337 lines)
/** * swiper controller jQuery library for ZIP Image Gallery * * Copyright 2017 - TDSystem Beratung & Training, Thomas Dausner */ (function($) { $(document).ready(function() { /* * test for mobile browser */ var isMobile = function() { var userAgent = navigator.userAgent || navigator.vendor || window.opera; return /android|ipad|iphone|ipod|windows phone/i.test(userAgent); }; /* * browser independent full screen toggle */ var setFullScreen = function(on) { if (isMobile()) { var doc = window.document; var docEl = doc.documentElement; var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen; var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen; if (on) { if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement && requestFullScree) requestFullScreen.call(docEl); } else if (cancelFullScreen) { cancelFullScreen.call(doc); } } }; var thumbNails = { width: 50, height: 50 }; /* * IPTC keys for image caption * * Pseudo (non IPTC) keywords: * filename index localised * * IPTC keywords: * authorByline authorTitle caption * captionWriter category cdate * city copyright country * headline OTR photoSource * source specialInstructions state * subcategories subject title * urgency * * Order and presence are defined in the 'dfltCaption' string. */ var dfltCaption = '{%localised% }{<b class="dim">©%copyright%} - %index% - %filename%</b>'; var dfltThumbSize = [ thumbNails.width, thumbNails.height ]; /* * make caption from template and iptc tags */ var get_title = function (iptc, captionTpl) { var caption = ''; var groups = captionTpl.split(/[{}]/); for (var grp = 0; grp < groups.length; grp++) { var capGrp = groups[grp]; if (capGrp !== '') { var keys = capGrp.match(/%\w+%/g); var keyFound = false; for (var j = 0; j < keys.length; j++) { var tag = keys[j].replace(/%/g, ''); var re = new RegExp(keys[j]); if (iptc[tag] !== undefined) { capGrp = capGrp.replace(re, iptc[tag]); keyFound = true; } else { capGrp = capGrp.replace(re, ''); } } if (keyFound) caption += capGrp; } } return caption; }; /* * align image and text geometry */ var thumbsBorders = 2; var thumbsHeight = thumbNails.height + thumbsBorders; var vp = {}; var alignGeometry = function(thumbsOff) { if (thumbsOff === true) { thumbsHeight = 0; $('a.download').addClass('no-thumbs'); } else if (thumbsOff === false) { thumbsHeight = thumbNails.height + thumbsBorders; $('a.download').removeClass('no-thumbs'); } vp = { fullHeight: document.documentElement.clientHeight, width: document.documentElement.clientWidth }; vp.height = vp.fullHeight - thumbsHeight; $('div.gallery-top div.swiper-slide').each(function() { var $sl = $(this); var img = { orgHeight: $sl.data('height'), orgWidth: $sl.data('width'), height: 0, width: 0 }; var ratio = img.orgWidth / img.orgHeight; var left, top; if (vp.height * ratio < vp.width) { img.width = vp.height * ratio; img.height = vp.height; left = (vp.width - img.width) / 2; top = 0; } else { img.width = vp.width; img.height = vp.width / ratio; left = 0; top = (vp.height - img.height) / 2; } $sl.css({ top: top + 'px' }); $('img', $sl).css({ height: img.height + 'px', width: img.width + 'px' }); $('div.text', $sl).css({ left: left + 'px' }); $('div.gallery-top').css({ minHeight: vp.height }); }); }; /* * set download link */ var setDownloadLink = function(slider) { var $img = $('img', slider.slides[slider.realIndex]); var url = $img.data('src') === undefined ? $img.attr('src') : $img.data('src'); var file = url.replace(/.*\//, ''); $('a.download').attr('href', url).attr('download', file); }; /* * process all links identifying ZIP gallery files */ $('a.gallery').each(function() { // // remove file extension '.zip' // var $a = $(this); var zipUrl = $a.attr('href'); var galleryName = zipUrl.replace('.zip', ''); $a.attr('href', galleryName); // // initilaisation of caption and thumbs // var captionTpl = $a.data('caption') === undefined ? dfltCaption : $a.data('caption'); var tns = dfltThumbSize; if ($a.data('thumbsize') !== undefined) { tns = $a.data('thumbsize').split('x'); } thumbNails.width = parseInt(tns[0]); thumbNails.height = parseInt(tns[1]); // // set click handler // $a.click(function(e) { e.preventDefault(); setFullScreen(true); // // get info // $('body, a').css('cursor', 'progress'); $.ajax( { type: 'GET', url: zipUrl + '/ReWriteDummy?info=true&tnw=' + thumbNails.width + '&tnh=' + thumbNails.height, dataType: 'json', success: function(info) { $('body, a').css('cursor', ''); if (info.length === 0) { alert('Die Gallerie <' + galleryName + '> enthält keine gültigen Bilder.'); } else { // // prepare swiper gallery // $('body').append('<div id="zipGallery">' + '<div class="close"></div>' + '<a class="download swiper-button-next swiper-button-white"> </a>' + '<div class="swiper-container gallery-top">' + '<div class="swiper-wrapper">' + '</div>' + '<div class="swiper-button-next swiper-button-white"></div>' + '<div class="swiper-button-prev swiper-button-white"></div>' + '</div>' + '<div class="swiper-container gallery-thumbs">' + '<div class="swiper-wrapper">' + '</div>' + '</div>' + '</div>' ); var thumbStyle = 'width: ' + thumbNails.width + 'px; height:' + thumbNails.height + 'px;'; var lang = $('html').attr('lang'); if (lang === undefined) { lang = lang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage; lang = lang.substr(0, 2); } for (var idx = 0; idx < info.length; idx++) { var comp = info[idx].exif.COMPUTED; info[idx].iptc.index = idx + 1; var localised = info[idx].iptc.title; var caption = info[idx].iptc.caption; if (caption !== undefined) { var loc = caption.toString().split(/[{}]/); if (loc.length > 0 && loc.length % 2 == 1) { for (var lidx = 0; lidx < loc.length; lidx += 2) { if (loc[lidx].trim() == lang) { localised = loc[lidx + 1].trim(); break; } } } } info[idx].iptc.localised = localised; $('.gallery-top .swiper-wrapper') .append('<div class="swiper-slide swiper-zoom-container" ' + 'data-height="' + comp.Height + '" data-width="' + comp.Width + '">' + '<img data-src="' + zipUrl + '/' + info[idx].name + '" class="swiper-lazy">' + '<div class="swiper-lazy-preloader"></div>' + '<div class="text">' + '<p>' + get_title(info[idx].iptc, captionTpl) + '</p>' + '</div>' + '</div>' ); $('.gallery-thumbs .swiper-wrapper') .append('<div class="swiper-slide" style="' + thumbStyle + ' background-image:url(data:image/jpg;base64,' + info[idx].thumbnail + ')"></div>'); } alignGeometry(false); /* * open image gallery */ var swOpts = { keyboardControl: true, mousewheelControl: true, // not with thumbnails loop: true, nextButton: '.swiper-button-next', prevButton: '.swiper-button-prev', zoom: true, zoomMax: 5, preloadImages: false, lazyLoading: true, lazyLoadingInPrevNext: true, speed: 400, spaceBetween: 10, onInit: setDownloadLink, onSlideChangeEnd: setDownloadLink, onTouchStart: function(swiper, event) { if (event.x === undefined) event = event.changedTouches[0]; swOpts.touch.x = event.screenX; swOpts.touch.y = event.screenY; }, onTouchEnd: function(swiper, event) { if (event.x === undefined) event = event.changedTouches[0]; var dx = swOpts.touch.x - event.screenX; var dy = swOpts.touch.y - event.screenY; if (swOpts.touch.y > vp.height / 2 && Math.abs(dy) / vp.height >= 0.2) { // start gesture in lower half of viewport, min 20% offset var ratio = dy / Math.abs(dx); if (ratio < -2.0) { // gesture down alignGeometry(true); } else if (ratio > 2.0) { // gesture up alignGeometry(false); } } }, touch: { x: 0, y: 0 } }; if (!isMobile()) { swOpts.lazyLoadingInPrevNextAmount = 10; swOpts.slidesPerView = 'auto'; } var galleryTop = new Swiper('.gallery-top', swOpts); var galleryThumbs = new Swiper('.gallery-thumbs', { centeredSlides: true, slidesPerView: 'auto', touchRatio: 0.8, slideToClickedSlide: true }); galleryTop.params.control = galleryThumbs; galleryThumbs.params.control = galleryTop; /* * gallery close click/escape handler */ $('#zipGallery div.close').click(function() { $('#zipGallery').remove(); setFullScreen(false); }); $(document).keydown(function(evt) { evt = evt || window.event; var isEscape = false; if ("key" in evt) { isEscape = (evt.key == "Escape" || evt.key == "Esc"); } else { isEscape = (evt.keyCode == 27); } if (isEscape) { $('#zipGallery').remove(); setFullScreen(false); } }); /* * window resize handler */ $(window).resize(alignGeometry); } }, error: function( xhr, statusText, err ) { $('body, a').css('cursor', ''); alert('Fehler beim Laden der Info aus Datei <' + zipUrl + ">\n" + statusText); } }); }); }); }); }) (window.jQuery);

Lines [40 - 52] do list the keywords being evaluated in the present JavaScript file version:

         * IPTC keys for image caption
         * 
         * Pseudo (non IPTC) keywords:
         *    filename             index                localised                
         * 
         * IPTC keywords:
         *    authorByline         authorTitle          caption
         *    captionWriter        category             cdate
         *    city                 copyright            country
         *    headline             OTR                  photoSource
         *    source               specialInstructions  state
         *    subcategories        subject              title
         *    urgency

Keywords filename, index and localised are  virtual keywords not defined in the IPTC specification:

  • filename   is the name of the file
  • index      is index (1 ... N) of file in ZIP archive
  • localised  is a web site local language controlled image title

The value for virtual keyword localised is constructed from IPTC field caption, if present. The field caption should have the format

lc { loc_message } [lc { loc_message }]*

having

  • lc           language code as of RFC5646
  • loc_message  localised message

With web site language set to for example en a caption entry

de { Das ist schön! } en { This is beautiful! } no { Det er pent! }

is evaluated to image title "This is beautiful!". In lack of such an entry in caption IPTC field or with no match to lc value the content of IPTC field title is utilised.

Other fields (keywords) are filled with data or not, depending on the image management program of your choice. For the mapping of IPTC keywords to data entry fields have a look at the documentation of your fancied image management program!

An image caption (attribute data-caption of <a> tag) is a single HTML coded line. It may contain <br> tags. Keywords are embedded following format %keyword%. Thus %filename% represents the image file name.

Captions may contain fillers. Those fillers should not display if a keyword isn't set (is empty). To suppress the fillers in that case parts of the caption can be grouped by curly braces ({}). Have a look at the default caption:

{%localised%&ensp;}

The localised image title is grouped with an n-space (&ensp;). The n-space filler is displayed if the localised image title isn't empty. In case of "Test gallery one" most images do have image titles set. The image with index 5 has no title set and the n-space hence is suppressed.

Code comments

  • Function isMobile() [11 - 14] checks presence of a mobile browser.
  • Function setFullScreen() [18 - 34] enables or disables full screen mode for mobile browsers.
  • [35 - 38] defines default thumbnail size.
  • Code for resolution of IPTC keywords is contained in function get_title()[61- 85].
  • Function alignGeometry() [89 - 141] is the core piece for setting positions and width/height of images, as the Swiper plugin is executed in a modal window. This function is called
    • next to initialisation of an image gallery [249],
    • after show or hide of thumbnails [284, 287] and
    • resulting of browser window resizing (window.resize event).
  • Function setDownloadLink() [145 - 150] sets current image download link (on mobile browsers).

Other code [154 et seq.] is executed for each ZIP image gallery link (<a class="gallery"> tag):

  • At link target (attribute href) the suffix .zip is removed [158 - 161].
  • Lines [165 - 172] do set image title and thumbnail sizes. A mobile download link is initialised.
  • A click handler is established [175 et seq.].

Click handler

Line [176] terminates execution of other click event handlers. Full s screen mode is established [177] for mobile browsers. Next to setting the cursor to "wait" (progress) [181] an asynchronous AJAX request is issued for gaining the ZIP archive file information [182- 185]. Request URL is

base-url-of-zip-archive.zip/ReWriteDummy?info=true

The request file name (ReWriteDummy) is just a dummy. The parameter info=true demands ZIP archive information.

On AJAX success [186 et seq.] the cursor is set to "normal" [187] and the retrieved information processed. On information size 0 the linked ZIP archive doesn't contain any valid image files. As of now the ZIP archive may solely contain JPEG images [188, 189].

Lines [194 - 208] do set up the modal window and Swiper frames for the image gallery including thumbnails. Thumbnail size for CSS style attributes [209] is set. Localisation is detected [210 - 214].

In a for-loop all entries are processed [215 - 245]:

  • Index value for virtual keyword index is set [217].
  • Field value for virtual keyword localised is computed from IPTC fields title and caption [218 - 231].
  • While creating HTML code for an image [232 - 241] the image geometry data is set as two data attributes (data-height and data-width), information demanded in function alignGeometry().
  • HTMl code for a thumbnail is set in [242 - 244]. Thumbnail image data is taken from JSON.

After creation of HTML code for the Swiper based image gallery and embedding into DOM the image and other position and sizes are adjusted [246]. Swiper options set in var swOpts contain selected constants (see Swiper documentation) as well as  code for recognition of up or down cursor moves [265 - 287]. These cursor moves are used to control visibility of thumbnails (move down := hide, move up := show).

Swiper initialisations take place in [294] and [295 - 300]. Both image galleries (upper and thumbnail galleries) are synchronised in [301, 302].

Eventually a click handler and an ESC handler for closing of the ZIP image gallery are established accompanied by a windows.resize handler.

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.