#!/usr/bin/perl # # image_album # # AUTHOR: # Dan Harkless # # COPYRIGHT: # This file is Copyright (C) 2025 by Dan Harkless, and is released under the # GNU General Public License . # # OVERVIEW: # This CGI generates HTML on the fly for navigation of albums of related # images; for example, online photo albums. The best way to understand it is # probably to see it in action, which you can do at the following URL: # # http://harkless.org/dan/art/images/image_album.pd/photos/ # # But for the purposes of discussion, let's consider this hypothetical # directory/file structure: # # / # image_album.cgi # # /photos/ # image_album-parent.html # # /photos/2000-11-02_to_2000-11-06--Walt_Disney_World/ # image_album-description.html # WDW00-1-ferry_to_Magic_Kingdom.jpg # WDW00-1-ferry_to_Magic_Kingdom-thumbnail.jpg # WDW00-2-Enchanted_Tiki_Room.jpg # WDW00-2-Enchanted_Tiki_Room-thumbnail.jpg # WDW00-3-Haunted_Mansion.jpg # WDW00-3-Haunted_Mansion-thumbnail.jpg # # /photos/2001-12-25--Christmas/ # image_album-description.html # Xmas01-1-Christmas_tree.jpg # Xmas01-1-Christmas_tree-caption.html # Xmas01-1-Christmas_tree-full-size.jpg # Xmas01-1-Christmas_tree-full-size.png # Xmas01-1-Christmas_tree-large.jpg # Xmas01-1-Christmas_tree-thumbnail.jpg # Xmas01-2-stockings.jpg # Xmas01-2-stockings-caption.html # Xmas01-2-stockings-full-size.jpg # Xmas01-2-stockings-full-size.png # Xmas01-2-stockings-large.jpg # Xmas01-2-stockings-thumbnail.jpg # # Given that, this URL: # # http:///image_album.cgi/photos/ # # is the simplest way to call image_album and will display a page called # "Image Albums in: photos". (Note this URL specifies the "/photos/" path in # the so-called "PATH_INFO" part of the URL rather than as a CGI query # parameter -- we'll discuss this more below.) # # If image_album-parent.html isn't zero-length, it'll be printed as a caption # for the album collection. After that, # 2000-11-02_to_2000-11-06--Walt_Disney_World and 2001-12-25--Christmas will # appear as hyperlinks. # # Clicking on 2000-11-02_to_2000-11-06--Walt_Disney_World will display a page # called "Image Album: photos/2000-11-02_to_2000-11-06--Walt_Disney_World: # Index". If the image_album-description.html file in that directory isn't # zero-length, it'll be printed as a caption for the album. Below it will be # canned instructions, followed by a table looking like this: # # .jpg: .jpg: # File Thumbnail Normal # WDW00-1-ferry_to_Magic_Kingdom 2.1 kB 86.9 kB # WDW00-2-Enchanted_Tiki_Room 1.9 kB 90.7 kB # WDW00-3-Haunted_Mansion 1.7 kB 89.1 kB # # To the left of each file name, the thumbnail version of that image appears # inline, indicated abstractly above by "". As a special case, clicking # on the word "File" will contract the table to not include these inline # thumbnails. If you click on one of the other column headings, you'll get a # page with ALL the images displayed in that particular size/format. # # If you click on a particular file size figure, you'll get a page with just # that image (in the chosen size/format), along with its timestamp (which for # digital camera images should be the date/time the photo was taken, assuming # the timestamp wasn't wiped out in a file transfer). The page for # "WDW00-2-Enchanted_Tiki_Room" will have clickable links called "Prev" and # "Next" to get to the previous or next images in the album. # "WDW00-1-ferry_to_Magic_Kingdom", since it's the first in the album, will # have links called "Index" and "Next", and "WDW00-3-Haunted_Mansion", since # it's the last, will have "Prev" and "Index". In addition, all the Thumbnail # image pages will have a link called "Zoom In" to go to the Normal size page # for that image, and all the Normal image pages will have a link called "Zoom # Out" to do the reverse. # # Going back up to the top level, clicking on 2001-12-25--Christmas instead of # the Walt Disney World album will give you an index page with a table like # this: # # .jpg: .jpg: .jpg: .jpg: .png: # File Thumbnail Normal Large Full-Size Full-Size # Xmas01-1-Christmas_tree 2.4 kB 39.6 kB 230.2 kB 460.1 kB 1.6 MB # Xmas01-2-stockings 1.8 kB 31.8 kB 204.8 kB 403.7 kB 1.4 MB # # Here we have images available in four different resolutions and two # different file formats (judging by the use of the PNG format, the files are # probably film-based photos that were scanned in at a fairly high resolution, # or perhaps high-res digital camera RAW files that've been converted, rather # than being fairly low-resolution digital camera photos as in case of the # Walt Disney World album). # # Another difference with this album (if you refer back to the directory/file # listing above) is that each file has a -caption.html file. This # means that on the individual image and all-images pages, rather than just # displaying the file name, we display this caption, which hopefully is more # descriptive. # # Now that we've given an overview of the behavior of this CGI script, let's # move on to the files and in-URL option settings you can use to customize # this behavior. # # REQUIRED INPUT FILES: # . # Images can be in any format. However, at present, all images are # presented as tags, so if you have some oddball format that requires # an tag or something, your images will not successfully appear # inline. However, the individual image pages allow you to click on the # image to display just it in your browser, so as long as your browser # knows how to view the image (or which external application to run to view # it), there'll still be a way to view it from the image album. # # -full-size. # If . is the "normal"-sized image for convenient # viewing even on lower-resolution displays, this is the full-size version # of the image in its original resolution. # # -large. # If "-large" files exist, they're intended to be a size between "normal" # and "full-size". For instance, scaled to not be larger than 960x540 / # 540x960. # # -thumbnail. # "-thumbnail" files, if you have them, will be displayed in tables of # files, as a small preview. # # image_album-description.html # As a security measure, only directories containing a file with this name # will be processed by image_album (true for all modes but all_dirs -- for # it, see image_album-parent.html below). Otherwise, the CGI would allow # listing of all non-dot, non-extensionless files in any directory on the # machine readable by the webserver user (including outside the web root), # since we intentionally don't limit the file extensions of the files we'll # process to just .(gif|jpg) or something. In fact, watch out for that, # since if you have any file in your directory with an extension that isn't # known by image_album to be a non-image file (as of this writing, this list # is .(cgi|html|pd|pl|txt|url), plus any file with no extension), it will # appear in your "image" listing (unless the filename starts with a '.' -- # we follow UNIX conventions of dot files being accessible but not showing # up in directory listings). # # Now, for minimalists, your image directories can contain a zero-length # copy of this file to allow image_album to traverse into them, but if # you're like me, you'll want to give an overall caption for your image # album -- put it in this file. As the extension implies, you can use # arbitrary HTML in here (note that any necessary HTML-level or URL-level # illegal character escaping should already be done). If you have any # relative hyperlinks, they should be relative to the image directory # image_album is working on, not relative to the directory image_album lives # in (assuming those are different). Note that that's true for all the # input files that can contain URLs. # # Now, naturally, image_album-description.html is just an HTML fragment, not # a full HTML document, so don't put in tags like , , etc. If # you don't want to mess with HTML (one of the main benefits of this CGI is # that you can just upload your images and not have to mess with a bunch of # HTML authoring), the file can contain just plain text, but note that if # you have multiple paragraphs and want them to come out as such, you'll # have to use the

tag at a minimum (or
or

[...]
). # # image_album-parent.html # If you want to run image_album in a directory that's not an actual image # album directory, but is rather a parent of them (e.g. "photos" in the # example above), put in this file. Otherwise, again, image_album will # refuse to play ball, as a security measure. To use the "mode" terminology # introduced below, you need an image_album-parent.html file in any # directory you wish to have image_album run in in "all_dirs" mode. As # before, this file can be zero-length or can contain an HTML description of # the album collection. # # Note that you may not have both an image_album-parent.html and an # image_album-description.html file in the same directory. Image # directories must be "leaf nodes" in your directory hierarchy. # # Well, actually, I do sometimes make a "one_dir" directory (see below) be # the parent directory of another one_dir directory, for instance if the # main page of an album has my favorite shots from that photo session, and # then it has a subdirectory called "additional", with the shots that were # less good but still worth saving. In this case, however, image_album will # not automatically create a link to the subdirectory. You'll have to # manually put the link in your image_album-description.html text in the # album's main page. Be aware when doing this that there's a tag in # effect, making relative links be off of the image directory (i.e. a URL # not containing "/image_album.cgi/" or "/o==/"). When using this # directory structure, do not put an image_album-parent.html file in the # album's main directory, since image_album-parent.html overrides # image_album-description.html and puts you into "all_dirs" mode rather than # "one_dir" mode. Simply put an image_album-description.html file in both # the album's main directory and its "additional" subdirectory (and an # image_album-parent.html file in the parent directory of your albums, as # usual), and you'll be good to go. # # OPTIONAL INPUT FILES: # -caption.html # If this file exists, it'll be printed along with the image on the # one-image and all-images pages rather than just printing the file name. # If the file is only one line long, the caption will be centered below the # image on the one-image page. If it's more than one line long, it'll be # left-justified on that page (the captions are always left-justified on the # all-images pages). # # If you have a short caption whose source is quite long due to a hyperlink # URL, just make sure to make it one long line rather than wrapping at 80 # cols. or something, so that it'll be appropriately centered. # Contrariwise, if you don't want your short caption to be centered, just # artificially make it longer than one line (e.g. with whitespace or with # "

" and "

