File: thumbspage/thumbspage.py

#!/usr/bin/env python3
"""
===========================================================================
thumbspage.py - an image-gallery builder
Version 1.6, September 26, 2018

Synopsis: Make an HTML thumbnail-links page plus HTML image-viewer 
          pages for all the images in a source folder.  The static 
          results can be viewed offline or online in any browser.

Requires: Any Python 3.X, plus the Pillow (PIL) image library 
          available at https://pypi.python.org/pypi/Pillow.

Runs on:  Any platform supporting both Python and Pillow (e.g.,
          Mac OS, Windows, and Linux).  Generated pages can be
          viewed in any desktop or mobile web browser.  

Usage:    Run thumbspage.py, input parameters in the console.
          Results are created in the images source folder.
          See also USAGE EXAMPLE and CUSTOMIZATION ahead.

Web page: https://www.learning-python.com/thumbspage.html
Examples: See learning-python.com/trnpix, and /examples here.
License:  Provided freely but with no warranties of any kind.
Author:   Copyright M. Lutz (learning-python.com), 2016-2018.

*CAUTION*: by design, this program will modify your images source 
folder in-place.  It adds an HTML index-page file (by default named
"index.html") and a subfolder with thumbnails and HTML viewer pages 
(by default named "_thumbspage"), and as preconfigured rotates any 
tilted images after saving backup copies of their originals with  
".original" extensions.  Run this program on folder copies if you
don't want it to change your valued photo collections directly.


===========================================================================
OVERVIEW
===========================================================================

Given a folder of image files, this script generates an HTML index
page with thumbnail links for each image in the folder.  This page's
links in turn open either generated HTML viewer pages with navigation
links, or the full-size images directly using browser-native display.

The net effect is intentionally static: generated results reflect 
the folder's contents at build time only, but do not require a web 
server, and can be viewed both offline and online in any desktop
or mobile browser.  As such, this script can be used both for
websites and non-web use cases such as program documentation.

When run, this script skips non-image files; uses optional header 
and footer HTML index inserts; makes an automatic bullet list for 
subfolders in the images folder (not named with a leading "_"); 
and creates the output index page in the images folder itself, along
with a subfolder for thumbnail images and viewer pages' HTML.  

After a run:
 - To view results, open the output index page created in your 
   images folder (it's named "index.html" by default).

 - To publish results, copy the entire images folder, including its 
   generated thumbs subfolder and index file (named "_thumbspage" 
   and "index.html" by default, respectively).  

 - To publish results to a remote website, upload the entire images 
   folder to the folder representing your page on your site's 
   web-server host; zip first for convenience.

As of 1.3, non-ASCII Unicode filenames and content are supported. 
As of 1.4, all output pages are more mobile-friendly.  As of 1.5,
formatted image-viewer pages with next/previous links can also be
generated; when omitted, index links open images per browsers.
As of 1.6, JavaScript is used to dynamically scale viewer-page 
images without changing aspect ratio or overflowing pages, and 
tilted images are automatically rotated to display right-side up.


===========================================================================
USAGE EXAMPLE
===========================================================================

thumbspage is run from a command line (e.g., Terminal on Mac OS and
Linux, and Command Prompt on Windows).  Its main options are chosen
with console replies or their enter-key defaults on each run:

   /.../thumbspage/_private/test16$ python3 ../../thumbspage.py 
   Clean thumbs folder [y or n] (enter=n)? y
   Thumbs per row [int] (enter=4)? 
   Thumb max size [x, y] (enter=(100, 100))? 
   Images folder path [. or dir] (enter=.)? trnpix
   Use image-viewer pages [y or n] (enter=n)? y
   Running
   Cleaning: trnpix/_thumbspage/1996-first-pybook.png
   Cleaning: trnpix/_thumbspage/1996-first-pybook.png.html
   ...
   Skipping: .DS_Store
   Making thumbnail: trnpix/_thumbspage/1996-first-pybook.png
   Making thumbnail: trnpix/_thumbspage/1998-puertorico-1.jpg
   ...
   Skipping: _cut
   Skipping: _HOW.txt
   ...
   Generating thumbnails index page
   Generating view page for: 1996-first-pybook.png
   Generating view page for: 1998-puertorico-1.jpg
   ...
   Finished: see results in images folder.

You should generally clean the thumbs folder (reply #1) unless 
images have only been added, and use viewer pages (reply #5).
Replies #2 and #3 allow you to tailor the index-page thumbs;
thumbs size should generally be square (x=y).  Reply #4 accepts 
an absolute or relative folder pathname ("." means current dir);
this is the source-image folder, where results will also appear.

See also the sections CUSTOMIZATION and USAGE NOTES that follow 
for additional configuration options and usage details.


===========================================================================
CUSTOMIZATION
===========================================================================

The most common thumbspage options are available as console inputs
on each run; see the preceding section USAGE EXAMPLE.

Additional customizations are available as Python settings in file
"user_configs.py".  See either that file or CONFIGURE, PART 1 ahead
for its options.  As examples, that file defines the names of the
generated index page and thumbs folder; as of 1.5, it configures most
colors; as of 1.6, it allows images to expand beyond actual sizes
and allows users to control auto-rotation of images.

For more custom behavior, add unique HTML code to the top and bottom
of the index page by placing it in files in the images folder named
"HEADER.html" and "FOOTER.html", respectively.  Both are optional;
if these files are not present, generic HTML and text is generated 
in the index page around the thumbs table.  For details on how to 
code these files, see the examples in examples/ here.  In brief:

HEADER.html 
   should be a full HTML preamble (with <!doctype>, <html>, the 
   <head> with mobile viewport meta, content type/encoding meta, 
   <style> if used, and <title>), followed by the start of <body> 

FOOTER.html 
   should add any post-table content and close both <body> and <html>

The HEADER.html file also allows index-page fonts to be tailored 
with CSS code; see the docstring in "user_configs.py," as well as
the online demo site learning-python.com/site-mobile-screenshots/.

As of 1.6, viewer pages can also be changed arbitrarily by editing
the template file "template-viewpage.html" in this script's folder.
For example, such edits might add site icons or navigation widgets. 
Edit with care and rerun this script to use your customized template.


===========================================================================
USAGE NOTES
===========================================================================

Viewer pages:
   The image-viewer pages added in version 1.5 are optional.  If subpar
   in your use case, they can be disabled via the last console reply 
   (see USAGE EXAMPLE above) to use the former browser-native display.
   As of 1.6, though, these pages scale displayed images well everywhere;
   users may also click these pages' "Raw" links for browser-native view.

Image filenames:
   Although thumbspage properly escapes and handles arbitrary image  
   filenames, those in your galleries should generally avoid characters
   illegal on some filesystems (e.g., "), especially if they are to be 
   viewed on multiple devices.  Also note that because filenames used on 
   labels are not line-wrapped, their width largely determines column 
   spacing on the index page; use shorter filenames for narrower columns.

Image-folder contents:
   Images in the source folder are detected by MIME type (i.e., filename
   extension).  All non-image items there are simply ignored, so it's 
   safe to include arbitrary files and folders.

Result URLs:
   When referencing this program's results in hyperlink URLs, note that 
   links to generated folders of forms ".../folder" and ".../folder/" 
   work only when a web server is present.  Use "...folder/index.html" 
   to also (or only) view results offline.

Tilted images:
   In version 1.6, any images that are "tilted" (stored by cameras and 
   smartphones with orientation tags, and content shifted to a side) are 
   by default automatically rotated to display right-side up - both the
   image itself and its thumbnail.  Because this changes the source 
   image file, the original image is by default saved with a ".original"
   filename extension in the source folder.  See user_configs.py for
   settings that allow users to disable this feature and its backups,
   and 1.6 release notes in VERSION HISTORY below for more details.  

Cleaning the thumbs folder:
   thumbspage always regenerates viewer pages for each image on each 
   run, so they correctly reflect your image set.  For speed, though, 
   image thumbnails created in prior runs are reused if present in the 
   thumbs subfolder: new versions are not created, even if images have 
   changed.  Hence, you should always select the "Clean thumbs folder" 
   console option (see USAGE EXAMPLE above) to force regeneration of 
   thumbnails if any images have been changed or removed.  

   Conversely, folder cleaning is _not_ required if images are only 
   added to the images folder - thumbs will be made for new images 
   only, and thumbs for previously added images will be retained.
   This can save time when extending large image folders.  But when 
   in doubt, clean; this script's work is done once, before views. 

Thumbs-folder name:
   The folder used to store generated thumbnails and viewer pages is 
   named "_thumbspage" by default, in user_configs.py.  It is _not_
   named with a leading "." to make it a Unix hidden file, because
   hidden files are rude (you should really see what programs do to
   your computer), and may be skipped by some compression and backup 
   programs (e.g., see ziptools and Mergeall at learning-python.com).
   The name can be freely changed if "." tradeoffs are acceptable.


===========================================================================
GUI MODE
===========================================================================

You can also view the thumbnails and images in GUI mode (i.e., 
without a web browser) by running the PP4E book example included 
and used here on Windows (any version) or Unix (Mac OS and Linux):

   c:\...\thumbspage> py -3.3 viewer_thumbs.py examples\test\images
   /.../thumbspage$ python3 viewer_thumbs.py examples/test/images

This is, however, a _very_ basic and limited viewer GUI.  The book's 
later PyPhoto example is a much more useful GUI, with navigation, 
image scaling, and scrollbars for both indexes and images.

   Update: a much-upgraded PyPhoto is now available standalone, at
   http://learning-python.com/pygadgets.html (it's a bundled gadget).

Also note that thumbspage itself can serve as an image viewer too.
Because its results can be viewed both online and offline in any 
desktop or mobile browser, they can be used for same-device viewing.


===========================================================================
VERSION HISTORY
===========================================================================

This section documents version changes, but also provides assorted
usage notes and context along the way.

---------------------------------------------------------------------------
1.6, Sep-26-2018: scale images with JavaScript, auto-rotate tilted images
---------------------------------------------------------------------------

This version was released multiple times with new features in each 
release; release dates identify point releases.  Its main extensions
were dynamic image scaling and image auto-rotation.

Dynamic image scaling:

   Despite the prior version's hand-waving, thumbspage's image-viewer
   pages now use JavaScript in their generated HTML to dynamically 
   scale images to available display size, while preserving original 
   aspect ratio and avoiding content screen overflow.  The former CSS
   scaling scheme is still used, but only for no-JavaScript views and
   iOS landscape mode.

   On desktop browsers:
      This is a complete solution.  It emulates browser-native scaling 
      well both initially and when windows are arbitrarily resized, on 
      all desktop browsers and platforms tested - including Chrome, 
      Firefox, Safari, Edge, and IE 9 and 11, on Mac OS, Windows, and 
      Linux.  Users may freely resize their window to change the size 
      of the image, but need no longer resize to view it in full. 

   On mobile browsers:
      This is an improvement.  Images are now scaled to available space
      well on all mobile browsers and platforms tested - including Chrome
      and Firefox on Android, and Chrome, Safari, and Firefox on iOS.  
      Taller images, for example, may now occupy more available space.

      Exception: on iOS only, images are always fully scaled in portrait 
      device orientation, but for implementation reasons fall back on 
      version 1.5's CSS scrollable scaling result in landscape device 
      mode.  Android fully scales images always, which yields smaller 
      landscape images (arguably better, but open to feedback).

   Version 1.5 called JavaScript a "nonstarter" partly because of the
   added complexity, but mostly because locking out users who don't 
   wish to run JavaScript is a rude non-option.  Here, though, pages 
   still work when JavaScript is disabled - they use the former 1.5 
   CSS scaling, with a note recommending JavaScript results.  Turn 
   off JS in your browser to see how 1.5 CSS scaling compares.

Image auto-rotation:

   thumbspage now automatically reorients (rotates) tilted source
   images and their generated thumbnails to be right-side up (top 
   edge on top), if they have valid "Orientation" Exif tags.  This
   is really just an automatic alternative to manually rotating 
   tilted images before making thumbs, but is especially useful 
   for photos shot on smartphones that commonly tilt photos shot 
   in the natural portrait (vertical) device orientation.

   Less positively, this feature applies only to images with Exif
   tags (JPEG and TIFF) from cameras and tools that tag as expected. 
   More intrusively, this feature must rotate source-image files too 
   (not just their thumbnails), because not all viewers will rotate 
   images when opened from thumbnails.  In thumbspage specifically,
   web browsers will not rotate tilted source images (except on "Raw" 
   clicks), because its viewer pages display source images as in-page
   elements.  The related PyPhoto program may eventually adjust for 
   orientation in memory and not require image copies, but uses 
   forked thumbnail-generation code (learning-python.com/pygadgets).

   Because this feature modifies source images, it by default saves
   them to backup copies with a ".original" extension before making
   changes.  Users may also control the feature with two new settings 
   in user_configs.py: "autoRotateImages" can turn the feature off, 
   and "backupRotatedImages" can skip ".original" backups.  Disable
   rotation if needed or desired, and manually rotate tilted images 
   before running thumbspage as preferred.

   See examples/reorientation for a test case's results; that 
   folder's restore-originals.py utility to restore from backups;
   and module viewer_thumbs.py for implementation code (rotation 
   is neither automatic nor easy option for Pillow/PIL thumbnails).
   Caveat: rotated source images drop the originals' Exif tags;
   but they are not the same, and are viewed in HTML pages only.

Version 1.6 also:

   1) Moves config options and viewer-page HTML to separate files 
      for easier viewing and edits.  See CUSTOMIZATION above.

   2) Uses "_thumbspage" as the default name of its thumbnails +
      viewer-pages subfolder, to avoid clashing with other content.
      The former "thumbs" default may still appear in some docs.
      PRIOR-VERSION USERS: your "thumbs" will be unused; manually
      delete or rename, or set THUMBS='thumbs' in user_configs.py.

   3) Expands image-folder name "." to its true basename in generated
      default-header text, and appends a "/" to subfolder hyperlinks 
      so they will not trigger redirects or clash with files if/where 
      it matters.  Because the "." fix uses os.path.abspath() to expand
      the dot, it also properly names image-folder paths with any "." 
      or ".." (e.g., "..", "../..", "../Desktop/trnpix/../trnpix/.").

   4) Adds an "expandSmallImages" option in user_configs.py.  If 
      False, the maximum viewer-page scale ratio is 1.0, which 
      constrains images to an actual-size maximum, and thereby
      avoids stretching and possibly blurring small images.  If 
      True, smaller images are always expanded to display size. 

      The default is False, because this is generally better when 
      smaller images are present (e.g., icons, window screenshots).
      Use True for the prior expanding behavior which may be
      preferred in some contexts.  Note that this setting applies 
      only to small images; most digital photos and scans are far 
      larger than display areas, and will only be scaled down.

   5) Uses JavaScript viewer-page code to avoid adding viewer pages
      to browser history where supported, so that N image views 
      don't require N browser Back clicks to get back to pre-gallery
      context.  This works everywhere BUT Chrome on iOS, where the
      feature is disabled (i.e., viewer pages are stacked on history
      and retraced by Back clicks for this browser only).  The JS 
      template file hosts most of the implementation's code.

      This feature works and is used in Chrome on Android+Windows+Mac;
      in Safari+Firefox on iOS; and in all other 20+ browsers tested 
      (including IE 9, and other Android).  The failure in Chrome on 
      iOS is clearly a bug in its location.replace() of versions 64 
      and 68 tested; if used, Back clicks redisplay the same page N 
      times for N image views, which is worse than stacking pages. 

      Because this might be fixed in the future, user_configs.py's 
      "chromeiOSBackFixed" controls the iOS Chrome disabling.  At 
      present, though, Chrome on iOS is just 0.40% of this site's 
      audience (just 10.96% of iOS, which is itself just 3.67% here); 
      it makes no sense to omit an enhancement for 99.6% of users, 
      for the sake of just 0.40%.  For more of this site's analytics:  
      learning-python.com/site-traffic-half1-2018.txt.
  
   6) Works around a Pillow (PIL) library bug that could occur only
      in limited usage contexts for folders having very many images.  
      In brief, Pillow's auto-close of loaded image files does not 
      work as documented, which can lead to "Too many open files" 
      errors after many thumbnails have been generated.  Here, 
      this meant that results could reflect partial source content  
      for very large folders, though only on Mac OS in general.

      The best fix is to manually open and close image files, 
      instead of passing filenames to Pillow.  With this change, 
      arbitrarily large folders are supported in all contexts. 
      For more details on both the bug and its workaround, see 
      module viewer_thumbs.py here, where the fix is coded.

   7) Works around a Chrome issue on Windows and Linux, by using
      "auto-scroll: hidden" CSS for the body, to forcibly hide 
      the vertical scrollbar.  Else, Chrome (and possibly older
      Firefox) flash a scrollbar momentarily during viewer-page
      loads.  This setting doesn't impact displays in any other 
      way, and scrollbars are never required on viewer pages.
 
      Caveat: Android Chrome (only!) may sometimes briefly flash 
      a vertical scrollbar anyhow (or is it an indicator?).  This
      seems a browser bug; applying hidden to 'html' doesn't help.

   8) Works around a Chrome desktop peculiarity, by using "thin"
      instead of "1px" for CSS <img> border-width in both index 
      and viewer pages.  "1px" can cause some borders to not be 
      drawn at zoom levels < 100% due to fractional pixel math.  
      "thin" is equivalent to "1px" in size everywhere today (not
      "2px", as once rumored on the web), but does not suffer from 
      pixel-math cloaking, and doesn't impact displays otherwise. 

      This works through Chrome desktop zoom level 50%; below that 
      Chrome (only) may drop viewer-page bottom borders for some 
      window sizes, but that's a reasonable usage cutoff.  Using a
      "1.5px" almost works, but can add empty space between border 
      and content.  To test: border-width at developer.mozilla.org.
      Image border color was also made configurable for both page 
      types; set border color to background color to omit borders.
      Note: index-table top/bottom borders work unchanged as "1px".
         
---------------------------------------------------------------------------
1.5, Aug-12-2018: add formatted image-viewer pages
---------------------------------------------------------------------------

This version's main feature was the introduction of image viewer pages,
which were further improved in version 1.6 (see above).

Image viewer pages:
 
   In addition to thumbnail-index pages, thumbspage now generates
   a styled viewer page for each image in the folder, instead of 
   relying on each browser's native image display.

   Viewer pages are opened on index-page thumbnail clicks, and have
   filename, the image scaled per CSS, view-native and go-to-index 
   links, and previous/next-image links that cycle through all images
   without having to return to the index page.  Their images are 
   centered horizontally, but not vertically; the latter is too 
   jarring during next/previous navigation. 

   Viewer pages are generated in the thumbs folder, along with the 
   prior version's thumbnail-index image files.  They can also be
   suppressed via console input prompts; when omitted, images open 
   in browsers directly and natively, as before.  An example client:
   http://learning-python.com/trnpix/.

   Caveat: because image scaling is weak in CSS (and JavaScript is 
   overkill here), 1.5 image-viewer pages should be considered an 
   optional feature.  See "1.5 USAGE NOTE" above for deployment 
   suggestions, and the code's notes below for more details.  

   [Update: 1.6 added JavaScript dynamic image scaling for viewer 
   pages, and deleted the now-moot "1.5 USAGE NOTE" referenced above.]

Version 1.5 also:

   1) Formalizes index/navigation ordering.  It's now case sensitive
      by default everywhere, but can be changed in user_configs.py
      if case-neutral (Windows-like) ordering is preferred.

   2) Formalizes URL-escapes Unicode encoding.  It's now always 
      UTF-8 regardless of the encoding used for whole HTML pages,
      because this seems to be required by standards and browsers.
      See url_escape() in code below for more details.

   3) Sets body font to Arial (sans serif) for default-header index 
      pages (which looks nicer and matches new viewer pages' font,
      but is not inlined so it can differ in a custom HEADER.html).

   4) Works around a desktop Chrome <hr> bug (see "CAVEATS" #6 
      ahead), by restyling the thumbs table with table borders 
      instead of <hr>s.  As a consequence, the thumbs table now
      always stretches to 100% window width, to extend the border
      lines (this was formerly a configuration, off by default).

   5) Sets thumbs-table background color to light grey as part of 
      <hr> restyling, and allows it to be changed in user_configs.py.
      1.5 viewer page colors can be similarly tailored in that file.

   6) Refactors its code to functions (it's now large enough 
      that top-level code is difficult to navigate), and cleans
      up its page output (HTML/CSS is tough to read as it is).

   7) Still uses all-inline styles for index pages, not <style> 
      blocks, so that custom HEADER.html files need not include 
      or link to styles for the generated table.  Viewer pages 
      use a <style> block, as they are not customizable (yet?).

---------------------------------------------------------------------------
1.4, Mar-4-2018: restyle for mobile and improved table display
---------------------------------------------------------------------------

This script's output page now better supports browsers on smaller 
screens, and looks nicer in general.  Its new generated CSS code:

   1) Autoscrolls the thumbs table to appease mobile browsers

   2) Adds padding to thumb cells to reduce run-together

   3) Center-aligns thumbs images for a more even look; this 
      helps overall, but especially for narrow/portrait images 

   4) Uses nowrap paragraphs for image labels; the former <br>+wrap
      scheme looked jumbled, and made Chrome (only!) arrange thumb 
      table columns unevenly in small windows (left was narrower)

This version also adds a mobile-friendly viewport <meta> tag to 
default headers if "useViewPort".  This may impact other page 
components; use a custom HEADER.html for more options and control.  

TIP: because filenames used on labels are now not wrapped, their 
width largely determines column spacing in the index page; use 
shorter filenames for less space between (narrower) columns.

---------------------------------------------------------------------------
1.3, Aug-8-2016: support non-ASCII Unicode filenames and content
---------------------------------------------------------------------------

This version makes several changes to better accommodate arbitrary
Unicode content:

   1) HTML-escape all added text - image, folder, subfolder names.

   2) URL-escape all added links - to thumbs, images, subfolders.

   3) Output the index file in UTF-8 Unicode encoding by default,
      with a content-type <meta> tag.  ASCII content is unchanged,
      as it is a subset of UTF-8.  Other encodings may be used for
      the output file via setting "outputEncoding" in user_configs.py.

   4) Load any header and footer inserts per UTF-8 Unicode encoding
      by default, as it is general and supports ASCII directly.
      Other encodings, including the platform's default, may be
      used for inserts via setting "insertEncoding" in user_configs.py

   5) Assume any inserts are both HTML-safe and UTF-8-compatible.

See examples\test\escapes\images for a Unicode-content test case.
[Update: the Unicode test's folder is now examples/unicode/images
(and thumbspage development has switched from Windows to Mac OS).]
     
---------------------------------------------------------------------------
1.2, Aug-1-2016: add table styling options
---------------------------------------------------------------------------

This version adds assorted cosmetic tweaks, subject to settings in 
file user_configs.py:

   1) Uniform-width columns, not per content (_on_ by default).

   2) Stretch thumbs table to fill whole window (_off_ by default).
      [Update: this is now always _on_ for 1.5 table borders (<hr> fix).]
 
   3) Scrollbar if window too small? (skipped in 1.2, added in 1.4).
      [Update: this was obsoleted by 1.4's auto-scrolls.]

---------------------------------------------------------------------------
1.1, Jul-27-2016: add automatic subfolders links
---------------------------------------------------------------------------

This version adds an automatic subfolder-links list, if enabled by 
setting "listSubfolders" in user_configs.py.  This is _on_ by default, 
and skips folders prefixed with a "_", as well as the generated 
thumbs folder (whether it's named with a "_" prefix or not).

---------------------------------------------------------------------------
1.0, Jul-24-2016: initial release  
---------------------------------------------------------------------------

A basic thumbnails-index page, with browser-native views.


===========================================================================
DEVELOPER NOTES
===========================================================================

This section provides additional details and musings mostly of 
interest to programmers, not users (but Android users: see #C).
Items are prefixed by the version relevant or current when added.

1) (all) SCOPE: Apart from header/footer inserts, this script makes
   fairly simplistic image and subfolder links only; edit either
   the output HTML page or the code here that creates it as desired.
   [Update: this program has grown less simple but more useful, with
   version 1.4 styling, 1.5 image-viewer pages, and 1.6 image scaling.]

2) (1.1) SUBFOLDERS: Makes a bullet list for any subfolders in the
   images folder (else they cannot be navigated to), but skips all
   other non-image content; put any doctype, readme, font, CSS code,
   etc., in header or footer.  Run on each image tree subfolder manually;
   automating this and other content seems too clever and complex.

3) (1.2) STYLING: Stretching the thumbs table to fill the whole window
   is off by default, as it can appear too spread out for short names in
   large windows.  Enable this via the user_configs.py setting if desired.
   [Update: as of 1.5, stretching is always on, for new table borders.]

4) (1.2) STYLING: A table overflow scrollbar was skipped, as it
   appears only at table bottom, which can be very far away on large
   pages.  Instead, use the browser's automatic general window scroll.
   [Update: changed in 1.4 - now uses CSS autoscroll for thumbs table,
   which is more mobile-friendly, and shows the scrollbar iff needed.]

5) (1.3) UNICODE: The script now fully supports images with non-ASCII
   names, but getting them onto your web server may take special steps.
   On one Linux server, an "unzip" of the WinZip zip package failed on the
   Unicode test images, but GoDaddy's browser-based file upload worked
   (and in later use, Mac OS and cPanel uploads both handle Unicode well).
   See examples/unicode/_README.txt for more on uploading result files.
   See also learning-python.com/ziptools for a Python zip alternative.

6) (1.3) UNICODE: the insertEncoding setting now defaults to UTF-8,
   as this is a general scheme which also works for ASCII files.  To use
   your platform's default encoding instead, set this variable to None;
   this is not required if header/footer inserts are saved as UTF-8.

7) (1.3) UNICODE: If (but only if) file or folder names are non-ASCII,
   custom HEADER.html files must include a <meta> content-type tag with
   a charset matching the outputEncoding (which is UTF-8 by default).
   Default headers include this tag automatically (see code ahead).

8) (all) CSS: Note that index-page styling must be all inline, to 
   avoid asking custom HEADER.html files to code or link to this CSS code.

9) (all) RELATED: See also pixindex, a related tool that adds images
   to a zip file and FTPs them to a sever: learning-python.com/pixindex.

A) (all) DESIGN: As noted in OVERVIEW, this script's results are 
   static.  Although adapting it to produce results dynamically on a 
   server would support changes in server-folder content, this would 
   require a web server, rendering results unusable offline (e.g., in 
   program docs).  Generating results dynamically on a server would 
   also require redesign of thumbnail generation to avoid long delays.

B) (1.6) LOADING INDICATOR: viewer page images are hidden by 
   JavaScript until loaded, to avoid an annoying 'flash' in some
   contexts.  If JavaScript is enabled, a "Loading..." paragraph 
   is also posted temporarily (else the screen is blank on loads).
   Index pages load thumbnails quickly, and require no indicator. 

C) (1.6) ANDROID "RAW" AND USER SETTINGS: the native image display
   on some Android Chromes may initially render some images oddly and 
   too large in portrait orientation, but if and only if the seemingly 
   unrelated "Force enable zoom" is selected in Accessibility Settings. 

   The "Raw" links of viewer pages trigger this, but it is not caused by 
   this program - it also happens when visiting images' URLs directly,
   completely outside the script's pages.  It's also limited - it occurs
   for some images only, and never when the Setting is cleared.  This is
   sadly typical of browsers' fragility.  Indeed, at times it seems a 
   website could be laid low by a good sneeze...

   Users may be best served by clearing this setting in this use case.
   For more clues, try a search on "android chrome force enable zoom 
   displays images too large", or this possibly related bug report:
   see https://bugs.chromium.org/p/chromium/issues/detail?id=464295.


===========================================================================
CAVEATS AND TBDS
===========================================================================

This section lists recently closed and current issues, some of 
which are open to user feedback (via lutz@learning-python.com).
This list may not be complete; search for "caveat" in this and
other code files here for additional items, and see section 
"VERSION HISTORY" above for more on version changes noted here.

--CLOSED--

1) Python 3.X assumes local platform's Unicode encoding default
   for header/footer inserts and the output page: change as needed.

   ==> Addressed and resolved in 1.3, per version notes above.

2) This script would probably run on Python 2.X too if the
   viewer_thumbs module didn't import "tkinter" (a book legacy).

   ==> But no longer as is, as of 1.3's Unicode enhancements: 
       punt on trying to maintain 1.2 compatibility.

3) Styling of the table is open ended.  For example, a <table>
   style "border-spacing: 4px;" may help avoid names running into
   each other, though the default (2px?) seems adequate.

   ==> Addressed in version 1.4 with extra CSS styling.

4) The reused viewer_thumbs module skips non-image files by simply
   catching errors; this could use Python's mimetypes module instead.

   ==> See PyPhoto's newer version of this module, which uses 
       mimetypes as described; alas, it's a fork.  Here's the 
       newer code's online link:
          http://learning-python.com/
          pygadgets-products/unzipped/_PyPhoto/PIL/viewer_thumbs.py

   ==> Addressed in version 1.5: incorporated the PyPhoto mods here,
       though PyPhoto's viewer_thumbs may still vary arbitrarily.

5) Relying on browser-specific display of full images isn't ideal,
   because such displays vary in scaling, background color, etc.

   ==> Addressed by version 1.5's image-viewer pages (see above).

6) Desktop Chrome (only) botches <hr>s and <img> borders at some zoom 
   levels: the former may be lighter at some page locations than 
   others, and the latter may drop one or more sides altogether.  

   This was first seen when odd Unicode characters were on-page, but 
   can happen anywhere - at user-zoom levels at or below 90%, all bets 
   are off on <hr>- and <img>-border rendering.  It also happens on both
   index pages and 1.5 viewer pages; the latter's CSS <img> borders may
   drop one or more sides at <= 90% zoom.

   This has been observed on desktop Chrome only, and in versions 66 
   and 68 on Mac OS and Windows.  The only fix seems to be to not use
   <hr> or <img> borders - an extreme workaround.  Styling <hr>s (e.g., 
   style="height: 0.5px; background-color: black;") doesn't fix the 
   issue at all zoom levels.  This is clearly a (temporary?) browser 
   bug, but unfortunate in this site's most widely used browser.

   ==> Addressed <hr>s in 1.5 by replacing them with table borders.
       Chrome borders around <img>s still vanish at zoom <= 90%,
       but they are nice enough to keep, even if only 3 sided.

   ==> Addressed <img> in 1.6 by using "thin" instead of "1px" for 
       all CSS image border widths.  This fixes <img> borders on both 
       index and viewer pages.  Per above, index-page <hr>s became 
       table borders in 1.5, for which "1px" works correctly.  

7) Viewer-page image scaling in CSS is lousy - there's no way to 
   adjust to changes in window aspect ratio well.  For more details, 
   see [defunct] "USAGE NOTE" above, and code notes below.  JavaScript
   may help, but using Python to generate HTML that embeds both CSS 
   and JavaScript seems a bit much for a simple Python image viewer...

   ==> Addressed by 1.6's JavaScript-based dynamic image scaling.  
       This makes both this script and its output a bit of a tangled
       mess, but such is life in web development today.

--OPEN--

1) A ".." parent link is not generated in automatic subfolder
   lists; should it be (the parent may or may not have images)?

2) Creating the index file in the image folder might preclude
   use of some page-generation tools (see trnpix template copies).

3) Assuming UTF-8 for all insert files' Unicode encoding is simple
   (and also works for ASCII, as it's a UTF-8 subset), but requires
   either a minor script change or file conversions for differing
   platform defaults; is this a significant usage factor?

4) This could generate a doctype and meta content-type tag in all
   index cases - not just for default headers - but that would limit
   doctype and Unicode options in custom headers.

5) Even after refactoring to functions in 1.5, there are still 
   a lot of globals in this code; clean up more in a future release?

6) Which is better for 1.6 images scaling on mobile devices in 
   landscape orientation: iOS scrollable or Android shrunken?

7) Chrome on iOS has a location.replace() bug that required disabling
   a Back feature, per version 1.6 notes above; watch for a browser fix.

8) Colors on both page types (and more) can now be customized in 
   user_configs.py, and custom index-page fonts can be had via CSS in  
   a HEADER.html (e.g., "td {font-family: times; font-style: italic;}"),
   but customization is open ended - and pending usage feedback.  E.g., 
   default-header title and text could be configs, but vary per folder.

9) The number of columns on the index page might be dynamically 
   configured in JavaScript by viewport size (like images), but this
   may be complex (font size would matter) and could be too jarring.

A) This script adds 2 files for every 1 source image (thumbnail
   and viewer page).  That's negligible in most use cases, but may 
   become significant in archives with very many files (10k images 
   means 20k extra files).  This might be addressed by using a single
   thumbs file instead of a folder (see PyPhoto's pickle file), but 
   that would be complex and slow compared to the direct page-to-page
   and image links generated here.  It may also require cross-page
   state, local storage access, or a server, and break cross-platform 
   or offline use.  As is, users can zip thumbs folders where needed.

===========================================================================
"""

