#!/usr/bin/perl -w # # image_album # # AUTHOR: # Dan Harkless http://harkless.org/dan/software/ # # COPYRIGHT: # This file is Copyright (C) 2003 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.cgi?dir=photos # # But for the purposes of discussion, let's consider this hypothetical # directory/file structure: # # /cgi-bin/ # image_album # # /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.png # Xmas01-1-Christmas_tree-caption.html # Xmas01-1-Christmas_tree-large.jpg # Xmas01-1-Christmas_tree-thumbnail.jpg # Xmas01-2-stockings.jpg # Xmas01-2-stockings.png # Xmas01-2-stockings-caption.html # Xmas01-2-stockings-large.jpg # Xmas01-2-stockings-thumbnail.jpg # # Given that, this URL: # # http:///cgi-bin/image_album?dir=../photos # # is the simplest way to call image_album and will display a page called # "Image Albums in: ../photos". 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 # # As a special case, if you click on "File", the table will be expanded to # include one more column up-front with the thumbnail for each image inlined. # 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 by an FTP 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. # ferry_to_Magic_Kingdom, since it's the first in the album, will have links # called "Index" and "Next", and 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: .png: # File Thumbnail Normal Large Large # Xmas01-1-Christmas_tree 2.4 kB 39.6 kB 230.2 kB 1.6 MB # Xmas01-2-stockings 1.8 kB 31.8 kB 204.8 kB 1.4 MB # # Here we have images available in three different resolutions and two # different file formats (judging by the PNG format, it looks like the files # were film-based photos that were scanned in at a fairly high resolution # rather than being fairly low-resolution digital camera photos as in 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, let's move on # to the files and CGI parameters 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. # # -large. # If . is the normal-sized image, this is a large, # high-resolution version -- e.g., suitable for printing. # # -thumbnail. # If . is the normal-sized image, this is a # small-sized version suitable for use as a thumbnail / preview image. # # 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|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 directory (specified # by the "dir" parameter) 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, # this file 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 # (that 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 one of these files 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.
#
# OPTIONAL INPUT FILES:
#   -caption.html
#     If this file exists, it will 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 (e.g. with whitespace or 

and