" lines). # # Note of course that counting the lines in the caption file is just a # rule-of-thumb. Having more than one line in the file may or may not # translate to the caption taking up more than one line in a browser window # of some arbitrary width. In general, though, the rule-of-thumb should # result in attractive presentation. # # body_attributes.txt # If this file exists, it should be the attributes to apply to the # tag. As of HTML 3.2, the attributes that can go here are: ALINK, # BACKGROUND, BGCOLOR, LINK, TEXT, and VLINK. As an example, the file could # contain: # # BGCOLOR=black LINK=white TEXT=white VLINK=gray # # to make the image_album pages blend in with a white-on-black-themed site. # # Note that if you specify a BACKGROUND attribute, you can of course use a # relative URL, which will, as mentioned above, be interpreted as relative # to the directory containing your images. Note that we don't do any # escaping of the contents of the body_attributes.txt file, so be sure your # URL (and the file in general) doesn't contain any unescaped illegal # characters. # # If body_attributes.txt doesn't exist, we'll just put out a simple # tag with no attributes. # # doctype.html # If this file does not exist, we use the HTML5 DTD: # # # # If you want to use a different DTD, put the appropriate DOCTYPE # declaration line in this file. We'll also echo the HTML version you # specify there in the "Validated " link in the footer. # # footer.html # If this file exists, it will appear at the bottom of the document. That's # it. Next...? # # Um, okay, actually it's not quite that simple. This is not the entire # footer, just the user-modifiable portion of it. # # The default footer consists of an
followed by a table with a # left-justified on the left and a right-justified on the right. # The left will contain the timestamp of the directory or single image, # and the right will contain "Validated " (hyperlinked to # validator.w3.org -- see the doctype.html description above for how # is determined) and "Generator: image_album" (hyperlinked to # the anchor of this script on my Software web page). # # If there's a footer.html file, its contents will appear in the left , # prior to the timestamp. For the most part, the HTML in the user footer # file is spit out verbatim to the browser. The exception is the string # "!UP_URL!". If image_album sees this special string (note it must start # and end with an exclamation point), it will replace it with a constructed # URL appropriate to return to the parent page of the current image_album # screen. This special processing is to accomodate people who like to put # an "up arrow" or "parent folder" graphic in their footers, linked to the # parent directory. # # If one were to just put "../" as the link to the parent page, then going # up and up back to the top image_album page would not mimic all the same # steps it took to navigate downwards. # # It's okay to use "!UP_URL!" even in the footer of your top-level # image_album directory, which is usually an all_dirs directory, but could # be a one_dir directory if you only had one album. In this case, # "!UP_URL!" will be substituted with "../" at the top level to go up to the # parent directory. # # head_additions.html # Lines to add to the section, such as stylesheet, favicon, and # touch-icon s, tags, etc. Example head_addition.html (no # indentation needed): # # # # # # # # and tags should not be added to this file since we generate # them programmatically. The head_additions.html lines will be put in the # <HEAD> section after the <BASE> tag and before the <META NAME=Generator> # and <TITLE> tags. # # hr.html # If you want to use a graphical horizontal rule in the footer rather than # the usual one generated by <HR>, and you can't accomplish this by just # making the appropriate settings in your stylesheet (e.g. because you want # to work around Netscape 4's problem of only doing even multiple repeats of # background-images, which doesn't happen, for instance, in <TD>s), put your # desired HTML in this file. # # Another use for this file is if you have some text you want to appear on # an all_dirs page, but you want it to go at the bottom of the page, just # before the footer, rather than at the top of the page, as would happen if # you put it in the image_album-parent.html file. Just put the text in this # file, and then end it with an <HR> tag (or more complicated HTML if you're # doing a graphical rule, as per the above paragraph). # # lang.txt # If this file exists, it should contain the language to output in the # <HTML> tag's "LANG" attribute. If not specified, we default to # "LANG=en". No LANG attribute is output if doctype.html specifies HTML 2 # or 3, or if lang.txt is empty or contains only whitespace. # # next.url # If this file exists, it should contain the URL of the icon image to use # rather than the word "Next" (or "Index", for the last one) on the # individual image pages. Likely candidate is an arrow facing to the right. # # prev.url # If this file exists, it should contain the URL of the icon image to use # rather than the word "Prev" (or "Index", for the first one) on the # individual image pages. Think left-facing arrow. # # representative_thumbnail.url # If this file exists, it should contain the URL of a thumbnail-sized image # from this directory's album that's considered to be "representative" of # the album as a whole. Actually, calling this a URL may be misleading -- # it'll just be a bare filename -- like other .url files, it's interpreted # relative to the particular *image* directory (even though in "all_dirs" # mode our <BASE> is the *parent* directory of that image directory). When # image_album is in "all_dirs" mode (see below), if a subdirectory contains # a representative_thumbnail.url file, that thumbnail will be displayed next # to the clickable directory name. Think of it as a "teaser" shot for a # particular album. # # title_template.txt # If this file exists, it should be a template for what the page title # should look like. The string "!GENERATED_TITLE!" will be substituted with # the actual generated title string. For instance, I use: # # Dan Harkless' !GENERATED_TITLE! # # so that, for instance, the script-generated title "Image Albums in: # photos" will be rendered as "Dan Harkless' Image Albums in: photos". Note # that this formatting will only be done on the copy of the title string # that appears in the actual page <TITLE>, not on the second copy that # appears as an <H1> in some modes. # # zoom_in.url # Similar to next.url and prev.url, except this is the URL of the image to # use instead of the words "Zoom In". I use a magnifying glass icon with a # plus sign in the middle of the lens. # # zoom_out.url # Like zoom_in.url, but for "Zoom Out". I use a magnifying glass icon with # a minus sign in the middle of the lens. # # PATH_INFO: # This is the key parameter, and is generally mandatory (if not set, it # defaults to "."). It specifies the directory and optionally image file for # image_album to work on. # # With most scripts, PATH_INFO paths are interpreted as absolute paths from # the web root. For instance, if you had: # # http://<website>/~joeblow/image_album.cgi/photos/ # # then usually you would take everything after the CGI script name, in this # case, "/photos/", and interpret that as an absolute path, so the image # directory being worked on would be: # # http://<website>/photos/ # # If Joe wanted to refer to a directory under his home directory rather than # off of the web root, he'd have to use a URL like: # # http://<website>/~joeblow/image_album.cgi/~joeblow/photos/ # # Note the repetition of "~joeblow/". If the "photos" directory were deeply # buried in Joe's directory structure, its entire pathname would have to be # duplicated, appearing twice in the URL. Another problem is that we use the # PATH_INFO as the name of the album in the <TITLE> and <H1> tags, and with a # long path to the images folder, this name would include much irrelevant # text. # # To be more flexible, image_album instead treats PATH_INFO as a relative path # rather than an absolute one. So the first example from above: # # http://<website>/~joeblow/image_album.cgi/photos/ # # is a valid way to get image_album to operate on the: # # http://<website>/~joeblow/photos/ # # folder, without having to respecify "/~joeblow" (again, the savings is much # greater when we're not talking about a toy example with the image folder at # the very top of a user's web hierarchy). # # There's a problem with this, however. Relative paths sometimes need to use # "../". For instance, if we had a case like this: # # http://<website>/~joeblow/cgi/image_album.cgi/../photos/ # # where Joe's "cgi" directory is controlled by system administrators (only # containing pre-approved scripts), he would need to use "../" as above to get # back to his top level web directory and specify "photos" relative to that. # Unfortunately this will not work as intended, because modern browsers will # pre-normalize the path prior to sending it to the webserver -- it'll get # sent as if Joe had typed: # # http://<website>/~joeblow/cgi/photos/ # # which doesn't accurately reflect his directory structure. # # Therefore, in cases where it's not possible to rework the directory # structure so that all image directories are in subdirectories of the # directory image_album is in (as it's not possible in the case just # discussed, for administrative reasons), image_album needs some way to # specify absolute pathnames. # # It would be great if one could just add two slashes to get an absolute path: # # http://<website>/~joeblow/cgi/image_album.cgi//~joeblow/photos/ # # But with Apache, at least, this does not work. The PATH_INFO environment # variable gets set with just one leading '/'. Another possibility would be # to use a URL-encoded '/': # # http://<website>/~joeblow/cgi/image_album.cgi/%2F~joeblow/photos/ # # but this doesn't work either -- the behavior is particularly odd with Apache # (using 2.0.40 + Red Hat backported patches, at least), because the browser # gets sent a 404 error, yet no correspoding message appears in the error_log # (just the access_log). # # So we need another approach. Remember the '!' characters flanking our # special substitution strings in footer.html and title_template.txt? Well, # the '!' is back. To specify an absolute URL to image_album, do it like # this: # # http://<website>/~joeblow/cgi/image_album.cgi/!/~joeblow/photos/ # # Now, one may wonder why we go to all this trouble trying to get the # PATH_INFO string to work the way we want it to. Why not use normal CGI # parameters in the QUERY_STRING part of the URL, where it's no problem to # specify absolute and relative paths, without tricks or restrictions? # # Well, originally image_album _did_ have you specify the path using 'dir' and # 'file' parameters, as in: # # http://<website>/~joeblow/cgi/image_album.cgi?dir=/photos&file=cow.jpg # # The problem is that search engines like Google and Alta Vista (as of this # writing in August 2003) do little to no spidering of CGI-generated pages. # How exactly they come to the conclusion a page is CGI-generated is not # publically documented, but one thing that's for sure is that URLs containing # a '?' are determined to be so. # # This is the reason image_album has been changed to use PATH_INFO, so that # its generated pages can be found by the popular search engines. (It's also # why on my server I define ".pd" (which stands for "pseudo-directory", since # with PATH_INFO the script name appears to be just another directory name) to # be a synonym for ".cgi" (or ".pl"), and call image_album as # "image_album.pd", just in case there's special checking for the string # ".cgi" (or ".pl") in any of the search engines' CGI-page recognition code.) # # Note that Microsoft's IIS breaks the CGI standard (gee...) by default and # returns the wrong portion of the URL for PATH_INFO. To get it to work # properly, see document KB184320. If you're using a server where it's not # possible to get that fixed (perhaps because the admin blindly trusts # Microsoft's bogus claims that PATH_INFO is disabled by default for # "security" reasons), you can use my old query-string-based version of # image_album, which I'll leave archived at: # # http://harkless.org/dan/software/old/image_album.pre-PATH_INFO # # I won't be making any further updates to that version, however. # # MODES: # In the past, image_album had to have its operational mode set in a # QUERY_STRING parameter in the URL. This is no longer necessary -- the modes # are now determined automatically based on the PATH_INFO path and on the # existence of image_album-parent.html and image_album-description.html files. # It's still handy to talk in terms of the names image_album uses internally # for these modes, however: # # all_dirs # All sudirectories of the specified dir which contain an # image_album-description.html file or image_album-parent.html file (for # hierarchies more than two levels deep) are listed as hyperlinks. # Directories containing representative_thumbnail.url files will have that # thumbnail displayed by the directory name, unless disable_dir_thumbs (see # the OPTIONS section below) is set. Example URL: # # http://<website>/image_album.cgi/photos/ # # one_dir # This is the Index page with the table of file name roots and their # individual format / size-class versions. Will have inlined thumbnails # unless disable_dir_thumbs is set. Example URL: # # http://<website>/image_album.cgi/photos/Xmas/ # # all_files # Displays all the files of the specified (all_files_extension) extension / # size-class, with their captions (if any, else filenames) to the right of # them. Example URL: # # http://<website>/image_album.cgi/photos/Xmas/all_files_extension=.jpg/ # # Note how image_album uses all_files_extension=.jpg as its own # pseudo-directory at the end of the URL rather than grouping it with the # "o==" options discussed below. This is so that even if you're using # robots.txt to prevent traversal with superfluous options, search engines # can still snarf up the all_files pages. This could be important because # on these pages all the captions for the entire album appear together, # which could cause someone's multi-term search query to find a match across # captions. # # Because we do this, you may not, of course, name any of your image # directories "all_files_extension=...". # # one_file # Displays a single image, with text labels or icons to navigate to previous # or next images, and zoom in or out. The image's caption (if any, else the # filename) appears below it, along with the number of this file in the # series and the total (e.g. "#2 / 10"). Also, this is the only mode that # shows the file's timestamp, which is potentially a significant piece of # data in the case of digital photos that were transferred in such a way # that their timestamps were preserved (i.e. not standard FTP). If the # Image::Info module is available, this page will also feature a toggle # called "Extra info" just above the footer. Activating it will cause all # the info that Image::Info has to offer on the image (such as comments, # dimensions, and EXIF data) to be printed. Example URL: # # http://<website>/image_album.cgi/photos/Xmas/tree.jpg/ # # OPTIONS: # These options appear in a comma-separated list after "o==", in a # pseudo-directory that comes immediately after the script name, like so: # # http://<website>/image_album.cgi/o==debug,extra_info/photos/Xmas/me.jpg/ # # We put the options in this pseudo-directory early in the PATH_INFO string so # that it's possible to tell search engines not to index redundant copies of # image_album pages with all combinations of the option settings, using a # robots.txt file like this: # # User-agent: * # Disallow: /image_album.cgi/o== # # Unfortunately since image_album always puts out the options in alphabetical # order rather than checking in which order they were specified in the current # page's URL and mimicking that when generating navigation links, this means # that someone who wanted to use robots.txt to keep spiders off of most of the # option settings but also wanted to hardcode extra_info on couldn't do # something like: # # User-agent: * # Disallow: /image_album.cgi/o==extra_info, # Disallow: /image_album.cgi/dir1/ # [...] # Disallow: /image_album.cgi/dirN/ # # but that would be really ugly anyway since it would require enumerating all # the image album subdirectories in the robots.txt file. If you want the # ability to let spiders see extra_info pages but not be able to mess with the # other options, let me know. # # Below are all the supported options. Under normal circumstances, it's not # necessary to specify any of them manually. # # all_files_extension # Tells image_album to display all files matching this extension. "What?" # you ask. "I thought you said above that all_files mode is signified by a # special all_files_extension setting masquerading as a filename in the # PATH_INFO." Indeed this is true. So why is all_files_extension also an # o== option? For navigational purposes. If you do the navigation all_dirs # mode -> one_dir mode -> all_files mode -> one_file mode, you'll have a URL # progression that looks something like this: # # http://<site>/image_album.cgi/pix/ # http://<site>/image_album.cgi/pix/Joe/ # http://<site>/image_album.cgi/pix/Joe/all_files_extension=.jpg/ # http://<site>/image_album.cgi/o==all_files_extension=.jpg/pix/Joe/1.jpg/ # # The o==all_files_extension option on the final URL tells image_album what # the parent page of the 1.jpg/ page needs to be, so that if you use an Up # arrow in the footer to navigate up to the parent rather than just using # your browser's Back button (especially handy if you've done # Next... Next... Next through a big album), the navigation will correctly # be the inverse of the progression shown above. # # As one final comment on this option, note that size qualifiers are part of # the extension, so so all_files_extension=-full-size.jpg, # all_files_extension=-large.jpg, all_files_extension=-thumbnail.jpg, and # all_files_extension=.jpg will produce four non-overlapping sets of images # (presuming, here, that you're using all four size classes in the given # album). # # debug # This option is only set while debugging this script. It does things like # making invisible table borders visible and enabling verbose diagnostics. # # disable_dir_thumbs # By default, all_dirs and one_dir modes feature thumbnail graphics (if the # image_album user has provided them). For a big album, this could mean the # index page would require hundreds of thumbnails to be downloaded, which # might not be desirable over a slow link. If you click on the "File" # column heading toggle on the one_dir page, this column will be removed # from the table, allowing low-bandwidth users to only load an image on each # one_file page. However, they'll still have to start the download of all # those graphics and then interrupt it midstream (possibly challenging if # they're using slow hardware or a not-very-capable mobile browser). # # To allow low-bandwidth users to avoid that, you may wish to make the # normal URL of your top image_album page be: # # http://<website>/image_album.cgi/photos/ # # but then provide a link in your image_album-parent.html text to: # # http://<website>/image_album.cgi/o==disable_dir_thumbs/photos/ # # (The link can be specified as a relative URL -- it doesn't have to be # absolute as in the above example.) # # In the future I may add automatically-generated "Global Settings" toggles # on all_dirs pages so you don't have to put in a link like that manually # (and so it can truly be a toggle, not a one-way setting or pair of one-way # settings). # # extra_info # If the Image::Info module is available, this option toggles the display of # extra info on an image (such as comment, dimensions, and EXIF data) in # one_file mode. The user can always toggle the extra info on an off by # clicking "Extra info" on the bottom of the one_file pages, so there's no # need for you, the webmaster, to mess with this option unless you want it # to default to on rather than off. # # NOTES: # You don't need to follow any particular naming convention on your # image_album directories. Note that directories and files will be listed in # alphabetical order, however, so if you want to force chronological order you # may want to name directories as I do -- YYYY-MM-DD--EVENT, with the files # inside named as # <shortened_form_of_EVENT>YY-N[N[N]][-<short_description>].<extension>. # # Note that when the directory name appears in the <H1> heading on all the # modes besides one_file, underscores will be changed to spaces and "--" will # be changed to " -- ", to allow line wrapping to occur. Also, the directory # separator '/' will be changed to '<SPAN STYLE="font-size: xx-small"> </SPAN> # /<SPAN STYLE="font-size: xx-small"> </SPAN>' so that line wrapping can occur # but it'll still look like a coherent pathname. # # One naming convention you _do_ have to follow for your image files is the # <file_root>-full-size.<extension>, <file_root>-large.<extension>, and # <file_root>-thumbnail.<extension> convention. In the future I might add # support for user customization files or parameters that would tell # image_album to expect different fixed names for those. For instance, # someone might have a big library of preexisting images named # <file_root>_big.<extension>, <file_root>_medium.<extension>, and # <file_root>_small.<extension> and not feel like renaming them. # # Right now, image_album makes the assumption that if you have multiple file # formats in the same directory, it's because you're storing both # lossy-compressed and non-lossy-compressed (e.g. .jpg and .png) versions of # the same images. Therefore, .jpg and .png will be different columns in the # file index table and as you use "Prev" and "Next" to navigate through the # images, you'll either see all the .jpg files or all the .png files (of a # given size-class -- e.g. all "-thumbnail.<extension>" files). JPEGs and # PNGs will not be interspersed (for that matter, even "file1.jpg" and # "file2.jpeg", or "file3.tif" and "file4.tiff" won't be interspersed, since # image_album is hands-off and future-proof as to image formats -- thus it's # best for you to stick to one standardized extension per format). In the # future I'd like to add an input file (wouldn't make sense as a URL option # since different directories could differ in makeup) called something like # collapse_image_formats that would cause image_album to assume you have files # in different formats just because it's a heterogenous collection, not # because you have different lossinesses for the same image. With this file # present, all images of a given size class, regardless of format / extension, # would be collapsed into a single column / series. # # image_album is case-insensitive when sorting file / directory names, and # in grouping different capitalizations of the same file extension # (e.g. .jpg, .Jpg, and .JPG) into one format category. It also properly # escapes characters in file and directory names that are illegal unescaped in # URLs (e.g. spaces). # # Note that if the Image::Info, Image::Magick, or Image::Size modules (see # CPAN) are installed, they will be used to determine image dimensions so # these can be hardcoded into the <IMG> tags and Netscape will be able to # render the page prior to all the images downloading, and Internet Explorer # won't do the jerky dynamic rendering dance as the images come in. Note that # versions of Image::Size between 2.902 and 2.96 will fail to work if all # directories in between "." (our working directory) and "/" (filesystem root) # aren't both executable AND readable by the webserver user (which often isn't # the case due to security/privacy issues concerning directory listing # ability). For the benefit of versions 2.97 and on, where my suggested # change was implemented, we turn on $Image::Size::NO_CACHE to prevent the # needless call to cwd() and get Image::Size to work even in this permissions # scenario. # # image_album supports "Once per session" browser caching for the HTML output, # but not cross-session caching (due to the code complication and execution # time penalty of determining the proper response to an If-Modified-Since # request). # # image_album produces named anchors in all_dirs, one_dir, and all_files # modes, allowing you to send someone a link to a particular album or photo # yet show it in the context of all its siblings. In all_dirs mode, the names # of the anchors are the subdirectory names, so an example URL would be # <http://harkless.org/dan/art/images/image_album.pd/photos/#1--Best_Of>. In # one_dir and all_files modes, the anchor names are the filename prefixes (the # names in the "File" column in one_dir mode). # # VERSION: $version = "2025-08-07"; # # DATE MODIFICATION # ========== ================================================================== # 2025-08-07 Added "padding: 2px" to TABLE.bordered TD & TH (& updated below). # 2025-08-02 We now default to HTML 5, rather than 4.01 Transitional. Also, we # now unconditionally use CSS instead of HTML 3.2-style attributes, # so if you need to generate HTML 3.2, see the change entry below. # If the user doesn't specify a stylesheet with head_additions.html, # we output the following inline mini-stylesheet: # # :root { # background-color: #151515; # color: white; # } # A:link {color: dodgerblue} # A:visited {color: mediumpurple} # A:active {color: crimson} # DIV.center {text-align: center} # P.center {text-align: center} # TABLE {border-collapse: collapse} # TABLE.bordered {border: 1px solid} # TABLE.bordered TD {border: 1px solid; padding: 2px} # TABLE.bordered TH {border: 1px solid; padding: 2px} # TD.center {text-align: center} # TD.left {text-align: left} # TD.right {text-align: right} # TH.left {text-align: left} # TR.bottom {vertical-align: bottom} # TR.center {text-align: center} # TR.top {vertical-align: top} # # Note that the above means that without a stylesheet specified, we # now default to "dark mode", rather than the old default of # BGCOLOR="#CCCCCC", with implicitly black text. # # If a stylesheet _is_ specified, it must contain compatible # definitions for the above class names (doesn't need to match the # above styling _exactly_). Mostly for HTML compactness, I decided # not to avoid potential name conflict issues by either putting # image_album in all the class names, or doing all the above with # inline STYLE= attributes. We also now default to UTF-8 rather # than ISO-8859-1. # 2025-08-02 The previous version is the last version that can output legal # HTML 3.2. A copy has been saved at <https://harkless.org/dan/soft # ware/old/image_album.pre-CSS-requirement>, $version "2025-08-02a". # 2025-08-02 Finally updated validator.w3.org link in footer to pass the URL # to check explicitly, rather than depending on Referer to be full # URL, as was the case before browsers tightened privacy on that. # Also, added "+ CSS", with CSS a link to jigsaw.w3.org on the URL. # 2025-08-01 In the footer, in all_dirs mode, instead of the overly technical # "Last subdirectory modification", say "Last album modification". # For one_dir and all_files, instead of "Directory last modified", # say "Album last modified". TBD: Have it look at all the file # timestamps rather than just dir timestamp, given new wording? Or # just say in docs to 'touch' dir, to have updates to existing files # (i.e. w/o any file creation, deletion, or renaming) be reflected? # 2017-06-28 Added new size category "full-size". "large" is now expected to # be in between "normal" and "full-size" (not that you're required # to have your images in all four sizes). In the link to # validator.w3.org, in the footer, match the "http" or "https" of # our own URL so that the Referer header gets passed as expected. # In one_dir mode, if one of the image-size-providing modules is # available, print each image's resolution underneath its file size. # 2016-07-04 No longer require if.pm to do 'use warnings' only for Perl 5.6.0+. # 'use warnings' has been legal in up-to-date 'perl's since 2000. # 2016-07-04 Changed the default DTD from 3.2 to 4.01 Transitional. # 2016-07-03 Although 'lang.txt' is a user-controlled file, html_escape() the # value just in case. # 2016-06-30 Added 'lang.txt' to specify document language; default to "en". # 2016-03-23 Changed 'and defined @image_info_hashes' to 'and # @image_info_hashes' per deprecation warning in Perl 5.16.*. # 2016-03-22 Wanted to add support for apple-touch-icons and other touch-icons, # but rather than adding one or more new input files, just added a # generic head_additions.html file for <LINK>s and <META>s. Removed # support for the favicon.url and stylesheet.url input files to # minimize disk accesses. Existing favicon.url and stylesheet.url # files will need to be converted to single 'head_additions.html's. # 2009-02-17 If a nonexistent directory is specified, abort with an error # rather than silently falling back to its parent directory. # 2008-09-02 "use English qw(-no_match_vars)": avoid regex performance penalty. # 2008-03-12 Added anchor names to all_dirs, one_dir, and all_files modes. # 2008-03-12 Use the new 'use warnings' feature rather than using -w on the # shebang line so we only output warnings for code in this script, # not in modules we use. To do this in a backwards-compatible way, # the if.pm module (available from CPAN) is now required. # 2006-12-11 Updated documentation to reflect recent unpublished changes. # 2006-08-21 Changed "format" to "file format" in the onscreen instructions. # 2006-07-04 Apparently the interface to Image::Magick changed at some point # and now (as of 6.0.7) functions return a false (undefined) value # when they _succeed_, not when they fail. The Perl API is not # well documented so I'm just going to follow the new convention # rather than trying to figure out when this was changed and do # different logic based on the version. # 2006-06-24 When converting this script to use PATH_INFO rather than 'dir' and # 'file' parameters, I left the other ones like 'disable_dir_thumbs' # and 'extra_info' as query string parameters, with the thought that # search engines would mostly ignore the URLs with those parameters # added and just index the parameter-free main versions of the URLs. # However, I now find that Google is doing a lot more spidering of # '?'-containing URLs than it used to, and because the link to # switch to disable_dir_thumbs=1 comes before any of the album and # image URLs when crawling, Google was indexing all the pages with # that parameter turned on, and then generally giving up before # indexing the normal versions, due to per-site page limits or # similar-content detection. Even if a search engine weren't doing # that, it wouldn't be particularly desirable for it to be indexing # every combination of all_files_extension, disable_dir_thumbs, and # extra_info settings for each page. Therefore it'd be nice to be # able to use robots.txt to keep the crawlers off the versions of # the URLs with the options set. Unfortunately the power of # robots.txt is incredibly limited -- you basically just have URL # prefixes to work with -- no arbitrary pattern matching. We could # generate the robots <meta> tag for more precision, but that would # mean search engines would be coming in and sucking up CPU time and # memory to generate a page we'd then be telling them they couldn't # have. Plus it'd be tricky to make the robots meta tag generation # customizable. Therefore, I have rewritten the handling of all # query string options to instead use a pseudo-directory of the form # /o==Option1,Option2,[...]OptionN/. This pseudo-directory comes # immediately after the script name, to make it easy to use # robots.txt to disallow robots from all image_album URLs with # non-default options turned on. # 2006-06-10 According to my testing, Google, Teoma, and Yahoo! don't bother to # go check the MIME type of URLs not containing '?' that end in # ".jpg". They just assume that such URLs are going to be # image/jpeg and only send their image crawlers after them, not # their HTML crawlers. Therefore these search engines (and probably # MSN Search too) were ignoring all our all_files and one_file # pages, and only indexing the all_dirs and one_dir pages. # Therefore, changed the all_files and one_file URLs so that instead # of ending in ".jpg", they end in ".jpg/" (which my testing shows # that these search engine all _will_ try to index as HTML). # 2006-04-07 Added support for favicon links via the new favicon.url file. # 2006-02-16 Removed \n from Generator -- Firefox displays it as a funky glyph. # 2003-09-22 Fixed warning due to lack of parens around 'my' variable list. # 2003-09-04 Oops -- I set up some one_dir pages that didn't have an all_dirs # directory above them. We need "!UP_URL!" to get changed to "../" # at the top level in this case. (Actually, "../" might not be # appropriate for some sites; if this is ever the case, I'll make # "!UP_URL!" be optionally specifiable as "!UP_URL{<url>}!".) # 2003-09-04 Added .pd to image_album's list of non-image file extensions. # 2003-08-17 footer.html's UP_URL{<url>} changed to !UP_URL! and title_template # .txt's GENERATED_TITLE changed to !GENERATED_TITLE!. # 2003-08-17 Since many people are likely to want dir_thumbs=1 by default, and # they won't want to have to stick that parameter in the URL of # their album, since that would stop search engines, inverted the # default behavior and renamed parameter to disable_dir_thumbs. # Extended it to affect not just one_dir pages but also all_dirs. # 2003-08-17 Removed the search-engine-unfriendly 'modified' dummy parameter. # 2003-08-17 Major retooling to use PATH_INFO rather than 'dir' and 'file' # query parameters, to allow search engines that won't touch URLs # containing '?'s to spider our pages. # 2003-06-15 Documented that hr.html can be used for arbitrary pre-footer text. # 2003-03-29 html_escape() and url_escape() calls were missing in a few places. # 2003-03-15 Use CGI.pm's DISABLE_UPLOADS and POST_MAX variables to protect # against DoS attacks. POSTs of more than 1 KiB will error. # 2002-12-01 Now we can also use Image::Magick to get image dimensions. # 2002-05-17 Added the hr.html input file for overriding "<HR>" in the footer. # 2002-04-02 Implemented the TBD from the previous entry -- caption files that # are only one line long are centered (with <DIV>) under the image # on one_file pages (but not, of course, on all_files pages). # 2002-03-30 Previously the captions on the one_file pages were surrounded by # a <DIV ALIGN=CENTER>, but while you could escape that with a <P # ALIGN=LEFT>, there's no equivalent for <UL>s (and other lists) # other than starting off the caption file with </DIV> and ending it # with <DIV ALIGN=CENTER>, which would result in illegal HTML on # all_files pages. Removed the <DIV> and added more space above and # to the sides of the caption so short captions don't look as bad # when left-aligned. If a webmaster (albumaster?) still wants # centered captions, they can use <P ALIGN=CENTERED> (or <DIV>), but # then they'll be centered on the all_files page as well, which is # not desirable. TBD: Automatically center captions that are only # one line long? Note that when there's no caption and we're using # the filename, we still center that under the one_file image. # 2002-03-26 Use the filename as the ALT tag on one_dir thumbnail images. # 2002-03-26 If stylesheet.url exists, add <LINK> tags to <HEAD> for each URL. # 2002-03-26 Allow user DOCTYPE declaration with new doctype.html input file. # 2002-03-26 For input files where we print some default if the file doesn't # exist (e.g. the caption), allow files to be zero-length to allow # the user to suppress that element entirely, should they so desire. # 2002-03-23 In footer of all_dirs mode, rather than "Directory last modified", # output the more useful "Last subdirectory modification". This # also makes it easier to determine what date to use in the # "modified" dummy parameter in the top-level URL, if you use that. # 2002-03-22 Changed title_format_string.txt to title_template.txt. Don't # feed to printf(), as that could have security implications in # situations where anonymous users are allowed to upload to an # album directory. Just do s/GENERATED_TITLE/$title/g instead. # 2002-03-21 It was possible for the "File timestamp" to be separated from the # "Generator" by just a single space, making the text run together. # 2002-03-13 The current timestamp works in IE to allow caching, but not # Netscape -- use a timestamp a minute in the past to be older than # the HTTP Date header (which Apache determines before calling us). # 2002-03-13 Output the current timestamp as a Last-Modified header so that at # least "Once per session" browser caching can be done. # 2002-03-13 Looks like the image dimension-getting code for Image::Info was # accidentally broken when expanding use of the module on 3-10. # 2002-03-12 Print the filename in the extra info subimage headers if we had a # caption (and thus didn't print the filename as a pseudo-caption). # 2002-03-12 Rather than adding alink.txt, link.txt, text.txt, and vlink.txt # and all the file I/O overhead that'd go along with that, collapsed # background.url and bgcolor.txt into body_attributes.txt, which can # support all of those attributes. # 2002-03-10 Zoom URLs got broken when I fixed the upper-case extension bug. # 2002-03-10 Now put directory modification dates in the "modified" parameter # in generated links from all_dirs mode so that web surfers will be # able to tell by link color when an album is updated. # 2002-03-10 Now, when Image::Info is available, we have a new toggle called # "Extra info" at the bottom of each one_file page -- clicking it # will cause all the info that Image::Info has to offer, including # comments, dimensions, EXIF data, etc., to be printed at the bottom # of the page. As you navigate, it'll stay on until toggled again. # 2002-03-09 Added debug parameter and assigned a couple of actions to it. # 2002-03-09 Didn't work with files that had extensions that weren't all lower # case (the way I always name *my* image files), because we were # throwing away the original capitalization of the extension on file # names recorded into the %file_hash as part of our merging of # extensions with multiple capitalizations into one format category. # Now instead of just setting %file_hash values to 1, we set them to # the actual spelling of the extension on that particular file. # 2002-03-08 Disallow '/' character in the file parameter to stop some *very* # limited ability to expose files readable by the webserver user # (ones called <file_root>-caption.html, where there's some other # <file_root>.<extension>) that are outside the webserver root. # 2002-03-03 I researched and can't figure out why ',' was added to the list of # reserved URI characters in RFC 2396 "since [it's] treated as # reserved within the query component". _We_ (and CGI.pm) are the # ones processing the query component, and we don't do anything # special with commas. De-escaped (I like 'em in some filenames). # 2002-03-03 While putting together an album of images from a friend where only # two had higher-res versions, I realized it would be silly to have # to name all the other files in the directory -reduced in such a # situation (or else only have a -reduced category for those two, # and have "full-size" vary wildly in meaning from picture to # picture). It would be most common to have just a normal-sized # picture, with or without an accompanying thumbnail, and only # sometimes have a high-res version as well. Therefore, the # "normal" case should be in the middle of the size classes, not on # the right. So instead of before, where we had, in ascending size # order, <file_root>-thumbnail.<extension>, # <file_root>-reduced.<extension>, <file_root>.<extension>, we now # have <file_root>-thumbnail.<extension>, <file_root>.<extension>, # <file_root>-large.<extension>. # 2002-03-03 Because captions can have multiple paragraphs, need to put a full # line height before image number in series in one_file mode. # 2002-03-02 UNIX convention: filenames starting w/ '.' hidden but accessible. # 2002-03-02 It was possible for caption text to get too close to the one_file # mode zoom icons -- changed the spacer column to two ' 's. # 2002-03-02 When captions were multiple paragraphs, in all_dirs mode, the # spacing before and after the last paragraph was identical, not # giving enough of a break between neighboring captions. # 2002-03-01 Just added more space prior to the footer in the modes missing it. # 2002-02-28 Along with background.url, also look for a bgcolor.txt file, and # if neither of those exists, use #CCCCCC rather than the default. # 2002-02-28 At my suggestion, Randy Ray fixed Image::Size version 2.97 to not # call cwd if NO_CACHE is set. Set it in case cwd is broken because # one of our ancestor directories is executable but not readable. # 2002-02-26 Original. ## Modules always used ######################################################### use CGI; use English qw(-no_match_vars); # allow use of names like @ARG rather than @_ use File::Basename; # for fileparse() use POSIX qw(strftime); use warnings; # output warnings for this script but not for modules used ## Initializations ############################################################# $CGI::DISABLE_UPLOADS = 1; $CGI::POST_MAX = 1024; # image_album doesn't use POSTs, so this should be fine $OUTPUT_AUTOFLUSH = 1; # let the browser render as dynamically as possible @MON = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); @WDAY = qw(Sun Mon Tue Wed Thu Fri Sat); ## Prototypes ################################################################## sub case_insensitive_sort; sub construct_url ($$$$$); sub dir_pre_html_start; sub extension_to_column_heading; sub extension_to_title_words; sub filter_html_file; sub html_escape; sub my_start_html; sub optional_dimensions; sub pretty_file_size; sub put_in_spaces_to_allow_line_wrapping; sub recursively_print_ref; sub size_numeric_to_verbal; sub size_verbal_to_numeric; sub sort_columns; sub url_escape; sub zoomed_in_pic; sub zoomed_out_pic; ## Subroutines ################################################################# sub case_insensitive_sort { lc($a) cmp lc($b); } sub construct_url ($$$$$) { my $path = shift; my $all_files_extension_in_path; my %param_val; if ($path =~ m</all_files_extension$>) { $all_files_extension_in_path = shift; } else { $param_val{"all_files_extension"} = shift; } $param_val{"debug"} = shift; $param_val{"disable_dir_thumbs"} = shift; $param_val{"extra_info"} = shift; my $url = $cgi->script_name() . "/"; my $had_an_option = 0; foreach $param_name (sort keys %param_val) { if ($param_val{$param_name}) { if (not $had_an_option) { $url .= "o=="; $had_an_option = 1; } else { $url .= ","; } $url .= url_escape($param_name); if ($param_val{$param_name} ne "1") { $url .= "=" . url_escape($param_val{$param_name}); } } } if ($had_an_option) { $url .= "/"; } if ($path_info =~ m</!/>) { $url .= "!"; } $url .= url_escape($path); if ($all_files_extension_in_path) { $url .= "=" . url_escape($all_files_extension_in_path) . "/"; } $url = html_escape($url); } sub die_pre_html_start { # Strip off the leading path from the script name. ($program_name_no_path = $PROGRAM_NAME) =~ s<.*/><>; $title = "ERROR from $program_name_no_path"; my_start_html(); print $cgi->h1("$title:"); print @ARG; print $cgi->end_html, "\n"; exit 1; } sub extension_to_column_heading { my $extension = shift; my $size; $extension =~ /^(-full-size|-large|-thumbnail)?(\.[^.]+)$/; if (not defined($1)) { $size = "Normal"; } elsif ($1 eq "-full-size") { $size = "Full-Size"; } elsif ($1 eq "-large") { $size = "Large"; } else { # $1 eq "-thumbnail" $size = "Thumbnail"; } $column_heading_url = construct_url("$dir/all_files_extension", $extension, $debug, $disable_dir_thumbs, $extra_info); return "<A HREF=\"$column_heading_url\">" . html_escape($2) . ": $size</A>"; } sub extension_to_title_words { my $extension = shift; my $size; $extension =~ /^(-full-size|-large|-thumbnail)?(\.[^.]+)$/; if (not defined($1)) { $size = "normal-sized"; } elsif ($1 eq "-full-size") { $size = "full-sized"; } elsif ($1 eq "-large") { $size = "large-sized"; } else { # $1 eq "-thumbnail" $size = "thumbnail"; } return "All $size $2 files"; } sub filter_html_file { my $html_file = shift; if (open(HTML_FILE, $html_file) and not -z HTML_FILE) { # File is openable and isn't zero-length. Cat / filter it. my ($child_dir, $parent_dir); if ($mode eq "all_dirs" or $mode eq "one_dir") { if (-e "../image_album-description.html" or -e "../image_album-parent.html") { # Parent page is either all_dirs mode or one_dir mode (if some # funky manual linking is going on) in our parent directory. ($child_dir, $parent_dir) = fileparse($dir); # parent has '/' $up_url = construct_url($parent_dir, undef, $debug, $disable_dir_thumbs, $extra_info); } else { # Parent page is a non-image_album-generated page. $up_url = "../"; } } elsif ($mode eq "all_files") { # Parent page is one_dir mode in our current directory. $up_url = construct_url("$dir/", undef, $debug, $disable_dir_thumbs, $extra_info); } elsif ($mode eq "one_file") { if ($all_files_extension) { # Parent page is all_files mode in our current directory. $up_url = construct_url("$dir/all_files_extension", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } else { # Parent page is one_dir mode in our current directory. $up_url = construct_url("$dir/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } } while (<HTML_FILE>) { # If the user put in an "!UP_URL!" string, we'll substitute the # above-calculated $up_url to go a previous mode. s/!UP_URL!/$up_url/g; print; } close HTML_FILE; } } sub html_escape { # This is a copy & paste of CGI.pm's internal escapeHTML() routine except # that I've changed '"' to escape as '"' instead of '"' since the # latter was accidentally left out of the HTML 3.2 DTD, making # http://validator.w3.org/ complain about pages that have transformed # strings like 'Vinyl 12" Single'. Also removed the here-meaningless # 'dontescape' checking. my $toencode = shift; return undef unless defined($toencode); $toencode=~s/&/&/g; $toencode=~s/\"/&\#34;/g; $toencode=~s/>/>/g; $toencode=~s/</</g; return $toencode; } sub my_start_html { # We want to be able to have arbitrary content in the body_attributes.txt # file and not have to parse it into named parameters for $cgi->start_html, # so we use our own start_html routine which can deal with plain old strings # of attribute values. if (open(DOCTYPE, "doctype.html")) { while (<DOCTYPE>) { if (m<//dtd (.+)//>i) { $html_version = $1; } print; } close DOCTYPE; } else { print "<!DOCTYPE HTML>\n"; } if (not defined $html_version) { $html_version = "HTML 5"; } if (open(LANG, "lang.txt")) { $lang = <LANG>; close LANG; } else { $lang = "en"; } print "<HTML"; if ($html_version !~ /[^X]HTML [23]/i and defined($lang) and $lang =~ /\S+/) { chomp $lang; $lang = html_escape($lang); print " LANG=\"$lang\""; } print ">\n"; print "<HEAD>\n"; # TBD: Can't url_escape() $base since it'll change "http://" to "http%3A//": print "<BASE HREF=\"$base\">\n"; if (open(HEAD_ADDITIONS, "head_additions.html")) { while (<HEAD_ADDITIONS>) { if ($ARG =~ /link.*rel.*=.*stylesheet/i) { $had_a_stylesheet = 1; } print; } close HEAD_ADDITIONS; } print "<META NAME=Generator CONTENT=\n \"image_album version $version, by" . ' Dan Harkless -- http://harkless.org/dan/software/">' . "\n"; print "<TITLE>", html_escape($title), "\n"; if (not $had_a_stylesheet) { print "\n"; } print "\n"; print "; close BODY_ATTRIBUTES; } print ">\n"; } sub optional_dimensions { my $file = shift; my $get_extra_info = shift; if ($have_Image_Info) { # We try Image::Info first, because it can also get EXIF data. my @local_image_info_hashes = Image::Info::image_info($file); if ($get_extra_info) { # We're being called on the main image in one_file mode, so save the # image info hashes so we don't need to waste time re-calling # image_info() when printing the extra info. @image_info_hashes = @local_image_info_hashes; } # Hopefully if there are multiple images in the file (e.g. main image # and thumbnail), the first one's dimensions are always representative # (e.g. it's the main image). Otherwise we'll always have to loop # through all the images and take the max dimensions. return Image::Info::html_dim($local_image_info_hashes[0]) . " "; } elsif ($have_Image_Magick) { # Next try Image::Magick, to avoid the below-described Image::Size bug. my $image_magick = new Image::Magick; if ($image_magick->Read($file)) { return ""; } # Note we have to use this method, and not Ping(), because the latter, # prior to Image::Magick 5.44, sometimes returns dimensions for JPEGs # that are a fraction of their real values (I've seen 100%, 50%, and 25% # dimensions returned). my @geometry = $image_magick->Get("width", "height"); if (scalar @geometry != 2) { return ""; } return "WIDTH=$geometry[0] HEIGHT=$geometry[1] "; } elsif ($have_Image_Size) { # Originally I had Image::Size first, since it's a smaller, one-trick # pony, and potentially faster. It also has seniority and perhaps # popularity on its side, requires fewer prerequisites, and supports a # few more formats. HOWEVER, due to its caching features (in between # versions 2.902 and 2.96), it requires CWD::cwd() to work, which is # bogus for web work, since it requires you to have all directories from # "." up to the root be _readable_ (not just executable) by the user # the webserver is run as. This forces you to make all your directories # (or at least all the ones not containing a file with a name like # "index.html") listable, and you may very well not want to do that. # # Unfortunately checking for Image::Size last isn't enough. If neither # of the above checked-for modules are out there but Image::Size is, and # there are any non-'r' directories in between here and root, cwd() will # run `pwd`, which will output "pwd: cannot determine current # directory!" to stderr (I've suggested to the Cwd.pm author to make # this external error catchable by an eval post-version-2.06). Since we # intentionally duplicate stderr to stdout above, so that fatal error # messages will be visible to the user of the CGI, this means that the # (nonfatal) pwd error message will end up on our HTML output, which is # of course unacceptable. Therefore, we need to temporarily redirect # stderr back to the web log while calling attr_imgsize(). In this # situation, it will return undef and we'll return a null string. # # Above I mentioned Image::Size versions 2.902 through 2.96. For 2.97 # (where my suggested change was implemented) and on, setting NO_CACHE # stops Image::Size from calling cwd, so we do that here. $Image::Size::NO_CACHE = 1; my $eliminate_used_only_once_warning = $Image::Size::NO_CACHE; print STDERR_saved ""; # eliminate bogus "used only once" warning open(STDERR, ">&" . STDERR_saved); $dimensions_str = Image::Size::html_imgsize($file); open(STDERR, ">&" . STDOUT); # go back to making errors visible if (defined $dimensions_str) { return $dimensions_str .= " "; } else { return ""; } } else { # If none of the supported image-dimension-getting modules are out there # / working, just return an empty string. IE will do the rendering # dance, and Netscape won't be able to show the page until all the # images are loaded. return ""; } } sub pretty_file_size { @file_stat = stat(shift); $file_size = $file_stat[7]; if ($file_size > 1_000_000) { return sprintf("%.1f MB", $file_size / 1_000_000); } elsif ($file_size > 1_000) { return sprintf("%.1f kB", $file_size / 1_000); } else { return "$file_size B"; } } sub put_in_spaces_to_allow_line_wrapping { $spaced_version = shift; $spaced_version =~ s/_/ /g; # underscores to spaces $spaced_version =~ s/--/ -- /g; # surround "--" with spaces # Surround '/' with small spaces so that line-wrapping can occur but paths # still look like paths. $spaced_version =~ s[/][ / ]g; return $spaced_version; } sub recursively_print_ref { my $arg = shift; my $arg_type = ref($arg); my $val; if ($arg_type eq "ARRAY") { my $first_one = 1; print "("; foreach $val (@$arg) { if (not $first_one) { print ", "; } else { $first_one = 0; } recursively_print_ref($val); } print ")"; } elsif ($arg_type eq "HASH") { my $first_one = 1; my $key; print "("; foreach $key (sort keys %$arg) { if (not $first_one) { print ", "; } else { $first_one = 0; } print html_escape($key), " = "; recursively_print_ref($$arg{$key}); } print ")"; } else { if ($arg =~ /^([\x00\t\n\r\x20-\x7E\xA0-\xFF]*?)\x00*$/) { # Characters are all ISO Latin 1 (or NUL, which we convert) -- # hopefully this means this value is human-readable (and not just a # selection of binary data that happens to only use Latin 1 values). $arg_without_trailing_NULs = $1; $arg_without_trailing_NULs =~ s/\x00/ /g; # embedded NULs -> spaces print html_escape($arg_without_trailing_NULs); } else { # Binary data -- we're hands-off. (Which unfortunately means that # right now we refuse to print non-Western-language comments. To # fix this, we could do special processing on comment fields (like # the EXIF UserComment) that are defined to allow, for instance, # Japanese comments. We'd also have to fix the charset we output to # be user-definable, though, which means we'd have to move the HTTP # header generation later or take the charset setting out of the # HTTP header and use HTTP-EQUIV.) print "(binary data)"; } } } sub size_numeric_to_verbal { my $numeric_size = shift; if ($numeric_size eq 1) { return "-thumbnail"; } elsif ($numeric_size eq 2) { return ""; } elsif ($numeric_size eq 3) { return "-large"; } else { # $numeric_size eq 4 return "-full-size"; } } sub size_verbal_to_numeric { my $verbal_size = shift; if (not defined $verbal_size or $verbal_size eq "") { return 2; # no special - means it's the normal-sized version } elsif ($verbal_size eq "-thumbnail") { return 1; } elsif ($verbal_size eq "-large") { return 3; } else { # $verbal_size eq "-full-size" -- caller must restrict to legal vals return 4; } } sub sort_columns { # We want to sort the columns in the table by extension first # (alphabetically), then by size (thumbnail, normal, large, full-size). # "$a" and "$b" are the standard arg. names to use in subroutines passed to # sort. Note we have already lower-cased the extensions by this point, so # no need to compare case-insensitively. $a =~ /(-full-size|-large|-thumbnail)?\.(.+)/; my $a_size = size_verbal_to_numeric($1); my $a_extension = $2; $b =~ /(-full-size|-large|-thumbnail)?\.(.+)/; my $b_size = size_verbal_to_numeric($1); my $b_extension = $2; $a_extension cmp $b_extension or $a_size <=> $b_size; } sub url_escape { # This is a copy & paste of CGI.pm's internal escape() routine, except that # that routine was overly restrictive -- it disallowed [!~*'()] (presumably # because they're shell-special characters) even though they're perfectly # legal to appear unquoted in URLs. I've also caused '/' not to be quoted # since the only way we're going to get it in the URL and have it not mean # the normal URL path separator character is for it to be in the PATH_INFO # or query parts of the URL, where it's legal to appear unescaped. Also de- # escaped ',' since there appears to be no reason in our case to escape it. my $toencode = shift; return undef unless defined($toencode); $toencode =~ s<([^a-zA-Z0-9_.\-!~*\'()/,])>eg; return $toencode; } sub zoomed_in_pic { my $file_root = shift; my $extension = shift; my $file_zoomed_in_extension; my $verbal_size = ""; $extension =~ /^(-full-size|-large|-thumbnail)?(\.[^.]+)$/; if (defined($1)) { $verbal_size = $1; } $file_type = $2; $numeric_size = size_verbal_to_numeric($verbal_size); for ($i = $numeric_size; $i < 4; $i++) { $next_bigger_verbal_size = size_numeric_to_verbal($i + 1); $file_zoomed_in_extension = $file_hash{$file_root}{"$next_bigger_verbal_size$file_type"}; if (defined($file_zoomed_in_extension)) { return "$file_root$file_zoomed_in_extension"; } } # Already maximum size. return undef; } sub zoomed_out_pic { my $file_root = shift; my $extension = shift; my $file_zoomed_out_extension; my $verbal_size = ""; $extension =~ /^(-full-size|-large|-thumbnail)?(\.[^.]+)$/; if (defined($1)) { $verbal_size = $1; } $file_type = $2; $numeric_size = size_verbal_to_numeric($verbal_size); for ($i = $numeric_size; $i > 1; $i--) { $next_smaller_verbal_size = size_numeric_to_verbal($i - 1); $file_zoomed_out_extension = $file_hash{$file_root}{"$next_smaller_verbal_size$file_type"}; if (defined($file_zoomed_out_extension)) { return "$file_root$file_zoomed_out_extension"; } } # Already minimum size. return undef; } ## HTTP header ################################################################# # We'll output the current timestamp (actually the timestamp as of a minute ago # -- see below) as the Last-Modified header for the page. This will allow "Once # per session" browser caching of the page. It won't allow cross-session # caching, or allow proxy caches to correctly reload the page only when it # changes due to an image file, user customization file, or the script itself # changing. # # To get caching working in the general case would require that we change the # script so that it outputs HTML to a string instead of to the HTTP connection, # and notes modification dates of all applicable files along the way, and then # waits until the last minute to output the HTML all in one fell swoop (rather # than letting the browser dynamically render), or output a 304 header if # nothing is newer than the If-Modified-Since timestamp specified in the HTTP # request header. # # All the work wouldn't necessarily be worth it, since the HTML pages we # generate tend to be small compared to the image data, and caching works fine # for the latter since that's a matter solely between the HTTP server and # client. The time savings for being able to cache the HTML in the above # situations wouldn't be that great since we'd still have to prepare the output # (unless we further complicated the code by having two passes when # If-Modified-Since is specified, the first only looking at all the different # file timestamps) just to determine not to send it, so the only time savings # would be that of the actual _transfer_ of the HTML (which would be especially # trivial in my situation until recently of a slow non-mod_perl server yet a # fast web browsing Internet connection). # # As mentioned above, we have to fake our time as being a minute in the past # because Netscape (oops, forgot to note which versions) has a problem where if # the Last-Modified header is newer than the Date header, the page won't be # considered cacheable. Apache takes the timestamp it's going to stick in the # Date header *before* it calls us, so we need to pretend to be in the past (the # amount by which we do so is arbitrary, but needs to be more than the time it # takes to compile the script and execute up to this point -- hopefully a minute # should always be okay). Not sure if Netscape's behavior is per the HTTP # specs, but FWIW, IE does not have this problem. $cgi = new CGI; if ($cgi->cgi_error()) { # Surprisingly, current browsers (as of 2003) don't respond properly to HTTP # status code 413, so we'll generate an HTML error page rather than calling # $cgi->header($cgi->cgi_error()). die_pre_html_start($cgi->cgi_error()); } # Note we can't use POSIX::strftime() because it'll output an illegal date if # the default locale the web server's running under isn't English. ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime(time - 60); $HTTP_timestamp = sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", $WDAY[$wday], $mday, $MON[$mon], $year + 1900, $hour, $min, $sec); # Do this as early as possible in case any errors occur: browser won't be happy # if we print them out before the HTTP header. print $cgi->header(-charset => "UTF-8", -Last_Modified => $HTTP_timestamp, -type => "text/html"); # Duplicate stderr to stdout so the user of the CGI will see any errors, and # they can report them to the webmaster. Before we do that, though, save STDERR # as STDERR_saved, since there's one instance where we need to temporarily stop # outputting errors to stdout and have them go silently to the web log. open(STDERR_saved, ">&" . STDERR); open(STDERR, ">&" . STDOUT); ## Modules optionally used ##################################################### if (eval "require Image::Info;") { # Look for Image::Info first, since it also allows us to get EXIF data. $have_Image_Info = 1; } elsif (eval "require Image::Magick;") { # Next look for Image::Magick. $have_Image_Magick = 1; } elsif (eval "require Image::Size;") { # Look for Image::Size last, because of a bug in older versions of it, # discussed above in the NOTES section and in optional_dimensions(). $have_Image_Size = 1; } ## Parameters ################################################################## $path_info = $cgi->path_info; if (not $path_info) { $path_info = "."; } $path = $path_info; if ($path =~ m<^(.*)/all_files_extension=(.*)/$>) { $path = $1; $all_files_extension = $2; } $border_on_debug = ""; # Remove "o==.../" pseudo-directory from the path. if ($path =~ m<^/o==([^/]*)/(.*)$>) { if (defined($2)) { $path = $2; } else { $path = ""; } @options = split /,/, $1; for ($i = 0; $i <= $#options; $i++) { if ($options[$i] =~ /all_files_extension=(.*)/) { $all_files_extension = $1; } elsif ($options[$i] eq "debug") { $debug = 1; # Only turn on verbose warnings during debugging, due to major # performance hit. use diagnostics; $border_on_debug = " CLASS=bordered"; } elsif ($options[$i] eq "disable_dir_thumbs") { $disable_dir_thumbs = 1; } elsif ($options[$i] eq "extra_info") { $extra_info = 1; } } } # In my testing with Apache, PATH_INFO always comes down with one initial '/', # even if you put multiple consecutive '/'s at the beginning of the PATH_INFO # part of the URL or use "/%2F". This would make it impossible to distinguish # between absolute and relative paths. Therefore, we treat PATH_INFO as # relative by default and then special-case "/!/" to signify the web root. We # also clip off the final '/' here because it isn't actually part of the # directory or file name. $path =~ s<^/><>; $path =~ s<^!><>; $path =~ s<>; $base = $cgi->url(); if ($path =~ m<^/>) { # Absolute dir specified -- remove all after "protocol://" for # base URL. $base =~ s<^([^/]+//[^/]+).*$><$1>; $document_root = $ENV{DOCUMENT_ROOT}; if (not defined $document_root) { die_pre_html_start("Absolute directory specified yet webserver doesn't" . ' set $ENV{DOCUMENT_ROOT}.'); } $filesystem_path = "$document_root/$path"; } else { # Relative dir specified -- for the base, just remove our script name and # let the webserver interpret any '../'s that get appended to resulting URL. $base =~ s; $filesystem_path = $path; } # One might think the below code would allow attackers to determine the -d # status of files outside the web root, but on Apache, at least, this is not the # case -- it'll return a 400 or 404 error if enough '../'s are used to try to # escape the root. if (-d $filesystem_path) { $dir = $path; $filesystem_dir = $filesystem_path; } elsif (-e $filesystem_path) { ($file, $dir) = fileparse($path); $dir =~ s<>; ($file, $filesystem_dir) = fileparse($filesystem_path); $filesystem_dir =~ s<>; } else { die_pre_html_start('"' . html_escape($path) . "\": $OS_ERROR."); } if (not chdir($filesystem_dir)) { die_pre_html_start('"' . html_escape($dir) . "\": $OS_ERROR."); } if (-e "image_album-parent.html") { $mode = "all_dirs"; } elsif (-e "image_album-description.html") { if (defined $file) { $mode = "one_file"; } elsif (defined $all_files_extension) { $mode = "all_files"; } else { $mode = "one_dir"; } } else { die_pre_html_start("Sorry, " . html_escape($dir) . " contains" . " neither an" . " image_album-description.html nor an" . " image_album-parent.html file."); } # We need to pretend this document was fetched from the specified directory so # that relative links in the customizable .html and .url files will work. if ($dir eq ".") { $dir_as_leading_path = ""; } elsif ($dir =~ m) { $dir_as_leading_path = $dir; } else { $dir_as_leading_path = $dir . "/"; } $base .= $dir_as_leading_path; $title = "Image Album"; if ($mode eq "all_dirs") { if ($dir_as_leading_path) { $title .= "s in: $dir"; } else { $title .= "s:"; } $heading_1 = put_in_spaces_to_allow_line_wrapping($title); } elsif ($mode eq "one_dir") { $title .= ": $dir"; $heading_1 = put_in_spaces_to_allow_line_wrapping($title); if ($disable_dir_thumbs) { $title .= ": No-Thumbnail Index"; } else { $title .= ": Index"; } } elsif ($mode eq "all_files") { $title .= ": $dir"; $heading_1 = put_in_spaces_to_allow_line_wrapping($title); $title .= ": " . extension_to_title_words($all_files_extension); } elsif ($mode eq "one_file") { if (not -e $file) { # TBD: I discovered by accident that $OS_ERROR gets set after a "not -e" # check (it wasn't in the documentation I was looking at). Does it work # on non-UNICES, as well, though? die_pre_html_start('"' . html_escape($file) . "\": $OS_ERROR."); } @file_stat = stat(_); # '_' means use the stat we got during -e operation $title .= ": $dir/$file"; } else { die_pre_html_start("Unknown mode \"" . html_escape($mode) . "\"."); } if (open(TITLE_TEMPLATE, "title_template.txt")) { # If title_template.txt file is readable, we need to use it as a template # for formatting the $title. (Zero-length allowed.) chomp($title_template = ); # get 1st line minus '\n', if any close TITLE_TEMPLATE; $title_template =~ s/!GENERATED_TITLE!/$title/g; $title = $title_template; } ## Do pre-HTML-start dir/file operations ####################################### if (not opendir(DIR, ".")) { die_pre_html_start('"' . html_escape($dir) . "\": $OS_ERROR."); } while ($dir_entry = readdir DIR) { if (not $dir_entry =~ /^\./) { if (-d $dir_entry) { if ($mode eq "all_dirs") { push @subdirs, $dir_entry; if (-e "$dir_entry/representative_thumbnail.url") { $at_least_one_representative_thumbnail = 1; } } } elsif (not $dir_entry =~ /^.*\.(cgi|html|pd|pl|txt|url)$/ and $dir_entry =~ /^(.+?)((-full-size|-large|-thumbnail)?\.[^.]+)$/) { # Can't just set this hash entry to 1 -- need to set it to the # original spelling of the extension (e.g. "JPG" or "Jpg") since we # throw away capitalization on the second hash key in order to merge # files with all different capitalizations of a given extension into # a single file format category. $file_hash{$1}{lc($2)} = $2; if ($mode eq "one_dir") { $extension_hash{lc($2)} = 1; if (defined $3 and $3 eq "-thumbnail") { $at_least_one_thumbnail_here = 1; } } } } } closedir DIR; ## HTML start ################################################################## my_start_html(); ## Body ######################################################################## # TBD: Use JavaScript to make "large" size be a scaling down of full-size to fit # in the browser window. if ($mode eq "all_dirs") { print $cgi->h1($heading_1), "\n"; # If album parent directory description file is readable and isn't # zero-length, cat it. if (open(DESCRIPTION, "image_album-parent.html") and not -z DESCRIPTION) { print while ; close DESCRIPTION; } print "