#
# Library tools
#

import os, sys, glob, re                    # [1.5] re for input tests
import html, cgi, urllib.parse              # [1.3] text escapes

if sys.version[0] == 2: input = raw_input   # 2.X compatibility (now unused!)

from viewer_thumbs import makeThumbs        # courtesy of the book PP4E
from viewer_thumbs import isImageFileName   # courtesy of standalone PyPhoto
from viewer_thumbs import imageWideHigh     # courtesy of Pillow/PIL


def html_escape(text, **options):
    """
    -----------------------------------------------------------------------
    HTML escapes - for text inserted in HTML code
 
    [1.5] Both the HTML and CGI escaping functions take an additional
    'quote' argument which defaults to True for html.escape, but False 
    for cgi.escape; a True is needed iff the result is embedded in a 
    quoted HTML attribute (e.g., <tag attr="%s">, but not <tag>%s</tag>).

    [1.3] cgi.escape is subsumed by html.escape which was new in 3.2.
    Both escape HTML-syntax text: 'good>&day' => 'good&gt;&amp;day'.
    -----------------------------------------------------------------------
    """
    escaper = html.escape if hasattr(html, 'escape') else cgi.escape
    return escaper(text, **options)


def url_escape(link):
    """
    -----------------------------------------------------------------------
    URL escapes - for the text of inserted links
 
    [1.5] Always use UTF-8 here, not outputEncoding, per the following.
 
    [1.3]: The 'encoding' here is used only to preencode to bytes before 
    applying escape replacements.  The returned URL is an ASCII str string 
    with '%xx' escapes; it's a URL-escaped format of Unicode-encoded text. 

    How the resulting URL link is interpreted depends on the agent that 
    unescapes it later, but general UTF-8 handles encoding of arbitrary 
    content, and its unescaped (but still encoded) bytes are recognized 
    everywhere that this script's results have been tested. 

    Subtly, the encoding used for the whole enclosing HTML page's content 
    and declared in its <meta> tag (this script's 'outputEncoding') has 
    nothing to do with the encoding used for embedded and escaped URL links
    (e.g., the HTML/URL encodings pair UTF-16/UTF-16 fails in browsers 
    tested, but UTF-16/UTF-8 works correctly).

    In fact, UTF-8 appears to be required for URLs per standards docs,
    which makes urllib's alternative encoding option seem a bit dubious:
        https://tools.ietf.org/html/rfc3986#section-2.5 (older)
        https://tools.ietf.org/html/rfc3987#section-6.4 (newer)

    Tool examples:
    >>> ord('☞'), hex(ord('☞'))                   # code points
    (9758, '0x261e')
    >>> '☞'.encode('utf8'), '☞'.encode('utf16')   # encoded bytes
    (b'\xe2\x98\x9e', b'\xff\xfe\x1e&')

    >>> from urllib.parse import quote            
    >>> quote('http://a+b&c☞', encoding='utf8')   # encode + escape
    'http%3A//a%2Bb%26c%E2%98%9E'
    >>> quote('http://a+b&c☞', encoding='utf16')
    '%FF%FEh%00t%00t%00p%00%3A%00/%00/%00a%00%2B%00b%00%26%00c%00%1E%26'

    Other ideas: it's possible to skip urllib's Unicode encoding step by 
    calling its quote_from_bytes(), but this just passes the buck - it 
    requires a manually encoded bytes.  URLs might also be embedded in 
    HTML pages using the whole-page Unicode encoding, with only HTML 
    escapes, or with no escapes (e.g., url_escape = lambda link: link,
    or url_escape = lambda link: html_escape(link, quote=True)); this 
    almost works for UTF-16, but some pathological filename links fail.
    -----------------------------------------------------------------------
    """
    return urllib.parse.quote(link, encoding='UTF-8')



