#!/usr/bin/perl -w use strict; use File::Copy; use Term::ReadLine; =pod =head1 NAME AutoScrapbook-NEW - index images and films (and more) and create a static site =head1 SYNOPSIS B [--[no]options...] dir1 [dir2 dir3 ... dirN] The following options can be used preceded by two dashes (e.g. --help), 0/1 options can be used with --no (e.g. --nobig), the default values are given here. 'askdirreproc' => 0 # ask (1) or not (0) before reprocessing a dir 'author' => 'Firsty Second' # Your Name 'autorotate' => 0 # try to rotate correctly images and films (EXIF based) 'backup' => 1 # should the original of a file be saved or not 'big' => 1 # copy full-sized picture (1/0) 'comment' => 1 # don't force existence of comment with --nocomment 'config' => 'default' # different configs are possible 'debug' => 0 # debug ON or OFF (1/0) 'delorig' => 0 # when deleting a file, should we delete the original 'display' => 1 # display or not image while modifying it 'email' => 'example@example.net' # Your Email 'exif' => 1 # do we want or not a column with exif information 'exif-thumbs' => 1 # do we use thumbfile in EXIF information? 'force-change' => 0 # force change (same effect as "touch $0"). 'fps' => '24' # wished frames per seconds of film 'help' => 0 # get help 'medium-geometry' => '800x800' # geometry of medium size image 'modify' => 1 # make modification of original picture possible (1/0) 'newdirproc' => 'ask' # process new dirs (yes, no or ask) 'oldmm' => 0 # use old version of MPlayer and MjpegTools 'parent' => 1 # should index.html reference back to parent directory? 'medium-cutoff' => '200', # max. size in KB to create medium image 'skip' => 1 # if unset (--noskip), ignore the skip flag. 'target' => '' # if images and webpages should be copied elsewhere 'thumb-geometry' => '100x100' # geometry of thumbnails 'tvnorm' => '+p' # TV Norm to use (+p for PAL, +n for NTSC) =head1 DESCRIPTION AutoScrapbook-NEW (short I) scans directories, looking for images (mostly JPEG but also other types) and films (in MJPEG format). The user can then comment, modify, skip or delete these files. Out of the information gathered, plus EXIF information if available, I then creates static web pages, with thumbnail overviews, and sub-directories. Basically, each directory given on the command line is scanned, the user is asked for a name and a description. Then for each file found, the user can give a comment as well, modify the file (depending on the type of the file, things like rotate, edit), delete it or skip it (the file is then ignored). Afterwards, I creates a thumbnail image for each file and, if the original file is "too big", a medium sized file: =over =item * for images, just a smaller image. =item * for MJPEG films, a film with the same resolution but with video in MPEG1 format and sound in MP2 format. The framerate is also adapted to something standard for MPEG1 (24, 25 or 30 are recommended). =back Out of all this information, one webpage C is created in each sub-directory, and, per default, linked together. =head1 OPTIONS =over 5 =item --[no]B ask for directory reprocessing When a new directory is discovered by I, the user is asked if he wants to process it. If the answer is positive, the user is not asked anymore in successive runs. This flag allows to change this. =item --B=C name of the author Just give your name, like C<--author='First Last'>. This information will be used for the creation of the webpages. See also the B option. =item --[no]B try to rotate correctly images and films Based on EXIF information, it is possible to place images (and films) in the correct direction. This options allows you to do it automatically. =item --[no]B backup original file Main aim of the program for me was to process photos and films created with my digital camera, without risking to loose the original. Therefore the program saves per default the unmodified version of each file (this does not mean that each version is backuped, but only the first one). Each modified file is saved once in the hidden directory C<.orig> in each sub-directory. Using C<--nobackup>, the original version is not saved. =item --[no]B use full-size picture Per default the original file, which might be very I is linked into the webpage, respectively copied when using the B option. By using C<--nobig>, the original file is neither copied nor linked, as long as a medium sized file exists; this is very handy for websites. =item --[no]B force use of comment Per default, every file must have a comment, using C<--nocomment> you remove this obligation (see examples in combination with C<--nomodify>). =item --B=C configuration name It is possible to save different configuration (i.e. which files are skipped, order, comments...). The configuration name can only contain small caps letters, the default is... 'default'. =item --[no]B debug mode Yep, with this one, you can have a lot of debug messages. =item --[no]B delete original file As described for the B option, per default, the original file is saved before any modification. Per default as well, a backup is made or kept if the file is deleted. Using C<--delorig>, you can completely wipe out files, without any remaining backup. Your choice. =item --[no]B display file Per default, each file is displayed, so that you can see what you are doing. With C<--nodisplay>, nothing is shown, which can spare time (especially if you use a file browser in parallel). =item --B=C email of the author Just give your email address, like C<--email='first@last.org'>. This information will be used for the creation of the webpages. See also the B option. =item --[no]B exif information The table created by I contains per default a column with all possible EXIF information. With C<--noexif>, this column is suppressed. =item --[no]B thumbfiles from the EXIF information The EXIF information might contain also a thumbnail of the picture. Per default this EXIF-thumbnail is used. If you prefer to have the thumbnails created on the fly, use C<--noexif-thumb>. =item --B re-process all files Per default, unmodified files are not re-processed; you can force re-processing of all files with this option. The same effect is obtained when the script I is modified. This means that the index.html file is recreated. =item --B=C frames per second The films of my Canon Ixus v3 have 15fps, which is not an accepted standard for MPEG1 or 2 films, therefore I re-sample them to 24 fps. But if you prefer another rate, you can use this option. =item --B get help Some short reminders... =item --B=C geometry of medium sized image If a medium image needs to be created, it will have maximum this size. The default is 800x800, which means that no dimension will be bigger than 800 (e.g. a file of 1200x1600 will be reduced to 600x800). =item --[no]B modify files Per default, you can modify files (edit, rotate, delete, skip, etc...). With I<--nomodify>, you can do this only for images that have not yet been commented. =item --B=C process new directories or not When a new directory is discovered by I, the user is asked if he wants to process it by default (or set to I). With this option set to I, each new directory is processed; set to I, no new directory is processed (only old ones). =item --[no]B use old versions of MPlayer and MjpegTools I stands for MultiMedia, and means MPlayer and MjpegTools. set I<--oldmm> if you have MPlayer 0.9.2 and mjpegtools 1.6.0 and I<--nooldmm> (the default) if you have MPlayer 1.0pre3 and mjpegtools 1.6.2. For other versions, you'll need to try, but obviously mixing is not possible. =item --[no]B reference to parent directory Webpages created in sub-directories have per default a reference to the webpage of the parent directory. Using C<--noparent> this reference is removed. =item --B=C minimum size for medium file creation Medium sized files are only created if the original file is bigger than this size in KB. The default is 200KB. =item --[no]B respect skip decision During processing of the files, you can decide to skip a certain file, and this decision is normally respected: you won't be asked anymore about this file (as long as you work under the same configuration name, see option B). Using C<--noskip>, you'll see again all the files and can change your decision. =item --B=C target directory By using this option, you can create webpages, medium and thumbnail files in another directory than the original one; without the C<--nobig> option, the original files are also copied to the new directory. Take care that the end-directory is relative; if you call: asb --target=/home/user/public_html Images the end directory will be C. =item --B=C geometry of thumbfiles Thumbnail images that are not created out of the EXIF information will have this size. The default is 100x100, which means that no dimension will be bigger than 100 (e.g. a file of 1200x1600 will be reduced to 75x100). =item --B=C<+p|+n> TV Norm This is the TV Norm your films are. For non-standard films, as created by my Canon Camera, the mjpegtools are not able to guess the standard used. Possible values are C<+p> for PAL (default) and C<+n> for NTSC. =back =head1 EXAMPLES The simplest command, in order to process the sub-directory C in the current directory: asb Process the directory C without EXIF information: asb --noexif Image A good one to create a Website (the original pictures are not copied, you can put specific comments and skip private pictures you don't want to publish) is: asb --config=web --nobig --target=/home/user/public_html Pictures With the following command, you get a new chance to process formerly skipped files, all index.html files will be recreated, but you will get only the files to modify that do not have yet a comment, everything for the current directory: asb --noskip --force-change --nomodify . And using the C<--nocomment> switch, you can recreate the webpages without being bothered by any question (even if certain files or directories don't have a comment): asb --force-change --nomodify --nocomment . =head1 TIPS 'n' TRICKS =over =item * shorter commands During the modification phase, you can enter the shortest possible combination for the command to be unique (in small and big caps): e.g. 'c', 're', 'ro' (and everything in between) for COMMENT, RENAME and ROTATE. =item * fancy comments Put HTML code in your comments and they will be represented in the index.html. =back =head1 DEPENDENCIES I would recommend to have most of the following commands (together with the variables you might want to modify if your utilities are placed otherwise): =head2 Commands for JPEG images =head3 From a package called C, or C or C $JPEGTRAN = '/usr/bin/jpegtran'; $RDJPGCOM = '/usr/bin/rdjpgcom'; =head3 For exif manipulation $EXIF = '/usr/bin/exif'; $EXIFTAGS = '/usr/bin/exiftags'; $JHEAD = '/usr/bin/jhead'; Those commands are slightly less critical than all the other ones. I would still recommend to have C and either C or C. =head2 Commands for all kind of images =head3 From the C package $MOGRIFY = '/usr/bin/mogrify'; $IDENTIFY = '/usr/bin/identify'; $DISPLAY = '/usr/bin/display'; $CONVERT = '/usr/bin/convert'; =head3 Any image editor $IMAGEEDITOR='/usr/bin/gimp'; But it can be as well any of your favorite image editor. =head2 Commands for AVI MJPEG files =head3 from the C package $MPLAYER = '/usr/bin/mplayer'; $MENCODER = '/usr/bin/mencoder'; For the SuSE distribution (and perhaps others), the standard package doesn't fit as it doesn't contain C. Therefore compile yourself your package or use the package from L =head3 from the C package $MP2ENC = '/usr/bin/mp2enc'; $MPEG2ENC = '/usr/bin/mpeg2enc'; $MPLEX = '/usr/bin/mplex'; $JPEG2YUV = '/usr/bin/jpeg2yuv'; $LAVTRANS = '/usr/bin/lavtrans'; $LAVADDWAV = '/usr/bin/lavaddwav'; For the SuSE distribution (and perhaps others), the standard package doesn't fit as it doesn't contain the encoder. Therefore compile yourself your package or use the package from L. The man page of mp2enc recommends to use C from L instead, as being of better quality, but I was to lazy to package and test. =head1 VERSION This is AutoScrapbook-NEW 3.9 (2004-11-28) [beta9 of version 4.0] =head1 AUTHOR Eric Lavarde Based on AutoScrapbook 3.7 (Jan 23, 2003), program written by Kirk Bauer Thanks a lot as well to Ben Edgington for his introduction to "film-making" under L. =head1 LICENSE 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 the authors credit for the program. This is free software. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ##################################################################### Copyright (c) 2003 Kirk Bauer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ##################################################################### For more information, please visit L =head1 FILES In each processed directory, the following directory and files are created: =over =item * .asbdir- One directory per configuration, each containing the following files: =over 2 =item * .info A text file containing all the information concerning the file named . =item * dir.info A text file containing all the information concerning the current directory. =item * dir.order A text file containing a list of the items (sub-directories and files) contained in the current directory, and the order in which they should be processed. This file must be edited manually to change this order. New items are added at the end of the list. =back =item * .orig This is the directory where all the original files are backuped. =item * .thumbs This is the directory where thunbnail and medium files are created. =item * index.html The webpage created by I. =back =head1 SEE ALSO All the commands in the L section. =head1 BUGS =over =item * Possibly a lot of undiscovered bugs... =item * the option C<--noparent> should only remove the reference from top-directories, but currently directories and sub-directories are processed the same. =item * no editor command is defined for films =item * developer documentation is very sparse =item * If the C command is present, thumbnails are not rotated or whatsoever (because they are extracted directly from the EXIF information). =item * Often mplayer doesn't get killed properly after display, user needs to close the window manually. =back =head1 TODO =over =item * Clearly separate input (comments, modifications, etc...) and output (index.html). =item * Continue with refactorization and make the output modular (for other HTML formats, VCD, SVCD, etc...). =item * Make the whole stuff object oriented and really modular (input and output). =back =head1 DEVELOPER DOCUMENTATION (I) =over =cut # default options, see also ### MAIN ### part my %ScrapOpts = ( 'askdirreproc' => 0, # ask (1) or not (0) before reprocessing a dir 'author' => 'Eric Lavarde', # Your Name 'autorotate' => 0, # try to rotate correctly images / films (EXIF based) 'backup' => 1, # should the original of a file be saved or not 'big' => 1, # copy full-sized picture (1/0) 'comment' => 1, # don't force existence of comment with --nocomment 'config' => 'default', # different configs are possible 'debug' => 0, # debug ON or OFF (1/0) 'delorig' => 0, # when deleting a file, should we delete the original 'display' => 1, # display or not image while modifying it 'email' => 'asb@zorglub.s.bawue.de', # Your Email 'exif' => 1, # do we want or not a column with exif information? 'exif-thumbs' => 1, # do we use thumbfile in EXIF information? 'force-change' => 0, # force change (same effect as touch $0). 'fps' => '24', # wished frames per seconds of film 'help' => 0, # get help 'medium-geometry' => '800x800', # geometry of medium image 'modify' => 1, # make modification of original picture possible (1/0) 'newdirproc' => 'ask', # process new dirs (yes, no or ask) 'oldmm' => 0, # use old version of MPlayer and MjpegTools 'parent' => 1, # should index.html reference back to parent directory? 'medium-cutoff' => '200', # max. size in KB to create medium image 'skip' => 1, # if unset (--noskip), ignore the skip flag. 'target' => '', # if images and webpages should be copied elsewhere 'thumb-geometry' => '100x100', # geometry of thumbnails 'tvnorm' => '+p', # TV Norm to use (+p for PAL, +n for NTSC) ); sub usage() { print <<"EOF"; AutoScrapBook Version 3.9 beta9 Synoptics: $0 [--options...] dir1 dir2 ... The following options can be used preceded by two dashes (e.g. --help), yes/no, on/off, 0/1 options can be used with --no (e.g. --nobig if you don't want to copy big files, useful for web sites): 'askdirreproc' => 0 # ask (1) or not (0) before reprocessing a dir 'author' => 'Firsty Second' # Your Name 'autorotate' => 0 # try to rotate correctly images / films (EXIF based) 'backup' => 1 # should the original of a file be saved or not 'big' => 1 # copy full-sized picture (1/0) 'comment' => 1 # don't force existence of comment with --nocomment 'config' => 'default' # different configs are possible 'debug' => 0 # debug ON or OFF (1/0) 'delorig' => 0 # when deleting a file, should we delete the original 'display' => 1 # display or not image while modifying it 'email' => 'example\@example.net' # Your Email 'exif' => 1 # do we want or not a column with exif information 'exif-thumbs' => 1 # do we use thumbfile in EXIF information? 'force-change' => 0 # force change (same effect as "touch $0"). 'fps' => '24' # wished frames per seconds of film 'help' => 0 # get help 'medium-geometry' => '800x800' # geometry of medium size image 'modify' => 1 # make modification of original picture possible (1/0) 'newdirproc' => 'ask' # process new dirs (yes, no or ask) 'oldmm' => 0 # use old version of MPlayer and MjpegTools 'parent' => 1 # should index.html reference back to parent directory? 'medium-cutoff' => '200', # max. size in KB to create medium image 'skip' => 1 # if unset (--noskip), ignore the skip flag. 'target' => '' # if images and webpages should be copied elsewhere 'thumb-geometry' => '100x100' # geometry of thumbnails 'tvnorm' => '+p', # TV Norm to use (+p for PAL, +n for NTSC) You can get a lot more of information by using: 'perldoc -F $0'. EOF exit 0; } ########################################################### my %TypeActions; $TypeActions{'JPEG'} = { 'isA' => \&ImageIsA, 'ROTATE' => \&JpegRotate, 'EDIT' => \&ImageEdit, 'RENAME' => \&FileRename, 'DELETE' => \&FileDelete, 'SKIP' => \&FileSkip, 'COMMENT' => \&FileComment, 'loadExtraInfo' => \&JpegLoadExtraInfo, 'saveExtraInfo' => \&JpegSaveExtraInfo, 'getGeometry' => \&ImageGetGeometry, 'createOtherFiles' => \&ImageCreateOtherFiles, 'beginDisplay' => \&ImageBeginDisplay, 'endDisplay' => \&FileEndDisplay }; $TypeActions{'IMAGE_NOT_JPEG'} = { 'isA' => \&ImageIsA, 'ROTATE' => \&ImageRotate, 'EDIT' => \&ImageEdit, 'RENAME' => \&FileRename, 'DELETE' => \&FileDelete, 'SKIP' => \&FileSkip, 'COMMENT' => \&FileComment, 'loadExtraInfo' => \&NullLoadExtraInfo, 'saveExtraInfo' => \&NullSaveExtraInfo, 'getGeometry' => \&ImageGetGeometry, 'createOtherFiles' => \&ImageCreateOtherFiles, 'beginDisplay' => \&ImageBeginDisplay, 'endDisplay' => \&FileEndDisplay }; $TypeActions{'MJPG'} = { 'isA' => \&MjpgIsA, 'ROTATE' => \&MjpgRotate, # 'EDIT' => \&MjpgEdit, # TODO: do you know a good film editor? glav? 'RENAME' => \&FileRename, 'DELETE' => \&FileDelete, 'SKIP' => \&FileSkip, 'COMMENT' => \&FileComment, 'loadExtraInfo' => \&MjpgLoadExtraInfo, 'saveExtraInfo' => \&NullSaveExtraInfo, 'getGeometry' => \&FilmGetGeometry, 'createOtherFiles' => \&MjpgCreateOtherFiles, 'beginDisplay' => \&MjpgBeginDisplay, 'endDisplay' => \&FileEndDisplay }; sub NullLoadExtraInfo($\%) { return 1; } sub NullSaveExtraInfo($\%) { return 1; } ########################################################### # Customize external program locations and arguments here ########################################################### sub which($) { # simple function to find the correct binary in the PATH my $exec = shift; return '' unless ($exec); # wrong call or so return $exec if (-x $exec and $exec =~ /^\//); # an absolute path foreach ( split(/:/,$ENV{'PATH'}) ) { return "$_/$exec" if (-x "$_/$exec"); } return ''; # not found, doesn't exist } # Commands for JPEG and other images # from the 'jpeg' package (under SuSE) my $JPEGTRAN = which 'jpegtran'; my $RDJPGCOM = which 'rdjpgcom'; # from the 'ImageMagick' package my $MOGRIFY = which 'mogrify'; my $IDENTIFY = which 'identify'; my $DISPLAY = which 'display'; my $CONVERT = which 'convert'; # other utilities my $IMAGEEDITOR=which 'gimp'; my $EXIF = which 'exif'; my $EXIFTAGS = which 'exiftags'; my $JHEAD = which 'jhead'; # Commands for AVI MJPEG files (from mplayer or mjpegtools packages) # from the 'MPlayer' package from my $MPLAYER = which 'mplayer'; my $MPLAYER_Q = '-quiet -nojoystick -nortc -nolirc'; my $MPLAYER_RQ = '-really-quiet -nojoystick -nortc -nolirc'; my $MPLAYER_MUTE = ' >/dev/null 2>&1 , patched # by myself. my $MP2ENC = which 'mp2enc'; # man page recommends toolame from # http://mikecheng.d2.net.au/ instead my $MPEG2ENC = which 'mpeg2enc'; my $MPLEX = which 'mplex'; my $JPEG2YUV = which 'jpeg2yuv'; my $LAVTRANS = which 'lavtrans'; my $LAVADDWAV = which 'lavaddwav'; ########################################################### my $ASBDIR; 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', ## These are from mplayer for films #'ID_FILENAME', #'ID_VIDEO_FORMAT', #'ID_VIDEO_BITRATE', #'ID_VIDEO_WIDTH', #'ID_VIDEO_HEIGHT', #'ID_VIDEO_FPS', #'ID_VIDEO_ASPECT', #'ID_AUDIO_CODEC', #'ID_AUDIO_FORMAT', #'ID_AUDIO_BITRATE', #'ID_AUDIO_RATE', #'ID_AUDIO_NCH', #'ID_LENGTH', ); ########################################################### # 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'; ########################################################### my %Renamed; my $term = new Term::ReadLine 'AutoScrapbook'; sub _debug { $ScrapOpts{'debug'} and print 'DEBUG: ',@_,"\n"; } sub _debugf { $ScrapOpts{'debug'} and print ' ',@_,"\n"; } sub _note { print 'NOTE: ',@_,"\n"; } sub _warning { return ('WARNING: ',@_,"\n"); } # for use with warn sub _error { return ('ERROR: ',@_,": $!"); } # for use with die sub _follow { print ' ',@_,"\n"; } =item Function firstOlderThanLast(file1,file2) Returns true if the first file is strictly older than the second. file1 and/or file2 can also be timestamps (integers in seconds since epoch). If a file doesn't exist, its timestamp is supposed to be 0. =cut sub firstOlderThanLast($$) { my $first = shift; my $mtime1 = 0; my $second = shift; my $mtime2 = 0; if ( $first =~ /^[0-9]+$/ ) { $mtime1 = $first; } else { $mtime1 = (stat $first)[9] if (-e $first); } if ( $second =~ /^[0-9]+$/ ) { $mtime2 = $second; } else { $mtime2 = (stat $second)[9] if (-e $second); } return ($mtime1 < $mtime2); } sub runCmd($) { my $cmd = shift; print "\n"; my $pid = open(PROC, "( $cmd ) 2>&1 |") or die _error("Command '$cmd' failed"); print "PROCESSING: "; $| = 1; # un-buffered output while () { if (/WARN|ERROR/i) { print "\n" . $_; } else { print '.'; # progress status #--DEBUG: [%s] %s # INFO: [%s] %s } } $| = 0; # buffered output close(PROC) or return $?; print "\n"; return 0; } sub createDirectory($) { my $dir = shift; my $bool = 0; if (not -d $dir) { $ScrapOpts{'debug'} and $bool = 1; use File::Path; mkpath ($dir, $bool, 0755) or die _error("Directory '$dir' can't be created"); _debug("Created directory '$dir'."); } else { chmod (0755, $dir) or die _error("Directory '$dir' can't be set up correctly"); } } sub deleteDirectory($) { my $dir = shift; my $bool = 0; if (-d $dir) { # $ScrapOpts{'debug'} and $bool = 1; # not really helpful use File::Path; rmtree($dir, $bool) or die _error("Directory '$dir' can't be removed"); _debug("Removed directory '$dir'."); } } sub reCreateDirectory($) { my $dir = shift; deleteDirectory($dir); return createDirectory($dir); } sub getInput($;$) { my $question = shift; my $default = shift; my $input; print $question . "\n" if ($question); unless ($input = $term->readline('> ')) { if (defined $default) { $input = $default; } else { $input = ''; } } $input =~ s/^\s+//; $input =~ s/\s+$//; return ($input); } sub formatTime($) { my $time = shift; if ($time !~ /^\d+$/) { # we assume EXIF date "YYYY:MM:DD HH:MM:SS" my ($year, $mon, $mday, $hour, $min, $sec) = split /:| /, $time; --$mon; $year -= 1900; use Time::Local; $time = timelocal($sec, $min, $hour, $mday, $mon, $year); } my $newtime = localtime($time); return $newtime; } sub infoFile($$) { my $dir = shift; my $file = shift; return "$dir/$ASBDIR/$file.info"; } sub origFile($$) { my $dir = shift; my $file = shift; return "$dir/.orig/$file"; } sub backupFile($$) { my $fromFile = shift; my $toFile = shift; my $mtime = (stat $fromFile)[9]; copy($fromFile,$toFile); utime $mtime, $mtime, $toFile; _debug("Saved '$fromFile' to '$toFile'."); } sub restoreFile($$) { my $fromFile = shift; my $toFile = shift; if (-f $fromFile) { move($fromFile,$toFile); _debug("Restored '$toFile' from '$fromFile'."); return 1; } else { return 0; } } sub renameFile($$) { my $fromFile = shift; my $toFile = shift; if (-f $fromFile) { move($fromFile,$toFile); _debug("Renamed '$fromFile' to '$toFile'."); return 1; } else { return 0; } } =item Function backupOrig (dir, \%info) This function is just meant to save the original file, so that subsequent changes can be rollbacked (especially with multiple lossy changes and JPEG). In other words, a file is only saved once and then no more. =cut sub backupOrig($$) { my $dir = shift; my $info = shift; my $file = $info->{'NAME'}; return 0 unless ($ScrapOpts{'backup'}); my $origdir = "$dir/.orig"; createDirectory($origdir); my $origfile = "$origdir/$file"; if (not -f $origfile) { # we save the file only once backupFile("$dir/$file","$origfile"); if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { backupFile($dir . '/' . $info->{'aux'}->{$_}, $origdir . '/' . $info->{'aux'}->{$_}); } } return 1; } else { return 0; } } sub restoreOrig($$) { my $dir = shift; my $info = shift; my $file = $info->{'NAME'}; if (restoreFile(origFile($dir,$file),"$dir/$file")) { if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { restoreFile(origFile($dir,$info->{'aux'}->{$_}), $dir . '/' . $info->{'aux'}->{$_}); } } return 1; } else { return 0; } } sub deleteOrig($$) { my $dir = shift; my $info = shift; unlink(origFile($dir,$info->{'NAME'})); if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { unlink(origFile($dir,$info->{'aux'}->{$_})); } } } sub renameOrig($$$) { my $dir = shift; my $info = shift; my $newfile = shift; my $oldfile = $info->{'NAME'}; if (renameFile(origFile($dir,$oldfile),origFile($dir,$newfile))) { if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { my $ofile = $info->{'aux'}->{$_}; my $nfile = $ofile; $nfile =~ s/$oldfile/$newfile/; renameFile(origFile($dir,$ofile), origFile($dir,$nfile)); } } return 1; } else { return 0; } } sub renameAux($$$) { my $dir = shift; my $info = shift; my $newfile = shift; my $oldfile = $info->{'NAME'}; if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { $info->{'aux'}->{$_} =~ s/$oldfile/$newfile/; } return 1; } else { return 0; } } =item Function SetOrder (directory, items) function takes care that items are always processed in the same order by saving order in the dir.order file. New items are appended, items not existing anymore are removed. returns the new sorted list of items. =cut sub SetOrder($@) { my ($Dir, @items) = @_; my $orderfile = "$Dir/$ASBDIR/dir.order"; my (@ret, @order); my $changed = 0; ## some legacy clean-up my $oldorderfile = "$Dir/.order"; if (-f $oldorderfile) { if (-f $orderfile) { unlink($oldorderfile); } else { move ($oldorderfile, $orderfile); } } # if file already exists, load it at once in @order if (-f $orderfile) { open (ORDER, $orderfile) or die _error("Could not open order file '$orderfile'"); @order = ; chomp(@order); close (ORDER); } # Check already ordered entries my ($entry, $found, $i, $j); for ($i = 0; $i <= $#order; $i++) { $entry = $order[$i]; $found = 0; for ($j = 0; $j <= $#items; $j++) { if ( ($entry eq $items[$j]) or ($Renamed{$entry} and ($Renamed{$entry} eq $items[$j])) ) { splice @items, $j, 1; $found = 1; last; } } if ($found) { # Found a match push @ret, $entry; } else { # File no longer exists... Don't put it in ret. $changed++; } } # Anything left are new items not found in order file... foreach (@items) { push @ret, $_; $changed++; } # if something changed, save order file if ($changed) { _debug( "Updating order file '$orderfile'" . " (edit to modify picture order)" ); open (ORDER, ">$orderfile") or die _error("Could not write order file '$orderfile'"); foreach (@ret) { print ORDER "$_\n"; } close (ORDER); } return @ret; } sub FileLoadInfo($$) { my $dir = shift; my $file = shift; my $infofile = infoFile($dir,$file); my $fullfile = "$dir/$file"; my %info; my $tmpkey; my $tmpval; # check that the file is a file in the asb sense of it if (not FileIsA('FILE',$fullfile)) { $info{'SKIP'} = 'YES'; $info{'DELETE'} = 'no'; return %info; } # if the file exists, we load it first if ( -f $infofile ) { open(INFO,"<$infofile") or die _error("Could not open parameter file '$infofile'"); while () { (undef,$tmpkey,$tmpval) = split /^([^=]+)\s*=\s*/; if ($tmpkey) { chomp($tmpval); $info{$tmpkey} = $tmpval; } } close (INFO); } if ( firstOlderThanLast($infofile, $fullfile) ) { ($info{'SIZE'},$info{'MTIME'}) = (stat $fullfile)[7,9]; $info{'SIZE'} = int($info{'SIZE'}/1024); # in KBytes $info{'NAME'} = $file; foreach (keys %TypeActions) { if ( &{$TypeActions{$_}->{'isA'}}($_,$fullfile) ) { $info{'funcs'} = $TypeActions{$_}; $info{'TYPE'} = $_; last; } } if ($info{'funcs'}) { $info{'GEOMETRY'} = &{$info{'funcs'}->{'getGeometry'}}($fullfile); &{$info{'funcs'}->{'loadExtraInfo'}}($dir,\%info); $info{'COMMENT'} = '' unless (defined $info{'COMMENT'}); $info{'changed'} = 'FULLFILE'; } } else { # Legacy clean-up of old types my %oldtype = ( 'JPEG image data' => 'JPEG' ); if ($oldtype{$info{'TYPE'}}) { $info{'TYPE'} = $oldtype{$info{'TYPE'}}; $info{'changed'} = 'TYPEUPDATE'; } $info{'funcs'} = $TypeActions{$info{'TYPE'}}; } if ($info{'AUX'}) { # we transform a flat structure into an hash my %aux = split(/[:;]/,$info{'AUX'}); $info{'aux'} = \%aux; } $info{'SKIP'} = 'no' unless ($ScrapOpts{'skip'}); $info{'SKIP'} = 'yes' unless ($info{'funcs'}); # unknown type $info{'SKIP'} = 'no' unless ($info{'SKIP'}); $info{'DELETE'} = 'no' unless ($info{'DELETE'}); return %info; } sub FileSaveInfo($%) { my $dir = shift; my %info = @_; my $infofile = infoFile($dir,$info{'NAME'}); my $fullfile = "$dir/$info{'NAME'}"; # we save the file only if something has changed return 0 if (not $info{'changed'}); # with this, we flatten the structure containing additional files if ($info{'aux'}) { $info{'AUX'} = ''; foreach (keys %{$info{'aux'}}) { $info{'AUX'} .= ';' . $_ . ':' . $info{'aux'}->{$_}; } $info{'AUX'} =~ s/^;//; # remove leading semi-column } else { delete $info{'AUX'}; } _debug("Saving file info for '$info{'NAME'}':"); open(INFO,">$infofile") or die _error("Could not open parameter file '$infofile'"); foreach (grep {/^[A-Z]/} keys %info) { # we save only keys with caps print INFO $_ . "=" . $info{$_} . "\n"; _debugf($_ . "=" . $info{$_}); } close (INFO); &{$info{'funcs'}->{'saveExtraInfo'}}($dir,\%info); } sub DirLoadInfo($) { my $dir = shift; my $infofile = infoFile($dir,'dir'); my %info; my $tmpkey; my $tmpval; # handle legacy directory information my %old = ( "$dir/.name" => "TITLE", "$dir/.info" => "COMMENT" ); foreach (keys %old) { if ( -f $_ ) { open (FILE, $_) or die _error("File '$_' could not be open for read"); $info{$old{$_}} = ; $info{'changed'} = 'oldfiles'; chomp($info{$old{$_}}); close (FILE); unlink($_); } } # One step required to handle legacy directory name if (-d "$dir/.asbdir" and not -d "$dir/.asbdir-default") { move("$dir/.asbdir", "$dir/.asbdir-default"); _debug("Moved '$dir/.asbdir' to '$dir/.asbdir-default'."); } # DirLoadInfo is a prerequisite to all other Load/Save*Info functions # because it creates the necessary directory. if (not -f $infofile) { # we create the directory only if we don't try to get a parent createDirectory("$dir/$ASBDIR") if (index($dir,'/..') == -1); return %info; } open(INFO,"<$infofile") or die _error("Could not open parameter file '$infofile'"); while () { (undef,$tmpkey,$tmpval) = split /^([^=]+)\s*=\s*/; chomp($tmpval); $info{$tmpkey} = $tmpval if $tmpkey; } close (INFO); return %info; } sub DirSaveInfo($%) { my $dir = shift; my %info = @_; my $infofile = infoFile($dir,'dir'); # we save the file only if something has changed return 0 if (not $info{'changed'}); _debug("Saving dir info for '$dir':"); open(INFO,">$infofile") or die _error("Could not open parameter file '$infofile'"); foreach (grep {/^[A-Z]/} keys %info) { # we save only keys with caps print INFO $_ . "=" . $info{$_} . "\n"; _debugf($_ . "=" . $info{$_}); } close (INFO); } sub MjpgLoadExtraInfo($\%) { my $dir = shift; my $info = shift; my $file = "$dir/$info->{'NAME'}"; my $shortFile = $file; $shortFile =~ s/\.[^.]+$//; # Canon Ixus v3 creates an additional file with EXIF information # for the film. my @exifs = <$shortFile.[tT][hH][mM]>; my $exifFile = (@exifs == 1? $exifs[0] : ''); if ($exifFile) { # for renaming purposes, it's important that the name of the # main file is contained in the name of the auxiliary file. move($exifFile,"$file.THM"); } if (-f "$file.THM") { $info->{'AUX'} = 'THM:' . $info->{'NAME'} . '.THM' ; } if ($ScrapOpts{'autorotate'}) { MjpgRotate('ROTATE', $dir, $info, 'auto'); } return 1; } sub JpegLoadExtraInfo($\%) { my $dir = shift; my $info = shift; my $file = "$dir/$info->{'NAME'}"; # without proper command, we return a warning return 2 unless ($RDJPGCOM); # rdjpgcom comes together with jpegtran my @comment = `$RDJPGCOM "$file" 2>/dev/null`; chomp @comment; # we remove old AutoScrapbook comments, empty lines and GIMP standard # comment. my $comment = join("
", ( grep {!/^AUTOSCRAPBOOK|^\s*$|GIMP$/} @comment ) ); $comment =~ s/^\s+//; $comment =~ s/\s+$//; # we only take the comment if it has some real content... if ($comment !~ /^(()|\s)*$/) { $info->{'COMMENT'} = $comment; } if ($ScrapOpts{'autorotate'}) { JpegRotate('ROTATE', $dir, $info, 'auto', 1); } return 1; } sub JpegSaveExtraInfo($\%) { my $dir = shift; my $info = shift; my $file = "$dir/$info->{'NAME'}"; my $comment = $info->{'COMMENT'}; my $rc = 0; # wrjpgcom can't write comment in-place, so we use jhead... if ($comment and $JHEAD) { my $mtime = (stat $file)[9]; open (FILE, ">$file.TMP") or die _error("File '$file.TMP' could not be open for write"); foreach (split / /,$comment) { print FILE "$_\n"; } close (FILE); $rc = system($JHEAD, "-ci", "$file.TMP", $file); utime $mtime, $mtime, $file; # we don't want to change timestamp } unlink "$file.TMP"; # Perl doesn't complain and we clean up this way. return $rc; } sub FileIsA($$) { my $type = shift; my $fullfile = shift; return 0 unless (-f $fullfile); # put here all auxiliary files, which shouldn't be considered return 0 if ($fullfile =~ /\/index\.html$/); # file created by asb return 0 if ($fullfile =~ /\.THM$/i); # aux. file for Canon MJPG files return 1; } sub ImageIsA($$) { my $type = shift; my $fullfile = shift; my $fileout = `file -b "$fullfile"`; chomp($fileout); $fileout =~ s/,.*$//; # we keep only the first part before the comma if ($fileout !~ /image data/) { _debug("File '$fullfile' is not an image."); return 0; } my $jpeg = 0; $jpeg = 1 if ($fileout =~ /JPEG/); if ($jpeg) { if ($type eq 'JPEG') { _note("File '$fullfile' is a JPEG image."); return 1; } } else { if ($type eq 'IMAGE_NOT_JPEG') { _note("File '$fullfile' is an image (not JPEG)."); return 1; } } # /(?/dev/null"); my @output = `$MPLAYER $MPLAYER_Q $ver1 -identify "$file" 2>/dev/null`; chomp(@output); return map {split(/=/,$_)} (grep {/^ID_[A-Z_]+=[^=]*$/} @output); } sub FilmGetGeometry($) { my $file = shift; my %output = FilmGetInfo($file); return "$output{'ID_VIDEO_WIDTH'}x$output{'ID_VIDEO_HEIGHT'}"; } sub ImageEdit($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; my $file = $info->{'NAME'}; my $fullfile = $dir . '/' . $file; my $editor = $IMAGEEDITOR; $editor = $value if ($value); if ($editor and -x $editor) { my $tmpfile = $dir . '/edit-' . $file; my $backup = backupOrig($dir,$info); # save a copy of the file with the right date if (not $backup) { move($fullfile, $tmpfile); copy($tmpfile, $fullfile); } system($editor, $fullfile); my $answer = getInput('Are you happy with the result? [Y/n]', 'Y'); if ($answer =~ /^[yY]/) { # if possible transplant EXIF from old into new file if ($JHEAD and $info->{'TYPE'} =~ /JPEG/) { if ($backup) { system($JHEAD, "-te" , origFile($dir,$file) , $fullfile); } else { system($JHEAD, "-te", $tmpfile , $fullfile); } } unlink($tmpfile); # as always, Perl doesn't complain _note("File '$file' successfully edited."); } else { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } _note("Edition of file '$file' cancelled."); } } else { warn _warning("Edition of '$fullfile' failed," , " command '$editor' incorrect."); } return 1; } sub ImageBeginDisplay($) { my $file = shift; my $pid; return 0 unless ($ScrapOpts{'display'} and $DISPLAY); # fork is a bit scary but I got it from the Perl Cookbook if ($pid = fork) { # that's the parent branch which quits the function _debug("Created children with PID $pid."); return $pid; } else { # that's the children branch which exits and never quits the # function, difference is the value of $pid. die _error("Cannot fork") unless defined $pid; exec($DISPLAY, '-geometry', '640x640-0+0', $file); exit $?; } } sub MjpgBeginDisplay($) { my $file = shift; my $pid; return 0 unless ($ScrapOpts{'display'} and $MPLAYER); # fork is a bit scary but I got it from the Perl Cookbook if ($pid = fork) { # that's the parent branch which quits the function _debug("Created children with PID $pid."); return $pid; } else { # that's the children branch which exits and never quits the # function, difference is the value of $pid. die _error("Cannot fork") unless defined $pid; _debug("$MPLAYER $MPLAYER_RQ '$file' $MPLAYER_MUTE"); exec("$MPLAYER $MPLAYER_RQ '$file' $MPLAYER_MUTE"); exit $?; } } sub FileEndDisplay($) { my $pid = shift; if ($pid) { _debug("Killing children with PID $pid."); kill SIGINT => $pid; } } sub JpegRotate($$\%$;$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; my $straight = shift; #optional, true if straight action is required my $file = $info->{'NAME'}; my $fullfile = $dir . '/' . $file; $value = getInput('Please enter rotation (90, 180, 270 or auto):') unless ($value); if ( $value !~ /^(90|180|270|auto)$/i ) { warn _warning("'$value' not valid for rotation. Try EDIT."); return 2; } if ($value =~ /auto/i) { if ($JHEAD) { my @cmd = ($JHEAD, '-autorot', $fullfile); if (system(@cmd) == 0) { if (not $straight) { _note("File '$fullfile' auto-rotated."); my $temp = ImageGetGeometry($fullfile); if ( $temp ne $info->{'GEOMETRY'} ) { $info->{'GEOMETRY'} = $temp; $info->{'changed'} = 'ROTATE'; } } } else { die _error("Rotation failed for '$fullfile'"); } } # we could do an else branch and use jpegtran ourself to # automatically rotate the image, but we're lazy... else { warn _warning("Auto-rotation impossible for '$fullfile'" , ", no command defined."); return 2; } } elsif ($JPEGTRAN) { my $tmpfile = $dir . '/rot-' . $file; my $fullfile = $dir . '/' . $file; my $cmd = $JPEGTRAN . ' -rotate ' . $value . ' -copy all ' . "'$fullfile' > '$tmpfile'"; if (system($cmd) == 0) { move ($tmpfile,$fullfile); } else { die _error("Rotation failed for '$fullfile'"); } chmod (0644, $fullfile); if (not $straight) { _note("Rotated (lossless) '$file' by $value."); $info->{'GEOMETRY'} = ImageGetGeometry($fullfile); $info->{'changed'} = 'ROTATE'; } } else { return ImageRotate($action, $dir, $info, $value, $straight); } return 1; } sub ImageRotate($$\%$;$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; my $straight = shift; #optional, true if straight action is required $value = getInput('Please enter rotation (90, 180 or 270):') unless ($value); if ( $value !~ /^(90|180|270)$/i ) { warn _warning("'$value' not valid for rotation. Try EDIT."); return 2; } my $file = $info->{'NAME'}; my $fullfile = $dir . '/' . $file; if ($MOGRIFY) { my $tmpfile = $dir . '/rot-' . $file; my $backup = ($straight? 0 : backupOrig($dir,$info)); # save a copy of the file with the right date if (not $backup) { move($fullfile, $tmpfile); copy($tmpfile, $fullfile); } my @cmd = ($MOGRIFY, '-rotate', $value, $fullfile); if (system(@cmd) != 0) { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } die _error("Rotation failed for '$fullfile'"); } else { unlink($tmpfile); } chmod (0644, $fullfile); if (not $straight) { _note("Rotated (lossy) '$file' by $value."); $info->{'GEOMETRY'} = ImageGetGeometry($fullfile); $info->{'changed'} = 'ROTATE'; } } else { warn _warning("Rotation impossible for '$fullfile'," . " no command defined."); return 2; } return 1; } sub FilmExplodeCmd($$) { my $dir = shift; my $file = shift; my $cmd = "$MPLAYER $MPLAYER_RQ -nosound -vo jpeg" . " -jpeg outdir='$dir' '$file' " . $MPLAYER_MUTE; _debug($cmd); _note("Exploding '$file' to JPEG files. Be patient..."); return $cmd; } sub MjpgRotate($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; $value = getInput('Please enter rotation (90, 180, 270 or auto):') unless ($value); if ( $value !~ /^(90|180|270|auto)$/i ) { warn _warning("'$value' not valid for rotation. Try EDIT."); return 2; } my $file = $info->{'NAME'}; my $fullfile = $dir . '/' . $file; if (not ($MPLAYER and $MENCODER and $LAVTRANS and $LAVADDWAV) ) { warn _warning("Rotation impossible for '$fullfile'," . " no command defined."); return 2; } my $tmpdir = $dir . '/.tmpmjpg'; my $tmpfile = $tmpdir . '/rot-' . $file; my $backup = backupOrig($dir,$info); # save a copy of the file with the right date if (not $backup) { move($fullfile, $tmpfile); copy($tmpfile, $fullfile); } reCreateDirectory($tmpdir); my $cmd = FilmExplodeCmd($tmpdir,$fullfile); if (system($cmd) != 0) { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } die _error("Command '$cmd' failed"); } opendir (DIR, $tmpdir); my @jpegs = grep {/^[0-9]{8}.jpg$/} readdir(DIR); closedir(DIR); print "ROTATING: "; $|=1; # un-buffered output foreach (@jpegs) { my %tmpinfo = ( 'NAME' => $_ ); my $tmp = JpegRotate($action,$tmpdir,%tmpinfo,$value,"yes"); if ($tmp != 1) { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } die _error("Film '$file' couldn't be rotated because " . "rotation of image '$_' failed"); } print '*'; # progress status } $|=0; # buffered output print "\n"; my %output = FilmGetInfo($fullfile); my $w = $output{'ID_VIDEO_WIDTH'}; my $h = $output{'ID_VIDEO_HEIGHT'}; ($w, $h) = ($h, $w) if ($value == 90 or $value == 270); # MjpgResample($tmpdir,$output{'ID_VIDEO_FPS'}, $ScrapOpts{'fps'}); my $wavfile = $tmpdir . '/film.wav'; my @cmd = ( "$LAVTRANS $ScrapOpts{'tvnorm'} -fw -o $wavfile $fullfile", $ScrapOpts{'oldmm'} ? # MPlayer 0.9.2 (required because of bug) "$MENCODER $tmpdir/\\*.jpg -mf on:fps=$output{'ID_VIDEO_FPS'} " . " -o $tmpdir/$file " . " -ovc lavc -lavcopts vcodec=mjpeg:vhq" : # MPlayer 1.0 "$MENCODER mf://$tmpdir/\\*.jpg" . " -mf w=$w:h=$h:type=jpeg:fps=$output{'ID_VIDEO_FPS'}" . " -o $tmpdir/$file " . " -ovc lavc -lavcopts vcodec=mjpeg:vhq", # a workaround because LAVADDWAV recognizes only files with # lowcase extension .avi "$LAVADDWAV $tmpdir/$file $wavfile $fullfile.avi" ); foreach $cmd (@cmd) { _debug($cmd); if (runCmd($cmd) != 0) { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } die _error("Command '$cmd' failed"); } } # rollback the workaround for LAVADDWAV move("$fullfile.avi",$fullfile); $info->{'GEOMETRY'} = FilmGetGeometry($fullfile); $info->{'changed'} = 'ROTATE'; # we need to rotate the THM auxiliary file if it exists if ($info->{'aux'} and $info->{'aux'}->{'THM'}) { my $thm = $info->{'aux'}->{'THM'}; my %tmpinfo = ( 'NAME' => $thm ); my $tmp = JpegRotate($action,$dir,%tmpinfo,$value,"yes"); if ($tmp != 1) { if ($backup) { restoreOrig($dir,$info); } else { move($tmpfile,$fullfile); } die _error("Film '$file' couldn't be rotated because " . "rotation of image '$thm' failed"); } } deleteDirectory($tmpdir); return 1; } sub FileModify($\%) { my $dir = shift; my $info = shift; my $input; my $action; my $value; my $pid = 0; my $question = 'Please enter action ('; my @actions = (grep {/^[A-Z]/} keys %{$info->{'funcs'}}); $question .= join(', ',@actions); $question .= "), possibly followed by an '=parameter':"; # This allows us to enter the shortest possible action # e.g. 'c', 're', 'ro' (and everything in between) for COMMENT, # RENAME and ROTATE. use Text::Abbrev; my %actions = abbrev(@actions); print "\n--- $info->{'NAME'} ---\n"; while ( $ScrapOpts{'modify'} or ($ScrapOpts{'comment'} and not $info->{'COMMENT'}) ) { &{$info->{'funcs'}->{'endDisplay'}}($pid) if $pid; $pid = &{$info->{'funcs'}->{'beginDisplay'}} ($dir.'/'.$info->{'NAME'}); $input = getInput($question); if ( not $input ) { last if ($info->{'COMMENT'}); $info->{'COMMENT'} = getInput("Please enter comment:"); $info->{'changed'} = 'COMMENT'; last; } if ( $input =~ /^([A-Za-z]+)\s*=?\s*(.*)$/ and $action = $actions{uc($1)} ) { # shortest possible action $value = $2; } else { warn _warning("Wrong entry,", " format is 'ACTION[=VALUE]'."); next; } if ($info->{'funcs'}->{$action}) { # function exists &{$info->{'funcs'}->{$action}} ($action,$dir,$info,$value) or last; # stop if returned value is FALSE } else { warn _warning("Unknown Action '$action'. Try again."); next; } } &{$info->{'funcs'}->{'endDisplay'}}($pid) if $pid; } sub FileComment($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; _debug("*FileComent* $action, $dir, $info, $value"); if ($value) { $info->{'COMMENT'} = $value; $info->{'changed'} = 'COMMENT'; } else { my $temp = $info->{'COMMENT'}; # next line to avoid warning about uninitialized value in string $temp = '' unless ($info->{'COMMENT'}); $info->{'COMMENT'} = getInput("Please enter comment: [$temp]" , $temp); $info->{'changed'} = 'COMMENT' if ($temp ne $info->{'COMMENT'}); } return 1; } sub FileSkip($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; _debug("*FileSkip* $action, $dir, $info, $value"); my $oldvalue = $info->{'SKIP'}; if ( (not $value) or ($value =~ /^yes$/i) ) { $info->{'SKIP'} = 'yes'; _debug("file skipped."); } else { $info->{'SKIP'} = 'no'; _debug("file un-skipped."); } if ( $oldvalue ne $info->{'SKIP'} ) { $info->{'changed'} = 'SKIP'; } return 0; } sub FileDelete($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; _debug("*FileDelete* $action, $dir, $info, $value"); if ( getInput("Are you sure? [y/N]", "N") =~ /^[yY]/ ) { $info->{'DELETE'} = 'yes'; $info->{'changed'} = 'DELETE'; if ($ScrapOpts{'delorig'}) { deleteOrig($dir,$info); } else { backupOrig($dir,$info); } if ($info->{'aux'}) { foreach (keys %{$info->{'aux'}}) { unlink($dir . '/' . $info->{'aux'}->{$_}); } } unlink($dir.'/'.$info->{'NAME'}); unlink( infoFile($dir,$info->{'NAME'}) ); _note("File '$info->{'NAME'}' deleted."); return 0; } else { _note("Delete cancelled."); return 1; } } sub FileRename($$\%$) { my $action = shift; my $dir = shift; my $info = shift; my $value = shift; _debug("*FileRename* $action, $dir, $info, $value"); $value = getInput("Please enter new file name:") unless ($value); # new name without extension gets the extension from the original file if ($value !~ /\.[^.\/]+$/ and $info->{'NAME'} =~ /(\.[^.\/]+)$/) { $value .= lc($1); # in small caps } _note("Renaming '$info->{'NAME'}' to '$value'."); if ( -f "$dir/$value" ) { warn _warning("File '$value' already exists. Please retry."); return 1; } if ( getInput("Are you sure? [y/N]", "N") =~ /^[yY]/ ) { $Renamed{$value} = $info->{'NAME'}; move($dir.'/'.$info->{'NAME'}, $dir.'/'.$value) or die _error("Renaming failed"); unlink( infoFile($dir,$info->{'NAME'}) ); renameOrig($dir, $info, $value); renameAux($dir, $info, $value); $info->{'oldname'} = $info->{'NAME'}; $info->{'ORIGNAME'} = $info->{'NAME'} unless ($info->{'ORIGNAME'}); $info->{'NAME'} = $value; $info->{'changed'} = 'RENAME'; _note("Renamed '$info->{'oldname'}' to '$info->{'NAME'}'."); } else { _note("Rename cancelled."); } return 1; } sub ImageCreateOtherFiles($$\%) { my $fromDir = shift; my $toDir = shift; my $info = shift; my $file = $info->{'NAME'}; my %PictureData; my $fromFile = "$fromDir/$file"; my $thumbFile = "$toDir/.thumbs/$file"; my $medFile = "$toDir/.thumbs/med-$file"; my $toFile = "$toDir/$file"; ## DO SOME CLEANUP ## # we don't need to check for existence, unlink doesn't complain. if ($info->{'oldname'}) { # if we renamed the file unlink("$toDir/.thumbs/".$info->{'oldname'}); unlink("$toDir/.thumbs/med-".$info->{'oldname'}); unlink("$toDir/".$info->{'oldname'}) if ($toDir ne $fromDir); delete $info->{'oldname'}; } if ($info->{'DELETE'} eq 'yes' or # or if we removed it $info->{'SKIP'} eq 'yes') { # or skipped it unlink($thumbFile); unlink($medFile); unlink($toFile) if ($toDir ne $fromDir); return %PictureData; } ## GENERATE THUMBNAIL ## my $EXIFThumbnail = 0; if ($EXIFTAGS) { foreach my $line (`$EXIFTAGS -q "$fromFile" 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 $ScrapOpts{'exif-thumbs'})) { # Determine EXIF data foreach my $line (`$EXIF -i "$fromFile" 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; } } } createDirectory("$toDir/.thumbs"); # Generate standard thumbnail if ( firstOlderThanLast($thumbFile,$info->{'MTIME'}) ) { if ($EXIFThumbnail and $ScrapOpts{'exif-thumbs'} and (system("$EXIF -e '$fromFile' >/dev/null 2>&1") == 0)) { # Use the EXIF thumbnail move ("$fromFile.modified.jpeg", $thumbFile) or die _error("Could not retrieve EXIF thumbnail for", " picture '$file'"); _note("Retrieved EXIF thumbnail for picture '$file'."); } else { # Create our own system "$CONVERT -geometry " . $ScrapOpts{'thumb-geometry'} . " '$fromFile' '$thumbFile'" and die _error("Could not make thumbnail of", " '$file'"); _note("Created thumbnail for picture '$file'."); } chmod (0644, $fromFile); chmod (0644, $thumbFile); } if ( (not $info->{'THUMBLINK'}) or ($info->{'THUMBLINK'} ne ".thumbs/$file") ) { $info->{'THUMBLINK'} = ".thumbs/$file"; $info->{'THUMBGEOMETRY'} = ImageGetGeometry($thumbFile); $info->{'changed'} = 'THUMB'; } ## GENERATE MEDIUM-SIZED IMAGE ## if ($ScrapOpts{'medium-cutoff'} and ($info->{'SIZE'} > $ScrapOpts{'medium-cutoff'})) { # Generate medium-sized image if ( firstOlderThanLast($medFile,$info->{'MTIME'}) ) { system "$CONVERT -geometry " . $ScrapOpts{'medium-geometry'} . " '$fromFile' '$medFile'" and die _error("Could not make medium image of $file"); chmod (0644, $medFile); _note("Created medium image for picture '$file'."); $info->{'MEDSIZE'} = int ( (stat $medFile)[7] / 1024); # in KBytes $info->{'MEDGEOMETRY'} = ImageGetGeometry($medFile); $info->{'MEDLINK'} = ".thumbs/med-$file"; $info->{'changed'} = 'MED'; } elsif ( (not $info->{'MEDSIZE'}) or (not $info->{'MEDGEOMETRY'}) or (not $info->{'MEDLINK'}) ) { # if someone removes the dir.info file without changing # the file, MEDSIZE and MEDGEOMETRY might get lost. $info->{'MEDSIZE'} = int ( (stat $medFile)[7] / 1024); # in KBytes $info->{'MEDGEOMETRY'} = ImageGetGeometry($medFile); $info->{'MEDLINK'} = ".thumbs/med-$file"; $info->{'changed'} = 'MED'; } } elsif ( $info->{'MEDSIZE'} or $info->{'MEDGEOMETRY'} or $info->{'MEDLINK'} ) { delete $info->{'MEDSIZE'}; delete $info->{'MEDGEOMETRY'}; delete $info->{'MEDLINK'}; $info->{'changed'} = 'NOMED'; } ## COPY FULL-SIZED IMAGE ## # we copy big images only if source and target directories are # different. Furthermore if we want big images or if there is no # medium image. if ( ($fromDir ne $toDir) and ($ScrapOpts{'big'} or not $info->{'MEDLINK'}) ) { if ( firstOlderThanLast($toFile,$info->{'MTIME'}) ) { copy $fromFile, $toFile; } } return %PictureData; } sub MjpgResample($$$) { my $dir = shift; my $rate = shift; # source rate my $destrate = shift; my $dest = 1; my $srcfile; my $destfile; my $lastsrcfile; my $lastdestfile; $rate /= $destrate; # if srcrate is 15 and destrate 24, rate is 15/24 print "RESAMPLING: "; $| = 1; # un-buffered output while (1) { $srcfile = $dir . sprintf('/%08.0f.jpg',($dest - 1)*$rate+1); $destfile = $dir . sprintf('/x%08d.jpg',$dest); print '#'; # progress status if ( -f $srcfile ) { move($srcfile,$destfile); $lastsrcfile = $srcfile; $lastdestfile = $destfile; } elsif ($srcfile eq $lastsrcfile) { copy($lastdestfile,$destfile); } else { last; } } continue { ++$dest; } $| = 0; # buffered output print "\n"; } sub MjpgCreateOtherFiles($$\%) { my $fromDir = shift; my $toDir = shift; my $info = shift; my $file = $info->{'NAME'}; my %PictureData; my $shortFile = $file; $shortFile =~ s/\.[^.]+$//; my $fromFile = "$fromDir/$file"; my $thumbFile = "$toDir/.thumbs/$shortFile.jpg"; my $medFile = "$toDir/.thumbs/$shortFile.mpg"; my $toFile = "$toDir/$file"; my $exifFile = ''; if ($info->{'aux'} and $info->{'aux'}->{'THM'}) { $exifFile = $fromDir . '/' . $info->{'aux'}->{'THM'}; } ## DO SOME CLEANUP ## # we don't need to check for existence, unlink doesn't complain. if ($info->{'oldname'}) { # if we renamed the file my $shortold = $info->{'oldname'}; $shortold =~ s/\.[^.]+$//; unlink("$toDir/.thumbs/$shortold.jpg"); unlink("$toDir/.thumbs/$shortold.mpg"); unlink("$toDir/$info->{'oldname'}") if ($toDir ne $fromDir); delete $info->{'oldname'}; } if ($info->{'DELETE'} eq 'yes' or # or if we removed it $info->{'SKIP'} eq 'yes') { # or skipped it unlink($thumbFile); unlink($medFile); unlink($toFile) if ($toDir ne $fromDir); return %PictureData; } # we get some information from the film itself %PictureData = FilmGetInfo($fromFile); ## GENERATE THUMBNAIL ## my $EXIFThumbnail = 0; if ($EXIFTAGS and $exifFile) { foreach my $line (`$EXIFTAGS -q "$exifFile" 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 $exifFile and ((not $EXIFTAGS) or $ScrapOpts{'exif-thumbs'})) { # Determine EXIF data foreach my $line (`$EXIF -i "$exifFile" 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; } } } # we now extract all the images from the film my $tmpdir = $fromDir . '/.tmpmjpg'; reCreateDirectory($tmpdir); my $cmd; createDirectory("$toDir/.thumbs"); # Generate standard thumbnail if ( firstOlderThanLast($thumbFile,$info->{'MTIME'}) ) { if ($EXIFThumbnail and $ScrapOpts{'exif-thumbs'} and (system("$EXIF -e '$exifFile' >/dev/null 2>&1") == 0)) { # Use the EXIF thumbnail move ("$exifFile.modified.jpeg", $thumbFile) or die _error("Could not retrieve EXIF thumbnail for", " film '$file'"); _note("Retrieved EXIF thumbnail for film '$file'."); # possible but we would get disconnect if editing the film #} elsif ($exifFile) { # copy($exifFile,$thumbFile); # _note("Copied thumbnail for film '$file'."); } else { # Create our own if (! -f "$tmpdir/00000001.jpg") { $cmd = FilmExplodeCmd($tmpdir,$fromFile); if (system($cmd) != 0) { die _error("Command '$cmd' failed"); } } system "$CONVERT -geometry " . $ScrapOpts{'thumb-geometry'} . " '$tmpdir/00000001.jpg' '$thumbFile'" and die _error("Could not make thumbnail of", " '$file'"); _note("Created thumbnail for film '$file'."); } chmod (0644, $fromFile); chmod (0644, $thumbFile); } if ( (not $info->{'THUMBLINK'}) or ($info->{'THUMBLINK'} ne ".thumbs/$shortFile.jpg") ) { $info->{'THUMBLINK'} = ".thumbs/$shortFile.jpg"; $info->{'THUMBGEOMETRY'} = ImageGetGeometry($thumbFile); $info->{'changed'} = 'THUMB'; } ## GENERATE MEDIUM-SIZED IMAGE ## if ($ScrapOpts{'medium-cutoff'} and ($info->{'SIZE'} > $ScrapOpts{'medium-cutoff'})) { # Generate medium-sized image if ( firstOlderThanLast($medFile,$info->{'MTIME'}) ) { if (! -f "$tmpdir/00000001.jpg") { $cmd = FilmExplodeCmd($tmpdir,$fromFile); if (system($cmd) != 0) { die _error("Command '$cmd' failed"); } } MjpgResample($tmpdir,$PictureData{'ID_VIDEO_FPS'}, $ScrapOpts{'fps'}); # Different rates used for encoding, if an under-run is # detected, you might want to adapt those (esp. deltar). my $sndr1 = 32; my $sndr2 = int($sndr1 * 1.10); my $vidr1 = 500; my $vidr2 = int($vidr1 * 1.10); my $datar = $vidr2 + $sndr2; my $mp2file = $tmpdir . '/film.mp2'; my $m1vfile = $tmpdir . '/film.m1v'; my $wavfile = $tmpdir . '/film.wav'; my $int = $ScrapOpts{'oldmm'} ? '' : '-I 0'; my @cmd = ( "$LAVTRANS $ScrapOpts{'tvnorm'} -fw -o $wavfile " . " $fromFile", "cat $wavfile | $MP2ENC -m -b $sndr1 -o $mp2file", "$JPEG2YUV -I p -L 1 -f $ScrapOpts{'fps'} -b 1 " . " -j $tmpdir/x%08d.jpg " . " | $MPEG2ENC -f 0 -q 5 -a 1 " . $int . " -B $sndr2 -b $vidr1 -o $m1vfile", "$MPLEX -V -r $datar $mp2file $m1vfile -o $medFile" ); foreach $cmd (@cmd) { _debug($cmd); if (runCmd($cmd) != 0) { die _error("Command '$cmd' failed"); } } chmod (0644, $medFile); _note("Created MPEG film for '$file'."); $info->{'MEDSIZE'} = int ( (stat $medFile)[7] / 1024); # in KBytes $info->{'MEDGEOMETRY'} = FilmGetGeometry($medFile); $info->{'MEDLINK'} = ".thumbs/$shortFile.mpg"; $info->{'changed'} = 'MED'; } elsif ( (not $info->{'MEDSIZE'}) or (not $info->{'MEDGEOMETRY'}) ) { # if someone removes the dir.info file without changing # the file, MEDSIZE and MEDGEOMETRY might get lost. $info->{'MEDSIZE'} = int ( (stat $medFile)[7] / 1024); # in KBytes $info->{'MEDGEOMETRY'} = FilmGetGeometry($medFile); $info->{'MEDLINK'} = ".thumbs/$shortFile.mpg"; $info->{'changed'} = 'MED'; } } elsif ($info->{'MEDLINK'}) { delete $info->{'MEDSIZE'}; delete $info->{'MEDGEOMETRY'}; delete $info->{'MEDLINK'}; $info->{'changed'} = 'NOMED'; } deleteDirectory($tmpdir); ## COPY FULL-SIZED IMAGE ## # we copy big images only if source and target directories are # different. Furthermore if we want big images or if there is no # medium image. if ( ($fromDir ne $toDir) and ($ScrapOpts{'big'} or not $info->{'MEDLINK'}) ) { if ( firstOlderThanLast($toFile,$info->{'MTIME'}) ) { copy $fromFile, $toFile; } } return %PictureData; } sub MjpgIsA($$) { my $type = shift; my $fullfile = shift; if (not $MPLAYER) { warn _warning("MPLAYER not defined, films can't be processed"); return 0; } my %output = FilmGetInfo($fullfile); if ($output{'ID_VIDEO_FORMAT'} and $output{'ID_VIDEO_FORMAT'} eq $type) { _note "File '$fullfile' is an MJPEG file,"; return 1; } else { _debug "File '$fullfile' isn't an MJPEG file,"; return 0; } } sub WriteHeader($\%$) { my ($Dir, $dirinfo, $fh) = @_; my $headerfile = "$Dir/$ASBDIR/dir.header"; ## some legacy clean-up my $oldheaderfile = "$Dir/.header"; if (-f $oldheaderfile) { if (-f $headerfile) { unlink($oldheaderfile); } else { move ($oldheaderfile, $headerfile); } } # write header print $fh <<"EOF"; $dirinfo->{'TITLE'}

$dirinfo->{'TITLE'}

EOF if (-f $headerfile) { open (HEADER, $headerfile); foreach (
) { print $fh $_; } close (HEADER); } print $fh "

" . $dirinfo->{'COMMENT'} . "



\n"; } sub WriteFooter($\%$) { my ($Dir, $dirinfo, $fh) = @_; my %parentinfo = DirLoadInfo("$Dir/.."); my $footerfile = "$Dir/$ASBDIR/dir.footer"; ## some legacy clean-up my $oldfooterfile = "$Dir/.footer"; if (-f $oldfooterfile) { if (-f $footerfile) { unlink($oldfooterfile); } else { move ($oldfooterfile, $footerfile); } } # write footer if (-f $footerfile) { open (FOOTER, $footerfile); foreach (