#!/usr/bin/perl -w use strict; use File::Copy; use Term::ReadLine; use Time::Local; ########################################################### # AutoScrapbook 4.1 (July 3, 2003) # (Copyright 2003, Kirk Bauer) (see license at end of file) ########################################################### # This program was written by Kirk Bauer # # It is distributed under the MIT license, which basically # says that you can do anything you want with this problem # (even commercial activities) for free, as long as you # give me credit for the program. # # For more information, please visit http://linux.kaybee.org/ ########################################################### ########################################################### # Put your local values here... ########################################################### my $YourName = 'Kirk Bauer'; my $YourEmail = 'kirk@kaybee.org'; # Set this to 1 to ask for file renaming, deletion, and rotation my $Renames = 1; # Set this to 1 to ask before re-processing a directory my $AskReprocess = 0; # Set this to 1 to enable debugging my $Debug = 0; ########################################################### ########################################################### # Customize external program locations and arguments here ########################################################### my $IDENTIFY = "identify"; my $MAKE_THUMBNAIL = "convert -geometry 100x100+0+0"; # (note: set $XV to '' to disable picture display) my $XV = "xv -wait 10 -ge \"400x300+10+10\""; # Set to '' to disable reading of EXIF information my $EXIF = "/usr/bin/exif"; # Will be used instead of exif when applicable my $EXIFTAGS = "/usr/bin/exiftags"; # Use jpegtran if you have it, otherwise convert will work my $ROTATE = "jpegtran -copy all -rotate"; #my $ROTATE = "convert -quality 85 -rotate"; # Set these if you want to make medium-sized images (otherwise set to 0) my $MED_SIZE_CUTOFF = 350; # Create a medium-sized image when over 350kb my $MAKE_MED = "convert -geometry 640x640+0+0"; # Set these if you want to make large-sized images (otherwise set to 0) my $LG_SIZE_CUTOFF = 1000; # Create a large-sized image when over 1000kb my $MAKE_LG = "convert -geometry 1280x1280+0+0"; # Do you want to use EXIF thumbnails if available? my $EXIFthumbs = 1; # If you have the 'jhead' program, picture descriptions will # be stored within JPEGs instead of in a separate file. If # you enable this, you can only edit these descriptions by # using the command 'jhead -ce picture.jpg' and then re- # generating the HTML for that directory. Be sure to leave # the "AUTOSCRAPBOOK" line in there somewhere. my $JHEAD = "/usr/bin/jhead"; ########################################################### my (%EXIFData, @EXIFAlways); ########################################################### # Display these EXIF tags by default ########################################################### @EXIFAlways = ( # These are from the 'exif' program 'Aperture', # These are from the 'exiftags' program 'F-Number', 'Camera Model', 'ISO Speed Rating', # These are from both programs 'Exposure Time', 'Flash', ); ########################################################### # All Possible EXIF Tags (comment out items to ignore) # Only matters if you are *not* using exiftags (only using exif) ########################################################### #$EXIFData{'0x0001'} = 'InteroperabilityIndex'; #$EXIFData{'0x0002'} = 'InteroperabilityVersion'; $EXIFData{'0x0100'} = 'Image Width'; $EXIFData{'0x0101'} = 'Image Length'; #$EXIFData{'0x0102'} = 'Bits per Sample'; #$EXIFData{'0x0103'} = 'Compression'; $EXIFData{'0x0106'} = 'Photometric Interpretation'; $EXIFData{'0x010a'} = 'Fill Order'; $EXIFData{'0x010d'} = 'Document Name'; $EXIFData{'0x010e'} = 'Image Description'; $EXIFData{'0x010f'} = 'Manufacturer'; $EXIFData{'0x0110'} = 'Model'; #$EXIFData{'0x0111'} = 'Strip Offsets'; #$EXIFData{'0x0112'} = 'Orientation'; #$EXIFData{'0x0115'} = 'Samples per Pixel'; #$EXIFData{'0x0116'} = 'Rows per Strip'; #$EXIFData{'0x0117'} = 'Strip Byte Count'; #$EXIFData{'0x011a'} = 'x-Resolution'; #$EXIFData{'0x011b'} = 'y-Resolution'; #$EXIFData{'0x011c'} = 'Planar Configuration'; #$EXIFData{'0x0128'} = 'Resolution Unit'; $EXIFData{'0x012d'} = 'Transfer Function'; $EXIFData{'0x0131'} = 'Software'; $EXIFData{'0x0132'} = 'Date and Time'; $EXIFData{'0x013b'} = 'Artist'; $EXIFData{'0x013e'} = 'White Point'; $EXIFData{'0x013f'} = 'Primary Chromaticities'; $EXIFData{'0x0156'} = 'Transfer Range'; $EXIFData{'0x0200'} = 'JPEGProc'; $EXIFData{'0x0201'} = 'JPEG Interchange Format'; $EXIFData{'0x0202'} = 'JPEG Interchange Format Lengt'; #$EXIFData{'0x0211'} = 'YCbCr Coefficients'; #$EXIFData{'0x0212'} = 'YCbCr Sub-Sampling'; #$EXIFData{'0x0213'} = 'YCbCr Positioning'; $EXIFData{'0x0214'} = 'Reference Black/White'; $EXIFData{'0x1000'} = 'RelatedImageFileFormat'; $EXIFData{'0x1001'} = 'RelatedImageWidth'; $EXIFData{'0x1002'} = 'RelatedImageLength'; $EXIFData{'0x828d'} = 'CFARepeatPatternDim'; $EXIFData{'0x828e'} = 'CFA Pattern'; $EXIFData{'0x828f'} = 'Battery Level'; $EXIFData{'0x8298'} = 'Copyright'; $EXIFData{'0x829a'} = 'Exposure Time'; $EXIFData{'0x829d'} = 'FNumber'; $EXIFData{'0x83bb'} = 'IPTC/NAA'; $EXIFData{'0x8769'} = 'ExifIFDPointer'; $EXIFData{'0x8773'} = 'InterColorProfile'; $EXIFData{'0x8822'} = 'ExposureProgram'; $EXIFData{'0x8824'} = 'Spectral Sensitivity'; $EXIFData{'0x8825'} = 'GPSInfoIFDPointer'; $EXIFData{'0x8827'} = 'ISO Speed Ratings'; $EXIFData{'0x8828'} = 'OECF'; #$EXIFData{'0x9000'} = 'Exif Version'; #$EXIFData{'0x9003'} = 'Date and Time (original)'; #$EXIFData{'0x9004'} = 'Date and Time (digitized)'; #$EXIFData{'0x9101'} = 'ComponentsConfiguration'; #$EXIFData{'0x9102'} = 'Compressed Bits per Pixel'; $EXIFData{'0x9201'} = 'Shutter speed'; $EXIFData{'0x9202'} = 'Aperture'; $EXIFData{'0x9203'} = 'Brightness'; $EXIFData{'0x9204'} = 'Exposure Bias'; #$EXIFData{'0x9205'} = 'MaxApertureValue'; $EXIFData{'0x9206'} = 'Subject Distance'; $EXIFData{'0x9207'} = 'Metering Mode'; $EXIFData{'0x9208'} = 'Light Source'; $EXIFData{'0x9209'} = 'Flash'; $EXIFData{'0x920a'} = 'Focal Length'; $EXIFData{'0x9214'} = 'Subject Area'; #$EXIFData{'0x927c'} = 'Maker Note'; $EXIFData{'0x9286'} = 'User Comment'; $EXIFData{'0x9290'} = 'SubsecTime'; $EXIFData{'0x9291'} = 'SubSecTimeOriginal'; $EXIFData{'0x9292'} = 'SubSecTimeDigitized'; #$EXIFData{'0xa000'} = 'FlashPixVersion'; $EXIFData{'0xa001'} = 'Color Space'; #$EXIFData{'0xa002'} = 'PixelXDimension'; #$EXIFData{'0xa003'} = 'PixelYDimension'; $EXIFData{'0xa004'} = 'RelatedSoundFile'; #$EXIFData{'0xa005'} = 'InteroperabilityIFDPointer'; $EXIFData{'0xa20b'} = 'Flash Energy'; $EXIFData{'0xa20c'} = 'Spatial Frequency Response'; #$EXIFData{'0xa20e'} = 'Focal Plane x-Resolution'; #$EXIFData{'0xa20f'} = 'Focal Plane y-Resolution'; #$EXIFData{'0xa210'} = 'Focal Plane Resolution Unit'; $EXIFData{'0xa214'} = 'Subject Location'; $EXIFData{'0xa215'} = 'Exposure index'; $EXIFData{'0xa217'} = 'Sensing Method'; #$EXIFData{'0xa300'} = 'File Source'; $EXIFData{'0xa301'} = 'Scene Type'; $EXIFData{'0xa302'} = 'CFA Pattern'; $EXIFData{'0xa401'} = 'Custom Rendered'; $EXIFData{'0xa402'} = 'Exposure Mode'; $EXIFData{'0xa403'} = 'White Balance'; $EXIFData{'0xa404'} = 'Digital Zoom Ratio'; $EXIFData{'0xa405'} = 'Focal Length In 35mm Film'; $EXIFData{'0xa406'} = 'Scene Capture Type'; $EXIFData{'0xa407'} = 'Gain Control'; $EXIFData{'0xa408'} = 'Contrast'; $EXIFData{'0xa409'} = 'Saturation'; $EXIFData{'0xa40a'} = 'Sharpness'; $EXIFData{'0xa40b'} = 'Device Setting Description'; $EXIFData{'0xa40c'} = 'Subject Distance Range'; $EXIFData{'0xa420'} = 'Image Unique ID'; ########################################################### sub CleanJpg($); # See if we have the exif program... unless (-x $EXIF) { $EXIF = ''; } # See if we have the jhead program... unless (-x $JHEAD) { $JHEAD = ''; } my $pmtime = (stat $0)[9]; my %Renamed; my $term = new Term::ReadLine 'AutoScrapbook'; sub GetInput { my $input; unless ($input = $term->readline('> ')) { return ''; } $input =~ s/^\s+//; $input =~ s/\s+$//; return ($input); } sub GetResolution ($) { my $file = $_[0]; my $res = `$IDENTIFY "$file"`; chomp($res); $res =~ s/^.*?(\d+x\d+).*$/$1/; return $res; } # Takes in time of form '2002:01:20 07:19:28' sub FormatEXIFTimeEpoch ($) { my ($year, $mon, $mday, $hour, $min, $sec) = split /:| /, $_[0]; return(timelocal($sec, $min, $hour, $mday, $mon-1,$year-1900)); } # Takes in time of form '2002:01:20 07:19:28' sub FormatEXIFTime ($) { my ($year, $mon, $mday, $hour, $min, $sec) = split /:| /, $_[0]; my $month = ('NONE', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon]; my $ampm = 'AM'; if ($hour == 12) { $ampm = 'PM'; } if ($hour > 12) { $ampm = 'PM'; $hour -= 12; } $hour =~ s/^0//; unless ($min =~ /\d\d/) { $min = "0$min"; } my $newtime = "$month $mday, $year ($hour:$min $ampm)"; return ($newtime); } sub FormatTime ($) { my $time = $_[0]; my ($sec, $min, $hour, $mday, $mon, $year) = localtime($time); my $month = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon]; $year = $year + 1900; my $ampm = 'AM'; if ($hour == 12) { $ampm = 'PM'; } if ($hour > 12) { $ampm = 'PM'; $hour -= 12; } unless ($min =~ /\d\d/) { $min = "0$min"; } my $newtime = "$month $mday, $year ($hour:$min $ampm)"; return ($newtime); } sub ImportDescription($$$) { my ($Dir, $This, $temp) = @_; return unless $JHEAD; return unless (system("echo AUTOSCRAPBOOK > $Dir/.$temp.jhead && /bin/cat $Dir/.$temp >> $Dir/.$temp.jhead") == 0); my $mtime = (stat("$Dir/$This"))[9]; $ENV{'EDITOR'} = "/bin/cp $Dir/.$temp.jhead"; if (system("$JHEAD -ce -ft $Dir/$This >/dev/null 2>&1") == 0) { # Delete the file if jhead was sucessful unlink "$Dir/.$temp"; chmod (0644, "$Dir/$This"); print "Description for $This stored in JPEG header...\n"; my $newmtime = (stat("$Dir/$This"))[9]; if ($newmtime > $mtime) { # Apparently the image did *not* have an EXIF time and this process just # changed its timestamp, so change it back utime $mtime, $mtime, "$Dir/$This"; } } unlink "$Dir/.$temp.jhead"; } sub GenerateSmallerImage ($$$$$$$$$) { my ($Dir, $This, $origsize, $mtime, $cutoff, $command, $prefix, $sizes, $clean) = @_; my $sizeschanged = 0; my $size = 0; my $res = 0; if ($cutoff and ($origsize > $cutoff)) { my $mtime2 = 0; # Generate smaller-sized image if (-f "$Dir/.thumbs/$prefix-$This") { $mtime2 = (stat "$Dir/.thumbs/$prefix-$This")[9]; } if ($mtime > $mtime2) { system "$command \"$Dir/$This\" \"$Dir/.thumbs/$prefix-$This\"" and die "Could not make smaller image of $This ($command)\n"; if ($clean) { CleanJpg("$Dir/.thumbs/$prefix-$This"); } chmod (0644, "$Dir/.thumbs/$prefix-$This"); print "Created $prefix image for picture: $This\n"; $res = GetResolution("$Dir/.thumbs/$prefix-$This"); } $size = (stat "$Dir/.thumbs/$prefix-$This")[7]; $size = int($size/1000); if ($sizes->{"$prefix-$This"}) { if ($res) { # Just generated if ($res ne $sizes->{"$prefix-$This"}) { $sizes->{"$prefix-$This"} = $res; $sizeschanged++; } } else { $res = $sizes->{"$prefix-$This"}; } } else { unless ($res) { $res = GetResolution("$Dir/.thumbs/$prefix-$This"); } $sizes->{"$prefix-$This"} = $res; $sizeschanged++; } } return ($sizeschanged, $res, $size); } sub GeneratePage ($@) { my ($Dir, @items) = @_; print "Generating page in $Dir\n"; my ($Name, $Desc, $SubName, $SubDesc, $SubNum, $mtime, $mtime2, $This, $Num, $SubNewest, $Newest, $PicDesc, $NewName, $PrevName); $Num = 0; open (OUT, ">$Dir/index.html.new"); open (NAME, "$Dir/.name"); $Name = ; chomp $Name; close (NAME); open (DESC, "$Dir/.info"); $Desc = ; chomp $Desc; close (DESC); print OUT "\n \n $Name\n \n"; print OUT " \n