#==========================================================================
# CONFIGURE, PART 1: manual settings for rarely changed options
#==========================================================================

# Now a separate module for easier access [1.6]

from user_configs import (
    THUMBS,              # built subfolder name (thumbs+viewers)
    INDEX,               # built index-page name ('default'?, 'home'?)

    listSubfolders,      # auto folder list? (or via header)
    uniformColumns,      # same-width columns? (else content)
    #spanFullWindow,     # stretch table to window's width? (now always [1.5])

    useViewPort,         # add mobile-friendly viewport? [1.4]
    caseSensOrder,       # index/nav order case sensitive? [1.5]

    thumbsBgColor,       # thumbs page table background color (was 'white') [1.5]
    thumbsFgColor,       # thumbs page table foreground color (text) [1.6]
    thumbsBorderColor,   # index-page thumbnail border color [1.6]

    viewerBgColor,       # viewer pages background color [1.5]
    viewerFgColor,       # viewer pages foreground color (text) [1.6]
    viewerJSColor,       # no-JavaScript note text color [1.6]
    viewerBorderColor,   # viewer-page image border color (=Fg?) [1.6]

    expandSmallImages,   # stretch beyond actual size on viewer pages? [1.6]

    insertEncoding,      # Unicode: header/footer loads
    outputEncoding,      # all generated pages 
    templateEncoding,    # viewer template load [1.6]

    chromeiOSBackFixed,  # stop disabling viewer-page history destacking? [1.6]

    autoRotateImages,    # rotate images+thumbnails to display right-side up? [1.6]
    backupRotatedImages  # copy rotated source images to ".original" backups? [1.6]
)


