#!/usr/bin/perl -w # # image_album_prep # # AUTHOR: # Dan Harkless # # COPYRIGHT: # This file is Copyright (C) 2025 by Dan Harkless, and is released under the # GNU General Public License . # # USAGE: # % image_album_prep [] [...] # # DESCRIPTION: # Prepares a group of image files for use with my image_album CGI script (or # other presentation methods). Can edit caption files, add comments, restore # file timestamps from EXIF data, create reduced-size copies of images # (e.g. for thumbnails), rename images to ., remove # embedded EXIF thumbnails, rotate portrait shots into the correct # orientation, and more. Some of these abilities require particular external # programs to be available, as noted below. At the very least, the # ImageMagick library and its Perl interface (see # ) must be present. # # Note that image_album and this script expect different-sized versions of an # image to be named in a certain way. If the "normal-sized" version of a file # (convenient for viewing even on low-resolution displays) is called # "cow.jpg", a larger (but not necessarily full-size) version is expected to # be called "cow-large.jpg", the original maximum resolution image is expected # to be called "cow-full-size.jpg", and a small, thumbnail version is expected # to be called "cow-thumbnail.jpg". image_album_prep enforces this naming # when it's creating resized files. The files specified on the commandline # are expected to be full-size, and they will be renamed to *-full-size.* if # they're not named that way already. # # Currently there's no option to convert files to a different format (which # you might want to do, for instance, if you saved scanned photos in PNG # format and wanted to make JPEG full-size and shrunken versions). I expect # to add this capability in the future. # # Note that EXIF data in JPEG files will also appear in shrunken versions # (this can be overridden for thumbnail files with # --rm-exif-data-from-thumbnails). Also note that the modification timestamp # of files specified on the commandline will be preserved, even if # modifications are made to the files. Likewise, shrunken versions of images # will get the same timestamp as the full-size file. # # image_album_prep does not save any backup files, so if you're not sure you # understand its operation, you might want to try things out first on a # 'cp -pr'd directory, and use --verbose so you can see what the script is # doing. # # COMMANDLINE OPTIONS: # --caption-edit # Edit the -caption.html file for each image specified on the # commandline. If the EDITOR environment variable is set, that editor will # be used. If it's not, the default is vi. # # --caption-print # Print out the caption file for this image, prior to any renaming. This is # primarily useful when using --rename-per-file, to give you a hint as to # what to enter for the per-file rename string. # # --clobber # By default when renaming files, if file X is going to be renamed as Y but # Y already exists, the script will abort. If you want existing files to # instead be overwritten when renaming, specify --clobber. Note that the # presence or lack of this option has no effect on other types of clobbering # that aren't due to renaming, such as when creating a thumbnail file (if # one already exists with the same name). # # --comment= # Write the given comment to each GIF and JPEG file on the commandline # before duplicating it to make shrunken files. Because ImageMagick doesn't # currently have the ability to write comments to files without # recompressing the image, we use the external command giftrans (see # for # GIFs, and wrjpgcom (see ) for JPEGs. No support for # adding comments to other formats (like PNG) currently. # # --exif-modtime # Call the external image_info_modtime script (see # ) on each image file # specified on the commandline to change the file modification time to the # time specified by its EXIF DateTimeOriginal timestamp. # # --large-max=(/|%|) # Create -large. files of the given size. If the # files given on the commandline don't match the pattern # -full-size., they'll be renamed to that as part of the # processing. Note that this renaming occurs in conjunction with the # --rename-* options (see below). # # After any necessary renaming, a "large"-sized version of the full-size # file will be created, resized using --large-max's parameter. The # parameter will be applied to the bigger (or "max") dimension of each # rectangular image file. If specified as a simple "", the file will # be resized to that many pixels in the bigger dimension. If specified as # "/", the bigger dimension of the full-size file will be divided by # the specified number to get the bigger dimension of the large-sized file. # If specified as "%", the bigger dimension will be that percentage # of the full-size file's bigger dimension. # # For example, if --large-max=900 is specified when running # image_album_prep on two images, one of which is in landscape format, sized # 3600x2700, and the other of which is in portrait format, sized 2700x3600, # the large-sized files created will be 900x675 and 675x900, # respectively. You wouldn't want to use --large-x=900 or --large-y=900 # when running on both of these files simultaneously, or you'd get the wrong # aspect ratio for one of them. (You could use a relative rather than # absolute resize value by specifying --large-x=/4 or --large-x=25% to # avoid that problem, though). # # --large-quality= # Sets the compression setting for the large file, if being created, to the # specified value. For JPEGs, this should be 0-100, with 0 being the # highest amount of compression and smallest file size. If not set, this # defaults to 90. # # --large-x=(/|%|) # Works like --large-max, but the resizing factor is always applied to the # X dimension rather than automatically being applied to the bigger # dimension. # # If --large-y is not specified together with --large-x, the Y dimension # will be calculated automatically to keep images in their original aspect # ratio. # # --large-y=(/|%|) # Just like --large-x, but specifies the Y dimension. If specified without # --large-x, the X dimension will be calculated automatically. # # --normal-max=(/|%|) # Like --large-max, but specifies the scaling of a . # file which will be created, after renaming the input filename to # -full-size., if it's not already in that format. # Scaling factors are relative to the dimensions of the files specified on # the commandline, not relative to the --large parameters, if those have # been specified. So, for instance, 'image_album_prep --large-max=50% # --normal-max=25%' would make sense, but 'image_album_prep --large-max=50% # --normal-max=50%' would not (because you'd get large-sized files and # normal-sized files of the same size). # # --normal-quality= # Sets the compression setting for the normal file, if being created, to # the specified value. For JPEGs, this should be 0-100, with 0 being the # highest amount of compression and smallest file size. If not set, this # defaults to 75. # # --normal-x=(/|%|) # --normal-x is to --normal-max as --large-x is to --large-max. # # --normal-y=(/|%|) # --normal-y is to --normal-max as --large-y is to --large-max. # # --rename-per-file # For each image file specified on the commandline, prompt for an extra # string to be added to the name of the file and to all files with the same # root name, whether created on this invocation or pre-existing. The # entered string is inserted into the name just prior to the size class # string and extension. For instance, if you had files called: # # Disneyland2002-1.jpg # Disneyland2002-1-caption.html # Disneyland2002-1-full-size.jpg # Disneyland2002-1-thumbnail.jpg # Disneyland2002-2.jpg # Disneyland2002-2-caption.html # Disneyland2002-2-full-size.jpg # Disneyland2002-2-thumbnail.jpg # # and you ran 'image_album_prep --rename-per-file *-full-size.jpg' and # entered "-Matterhorn" and "-Haunted_Mansion", respectively, when prompted, # the files would be renamed to: # # Disneyland2002-1-Matterhorn.jpg # Disneyland2002-1-Matterhorn-caption.html # Disneyland2002-1-Matterhorn-full-size.jpg # Disneyland2002-1-Matterhorn-thumbnail.jpg # Disneyland2002-2-Haunted_Mansion.jpg # Disneyland2002-2-Haunted_Mansion-caption.html # Disneyland2002-2-Haunted_Mansion-full-size.jpg # Disneyland2002-2-Haunted_Mansion-thumbnail.jpg # # Note that --rename-per-file can be used together with --rename-prefix-* # or independently. # # If you use a representative_thumbnail.url file for your album, # --rename-per-file will rewrite the file if it renames the representative # thumbnail. # # --rename-prefix-new= # Causes the image files specified on the commandline, as well as related # files with the same prefix, whether pre-existing or being created on this # invocation, to be renamed. In the name, the regular expression specified # by --rename-prefix-old will be changed to the string specified by # --rename-prefix-new plus a renumbered, zero-filled index. Also, file # extensions will be changed to their lower-case, three-letter versions # (e.g. .GIF to .gif, .Jpg to .jpg, and .JPEG to .jpg). # # The best way to explain the full behavior is probably through example. If # we have, for instance, a directory containing files called: # # DSC00124.JPG # DSC00129.JPG # DSC00277.JPG # # and we run 'image_album_prep --rename-prefix-old="^DSC[0-9]+" # --rename-prefix-new=Europe *.JPG', we'll get: # # Europe1.jpg # Europe2.jpg # Europe3.jpg # # Note that if --large-*, --normal-*, or --thumbnail-* are also specified, # as in 'image_album_prep --rename-prefix-old="^DSC[0-9]+" # --rename-prefix-new=Europe *.JPG --large-max=50%', DSC00124.JPG will be # renamed to Europe1-full-size.jpg and a reduced-size Europe1-large.jpg will # be created, and so on: # # Europe1-full-size.jpg # Europe1-large.jpg # Europe2-full-size.jpg # Europe2-large.jpg # Europe3-full-size.jpg # Europe3-large.jpg # # To take an example with existing related files, if we have: # # Halloween1.jpg # Halloween1-caption.html # Halloween1-thumbnail.jpg # Halloween2.jpg # Halloween2-caption.html # Halloween2-thumbnail.jpg # # and run 'image_album_prep --rename-prefix-old="^Halloween[0-9]+" # --rename-prefix-new=Halloween2002- *-thumbnail.jpg' (or *[0-9].jpg), we'll # get: # # Halloween2002-1.jpg # Halloween2002-1-caption.html # Halloween2002-1-thumbnail.jpg # Halloween2002-2.jpg # Halloween2002-2-caption.html # Halloween2002-2-thumbnail.jpg # # If we have a number missing in our image sequence because we've pruned a # file, as in: # # race-1-beginning.jpg # race-1-beginning-caption.html # race-1-beginning-large.jpg # race-1-beginning-thumbnail.jpg # race-3-end.jpg # race-3-end-caption.html # race-3-end-large.jpg # race-3-end-thumbnail.jpg # # we can specify the existing prefix to just get the renumbering behavior # without changing the prefix, as in 'image_album_prep # --rename-prefix-old="^race-[0-9]+" --rename-prefix-new=race- *-large.jpg' # (note that specifying the files as *[0-9].jpg won't work this time because # of the --rename-per-file strings after the index numbers) to get: # # race-1-beginning.jpg # race-1-beginning-caption.html # race-1-beginning-large.jpg # race-1-beginning-thumbnail.jpg # race-2-end.jpg # race-2-end-caption.html # race-2-end-large.jpg # race-2-end-thumbnail.jpg # # Note that above we've avoided passing '*.jpg' on the commandline if we # have multiple-sized images in the directory. If we didn't do this, the # number of digits to zero-fill the renumbered index with would be # calculated incorrectly (since it counts the number of files specified on # the commandline), amongst other problems. # # If you use a representative_thumbnail.url file for your album, # --rename-prefix-new will rewrite the file if it renames the representative # thumbnail. # # --rename-prefix-old= # A regular expression specifying the existing filename prefix to be renamed # to the value specified by the --rename-prefix-new option, plus a # renumbered index. See --rename-prefix-new for a full explanation. # # --resave-quality= # Sets the compression setting for the files specified on the commandline # (but not any created large-size, normal-sized, or thumbnail files), if # being re-saved due to some manipulation, to the specified value. For # JPEGs, this should be 0-100, with 0 being the highest amount of # compression and smallest file size. If not set, this defaults to 95. # # --rm-exif-data-from-all # Run 'exiftool' () on each image file to remove all # EXIF data. You might want to use this to protect your privacy with # cellphone cameras embedding your the GPS location of your home, for # instance, before sharing your images with others. # # --rm-exif-data-from-thumbnails # Run 'exiftool' () on each thumbnail file we create # to remove all EXIF data. You might want to do this to make your thumbnail # images as small and quick-loading as possible. # # --rm-exif-thumbnails-from-all # Run 'exiftool' () on each image file to remove # embedded EXIF thumbnails. You might want to do this to make your files as # small as possible (at the expense of preventing consumers of your image # from taking advantage of EXIF thumbnail viewing functionality of their # tools, but then again, these thumbnails can often be in the wrong # orientation, or represent uncensored views of photos that have been # intentionally edited). # # --rm-non-exif-metadata-too # When specified with one of the --rm-exif-data-from-* options (has no # effect otherwise), causes _all_ metadata to be deleted. Formerly called # --rm-flashpix-and-iptc-too, when we used exifedit rather than exiftool. # # --rotate= # Rotate each image on the commandline by degrees (and save it that # way) prior to any creation of shrunken versions. For JPEGs, if the number # of degrees specified is [-+]90, [-+]180, or [-+]270, we call external # program jpegtran (see ) to do the rotation # losslessly. For other degree values or for non-JPEGs, we use the # ImageMagick routine to do the rotation (which of course will require a # recompression of the image). # # Some digital cameras may put an indication somewhere in the EXIF data what # orientation a photo was taken at (yet still need non-landscape orientation # photos to be rotated in post-processing), but image_album_prep doesn't # currently know about such indications. Therefore, if you have a batch of # photos to process, some in landscape and some in portrait orientation, you # should run image_album_prep twice, once with --rotate=-90 (or 90, as the # case may be) on all the portrait shots and once without --rotate on all # the landscape shots. Of course you won't be able to use --rename-prefix-* # if you're processing the shots in two batches (since the renumbered # indices would overlap), but you can run image_album_prep with # --rename-prefix-old and --rename-prefix-new as a separate, final step on # all the images. # # --thumbnail-max=(/|%|) # Like --large-max, but specifies the scaling of a # -thumbnail. file which will be created, after # renaming the input filename to -full-size., if it's # not already in that format. Scaling factors are relative to the # dimensions of the files specified on the commandline, not relative to the # --large or --normal parameters, if those have been specified. So, for # instance, 'image_album_prep --large-max=50% --thumbnail-max=10%' would # make sense, but 'image_album_prep --large-max=50% --thumbnail-max=50%' # would not (because you'd get large-sized files and thumbnail-sized files # of the same size). # # --thumbnail-quality= # Sets the compression setting for the thumbnail file, if being created, to # the specified value. For JPEGs, this should be 0-100, with 0 being the # highest amount of compression and smallest file size. If not set, this # defaults to 50. # # --thumbnail-x=(/|%|) # --thumbnail-x is to --thumbnail-max as --large-x is to --large-max. # # --thumbnail-y=(/|%|) # --thumbnail-y is to --thumbnail-max as --large-y is to --large-max. # # --verbose # Print messages to stdout about what's being done. # # EXAMPLE SESSION: # (Note that this example does a fair amount of separation of steps, but in # theory you could do everything with a single image_album_prep invocation, at # least if you didn't need to do any selective rotation.) # # % cd needs_rotation # % image_album_prep --rotate=-90 *.JPG # % mv * .. # % cd .. # % rmdir needs_rotation # % image_album_prep --comment=http://harkless.org/dan/art/images/photos/ --re # name-prefix-new=Hawaii02- --rename-prefix-old="DSC[0-9]+" --rm-exif-data-fro # m-thumbnails --rm-exif-thumbnails-from-all --thumbnail-x=/8 --verbose *.JPG # # (Now create other image_album files like image_album_description.html and # bring up the image_album URL in a web browser, probably in "all_files" mode, # to see if any photos need pruning. If so, remove them then renumber the # photos.) # # % rm Hawaii02-13* Hawaii02-16* # % image_album_prep --rename-prefix-new=Hawaii02- --rename-prefix-old="Hawaii # 02-[0-9]+" --verbose *-thumbnail.jpg # # (Reload the album to see the new post-prune numbering and refer to it while # writing captions.) # # % env EDITOR=vi image_album_prep --caption-edit *-thumbnail.jpg # # (Reload the album with captions. If any need to be modified, edit them on # an individual basis. Once satisfied, enter very brief image descriptions, # with the words separated by underscores, using the captions as a guide.) # # % image_album_prep --caption-print --rename-per-file --verbose *-thumbnail.j # pg # # (Reload the album one more time as a final check.) # # DATE MODIFICATION # ========== ================================================================== # 2025-07-31 Use exiftool rather than the discontinued, 32-bit-only exifedit. # exiftool is compatible with a much wider range of metadata types, # and doesn't treat FlashPix and IPTC specially, so also changed # --rm-flashpix-and-iptc-too option to --rm-non-exif-metadata-too. # 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), and files on the # commandline now get renamed to *-full-size., if they aren't # already in that format, whenever any resized versions are being # created. --large-quality default is 90. --resave-quality default # changed to 95. # 2012-12-02 Added --rm-exif-data-from-all and --rm-flashpix-and-iptc-too (the # latter uses 'exifedit -e a', to update my 2006-08-21 change log). # 2012-12-02 Renamed --rm-thumbnail-exif-data to --rm-exif-data-from-thumbnails # and --rm-exif-thumbnails to --rm-exif-thumbnails-from-all for # clarity. Old option names still accepted for backwards compat. # 2009-05-23 Some types of file renaming were broken in the 2-13 revisions due # to missed instances of a variable rename. # 2009-02-13 Added --normal-max and --thumbnail-max. # 2009-02-13 Don't clobber existing files when renaming unless --clobber used. # 2009-02-13 The advertised feature of renaming files that don't include # "-large" to include it if --normal-x or --normal-y has been # specified got broken at some point. Fixed. # 2008-09-02 "use English qw(-no_match_vars)": avoid regex performance penalty. # 2006-08-21 I updated from EXIFutils 2.3.2 to 2.7.2, and 'exifedit -e' now has # to be specified as 'exifedit -e e'. # 2003-05-18 GetOptions()' optional string argument handling sucks. I think # there should be a setting where you require an '=' to separate # option name and the optional value (rather than allowing either an # '=' or a ' '). If this were the case, "--caption-edit file1 # file2" and "--caption-edit=vi file1 file2" would do the same # thing. As it is now, you need to specify the first one as # "--caption-edit -- file1 file2" for it to do what you expect. # Removed --caption-edit's optional argument -- use EDITOR now. # 2003-03-02 Apparently when I made the previous fix to the --rename options, # I broke the handling of upper-case file extensions. Fixed. # 2002-12-04 Added EXAMPLE SESSION section. # 2002-12-04 The old operation of --rename would cause strings previously added # by --rename-per-file to be lost. Changed this, requiring split # of --rename into --rename-prefix-old and --rename-prefix-new. # 2002-12-04 Added --rename-per-file and the related --caption-print. # 2002-12-04 Added --caption-edit. # 2002-12-04 To make the --verbose output cleaner and more compact, removed the # "image_album_prep: " prefix from --verbose lines. Left it on the # error lines, however, to make them stand out more when coming out # in the midst of --verbose stuff, and because it's more important # to be able to identify where errors are coming from than verbose # info. # 2002-12-04 Rewrite the representative_thumbnail.url file if rename thumbnail. # 2002-12-04 Made --rename recognize identical root names and use same number. # 2002-12-03 Original. ## Modules used ################################################################ use English qw(-no_match_vars); # allow use of names like @ARG rather than @_ use FileHandle; # for autoflush use Getopt::Long; # for GetOptions() use Image::Magick; use POSIX; # for log10() and strftime() ## Prototypes ################################################################## sub apply_dim_opt; sub default_dim1_from_dim2; sub maybe_rename; sub msg_err; sub msg_out; sub my_die; sub my_system; sub shrink; sub set_modtime; sub usage_error; ## Subroutines ################################################################# sub apply_dim_opt { my $dim_opt = shift; my $dim_before = shift; my $dim_after; if ($dim_opt =~ m<^/([0-9.]+)$>) { my $divisor = $1; $dim_after = int(($dim_before / $divisor) + .5); } elsif ($dim_opt =~ m<^([0-9.]+)%$>) { my $percent = $1 / 100; $dim_after = int(($dim_before * $percent) + .5); } elsif ($dim_opt =~ m<^[0-9]+$>) { $dim_after = $dim_opt; } else { usage_error(); } return $dim_after; } sub default_dim1_from_dim2 { my $slave_opt = shift; my $slave_axis_size = shift; my $master_opt = shift; my $master_axis_size = shift; if (not $slave_opt) { if ($master_opt =~ m<[/%]>) { $slave_opt = $master_opt; } else { $slave_opt = int(($slave_axis_size / ($master_axis_size / $master_opt)) + .5); } } return $slave_opt; } sub maybe_rename { my $old_name = shift; my $new_name = shift; if (-e $old_name) { if (-e $new_name and not $opt_clobber) { my_die "rename(\"$old_name\", \"$new_name\"): \"$new_name\" already" . " exists. --clobber can be used to override."; } if ($opt_verbose) { msg_out "$old_name: Renaming to \"$new_name\"."; } if (not rename($old_name, $new_name)) { # Treat rename errors as fatal to help prevent undesired clobbering. my_die "rename(\"$old_name\", \"$new_name\"): $OS_ERROR."; } if ($old_name eq $representative_thumbnail) { if (not open(REPRESENTATIVE_THUMBNAIL, ">representative_thumbnail.url")) { msg_err "open(\">representative_thumbnail.url\"):" . " $OS_ERROR."; msg_err "representative_thumbnail.url: Can't rewrite with", " new \"$new_name\" name."; } else { if ($opt_verbose) { msg_out "representative_thumbail.url: Rewriting with", " new \"$new_name\" name."; } print REPRESENTATIVE_THUMBNAIL "$new_name\n"; close REPRESENTATIVE_THUMBNAIL; } } } } sub msg_err { print STDERR "$program_name_no_path: ", @ARG, "\n"; $had_an_error = 1; } sub msg_out { print @ARG, "\n"; } sub my_die { msg_err @ARG; msg_err "Aborting."; exit 1; } sub my_system { if ($opt_verbose) { msg_out "Running '@ARG'."; } # TBD: Because this'll use /bin/sh to interpret the command, we'll need to # add taint-checking if we move this code to a CGI script. my $exit_status = system(@ARG); if ($exit_status != 0) { # Guess we'll be conservative and always die on external program errors. @command_name = split / /, $ARG[0]; if ($exit_status == -1) { my_die "Failed to run $command_name[0]."; } else { $exit_status /= 256; my_die "$command_name[0] exited with status $exit_status."; } } } sub shrink { my $shrunken_file = shift; my $shrunken_max_opt = shift; my $shrunken_quality = shift; my $shrunken_x_opt = shift; my $shrunken_y_opt = shift; my $shrunken_image = $image->Clone(); if (not $shrunken_image) { my_die "Image::Magick->Clone() failed."; } if ($shrunken_max_opt) { if ($image_x >= $image_y) { $shrunken_x_opt = $shrunken_max_opt; } else { $shrunken_y_opt = $shrunken_max_opt; } } $shrunken_x_opt = default_dim1_from_dim2($shrunken_x_opt, $image_x, $shrunken_y_opt, $image_y); $shrunken_y_opt = default_dim1_from_dim2($shrunken_y_opt, $image_y, $shrunken_x_opt, $image_x); my $shrunken_x = apply_dim_opt($shrunken_x_opt, $image_x); my $shrunken_y = apply_dim_opt($shrunken_y_opt, $image_y); if ($opt_verbose) { msg_out "$shrunken_file: Resizing to $shrunken_x x $shrunken_y." } if ($IM_err = $shrunken_image->Resize(width=>$shrunken_x, height=>$shrunken_y)) { my_die "Image::Magick->Resize(width=>$shrunken_x, height=>$shrunken_y):" . " $IM_err."; } if ($opt_verbose) { msg_out "$shrunken_file: Saving at quality $shrunken_quality." } if ($IM_err = $shrunken_image->Set(quality=>$shrunken_quality)) { my_die "Image::Magick->Set(quality=>$shrunken_quality): $IM_err."; } if ($IM_err = $shrunken_image->Write($shrunken_file)) { my_die "Image::Magick->Write(): $IM_err."; } } sub set_modtime { my $file = shift; my $file_modtime = shift; if (not utime(time, $file_modtime, $file)) { msg_err "utime(\"$file\"): $OS_ERROR."; } } sub usage_error { print STDERR "Usage: $program_name_no_path [] ", " [...]\n"; print STDERR "\n"; print STDERR "Options: --caption-edit\n"; print STDERR " --caption-print\n"; print STDERR " --clobber\n"; print STDERR " --comment=\n"; print STDERR " --exif-modtime\n"; print STDERR " --large-max=(/|%|)\n"; print STDERR " --large-quality=\n"; print STDERR " --large-x=(/|%|)\n"; print STDERR " --large-y=(/|%|)\n"; print STDERR " --normal-max=(/|%|)\n"; print STDERR " --normal-quality=\n"; print STDERR " --normal-x=(/|%|)\n"; print STDERR " --normal-y=(/|%|)\n"; print STDERR " --rename-per-file\n"; print STDERR " --rename-prefix-new=\n"; print STDERR " --rename-prefix-old=\n"; print STDERR " --resave-quality=\n"; print STDERR " --rm-exif-data-from-all\n"; print STDERR " --rm-exif-data-from-thumbnails\n"; print STDERR " --rm-exif-thumbnails-from-all\n"; print STDERR " --rm-non-exif-metadata-too\n"; print STDERR " --rotate=\n"; print STDERR " --thumbnail-max=(/|%|)\n"; print STDERR " --thumbnail-quality=\n"; print STDERR " --thumbnail-x=(/|%|)\n"; print STDERR " --thumbnail-y=(/|%|)\n"; print STDERR " --verbose\n"; exit 1; } ## Commandline options and initializations ##################################### use vars qw($opt_caption_edit $opt_caption_print $opt_clobber $opt_comment $opt_exif_modtime $opt_large_max $opt_large_quality $opt_large_x $opt_large_y $opt_normal_max $opt_normal_quality $opt_normal_x $opt_normal_y $opt_rename_per_file $opt_rename_prefix_new $opt_rename_prefix_old $opt_resave_quality $opt_rm_exif_data_from_all $opt_rm_exif_data_from_thumbnails $opt_rm_exif_thumbnails_from_all $opt_rm_non_exif_metadata_too $opt_rm_thumbnail_exif_data $opt_rotate $opt_thumbnail_max $opt_thumbnail_quality $opt_thumbnail_x $opt_thumbnail_y $opt_verbose); # Make sure stderr and stdout messages don't overlap if handles tied together # (e.g. using tcsh's "|&"), and that stderr and stdout from commands we run # comes out in the proper sequence with our own messages. autoflush STDERR 1; autoflush STDOUT 1; # Do this before we potentially output any errors. ($program_name_no_path = $PROGRAM_NAME) =~ s<.*/><>; if (not GetOptions("caption-edit", "caption-print", "clobber", "comment=s", "exif-modtime", "large-max=s", "large-quality=f", "large-x=s", "large-y=s", "normal-max=s", "normal-quality=f", "normal-x=s", "normal-y=s", "rename-per-file", "rename-prefix-new=s", "rename-prefix-old=s", "resave-quality=f", "rm-exif-data-from-all", "rm-exif-data-from-thumbnails", "rm-exif-thumbnails", "rm-exif-thumbnails-from-all", "rm-non-exif-metadata-too", "rm-thumbnail-exif-data", "rotate=f", "thumbnail-max=s", "thumbnail-quality=f", "thumbnail-x=s", "thumbnail-y=s", "verbose") or scalar @ARGV < 1) { usage_error(); } $file_root_prior_to_rename = ""; if (not $opt_large_quality) { $opt_large_quality = 90; } if (not $opt_normal_quality) { $opt_normal_quality = 75; } if ($opt_rename_prefix_new) { $rename_counter = 1; $rename_counter_digits = int(log10(scalar(@ARGV))) + 1; $rename_root_format_str = "$opt_rename_prefix_new%0${rename_counter_digits}d"; } if (($opt_rename_prefix_new or $opt_rename_prefix_old) and not ($opt_rename_prefix_new and $opt_rename_prefix_old)) { my_die "--rename-prefix-old and --rename-prefix-new must be specified" . " together."; } if (not $opt_resave_quality) { $opt_resave_quality = 95; } if ($opt_rm_exif_thumbnails) { # Backwards compatibility. $opt_rm_exif_thumbnails_from_all = $opt_rm_exif_thumbnails; } if ($opt_rm_non_exif_metadata_too) { $exiftool_removal_arg = "-All="; } else { $exiftool_removal_arg = "-EXIF:all="; } if ($opt_rm_thumbnail_exif_data) { # Backwards compatibility. $opt_rm_exif_data_from_thumbnails = $opt_rm_thumbnail_exif_data; } if (not $opt_thumbnail_quality) { $opt_thumbnail_quality = 50; } $new_per_file = ""; $representative_thumbnail = ""; ## Main ######################################################################## if (($opt_rename_per_file or $opt_rename_prefix_new) and -e "representative_thumbnail.url") { if (open(REPRESENTATIVE_THUMBNAIL, "representative_thumbnail.url")) { $representative_thumbnail = ; chomp $representative_thumbnail; if ($opt_verbose) { msg_out "representative_thumbail.url: Currently", " \"$representative_thumbnail\"."; } close REPRESENTATIVE_THUMBNAIL; } } foreach $file (@ARGV) { # Do this before we do any actual file operations. if ($file !~ /^(.+?)(-full-size|-large|-thumbnail)?\.([^.]+)$/) { msg_err "$file: Skipping because it has no extension."; next; } $prev_root = $file_root_prior_to_rename; $file_root = $1; $file_root_prior_to_rename = $file_root; $file_size_class = $2; if (not $file_size_class) { $file_size_class = ""; } $file_extension = $3; $file_extension_prior_to_rename = $file_extension; $caption_file = "$file_root-caption.html"; # Do this first-thing. @file_stat = stat $file; if (not @file_stat) { msg_err "stat(\"$file\"): $OS_ERROR."; next; } $file_modtime = $file_stat[9]; if ($opt_verbose) { msg_out "$file: Modification time is ", strftime("%Y-%m-%d %T local", localtime($file_modtime)), "."; } if ($opt_exif_modtime) { my_system "image_info_modtime -v \"$file\""; # Now get the replaced modtime. @file_stat = stat $file; if (not @file_stat) { msg_err "stat(\"$file\"): $OS_ERROR."; } else { $file_modtime = $file_stat[9]; } } # Do this before we do any renaming. if ($opt_caption_print) { if ($opt_verbose) { msg_out "$caption_file: Printing."; } if (not open(CAPTION, $caption_file)) { msg_err "open(\"$caption_file\"): $OS_ERROR."; } else { print while (); close CAPTION; } } # Do renaming before we start writing any files. $resizing = 0; if ($opt_large_max or $opt_large_x or $opt_large_y or $opt_normal_max or $opt_normal_x or $opt_normal_y or $opt_thumbnail_max or $opt_thumbnail_x or $opt_thumbnail_y) { $resizing = 1; } if ($opt_rename_per_file or $opt_rename_prefix_new or ($resizing and not $file_size_class)) { if ($resizing and not $file_size_class) { # The user is requesting shrunken versions be created, yet # "-full-size" doesn't appear in the filename, so add it. $file_size_class = "-full-size"; $rename_to = "$file_root$file_size_class.$file_extension"; maybe_rename($file, $rename_to); $file = $rename_to; } if ($opt_rename_per_file) { print "Enter per-file rename string: "; $new_per_file = ; chomp $new_per_file; } if ($opt_rename_prefix_new) { if ($file_root eq $prev_root) { $rename_counter--; # use same number for identical roots } if ($file_extension =~ /^jpe?g$/i) { $file_extension = "jpg"; } else { $file_extension = lc($file_extension); } $new_prefix_plus_index = sprintf($rename_root_format_str, $rename_counter); $file_root =~ s/$opt_rename_prefix_old/$new_prefix_plus_index/; $rename_counter++; } $rename_to = "$file_root$new_per_file$file_size_class.$file_extension"; if ($file ne $rename_to) { maybe_rename("$file_root_prior_to_rename." . $file_extension_prior_to_rename, "$file_root$new_per_file.$file_extension"); maybe_rename("$file_root_prior_to_rename-caption.html", "$file_root$new_per_file-caption.html"); maybe_rename("$file_root_prior_to_rename-full-size." . $file_extension_prior_to_rename, "$file_root$new_per_file-full-size.$file_extension"); maybe_rename("$file_root_prior_to_rename-large." . $file_extension_prior_to_rename, "$file_root$new_per_file-large.$file_extension"); maybe_rename("$file_root_prior_to_rename-thumbnail." . $file_extension_prior_to_rename, "$file_root$new_per_file-thumbnail.$file_extension"); $file = $rename_to; } } if ($opt_rm_exif_data_from_all) { my_system "exiftool $exiftool_removal_arg" . " -overwrite_original_in_place -preserve \"$file\""; } # Do this before any operations that might get applied to the embedded # thumbnail as well. if ($opt_rm_exif_thumbnails_from_all) { my_system "exiftool -overwrite_original_in_place -preserve" . " -ThumbnailImage= \"$file\""; } if ($opt_comment) { if ($file_extension =~ /^gif$/i) { my_system "giftrans -c \"$opt_comment\" \"$file\" -o" . " \"$file.giftrans\""; my_system "mv \"$file.giftrans\" \"$file\""; } elsif ($file_extension =~ /^jpe?g$/i) { my_system "wrjpgcom -comment \"$opt_comment\" -replace \"$file\" >" . " \"$file.wrjpgcom\""; my_system "mv \"$file.wrjpgcom\" \"$file\""; } set_modtime($file, $file_modtime); } $rotated_with_ext_prog = 0; if ($opt_rotate) { $opt_rotate = 270 if $opt_rotate == -90; $opt_rotate = 180 if $opt_rotate == -180; $opt_rotate = 90 if $opt_rotate == -270; if ($file_extension =~ /^jpe?g$/i and ($opt_rotate == 90 or $opt_rotate == 180 or $opt_rotate == 270)) { my_system "jpegtran -copy all -outfile \"$file.jpegtran\" -rotate" . " $opt_rotate \"$file\""; my_system "mv \"$file.jpegtran\" \"$file\""; set_modtime($file, $file_modtime); $rotated_with_ext_prog = 1; } } undef $image; # not sure if this is necessary to avoid memory leak... $image = Image::Magick->new; # need this each time or get multi-image seq. if (not $image) { my_die "Image::Magick->new() failed."; } if ($IM_err = $image->Read("$file")) { msg_err "Image::Magick->Read(): $IM_err."; next; } if ($opt_rotate and not $rotated_with_ext_prog) { if ($opt_verbose) { msg_out "$file: Rotating $opt_rotate degrees using ImageMagick."; } if ($IM_err = $image->Rotate($opt_rotate)) { my_die "Image::Magick->Rotate($opt_rotate): $IM_err."; } if ($opt_verbose) { msg_out "$file: Re-saving at quality $opt_resave_quality." } if ($IM_err = $image->Set(quality=>$opt_resave_quality)) { my_die "Image::Magick->Set(quality=>$opt_resave_quality): $IM_err."; } if ($IM_err = $image->Write($file)) { my_die "Image::Magick->Write(): $IM_err."; } set_modtime($file, $file_modtime); } if ($resizing){ ($image_x, $image_y) = $image->Get("width", "height"); if (not $image_x or not $image_y) { my_die "Image::Magick->Get(\"width\", \"height\") failed."; } if ($opt_verbose) { msg_out "$file: $image_x x $image_y pixels."; } if ($opt_large_max or $opt_large_x or $opt_large_y) { if ($opt_large_max and ($opt_large_x or $opt_large_y)) { my_die "It does not make sense to specify --large-x or" . " --large-y together with --large-max."; } $large_file = "$file_root$new_per_file-large.$file_extension"; shrink($large_file, $opt_large_max, $opt_large_quality, $opt_large_x, $opt_large_y); set_modtime($large_file, $file_modtime); } if ($opt_normal_max or $opt_normal_x or $opt_normal_y) { if ($opt_normal_max and ($opt_normal_x or $opt_normal_y)) { my_die "It does not make sense to specify --normal-x or" . " --normal-y together with --normal-max."; } $normal_file = "$file_root$new_per_file.$file_extension"; shrink($normal_file, $opt_normal_max, $opt_normal_quality, $opt_normal_x, $opt_normal_y); set_modtime($normal_file, $file_modtime); } if ($opt_thumbnail_max or $opt_thumbnail_x or $opt_thumbnail_y) { if ($opt_thumbnail_max and ($opt_thumbnail_x or $opt_thumbnail_y)) { my_die "It does not make sense to specify --thumbnail-x or" . " --thumbnail-y together with --thumbnail-max."; } $thumbnail_file = "$file_root$new_per_file-thumbnail.$file_extension"; shrink($thumbnail_file, $opt_thumbnail_max, $opt_thumbnail_quality, $opt_thumbnail_x, $opt_thumbnail_y); if ($opt_rm_exif_data_from_thumbnails) { # TBD: Just use ImageMagick routines to "forget" the data? my_system "exiftool $exiftool_removal_arg" . " -overwrite_original_in_place \"$thumbnail_file\""; } set_modtime($thumbnail_file, $file_modtime); } } if (defined $opt_caption_edit) { if (defined $ENV{EDITOR}) { $editor = $ENV{EDITOR}; } else { $editor = "vi"; } my_system "$editor $file_root$new_per_file-caption.html"; } } if ($had_an_error) { msg_err "Error exit delayed from previous errors."; exit 1; }