\n"; $latest_subdir_modtime = 0; foreach $subdir (sort case_insensitive_sort @subdirs) { if (-e "$subdir/image_album-parent.html" or -e "$subdir/image_album-description.html") { @subdir_stat = stat($subdir); # TBD: error handling for deleted dir $subdir_modtime = $subdir_stat[9]; if ($subdir_modtime > $latest_subdir_modtime) { $latest_subdir_modtime = $subdir_modtime; } $subdir_url = construct_url("$dir_as_leading_path$subdir/", undef, $debug, $disable_dir_thumbs, $extra_info); if (not $opened_the_table) { print "\n"; $opened_the_table = 1; } print "\n"; if (not $disable_dir_thumbs) { if (open(REP_THUMB, "$subdir/representative_thumbnail.url") and not -z REP_THUMB) { # Use the "teaser" thumbnail requested. chomp($rep_thumb_img = ); # 1st line w/o any '\n' close REP_THUMB; print "' 
                      . html_escape($rep_thumb_img) . '\n"; print "  \n"; } elsif ($at_least_one_representative_thumbnail) { print "  \n"; } } print "" . html_escape($subdir) . "\n"; print "\n"; if ($disable_dir_thumbs) { print " \n"; # spacer } else { print " \n"; # spacer } } } if ($opened_the_table) { print "\n"; } else { print "