#==========================================================================
# CONFIGURE, PART 2: console inputs for per-run options, enter=default
#==========================================================================

# Tools [1.5]

def ask(prompt, hint, default):
    return input('%s [%s] (enter=%s)? ' % (prompt, hint, default))

def askany(prompt, hint, default):
    return ask(prompt, hint, default) or default    # [1.6]

def askbool(prompt):
    return ask(prompt, 'y or n', 'n').lower() in ['y', 'yes']

def askint(prompt, hint, default):
    reply = ask(prompt, hint, default)
    return int(reply) if reply else default

def askeval(prompt, hint, default, require=None):
    reply = ask(prompt, hint, default)
    if reply and require: 
        # else eval() mildly dangerous [1.5]
        assert re.match(require, reply), 'Invalid input "%s"' % reply
    return eval(reply) if reply else default


# Inputs

# y or n => remove any exiting thumb files?
cleanFirst = askbool('Clean thumbs folder')

# int => fixed row size, irrespective of window
thumbsPerRow = askint('Thumbs per row', 'int', 4)    # 5->4 so less scrolling [1.4]

# (int, int) => max (x, y) pixels limit, preserving original aspect ratio
require = '\(?[0-9]+\s*,\s*[0-9]+\)?'   # 2-tuple, parens optional
thumbMaxSize = askeval('Thumb max size', 'x, y', (100, 100), require)