lines) make it longer # than one line. # # 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. The HTML-3.2-legal 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. # # If you want to use BODY atributes that don't exist in HTML 3.2, be sure to # specify the appropriate HTML version with the doctype.html file. # # 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 specified by the "dir" parameter (see below). 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 neither body_attributes.txt nor stylesheet.url exists, we put out the # following tag: # # # # #CCCCCC (note the necessary quotes due to the '#' character) is a # "web-safe" light grey that makes for a good neutral background and will # usually show off the edges of the images better than white (since, e.g., # digital camera CCDs tend to overload and white-out skies). # # If body_attributes.txt doesn't exist but stylesheet.url does, we'll just # put out a simple tag with no attributes. # # doctype.html # If this file does not exist, we use: # # # # because all the HTML we generate is HTML-3.2-legal. If you want to use # features from a later HTML spec (including stylesheet specification with # the stylesheet.url input file), put the appropriate DOCTYPE declaration # in this file. We'll use the HTML version you specify 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{}". 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. For the top-level page generated by image_album, # "UP_URL{}" will simply be replaced by "" (the most common # example would be "UP_URL{../}" being transformed to just "../"). As you # descend down into individual image album directories and individual image # pages, however, the URL will be replaced entirely, by a link to # image_album using the appropriate parameters to generate the logical # parent page (for instance, the parent of a "one_dir" page would be an # "all_dirs" page, to use the mode terminology described below, and the # parent of a "one_file" page would be either a "one_dir" page or an # "all_files" page, depending on how the user navigated). If you don't want # this behavior, and would rather have your "Up" link skip completely out of # all the image_album pages, just don't use UP_URL{}. # # The one unfortunate thing about our UP_URL processing is that because we # have to pass the all_files_extension parameter (see below) on to one_file # mode pages, so they'll know whether to go up to all_files mode or to # one_dir mode, if you visit one_file pages from all_files mode, the # corresponding links on the one_dir page won't be in the "VLINK" color when # you go back to it (since those links do not pass all_files_extension). If # we were to feel this were the worse evil, we might want to fix it by # making the convention be that going up from a one_file page *always* goes # to one_dir mode (or maybe always, as long as all_dirs_depth > 0, so we # wouldn't confuse the people who're visiting image_album installations # that've been set up to have all_files as the top-level mode). But # "VLINKing" of the URLs on the index page can also be irregular due to the # user toggling the "extra_info" parameter on or off and then navigating, so # we should probably just accept that we can't always get the VLINK color # when we'd like it. # # hr.html # If you want to use a graphical horizontal rule in the footer rather than # the usual one generated by
, 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 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
tag (or more complicated HTML if you're # doing a graphical rule, as per the above paragraph). # # 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 # (actually it'll usually 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 is the *parent* directory of that image # directory) from the current album that's considered to be "representative" # of the album as a whole. 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. # # stylesheet.url # If you want to use one or more stylesheets, put their URLs in this file, # one per line, and we'll add to the # for each one. If you do this, be sure to specify an appropriate # DTD in doctype.html, such as: # # # # since the default DTD we put out is for HTML 3.2, and it doesn't use # stylesheets. # # 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 , not on the second copy that # appears as an <H1> in some modes. # # zoom_in.url # Like 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. # # PARAMETERS: # all_dirs_depth # This parameter is for internal use only -- you shouldn't have to worry # about it. It's there to keep track of how many levels of all_dirs modes # deep we are (since your image album directory structure is not limited to # being one level deep, but can be arbitrarily-structured), so that UP_URL{} # can be correctly determined. # # all_files_extension # This parameter is for internal use only unless you've decided you want # all_files mode to be the top-level page image_album generates. In that # case, set it to the desired extension (e.g. "-thumbnail.jpg"). # # debug # This parameter is only set while debugging this script. It does things # like make invisible table borders visible and enable verbose diagnostics. # # dir # This is the only parameter that most people will have to mess with -- just # set up your directories like in the example directory structure above, # point dir to the top-level directory (e.g. ".../image_album?dir=photos"), # and away we'll go. If not specified, this defaults to ".". # # dir_thumbs # This is the only other parameter the average person might want to mess # with. By default, the Index (mode=all_files) pages do not include # Thumbnails. This is intentional because large image albums (some of mine # are hundreds of images) would take forever to load for people with # low-bandwidth connections. Those people would generally rather start with # a text-only Index, then click on the first thumbnail-sized image, and # browse through the images one-at-a-time, hitting "Zoom In" where desired. # # Anyone who wants the thumbnails to appear on the Index page (personally I # prefer to browse from all_files mode, so loading thumbnails on the Index # page is a waste of time) can click on the "File" column heading, as per # the on-screen instructions. # # If that's not acceptable to you, however, because, say, all your albums # are small, or everyone who might be visiting your albums have broadband # connections, you can set the default thumbnail display to on (still # togglable by clicking on "File") by adding "&dir_thumbs=1" to the # parameters when constructing the top-level URL to your image albums. # # extra_info # If the Image::Info module is available, this parameter 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 parameter unless you want # it to default to on ("1") rather than off ("0"). # # file # An internal-use parameter specifying the file to show in one_file mode. # # mode # As mentioned above, most people don't need to explicitly set this # parameter. It defaults to all_dirs, so point dir at your top-level # directory and things will be good, assuming you use a directory structure # like the above and want the default mode transitions. # # If for some reason you want a mode other than all_dirs to be the top-level # mode (e.g. because you only have a single image album), however, you can # explicitly set this. The modes are as follows: # # all_dirs # All sudirectories of the specified 'dir' that 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. # # 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 # if dir_thumbs=1. # # 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. # # 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. # # modified # This is effectively a dummy parameter. image_album doesn't ever look at # what this is set to, and the only time it puts it in a URL that it # generates is in links on all_dirs mode pages. The point of it is to # change URLs of albums (or album parent directories) whenever the # directories for those albums (or album parent directories) are changed. # Simply, the last modification time of the directory is embedded in the # URL, as in: # # http://[...]/image_album.cgi?dir=foo&mode=one_dir&modified=2002-03-10 # # The whole point of this has to do with LINK vs. VLINK colors as displayed # by web browsers. Once a web surfer has visited a particular album, its # link will be displayed in the VLINK color forever more (or at least until # the "keep links in history for X days" parameter has expired -- hopefully, # most surfers are savvy enough to bump up the ridiculously low default # value some browsers set that parameter to). But what if the webmaster # updates an album, e.g. by adding captions or whole new photos? The web # surfer wouldn't have any way of knowing the album's been updated, unless # the webmaster put notes in the image_album-parent.html file or on a # preceding page, and having to do that manually would be pretty onerous. # # Therefore, we let image_album and the browser automatically inform the web # surfer of modified albums -- the URL will change due to the new date in # the "modified" parameter, the link will go back to being in LINK color, # and the web surfer will know to re-check out the album (if they're so # inclined). # # Note that I use this scheme throughout my personal web page. For static # HTML and text files, I append "?<YYYY>-<MM>-<DD>", which the web server # effectively ignores for static pages, but which is useful to the web # surfer for the reasons described above. Likewise, on my static page which # links to my top-level image_album-generated page, I manually set the # "modified" parameter to the "Last subdirectory modification:" date printed # in the footer of that all_dirs page. # # Now, we could have image_album put the "modified" parameter into links to # individual images as well, so that if an image is changed the user will # know about it, but this would involve a whole lot of "stat" operations for # large albums, and wouldn't be of as much use as for links to whole albums, # since there are lots of tweakable parameters in the image viewing modes # that render the LINK vs. VLINK color distinction unreliable even _without_ # files being changed, which is unfortunate, but an unescapable consequence # of the HTTP event model. # # NOTES: # You don't need to name your directories as I do above, with leading dates or # date ranges -- I just do that so they'll be listed in chronological order in # the all_dirs listing. If you do use a similar naming scheme, however, 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 "<FONT SIZE=1> </FONT>/<FONT SIZE=1> # </FONT>" so that line wrapping can occur but it'll still look like a # pathname. # # On a related note, you don't have to name your files like I do above, as # <event>YY-N[N[N]]-<description>.<extension>. The only file naming # restriction is for the <file_root>-large.<extension> and # <file_root>-thumbnail.<extension> files. In the future I might add support # for user customization files or paramters 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>_med.<extension>, and <file_root>_small.<extension> and not feel # like renaming them. # # 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 "." and "/" 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. # # 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 # PNGFs 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 -- 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 parameter 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). # # 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). # # VERSION: $version = "2003-06-15"; # # DATE MODIFICATION # ========== ================================================================== # 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; # allow long English names like $PROGRAM_NAME instead of $0 use POSIX qw(strftime); ## 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); undef $up_url; ## 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 sort_columns; sub url_escape; sub verbal_size_to_relative_numeric_size; sub zoomed_in_pic; sub zoomed_out_pic; ## Subroutines ################################################################# sub case_insensitive_sort { lc($a) cmp lc($b); } sub construct_url ($$$$$$$$$) { my $all_dirs_depth = shift; my $all_files_extension = shift; my $debug = shift; my $dir = shift; my $dir_thumbs = shift; my $extra_info = shift; my $file = shift; my $mode = shift; my $modified = shift; my $url = $cgi->script_name() . "?"; if ($all_dirs_depth) { $url .= "all_dirs_depth=" . url_escape($all_dirs_depth) . "&"; } if ($all_files_extension) { $url .= "all_files_extension=" . url_escape($all_files_extension) . "&"; } if ($debug) { $url .= "debug=" . url_escape($debug) . "&"; } $url .= "dir=" . url_escape($dir); if ($dir_thumbs) { $url .= "&dir_thumbs=" . url_escape($dir_thumbs); } if ($extra_info) { $url .= "&extra_info=" . url_escape($extra_info); } if ($file) { $url .= "&file=" . url_escape($file); } $url .= "&mode=$mode"; if ($modified) { $url .= "&modified=" . url_escape($modified); } $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 =~ /^(-large|-thumbnail)?(\.[^.]+)$/; if (not defined $1) { $size = "Normal"; } elsif ($1 eq "-large") { $size = "Large"; } else { # $1 eq "-thumbnail" $size = "Thumbnail"; } $column_heading_url = construct_url($all_dirs_depth, $extension, $debug, $dir, $dir_thumbs, $extra_info, undef, "all_files", undef); return "<A HREF=\"$column_heading_url\">" . html_escape($2) . ": $size</A>"; } sub extension_to_title_words { my $extension = shift; my $size; $extension =~ /^(-large|-thumbnail)?(\.[^.]+)$/; if (not defined $1) { $size = "normal-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. if (not defined $up_url) { if ($mode eq "all_dirs" and $all_dirs_depth > 1) { # Parent page is all_dirs mode in our parent directory. ($parent_dir = $dir) =~ s</[^/]+/?$><>; $up_url = construct_url($all_dirs_depth - 1, undef, $debug, $parent_dir, $dir_thumbs, $extra_info, undef, "all_dirs", undef); } elsif ($mode eq "one_dir" and $all_dirs_depth > 0) { # Parent page is all_dirs mode in our parent directory. ($parent_dir = $dir) =~ s</[^/]+/?$><>; $up_url = construct_url($all_dirs_depth - 1, undef, $debug, $parent_dir, $dir_thumbs, $extra_info, undef, "all_dirs", undef); } elsif ($mode eq "all_files") { # Either parent page is one_dir mode in our current directory, # or the user linked directly from a static page to the # all_files page and they'll use a static "Up" URL, not UP_URL. $up_url = construct_url($all_dirs_depth, undef, $debug, $dir, $dir_thumbs, $extra_info, undef, "one_dir", undef); } elsif ($mode eq "one_file") { if (defined $all_files_extension) { # Either parent page is all_files mode in our current # directory, or user won't be using UP_URL{}. $up_url = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, undef, "all_files", undef); } else { # Either parent page is one_dir mode in our current # directory, or user won't be using UP_URL{}. $up_url = construct_url($all_dirs_depth, undef, $debug, $dir, $dir_thumbs, $extra_info, undef, "one_dir", undef); } } } while (<HTML_FILE>) { if (defined $up_url) { # If the user put in an "UP_URL{<url>}" string, we'll substitute # the above-calculated $up_url to go a previous mode. s/UP_URL\{[^}]*}/$up_url/g; } else { # not defined $up_url # Above us is whatever static URL first called image_album, so # just transform, e.g., "UP_URL{../}" to "../". s/UP_URL\{([^}]*)}/$1/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 PUBLIC \"-//W3C//DTD HTML 3.2//EN\">\n"; } if (not defined $html_version) { $html_version = "HTML 3.2"; } print "<HTML>\n"; print "<HEAD>\n"; print "<BASE HREF=\"$base\">\n"; # TBD: $base needs no escaping, right? if (open(STYLESHEET, "stylesheet.url")) { while (<STYLESHEET>) { print '<LINK REL=stylesheet HREF="'; chomp; print; print "\">\n"; } close STYLESHEET; $had_a_stylesheet = 1; } print "<META NAME=Generator CONTENT=\"image_album version $version, by\n" . 'Dan Harkless <software@harkless.org> --' . ' http://harkless.org/dan/software/">' . "\n"; print "<TITLE>", html_escape($title), "\n"; print "\n"; print "; close BODY_ATTRIBUTES; } elsif (not $had_a_stylesheet) { print ' BGCOLOR="#CCCCCC"'; } 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 (not $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 out of the real HTTP # header and use HTTP-EQUIV.) print "(binary data)"; } } } sub sort_columns { # We want to sort the columns in the table by extension first # (alphabetically), then by size (thumbnail, normal, large). "$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 =~ /(-large|-thumbnail)?\.(.+)/; my $a_size = verbal_size_to_relative_numeric_size($1); my $a_extension = $2; $b =~ /(-large|-thumbnail)?\.(.+)/; my $b_size = verbal_size_to_relative_numeric_size($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 separate character is for it to be in the query part # of the URL, where it's legal to appear unescaped. Besides, # dir=photos/2000-12-25 reads better than dir=photos%2F2000-12-25. 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 verbal_size_to_relative_numeric_size { my $verbal_size = shift; if (not defined $verbal_size) { return 2; # no special - means it's the normal-sized version } elsif ($verbal_size eq "-large") { return 3; } else { # $verbal_size eq "-thumbnail" return 1; } } sub zoomed_in_pic { my $file_root = shift; my $extension = shift; my $file_orig_extension; $extension =~ /^(-large|-thumbnail)?(\.[^.]+)$/; if (defined $1 and $1 eq "-large") { # Already maximum size. return undef; } if (defined $1 and $1 eq "-thumbnail") { # File is thumbnail -- check for normal-sized. $file_orig_extension = $file_hash{$file_root}{$2}; if (defined $file_orig_extension) { return "$file_root$file_orig_extension"; } } # File is either thumbnail or normal -- check for large. $file_orig_extension = $file_hash{$file_root}{"-large$2"}; if (defined $file_orig_extension) { return "$file_root$file_orig_extension"; } return undef; } sub zoomed_out_pic { my $file_root = shift; my $extension = shift; my $file_orig_extension; $extension =~ /^(-large|-thumbnail)?(\.[^.]+)$/; if (defined $1 and $1 eq "-thumbnail") { # Thumbnails are the smallest there is. return undef; } if (defined $1 and $1 eq "-large") { # File is large-sized -- check for normal-sized. $file_orig_extension = $file_hash{$file_root}{$2}; if (defined $file_orig_extension) { return "$file_root$file_orig_extension"; } } # File is either large or normal -- check for thumbnail. $file_orig_extension = $file_hash{$file_root}{"-thumbnail$2"}; if (defined $file_orig_extension) { return "$file_root$file_orig_extension"; } 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 current situation of a relatively 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(-type => "text/html; charset=iso-8859-1", -Last_Modified => $HTTP_timestamp); # 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 below 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 ################################################################## $all_dirs_depth = $cgi->param('all_dirs_depth'); if (not defined $all_dirs_depth) { $all_dirs_depth = 0; } $all_files_extension = $cgi->param('all_files_extension'); $dir = $cgi->param('dir'); if (not defined $dir) { $dir = "."; } $debug = $cgi->param('debug'); if ($debug) { # Only turn on verbose warnings during debugging, due to major performance # hit. use diagnostics; $border_on_debug = "BORDER=1"; } else { $border_on_debug = "BORDER=0"; } $dir_thumbs = $cgi->param('dir_thumbs'); if (not defined $dir_thumbs) { $dir_thumbs = 0; } $extra_info = $cgi->param('extra_info'); if (not defined $extra_info) { $extra_info = 0; } $mode = $cgi->param('mode'); if (not defined $mode) { $mode = "all_dirs"; } $base = $cgi->url(); if ($dir =~ 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}.'); } $chdir_dir = "$document_root/$dir"; } 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; $chdir_dir = $dir; } if (not chdir($chdir_dir)) { die_pre_html_start('"' . html_escape($dir) . "\": $OS_ERROR."); } if ($mode ne "all_dirs") { # We'll only process directories containing a file called # "image_album-description.html". Due to this security measure, we don't # bother to do taint checking (since all it would do is unnecessarily force # us to restrict the character set usable by file and directory names) or to # make sure that the user doesn't use "../../[...]" to escape the # webserver's document root. if (not -e "image_album-description.html") { die_pre_html_start("Sorry, " . html_escape($dir) . " does not" . " contain an" . " image_album-description.html file."); } } else { # $mode eq "all_dirs" # In all_dirs mode, an image_album-parent.html file is also acceptable, # indicating that a descendant directory should contain an # image_album-description.html file. if (not -e "image_album-parent.html" and not -e "image_album-description.html") { 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_path = ""; } elsif ($dir =~ m) { $dir_as_path = $dir; } else { $dir_as_path = $dir . "/"; } $base .= $dir_as_path; $title = "Image Album"; if ($mode eq "all_dirs") { if ($dir_as_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 ($dir_thumbs) { $title .= ": Thumbnail Index"; } else { $title .= ": Index"; } } elsif ($mode eq "all_files") { if (not defined $all_files_extension) { die_pre_html_start("In $mode mode, file extension must be specified" . " with all_files_extension parameter."); } $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") { $file = $cgi->param('file'); if (not defined $file) { die_pre_html_start("In $mode mode, image must be specified with" . " file parameter."); } elsif ($file =~ m) { # All directory traversal should be done with the dir parameter, so file # doesn't need to be allowed to have any slashes in it. And disallowing # them prevents some *very* limited leakage of content on the webserver # outside the web root (files called ../[...]/-caption.html). die_pre_html_start("file parameter (\"" . html_escape($file) . "\") contains a '/' character."); } elsif (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|pl|txt|url)$/ and $dir_entry =~ /^(.+?)((-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 ######################################################################## 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)) { @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; } ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($subdir_modtime); $subdir_modified = sprintf("%d-%02d-%02d", $year + 1900, $mon, $mday); undef $subdir_url; # It's possible for a directory to be both a parent directory and an # image directory (though I certainly wouldn't organize my directory # structure like that -- better to have image directories be leaf # nodes), so check for -parent before -description so we don't miss out # on the directory's children. If the subdir is ".", only check for # -description, or there'll be an infinite hyperlink loop. if ($subdir ne "." and -e "$subdir/image_album-parent.html") { $subdir_url = construct_url($all_dirs_depth + 1, undef, $debug, "$dir_as_path$subdir", $dir_thumbs, $extra_info, undef, "all_dirs", $subdir_modified); } elsif (-e "$subdir/image_album-description.html") { if ($subdir eq ".") { $dir_w_no_poss_of_slash_dot = $dir; } else { $dir_w_no_poss_of_slash_dot = "$dir_as_path$subdir"; } $subdir_url = construct_url($all_dirs_depth + 1, undef, $debug, $dir_w_no_poss_of_slash_dot, $dir_thumbs, $extra_info, undef, "one_dir", $subdir_modified); } if (defined $subdir_url) { if (not $opened_the_table) { print "\n"; $opened_the_table = 1; } print "\n"; 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 "\n"; print "\n"; } elsif ($at_least_one_representative_thumbnail) { print "\n"; } print "\n"; print "\n"; print "\n"; # spacer } } if ($opened_the_table) { print "
" . '' . html_escape($rep_thumb_img) . '    " . html_escape($subdir) . "
 
\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 ($dir_thumbs) { print "Clicking on the \"File\" column heading will collapse\n"; print "the table below to exclude the inline thumbnails.\n"; } else { print "Clicking on the \"File\" column heading will expand\n"; print "the table below to include 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 format\n"; print "and size, with their captions (if any) printed alongside them.\n"; print "

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

Images

\n"; print "\n"; print "\n"; if ($at_least_one_thumbnail_here) { if ($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)) { print "\n"; if ($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($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs ? 0 : 1, $extra_info, undef, $mode, undef); 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)) { print '' . 
			  html_escape('; last; } } } print "" . html_escape($file_root) . ""; $file_orig_extension = $file_hash{$file_root}{$extension}; if (defined $file_orig_extension) { $one_file_url = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, "$file_root$file_orig_extension", "one_file", undef); print ""; print pretty_file_size("$file_root$file_orig_extension"); print ""; } 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($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, $individual_pic, "one_file", undef); print "\n"; print ""; # spacer print "\n"; #spacer } } print "
"; 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"; } 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 "\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"; print "\n"; # Spacer row: print "\n"; # Caption and zoom controls row (actually zoom control span multiple rows): print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; # spacer row # Image number row: print "\n"; print "
"; # Find the index of the current file in the alphabetized file root list. $file =~ /^(.+?)((-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_mode = "one_file"; } else { $alt_tag = "Index"; $prev_mode = "one_dir"; } $prev_page = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, $prev_file, $prev_mode, undef); 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"; # 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_mode = "one_file"; } else { $alt_tag = "Index"; $next_mode = "one_dir"; } $next_page = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, $next_file, $next_mode, undef); 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 "
 
"; $zoomed_out_pic = zoomed_out_pic($file_root, $extension); if (defined $zoomed_out_pic) { $zoomed_out_url = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, $zoomed_out_pic, $mode, undef); 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 ""; $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 "
"; $zoomed_in_pic = zoomed_in_pic($file_root, $extension); if (defined $zoomed_in_pic) { $zoomed_in_url = construct_url($all_dirs_depth, $all_files_extension, $debug, $dir, $dir_thumbs, $extra_info, $zoomed_in_pic, $mode, undef); 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 "
 
#", $file_index + 1, " / ", scalar(@file_roots), "
\n"; if ($have_Image_Info and defined @image_info_hashes) { print "
\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "\n"; print "
\n"; print 'Extra info:', "\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 "\n"; } $hash_ref = $image_info_hashes[$i - 1]; foreach $key (sort keys %$hash_ref) { print "\n"; print "\n"; print "\n"; print "\n"; } } 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 "
" . html_escape($key) . ": "; recursively_print_ref($$hash_ref{$key}); print "
"; } else { print "Off"; } print "
\n"; } } ## Footer ###################################################################### if (open(HR, "hr.html")) { print while
; close HR; } else { print "
\n"; } print "\n"; print "\n"; print "\n"; print "\n"; # spacer 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 subdirectory 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 "Directory last modified: " . strftime("%B %e, %Y", $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); } print "  "; print 'Validated', " $html_version
\n"; print "Generator:"; print ' ' . 'image_album'; print "
\n"; ## HTML end #################################################################### print $cgi->end_html, "\n";