Sorry, there are no subdirectories of ", html_escape($dir), "\n", " containing image_album-description.html or\n", " image_album-parent.html files.

\n"; } } elsif ($mode eq "one_dir") { print $cgi->h1($heading_1), "\n"; # If album description file is readable and isn't zero-length, cat it. if (open(DESCRIPTION, "image_album-description.html") and not -z DESCRIPTION) { print "

Album Description

\n"; print while ; close DESCRIPTION; } print "

Instructions

\n"; print "

Simply click on any file size figure to get a page with just\n"; print "that image, including its caption (if any),\n"; if ($have_Image_Info) { print "a toggle for additional info (e.g. EXIF data),\n"; } print "and arrow buttons that will take you to the previous and next\n"; print "images.\n"; $optional_other = ""; if ($at_least_one_thumbnail_here) { if ($disable_dir_thumbs) { print "Clicking on the \"File\" column heading will expand\n"; print "the table below to include inline thumbnails.\n"; } else { print "Clicking on the \"File\" column heading will collapse\n"; print "the table below to exclude the inline thumbnails.\n"; } $optional_other = "other "; } print "Clicking on one of the $optional_other column headings will get\n"; print "you a page containing ALL the images in that particular file\n"; print "format and size, with their captions (if any) printed alongside\n"; print "them.