# str => images folder path: images, header/footer, output
imageDir = askany('Images folder path', '. or dir', '.')    # default = cwd

# y or n => create image viewer pages? [1.5]
useViewerPages = askbool('Use image-viewer pages') 


# Calcs

# don't make a thumbs folder if input dir bad [1.5]
assert os.path.isdir(imageDir), 'Invalid image folder'

# the output page created in images folder
indexPath = os.path.join(imageDir, INDEX + '.html')

# optional inserts in images folder, else generic text
headerPath = os.path.join(imageDir, 'HEADER.html')    
footerPath = os.path.join(imageDir, 'FOOTER.html')



#==========================================================================
# MAKE THUMBNAILS in image-folder subdir (via viewer_thumbs)
#==========================================================================

def makeThumbnails(imageDir):
    """
    -----------------------------------------------------------
    Reuse the (now modified) thumbnail generator from PP4E.
    Its [(imgname, thumbobj)] return value is unused here: 
    os.listdir() is run later to collect thumb names, and
    thumb objects are not used (building pages, not GUIs).
    -----------------------------------------------------------
    """
    if cleanFirst:
        # this cleans viewer pages too [1.5]
        for thumbpath in glob.glob(os.path.join(imageDir, THUMBS, '*')):
            print('Cleaning: %s' % thumbpath)
            try:
                os.remove(thumbpath)
            except:
                # ignore subfolder, locked file, etc. [1.6]
                print('**Cannot remove %s: skipped' % thumbpath)

    makeThumbs(imageDir,                     # create thumb images in subfolder
               size=thumbMaxSize,            # per user's replies and configs
               subdir=THUMBS, 
               rotate=autoRotateImages,
               backups=backupRotatedImages)