$Name

\n"; if (-f "$Dir/.header") { open (HEADER, "$Dir/.header"); foreach (
) { print OUT $_; } close (HEADER); } if ($Desc) { print OUT "

$Desc



\n"; } $Newest = 0; if (grep /\/$/, @items) { # There are subdirectories print OUT " \n"; print OUT " \n"; foreach $This (@items) { if ($This =~ /\/$/) { open (NAME, "$Dir/$This/.name"); $SubName = ; chomp $SubName; close (NAME); open (DESC, "$Dir/$This/.info"); $SubDesc = ; chomp $SubDesc; close (DESC); if (open (NUM, "$Dir/$This/.num")) { $SubNum = ; chomp $SubNum; close (NUM); } else { $SubNum = 0; } open (NEWEST, "$Dir/$This/.newest"); $SubNewest = ; chomp $SubNewest; close (NEWEST); $Num += $SubNum; if ($Newest < $SubNewest) { $Newest = $SubNewest; } my $temp = FormatTime ($SubNewest); if ($SubDesc) { $SubDesc = "($SubDesc)"; } print OUT " \n"; } } print OUT "
Sub-Categories# PicsNewest Picture
$SubName $SubDesc $SubNum $temp
\n"; } if (grep /[^\/]$/, @items) { # There are images... # Get cached image sizes my ($sizes, $szmtime) = GetImageSizes($Dir); my $sizeschanged = 0; print OUT "

\n"; print OUT " \n"; if ($EXIF) { print OUT " \n"; } print OUT "\n"; foreach $This (@items) { if ($This =~ /[^\/]$/) { my %PictureData; my $size; (undef, undef, undef, undef, undef, undef, undef, $size, undef, $mtime) = stat "$Dir/$This"; $size = int($size/1000); my $res; if (($sizes->{$This}) and ($mtime < $szmtime)) { $res = $sizes->{$This}; } else { $res = GetResolution("$Dir/$This"); $sizes->{$This} = $res; $sizeschanged++; } if ($Newest < $mtime) { $Newest = $mtime; } $Num++; my $temp = $This; $temp =~ s/\.[^\.]+$//; $PicDesc = ''; if (-f "$Dir/.$temp") { open (DESC, "$Dir/.$temp"); $PicDesc = ; chomp $PicDesc; close (DESC); unless ($PicDesc) { # Handle empty descriptions... $PicDesc = ' '; } if ($JHEAD and ($This =~ /(jpg|jpeg)$/i)) { # See if we can import this old-style description ImportDescription($Dir, $This, $temp); } } elsif ($JHEAD) { my $mtime = (stat("$Dir/$This"))[9]; # See if the description is stored in the JPEG header $ENV{'EDITOR'} = '/bin/cat'; $PicDesc = `$JHEAD -ce $Dir/$This 2>/dev/null`; if ($? == 0) { $PicDesc =~ s/Comment not modified//; if ($PicDesc =~ s/Modified: \S+//) { utime $mtime, $mtime, "$Dir/$This"; } chomp($PicDesc); if ($PicDesc =~ /Not JPEG/) { # No comment found... $PicDesc = ''; } elsif ($PicDesc and ($PicDesc !~ s/AUTOSCRAPBOOK//)) { # Did not find a valid AutoScrapbook description print "\nImage $This has a description not set by AutoScrapbook:\n"; print $PicDesc; $PicDesc = ''; } else { # Remove old-style description file if it exists unlink "$Dir/.$temp"; } } else { $PicDesc = ''; } } unless ($PicDesc) { my $OldName; if ($XV) { system ("$XV \"$Dir/$This\" &"); } if ($Renames) { my $ext = $This; $ext =~ s/^.+\.([^\.]+)$/$1/; print "\nDo you want to rename $This?\n"; print "Type in a new name or ENTER to keep current name (type 'DELETE' to delete)\n"; print "You can also type +90, +180, etc to rotate right (can still be followed by new name)\n"; $NewName = GetInput; if ($NewName =~ s/^\+(\d+)\s*//) { my $amount = $1; # Specified a rotation amount my $cmd = "$ROTATE $amount '$Dir/$This' "; if ($ROTATE =~ /jpegtran/) { $cmd .= '> '; } my $mtime = (stat("$Dir/$This"))[9]; $cmd .= "'$Dir/$This.rotated.$ext'"; if (system($cmd) == 0) { move ("$Dir/$This.rotated.$ext", "$Dir/$This"); if ($EXIF and $EXIFthumbs and (system("$EXIF -e '$Dir/$This' >/dev/null 2>&1") == 0)) { # Rotate the thumbnail too if (system("convert -rotate $amount '$Dir/$This.modified.jpeg' '$Dir/$This.thumb.$ext'") == 0) { if (system("$EXIF -n '$Dir/$This.thumb.$ext' '$Dir/$This' >/dev/null 2>&1") == 0) { move ("$Dir/$This.modified.jpeg", "$Dir/$This"); unlink "$Dir/$This.thumb.$ext"; } } } utime $mtime, $mtime, "$Dir/$This"; chmod (0644, "$Dir/$This"); unlink "$Dir/.thumbs/$This"; unlink "$Dir/.thumbs/med-$This"; unlink "$Dir/.thumbs/large-$This"; print "Rotated $This by $amount.\n"; } else { die "Rotation failed: $!\n"; } } $OldName = $This; if ($NewName) { if ($NewName eq 's') { # skip next; } if ($NewName eq 'DELETE') { unlink ("$Dir/$This"); next; } unless ($NewName =~ /\.[^\.]+$/) { $NewName .= ('.' . lc($ext)); } if (-f "$Dir/$NewName") { die "$NewName already exists!\n" } $This = $NewName; $temp = $This; $temp =~ s/\.[^\.]+$//; print "Will rename $OldName to $NewName (hit CTRL-C to abort!!)\n"; $Renamed{$NewName} = $OldName; } } print "\nGive a description for picture $This:\n"; $PicDesc = GetInput; unless ($PicDesc) { # Empty description $PicDesc = ' '; } open (FILE, ">$Dir/.$temp"); print FILE "$PicDesc\n"; close (FILE); if ($Renames and $NewName) { move ("$Dir/$OldName", "$Dir/$NewName"); } ImportDescription($Dir, $This, $temp); } my $EXIFThumbnail = 0; if ($EXIFTAGS) { foreach my $line (`$EXIFTAGS -q $Dir/$This 2>/dev/null`) { chomp $line; $line =~ s/^\s+//; $line =~ s/\s+$//; my ($tag, $value) = split /:\s+/, $line; if ($value) { if ($tag eq 'Image Created') { $tag = 'Date and Time'; } $PictureData{$tag} = $value; } } } if ($EXIF and ((not $EXIFTAGS) or $EXIFthumbs)) { # Determine EXIF data foreach my $line (`$EXIF -i $Dir/$This 2>/dev/null`) { chomp $line; if (($line =~ /^0x/) and (not $EXIFTAGS)) { my ($tag, $value) = split /\|/, $line; $value =~ s/^\s+//; $value =~ s/\s+$//; if ($value and $EXIFData{$tag}) { $PictureData{$EXIFData{$tag}} = $value; } } elsif ($line =~ /^EXIF data contains a thumbnail/) { $EXIFThumbnail = 1; } } } if ($JHEAD) { # If available, set file timestamp to EXIF timestamp system("jhead -ft '$Dir/$This' >/dev/null 2>&1"); } unless (-d "$Dir/.thumbs") { mkdir ("$Dir/.thumbs", 0755); chmod (0755, "$Dir/.thumbs"); } # Generate standard thumbnail if (-f "$Dir/.thumbs/$This") { $mtime2 = (stat "$Dir/.thumbs/$This")[9]; } else { $mtime2 = 0; } if ($mtime > $mtime2) { if ($EXIFThumbnail and $EXIFthumbs and (system("$EXIF -e '$Dir/$This' >/dev/null 2>&1") == 0)) { # Use the EXIF thumbnail if (move ("$Dir/$This.modified.jpeg", "$Dir/.thumbs/$This")) { print "Retrieved EXIF thumbnail for picture: $This\n"; } else { die "Could not retrieve EXIF thumbnail for picture: $This\n"; } } else { # Create our own system "$MAKE_THUMBNAIL \"$Dir/$This\" \"$Dir/.thumbs/$This\"" and die "Could not make thumbnail of $This\n"; if (keys %PictureData) { # Picture had EXIF data (but apparently no thumbnail), so remove EXIF data from generated thumbnail CleanJpg("$Dir/.thumbs/$This"); } print "Created thumbnail for picture: $This\n"; } chmod (0644, "$Dir/$This"); chmod (0644, "$Dir/.thumbs/$This"); } my $clean = ($EXIFThumbnail or (keys %PictureData)); my ($medsizeschanged, $medres, $medsize) = GenerateSmallerImage($Dir, $This, $size, $mtime, $MED_SIZE_CUTOFF, $MAKE_MED, 'med', $sizes, $clean); my ($lgsizeschanged, $lgres, $lgsize) = GenerateSmallerImage($Dir, $This, $size, $mtime, $LG_SIZE_CUTOFF, $MAKE_LG, 'large', $sizes, $clean); $sizeschanged += ($medsizeschanged + $lgsizeschanged); print OUT " \n"; chmod (0644, "$Dir/$This"); } } if ($EXIF) { print OUT " "; } else { print OUT " "; } print OUT "
PictureDescriptionDate/TimeEXIF Data
\n"; print OUT "\n"; } else { print OUT "\n"; } if ($lgsize > 0) { print OUT "\n"; } print OUT "\n"; print OUT "
\n"; if ($medsize > 0) { print OUT "$medres
$medsize kb
$lgres
$lgsize kb
$res
$size kb
$PicDesc \n"; if ($PictureData{'Date and Time'}) { my $epoch = FormatEXIFTimeEpoch($PictureData{'Date and Time'}); if ($Newest < $epoch) { $Newest = $epoch; } $temp = FormatEXIFTime($PictureData{'Date and Time'}); print OUT "Taken: $temp\n"; } else { $temp = FormatTime($mtime); print OUT "Posted: $temp\n"; } if (keys %PictureData) { foreach (@EXIFAlways) { if ($PictureData{$_}) { print OUT "$_: $PictureData{$_}
\n"; delete $PictureData{$_}; } } print OUT "
\n"; print OUT "

\n"; foreach (sort keys %PictureData) { print OUT "$_: $PictureData{$_}
\n"; } print OUT "
\n"; } print OUT "
(Click for larger pictures)
(Click for larger pictures)
\n"; if ($sizeschanged > 0) { SaveImageSizes($Dir, $sizes); } } if (-f "$Dir/.footer") { open (FOOTER, "$Dir/.footer"); foreach (