\n"; @extension_columns = sort sort_columns keys(%extension_hash); print "

Images

\n"; print "\n"; print "\n"; if ($at_least_one_thumbnail_here) { if (not $disable_dir_thumbs) { print " \n"; } print " \n"; } else { print "\n"; } foreach $extension_column (@extension_columns) { print " \n"; } print "\n"; foreach $file_root (sort case_insensitive_sort keys(%file_hash)) { $already_named_anchor_on_this_row = 0; print "\n"; if (not $disable_dir_thumbs and $at_least_one_thumbnail_here) { # Inline the thumbnail for this image, if there is one. If # there's a thumbnail in more than one format, we just take the one # in earliest alphabetical order. print " \n"; } print " \n"; foreach $extension (@extension_columns) { print " \n"; } print "\n"; } print "
"; $toggle_thumbs_url = construct_url("$dir/", $all_files_extension, $debug, $disable_dir_thumbs ? 0 : 1, $extra_info); print "FileFile" . extension_to_column_heading($extension_column) . "
"; foreach $extension (@extension_columns) { if ($extension =~ /-thumbnail\.[^.]+$/) { $file_orig_extension = $file_hash{$file_root}{$extension}; if (defined($file_orig_extension)) { # Put the named anchor around the thumbnail, if # available, to prevent it from being partially off the # top of the page. print ''; print '' . 
			  html_escape('; print ''; $already_named_anchor_on_this_row = 1; last; } } } print "" . html_escape($file_root) . ""; $file_orig_extension = $file_hash{$file_root}{$extension}; if (defined $file_orig_extension) { $one_file_url = construct_url("$dir/$file_root$file_orig_extension/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); print ""; print pretty_file_size("$file_root$file_orig_extension"); print ""; $dim = optional_dimensions("$file_root$file_orig_extension"); if ($dim ne "") { $width = 0; if ($dim =~ /width\s*=["'](\d+)['"]\s*height\s*=["'](\d+)['"]/) { $width = $1; $height = $2; } elsif ($dim =~ /height\s*=["'](\d+)['"]\s*width\s*=["'](\d+)['"]/) { $height = $1; $width = $2; } # TBD: else output an error? if ($width != 0) { print "
\n($width x $height)"; } } } print "

\n"; } elsif ($mode eq "all_files") { print $cgi->h1($heading_1), "\n"; # If album description file is readable and isn't zero-length, cat it. if (open(DESCRIPTION, "image_album-description.html") and not -z DESCRIPTION) { print "

Album Description

\n"; print while ; close DESCRIPTION; } print "

Instructions

\n"; print "

\n"; print "Click any image to go to its individual page. The image will\n"; print "appear in the next size up from that shown here, when available.\n"; print "

\n"; print "

Images

\n"; print "\n"; foreach $file_root (sort case_insensitive_sort keys(%file_hash)) { $file_orig_extension = $file_hash{$file_root}{$all_files_extension}; if (defined $file_orig_extension) { print "\n"; $zoomed_in_pic = zoomed_in_pic($file_root, $file_orig_extension); if (defined $zoomed_in_pic) { $individual_pic = $zoomed_in_pic; } else { $individual_pic = "$file_root$file_orig_extension"; } $individual_page = construct_url("$dir/$individual_pic/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); print "'; print "\""\n"; print "  "; # spacer print ""; if (open(CAPTION, "$file_root-caption.html")) { # If caption file is readable, cat it. (Zero-length allowed.) print while ; close CAPTION; } else { # Otherwise, just print the name of the file to the right of the # image hoping that it's named descriptively. print html_escape("$file_root$file_orig_extension"); } print "\n"; print "\n"; print ' ', "\n"; # spacer } } print "\n"; } elsif ($mode eq "one_file") { @file_roots = sort case_insensitive_sort keys(%file_hash); # Put the arrows above the image so they won't move around as the size of # individual photos vary. This way you can just hover the pointer over the # Right arrow and click from image to image without having to keep # adjusting. print "\n"; print "\n"; print ""; # Find the index of the current file in the alphabetized file root list. $file =~ /^(.+?)((-full-size|-large|-thumbnail)?\.[^.]+)$/; $file_root = $1; $extension = lc($2); $i = 0; while ($i <= $#file_roots) { if ($file_roots[$i] eq $file_root) { last; } $i++; } $file_index = $i; # See if there's a previous file in the list of the same format and size # class. undef $prev_file; $i--; while ($i >= 0) { $file_orig_extension = $file_hash{$file_roots[$i]}{$extension}; if (defined $file_orig_extension) { $prev_file = "$file_roots[$i]$file_orig_extension"; last; } $i--; } if (defined $prev_file) { $alt_tag = "Prev"; $prev_page = construct_url("$dir/$prev_file/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } else { $alt_tag = "Index"; $prev_page = construct_url("$dir/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } print ""; if (open(PREV, "prev.url") and not -z PREV) { # Use the graphic (e.g. left-pointing arrow) requested. chomp($prev_img = ); # get 1st line minus '\n', if any close PREV; print "\"$alt_tag\"'; } else { # User doesn't have, or forgot to request the use of a "Previous" # graphic -- just use a textual link. Print it in a monospaced font # (and with "Previous" shortened to "Prev") to keep the width of first # and third columns as even as possible (though they aren't quite even # when the text here is "Index"), and to distinguish from the # description font. print "$alt_tag"; } print ""; print "\n"; print "    \n"; # spacer column $html_escaped_file = html_escape($file); $url_escaped_file = url_escape($file); print ",
      \n"; print "    \n"; # spacer column print "\n"; # See if there's a next file in the list of the same format and size class. undef $next_file; $i = $file_index + 1; while ($i <= $#file_roots) { $file_orig_extension = $file_hash{$file_roots[$i]}{$extension}; if (defined $file_orig_extension) { $next_file = "$file_roots[$i]$file_orig_extension"; last; } $i++; } if (defined $next_file) { $alt_tag = "Next"; $next_page = construct_url("$dir/$next_file/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } else { $alt_tag = "Index"; $next_page = construct_url("$dir/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); } print ""; if (open(NEXT, "next.url") and not -z NEXT) { # Use the graphic (e.g. right-pointing arrow) requested. chomp($next_img = ); # get 1st line minus '\n', if any close NEXT; print "\"$alt_tag\"'; } else { # User doesn't have, or forgot to request the use of a "Next" graphic -- # just use a textual link. Print it in a monospaced font to keep the # width of first and third columns as even as possible (though they # aren't quite even when the text here is "Index"), and to distinguish # from the description font. print "$alt_tag"; } print ""; print "\n"; print "\n"; # Spacer row: print " \n"; # Caption and zoom controls row (actually zoom control span multiple rows): print "\n"; print ""; $zoomed_out_pic = zoomed_out_pic($file_root, $extension); if (defined $zoomed_out_pic) { $zoomed_out_url = construct_url("$dir/$zoomed_out_pic/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); print ""; if (open(ZOOM_OUT, "zoom_out.url") and not -z ZOOM_OUT) { # Use the zoom out graphic requested. chomp($zoom_out_img = ); # get 1st line minus '\n', if any close ZOOM_OUT; print "\"Zoom'; } else { # User doesn't have, or forgot to request the use of a zoom out # graphic -- just use a textual link. Print it in a monospaced font # to keep the width of first and third columns close to even (we # could keep them perfectly even by using "Zoom -" or "Dec. Res." # here instead). print "Zoom Out"; } print ""; } print "\n"; print "\n"; print ""; $had_a_caption = 0; if (open(CAPTION, "$file_root-caption.html")) { # If caption file is readable, cat it. (Zero-length allowed.) $first_line = ; if (eof(CAPTION)) { if ($first_line) { # The caption file is only one line, so we'll center it so it # doesn't look goofy. print "
\n"; print $first_line; print "
\n"; } } else { # Print the first line, then the rest, left-justified. print $first_line; print while ; } close CAPTION; $had_a_caption = 1; } else { # Otherwise, just print the name of the file below the image hoping # that it's named descriptively. print "

" . html_escape("$file") . "

"; } print "\n"; print "\n"; print ""; $zoomed_in_pic = zoomed_in_pic($file_root, $extension); if (defined $zoomed_in_pic) { $zoomed_in_url = construct_url("$dir/$zoomed_in_pic/", $all_files_extension, $debug, $disable_dir_thumbs, $extra_info); print ""; if (open(ZOOM_IN, "zoom_in.url") and not -z ZOOM_IN) { # Use the zoom in graphic requested. chomp($zoom_in_img = ); # get 1st line minus '\n', if any close ZOOM_IN; print "\"Zoom'; } else { # User doesn't have, or forgot to request the use of a zoom in # graphic -- just use a textual link. Print it in a monospaced font # to keep the width of first and third columns close to even (we # could keep them perfectly even by using "Zoom +" or "Inc. Res." # here instead). print "Zoom In"; } print ""; } print "\n"; print "\n"; print " \n"; # spacer row # Image number row: print "#", $file_index + 1, " / ", scalar(@file_roots), "\n"; print "\n"; if ($have_Image_Info and @image_info_hashes) { print "
\n"; print "\n"; print "\n"; print "\n"; print 'Extra info:', "\n"; print "\n"; print "\n"; if ($extra_info) { # A nested table isn't necessary for this layout, but simplifies the # logic a lot. print "\n"; $number_of_images_in_file = scalar @image_info_hashes; foreach $i (1 .. $number_of_images_in_file) { if ($i > 1) { print " \n"; # spacer row } if ($had_a_caption or $number_of_images_in_file > 1) { print ""; if ($had_a_caption) { # A caption was displayed instead of the filename, so # the filename won't be redundant info. Print it. print html_escape($file), ": "; } print "Subimage $i of $number_of_images_in_file"; print "\n"; } $hash_ref = $image_info_hashes[$i - 1]; foreach $key (sort keys %$hash_ref) { print "\n"; print "" . html_escape($key) . ": \n"; print ""; recursively_print_ref($$hash_ref{$key}); print "\n"; print "\n"; } } print ""; } else { print "Off"; } print "\n"; print "\n"; print "\n"; } } ## Footer ###################################################################### if (open(HR, "hr.html")) { print while
; close HR; } else { print "
\n"; } print "\n"; print "\n"; print ""; # If footer file is readable and isn't zero-length, cat it, substituting any # instances of !UP_URL! appropriately for the mode. filter_html_file("footer.html"); if ($mode eq "all_dirs") { ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($latest_subdir_modtime); print "Last album modification: " . strftime("%B %e, %Y", $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); } elsif ($mode eq "one_file") { ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($file_stat[9]); # We don't say "Time of image" or something similar here since if the image # is a photo that was scanned in rather than coming from a digital camera, # the timestamp on the file probably has nothing to do with the time of the # photo. Even if the photo did come from a digital camera, the person may # have wiped out the timestamp with an FTP upload. Hopefully in that case # the original timestamp is still available as EXIF data that the user can # cause to be displayed. print "File timestamp: " . strftime("%A, %B %e, %Y, %r", $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); } else { # $mode eq "one_dir" or $mode eq "all_files" @dir_stat = stat "."; # can't avoid by stat()ing DIR above: not filehandle ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($dir_stat[9]); print "Album last modified: " . strftime("%B %e, %Y", $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); } print "\n"; print "  \n"; # spacer print ""; $self_url = url_escape($cgi->self_url); # TBD: doesn't escape '?' print 'Validated $html_version + ", 'CSS
\n"; print "Generator:"; print ' ' . 'image_album'; print "\n"; print "\n"; print "\n"; ## HTML end #################################################################### print $cgi->end_html, "\n";