#==========================================================================
# GENERATE full index web page in images folder
#==========================================================================


def orderedListing(dirpath, casesensitive=caseSensOrder):
    """
    ----------------------------------------------------------------
    [1.5] A platform- and filesystem-neutral directory listing,
    which is case sensitive by default.  Called out here because 
    order must agree between index and view (navigation) pages.
    Uppercase matters by default: assumed to be more important.

    The os.listdir() order matters only on the build machine, 
    not server (pages built here are static), but varies widely:
    on Mac OS, HFS is case-insensitive, but APFS is nearly random.
    The difference, on APFS ("ls" yields the second of these):

    >>> os.listdir('.')
    ['LA2.png', 'NYC.png', 'la1.png', '2018-x.png', 'nyc-more.png']
    >>> sorted(os.listdir('.'))
    ['2018-x.png', 'LA2.png', 'NYC.png', 'la1.png', 'nyc-more.png']
    >>> sorted(os.listdir('.'), key=str.lower)
    ['2018-x.png', 'la1.png', 'LA2.png', 'nyc-more.png', 'NYC.png']
    ----------------------------------------------------------------
    """
    if casesensitive:
        return sorted(os.listdir(dirpath))
    else:
        return sorted(os.listdir(dirpath), key=str.lower)


def formatImageLinks(imageDir, styleImgThumbs):
    """
    ----------------------------------------------------------------
    Format index-page links text for each thumb.  When a thumb is
    clicked, open either the raw image or a 1.5 view/navigate page.
    ----------------------------------------------------------------
    """
    imglinks = []
    for thumbname in orderedListing(os.path.join(imageDir, THUMBS)):
        if not isImageFileName(thumbname):
            # skip prior viewer-page files if not cleaned [1.5]
            continue    

        if not useViewerPages:
            # click opens raw image in . (1.4)
            target = url_escape(thumbname)
        else:
            # click opens viewer page in thumbs/ [1.5]
            viewpage = thumbname + '.html'
            target = url_escape(THUMBS + '/' + viewpage)

        # index page uses image in thumbs/
        source = url_escape(THUMBS + '/' + thumbname)

        link = ('<A href="%s">\n\t<img src="%s" style="%s"></A>' %
                      (target, source, styleImgThumbs)) 
                  
        imglinks.append((html_escape(thumbname), link))   # use Unix / for web!
    return imglinks


def formatSubfolderLinks(imageDir):
    """
    ----------------------------------------------------------------
    Format index-page links text for any and all subfolders in the 
    images folder.  On link click, open folder or its index.html.
    ----------------------------------------------------------------
    """
    sublinks = []
    for item in orderedListing(imageDir):                 # uniform ordering [1.5]
        if item != THUMBS and not item.startswith('_'):   # skip thumbnails and '_*'
 
            # valid name
            itempath = os.path.join(imageDir, item)       # uses local path here
            if os.path.isdir(itempath):

                # folder: make link                       # may not have index.html
                escsub = html_escape(item)                # add "/" to href [1.6]
                target = url_escape(item + '/')
                sublinks.append('<A href="%s">%s</A>' % (target, escsub))
    return sublinks


#
# Content defs: HTML/CSS constants
#

# [1.5] workaround vanishing <hr> bug on desktop Chrome at zoom <= 90% 
styleTableThumbs = """                        /* not used: table-layout:fixed; */
    background-color: %s;                     /* for fun (former 'white') */
    width: 100%%;                             /* expand table+borderlines */
    margin-top: 5px; margin-bottom: 5px;      /* above/below borderlines */
    padding-top: 25px; padding-bottom: 5px;   /* under/over borderlines  */
    border-top: 1px solid black;              /* manual lines, _not_ <hr>s: */ 
    border-bottom: 1px solid black;           /* chrome botches <hr> at zoom <= 90%% */
""" % thumbsBgColor                            # configurable, default=light grey

# [1.6] use thin (not 1px) so Chrome still draws at zoom <= 90%, allow color config 
styleImgThumbs = 'border: thin solid %s;' % thumbsBorderColor   # was 1px, html border=1

# generate UTF-8 content tag for non-ASCII use cases
doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'

contype = '<meta http-equiv="content-type" content="text/html; charset=%s">'
contype = contype % outputEncoding

# [1.4]: mobile friendly
viewmeta = '<meta name="viewport" content="width=device-width, initial-scale=1.0">'

# [1.5]: default-header font for index pages (same in viewer pages)
indexfont = '<style>body {font-family: Arial, Helvetica, sans-serif;}</style>'

# [1.5]: what made this page? (for readers of page code)
createdby = '\n<!-- Generated by thumbspage: learning-python.com/thumbspage.html -->'


def generateIndexPage(imageDir):
    """
    ----------------------------------------------------------------
    Build and output the HTML for the thumbnails-index page in the
    images folder, referencing already-built thumbnail images.
    This uses all-inline CSS styling, because custom HEADER.html
    files are not expected to code or link to anything made here.
    This also uses individual prints, because most content varies.
    ----------------------------------------------------------------
    """
    print('Generating thumbnails index page')

    # collect href lists 
    imglinks = formatImageLinks(imageDir, styleImgThumbs)
    sublinks = formatSubfolderLinks(imageDir)

    # don't assume Unicode default (in locale module)
    save_stdout = sys.stdout
    sys.stdout = open(indexPath, 'w', encoding=outputEncoding)

    # header section
    if os.path.exists(headerPath):
        # assume complete, HTML-safe (pre-escaped), explicit Unicode
        insert = open(headerPath, 'r', encoding=insertEncoding)
        print(insert.read())
        print('<!-- end custom header -->'.upper())
    else:
        folderpath = os.path.abspath(imageDir)           # expand any '.' or '..' [1.6]
        foldername = os.path.basename(folderpath)        # use last (or only) component 
        escfoldername = html_escape(foldername)
        print(doctype)
        print('<html><head>')
        print(contype)
        if useViewPort:
            print(viewmeta)
        print(indexfont) 
        print('<title>Index of %s</title>'
              '\n</head>\n<body>\n'
              '<h1>Index of Image Folder "%s"</h1>' % ((escfoldername,) * 2))
        print('<!-- end default header -->'.upper())

    # subfolders bullet list (skip other content)
    if sublinks and listSubfolders:
        print('\n<!-- start subfolder links -->'.upper())
        print('<p><b>Subfolders here:</b><div style="margin-bottom: 30px;"><p><ul>')
        for link in sublinks:
              linkstyle = 'style="margin-bottom: 6px;"'    # add space for mobile [1.4]
              print('<li %s>%s' % (linkstyle, link))       # add space below list [1.4]
        print('</div></ul></p>')
          
    # thumb links table 
    print('\n<!-- start thumbs table -->'.upper())
    print('<p>')   # drop <hr>
    print('<div style="overflow-x: auto;">')   # table autoscroll on small screens [1.4]
                                               # whole-window scroll breaks mobile widgets
    # [1.5] styled top/bottom borders, not <hr> 
    print('<table style="%s">' %  styleTableThumbs)

    # thumb links cells
    while imglinks:
        row, imglinks = imglinks[:thumbsPerRow], imglinks[thumbsPerRow:]
        print('<tr>')
        for (escname, link) in row:
              colstyle  = 'style="'                # configurable text color [1.6]
              colstyle += 'padding: 3px; '         # avoid running together [1.4]
              colstyle += 'text-align: center; '   # center img in its cell [1.4]
              if  uniformColumns:
                  colstyle += 'width: %d%%; ' % (100 / thumbsPerRow)
              colstyle += 'color: %s;' % thumbsFgColor
              colstyle += '"'
              labstyle = 'style="white-space: nowrap; margin-top: 0px;"'
              print('<td %s>\n\t%s\n\t<p %s>%s</p></td>' % (colstyle, link, labstyle, escname))
        print('</tr>')
        #print('<tr><tr>')   # drop in [1.4]: use css as needed

    print('</table></div></p>')   # drop <hr>
    print('<!-- end thumbs table -->'.upper())

    # footer section
    if os.path.exists(footerPath):
        # assume HTML-safe (pre-escaped), explicit Unicode
        print(createdby)
        print('\n<!-- start custom footer -->'.upper())
        insert = open(footerPath, 'r', encoding=insertEncoding)
        print(insert.read())
    else:
        print('\n<!-- start default footer -->'.upper())
        print('<p><i>This page was generated by '
              '<A HREF="http://learning-python.com/thumbspage.html">thumbspage.py'
              '</A></i></p>')
        print('</body></html>')

    sys.stdout.close()         # this used to be script exit
    sys.stdout = save_stdout   # there's now more to the show...



#==========================================================================
# GENERATE image-viewer/navigation web pages in thumbs subfolder [1.5]
#==========================================================================


#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Template: HTML+CSS+JavaScript, upper for % dict-key replacements, %% = %
#
# [1.6] Split off to a separate file for easier mods and reads.
# With the addition of JavaScript to the template's HTML and CSS,
# this code was virtually incomprehensible when mixed with the 
# Python code here.  Four flavors of syntax plus browser-specific
# quirks make web-page coding more exciting than it should be...  
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

VIEWER_TEMPLATE = 'template-viewpage.html'


def generateViewerPages(imageDir):
    """
    -----------------------------------------------------------------------
    For each image and thumbnail, build and output the HTML for one 
    view/navigate page, opened on thumbnail clicks.  Navigation matches 
    the filesystem-neutral and case-specific image order on index page.

    Not run if not useViewerPages, in which case there may be some pages 
    from prior uncleaned runs, but we aren't linking to them, or making 
    any now.  If run, always makes new viewer pages (unlike thumbs). 

    This was an afterthought, might be improved with JavaScript for 
    scaling [and was in 1.6], and could better support customizations,
    but its Prev/Next links already beat browser-native displays.
    Assumes that thumbnail filenames are the same as full-size image 
    filenames in .., and uses a template string (not many prints) 
    because most page content here is fixed (unlike index pages).

    CSS need not be inlined here, because no parts can be custom files
    (the only customizations are colors, and manual template-file edits). 
    Caveat: this script may eventually need to use PyPhoto's pickle-file 
    storage scheme if many-photo use cases yield too many thumb files.

    -----------------------------------------------------------------------
    [UPDATE, 1.6] Despite the following, this script now uses JavaScript
    to dynamically scale images while retaining aspect ratio in all cases, 
    except iOS landscape orientation and no-JavaScript contexts (which both
    use the former CSS scaling scheme described below).  See the 1.6 version
    notes near the top of this file for more background, and the viewer 
    template file's JavaScript for more on scaling.  The prior CSS scaling 
    notes here were retained as backstory to the Saga of the Scaling.
     
    -----------------------------------------------------------------------
    [FORMER SCHEME, 1.5] About CSS image scaling: because scaling as 
    implemented here is less than ideal, viewer pages are optional.

    CSS is poor at this: it cannot emulate better browser-native display.
    This probably requires JavaScript + onresize event callbacks to do 
    better: CSS has no way to adjust as window size and aspect changes.

    As is, desktop users may need to resize their windows for optimal 
    viewing, because images may exceed page containers on window resizes.
    Mobile portrait mode shows landscape images well but portrait images 
    don't use all free space; landscape mode always requires scrolls.
    
    Dev notes on the assorted scaling attempts sketched below:
    - 'auto' seems busted for width; fractional % usage may vary
    - [max-height: 100%%; max-width: 100%%;] doesn't constrain high
    - [overflow-x/y: auto;] doesn't limit image size, on div or img
    - [object-fit: contain;] attempt didn't work on chrome (why?)
    - the math scales side1 to N%, and side2 to N% of its ratio to side1

    Failed attempt: portrait not scaled down, landscape no diff
    if imgwide >= imghigh:
        IMAGEWIDE, IMAGEHIGH = '100%', 'auto'
    else:
        IMAGEHIGH, IMAGEWIDE = '100%', 'auto'
 
    Failed attempt: portrait too tall, landscape no diff
    ratio = min(imgwide / imghigh, imghigh / imgwide)
    if imgwide >= imghigh:
        IMAGEWIDE, IMAGEHIGH = '100%', '%f%%' % (100 * (imghigh / imgwide))
    else:
        IMAGEHIGH, IMAGEWIDE = '100%', '%f%%' % (100 * (imgwide / imghigh))
    -----------------------------------------------------------------------
    """

    # load from a file in script's dir for easy edits (and humane reads) [1.6]
    templatedir  = os.path.dirname(__file__)
    templatepath = os.path.join(templatedir, VIEWER_TEMPLATE)
    templatefile = open(templatepath, mode='r', encoding=templateEncoding)
    templatetext = templatefile.read()
    templatefile.close() 

    # get thumb/image names
    allthumbs = orderedListing(os.path.join(imageDir, THUMBS))
    allthumbs = [thumb for thumb in allthumbs if isImageFileName(thumb)]
    thumb1, thumbN = 0, len(allthumbs) - 1

    # build viewer pages
    for ix in range(len(allthumbs)):
        thumbname = allthumbs[ix]
        imgwide, imghigh = imageWideHigh(imageDir, thumbname)   # per Pillow

        # CSS scaling: these work well on mobile, and on desktop if window sized;
        # now subsumed by 1.6 JavaScript scaling, unless iOS landscape or no JS;

        if imgwide > imghigh:
            IMAGEWIDE, IMAGEHIGH = '100%', 'auto'    # landscape
        else:
            IMAGEHIGH, IMAGEWIDE = '80%',  '%f%%' % (80 * (imgwide / imghigh))
    
        replacements = dict(
            # meta, not templateEncoding: load
            ENCODING = outputEncoding,

            # used in CSS text
            BGCOLOR = viewerBgColor,
            FGCOLOR = viewerFgColor,
            JSCOLOR = viewerJSColor,
            BDCOLOR = viewerBorderColor,

            # relative to '.' = thumbs/, always unix "/" on web
            IMAGENAME = html_escape(thumbname),
            IMAGEPATH = url_escape('../' + thumbname),  

            # nav links: pages in '.', wrap around at end/start 
            PREVPAGE = url_escape('%s.html' % allthumbs[(ix-1) if ix > thumb1 else -1]),
            NEXTPAGE = url_escape('%s.html' % allthumbs[(ix+1) if ix < thumbN else  0]),

            # scale larger dimension to viewport (see calcs and notes above)
            IMAGEWIDE = IMAGEWIDE,
            IMAGEHIGH = IMAGEHIGH,

            # stretch small images beyond actual sizes?
            IMAGESTRETCH = 'true' if expandSmallImages else 'false',    # js booleans

            # enable history destacking if this browser fixes location.replace()
            CHROMEIOSBACKFIXED = ['false', 'true'][chromeiOSBackFixed]  # js ditto
            )

        print('Generating view page for: %s' % thumbname)
        viewerpath = os.path.join(imageDir, THUMBS, thumbname + '.html')
        viewerfile = open(viewerpath, mode='w', encoding=outputEncoding)
        viewerfile.write(templatetext % replacements)
        viewerfile.close()


#==========================================================================
# MAIN LOGIC: kick off the functions above (no longer top-level code [1.5])
#==========================================================================

if __name__ == '__main__':
    print('Running')

    makeThumbnails(imageDir)
    generateIndexPage(imageDir)
    if useViewerPages:
        generateViewerPages(imageDir)

    print('Finished: see results in images folder.')
    # and view index.html in images folder, zip/upload images folder to site



[Home] Books Programs Blog Python Author Training Search Email ©M.Lutz