#!/usr/bin/perl -Tw # Thumbfixer for iMovie 2 # Copyright (c) John F. Whitehead 2002 # # $Id: thumbfixer,v 1.8 2002/02/03 15:12:00 jfw Exp $ # # iMovie is a trademark of Apple Computer, Inc. # Thumbfixer is not written by or endorsed by Apple Computer. # # SUMMARY # # Thumbfixer is a utility for automatically editing iMovie project files # and is provided as freeware under the GNU GPL (see below for license). # # PLEASE READ ALL DIRECTIONS, PARTICULARLY THE CAVEATS, OR YOU EDIT YOUR # PROJECTS AT YOUR OWN RISK. # # Thumbfixer fixes a couple of limitations of iMovie (as of version 2.1.1): # - iMovie sometimes corrupts the thumbnail displayed. (The original clip # is unaffected, but the thumbnail appears as static or distortion.) # I am unsure of the exact reason why this happens, but it seems to occur # most often during an undo of a split. # - You cannot specify which frame of a clip should be displayed as # its thumbnail. iMovie always displays frame 0, which may not be # the best representation of your clip. # # Thumbfixer corrects these issues by allowing you to specify the frame # number of the clip you want to use as the thumbnail. # # HOW TO INSTALL # # I presume you have a basic knowledge of Unix and the Terminal. # From within Terminal, install this program in an application # directory in your path, such as /usr/local/bin. Make sure it is # executable ("chmod 755 thumbfixer"). Log out and back in (or use # the "rehash" command if it is supported by your shell). # # HOW TO USE IT # # Open up a Terminal. cd to the directory where your movie is (such # as ~/Movies/MyMovie/). Type "thumbfixer MyMovie" and you will be # prompted to enter a new thumbnail for each clip. # # To fix corrupted thumbnails, just change them to a different value. # You can do this quickly to all clips by just hitting 1 # repeatedly. # # To specify customized thumbnails, type the frame as a number or a # time relative to the start of the clip (not relative to all clips, # which is the way the movie track displays the times). This allows # you to specify a thumbnail that is more representative of the clip # -- for example after a fade in or after action has focused on the # main subject. The first frame is 0, the last is the total number of # frames in the clip. See below for options and flexibility in how # you enter the numbers. # # There are two command line options you may also specify: # # -c compress shelf (removing empty spaces on shelf). This does not # compress the clips themselves; it just fills in the empty spaces with # clips from farther down the shelf. It does not sort them in any special # order, just the order it encounters them in the project file. # # -s swap backup file with current project file. This allows you to # restore the previous version if (in the unlikely event) something gets # corrupted. Only one backup is made, so if you run Thumbfixer more than # once you won't be able to restore the original backup, only the most # recent one. You can, however, copy any backup file you make for more # permanent backup. See more on this command below. # # HOW IT WORKS # # Thumbfixer DOES NOT alter your movie clips. It only edits your # iMovie project file, which is a text file. For example, the line # that indicates which thumbnail to use looks like this: # # Frames: 760 In:382 Out:1142 Thumb:0 # # Thumbfixer only changes the value of the field "Thumb". iMovie only # sets Thumb to display frame 0, but when it is changed to another # number, iMovie (at least version 2.1.1) will display the # corresponding frame as the thumbnail in the shelf and movie track # views. You could do this editing by hand, but Thumbfixer automates # the process so you won't create problems such as: # - entering an invalid frame number # - inadvertently altering the format of the file # - changing the type/creator of the file # # Thumbfixer also provides a couple of handy features: # - You can enter the thumbnail as a frame number, or, to match what you # see in iMovie, as a time. The time, as minutes/seconds/frames, can # be written in several ways, interpreted as follows: # - three numbers, two delimiters (1:03:10 or 1:03.10) = min:sec:frames # - two numbers, two delimiters (1:03: or 1:03.) = min:sec # - two numbers, one delimiter (3:10 or 3.10) = sec:frames # - one number, one delimiter (3: or 3.) = sec # - one number only (3 or 90 or 1800) = frames # This gives you shorthand alternates for numbers. For example, # the following are equivalent: 90 or 3. or 3:00 or 00:03:00. # Also note that either : or . can be the final delimiter. # - You can provide the thumbnail as an offset, such as 49:21 - 2:15 # or 1:03.02 + 100. This makes it easy to calculate frames that are in # clips in the movie track, since the times given are relative to the # beginning of all the clips. For example, note the beginning of the # clip (time A) and find the frame you want (time B). In Thumbfixer # enter B - A and the relative thumbnail will be calculated for you. # # If you have any suggestions or observations, don't hesitate to contact me. # # CAVEATS # # I have not yet tested Thumbfixer extensively with files containing a lot # audio or effects, so it is possible that those project files will not be # edited correctly because it contains formatting I have not anticipated. # # iMovie must not be running when you run Thumbfixer. This is to prevent # Thumbfixer from changing an open file. Thumbfixer checks once when it # starts up, so wait to open iMovie until it finishes. # # The main inconvenience is that if you want to pick and choose certain # frames (rather than just taking the 1st or 10th or same frame for # all your clips) you have to remember all the frame numbers by # writing them down. # # I have tried to make the user's input and the file writing as robust # and error-avoidable as possible, but it is conceivable that someone # could cause the program to create a project file that is corrupted. # If this happens, you can use the "-s" command to swap the project # file with its backup file. Please let me know your error and any # input you might have been providing. # # Only the last backup file is saved, so if you run Thumbfixer more # than once before checking your work in iMovie, you may not be able # to recover from other than the most recent changes. # # If you need to rename or move project files around, do so in the # Finder, not in Terminal, to preserve the type/creator codes # (required for iMovie to find them). # # WHY PERL? # # Because the iMovie project file is a text file, and Perl is great # for processing text files. And it is fast to program in. And I # know it better than Objective-C. # # FUTURE # # There are two tracks future development could take: # 1. For Thumbfixer, all this Perl logic could be converted into a native # Mac app which would allow you to visually select a frame and not worry # about frame numbers and times. This would be neat but I'm not sure I # have the time. Besides... # 2. For iMovie, future relases (a) could avoid the thumbnail # corruption and (b) could provide a mechanism internally so you can # specify any frame to be the thumbnail (perhaps in File / Get Clip # Info). If this happens, the need for Thumbfixer will go away. # # So I hope #2 happens soon... # # - jfw, 3 Feb 2002 # John F. Whitehead - - http://www.well.com/~jfw/ # # # Thumbfixer is distributed under the GNU General Public License: # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # http://www.gnu.org/copyleft/gpl.html use strict; use English; use Socket qw(:DEFAULT :crlf); use File::Copy; my $version_info = << 'EOT'; Thumbfixer version 1.0 (3 Feb 2002) Copyright (c) John F. Whitehead 2002 EOT $PROGRAM_NAME =~ m#([^/]+)$#; $_ = $1 || "thumbfixer"; my $usage = "Usage: $_ [switches] projectfile\n" . << 'EOT'; -c compress shelf (removing empty spaces on shelf) -s swap backup file with current project file -v print version info EOT $INPUT_RECORD_SEPARATOR = CR; # Mac-style newlines for file $ENV{PATH} = "/bin:/usr/bin:/usr/ucb"; if (`ps -ax | egrep iMovie.app | egrep -v egrep`) { die ("error: iMovie is currently running. Please quit it first.\n"); } my ($file); my ($compressShelf, $swap) = 0; while (@ARGV) { $_ = shift(@ARGV); /^-c/ && ($compressShelf = 1) && next; /^-s/ && ($swap = 1) && next; /^-v/ && (die $version_info); /^[^-]/ && (($file) = (/(.*)/)) && next; die "error: invalid option '$_'\nif your filename starts with a '-', write it as '/path/file' or './file'\n\n$usage"; } $file or die "error: no project file specified\n\n$usage"; my ($backfile) = "$file.backup"; my ($tempfile) = "$file.temp-$PID"; open(FILE, $file) or die ("error: can't open '$file' ($!)\n\n$usage"); if ($swap) { print "Swap '$file' with '$backfile'? [y] "; $INPUT_RECORD_SEPARATOR = LF; # Unix-style newlines for stdin chomp($_ = ); $INPUT_RECORD_SEPARATOR = CR; # Mac-style newlines for file if (/^\s*[Nn]/) { die "Swap aborted. No files changed.\n"; } copy($file, $tempfile) or die ("error: could not copy '$file' to '$tempfile' ($!)\nno files were changed"); copy($backfile, $file) or (unlink $tempfile) && die ("error: could not restore '$backfile' to '$file' ($!)\nno files were changed"); rename($tempfile, $backfile) or die ("error: could not rename '$tempfile' to '$backfile' ($!)\n"); print "Backup file successfully swapped with project file.\n"; exit; } open(TEMPFILE, ">$tempfile") or die ("error: can't open '$tempfile' ($!)\n"); my (@clips) = (); my ($clipName, $clipType, $line, $somethingChanged); foreach () { # process each line of project file # identify name if (/^(Clip|Shelf): (.*)/) { $clipType = $1; $clipName = $2; push(@clips, $_); print "$clipType: $clipName\n"; } # only before first clipName has been reached in file elsif (!$clipName) { print TEMPFILE; } # change thumb elsif (/^( Frames: (\d+) In:\d+ Out:\d+ Thumb:)(\d+)/) { my $frameLineStart = $1; my $frames = $2; # frames total includes In but not Out frame # (Out frame is really included in next clip) my $thumb = $3; print sprintf(" Duration: %s (%s frames)\n", &frames2time($frames), $frames); my ($error, $newThumb); do { # request new thumbnail frame value print sprintf(" Thumbnail: %s (frame %s)\n", &frames2time($thumb), $thumb); print "Change? [$thumb] "; $INPUT_RECORD_SEPARATOR = LF; # Unix-style newlines for stdin chomp($newThumb = ); $INPUT_RECORD_SEPARATOR = CR; # Mac-style newlines for file # new thumbnail frame was input $newThumb = &time2frames($newThumb); if (defined($newThumb) && ($newThumb != $thumb)) { if ($newThumb > $frames) { print " error: thumbnail ", &frames2time($newThumb), " exceeds clip duration\n"; $error = 1; } elsif ($newThumb < 0) { print " error: thumbnail ", &frames2time($newThumb), " is less than 0\n"; $error = 1; } else { print " Thumbnail: ", &frames2time($newThumb), " (frame $newThumb)\n"; $error = 0; push(@clips, $frameLineStart . $newThumb . CR); $somethingChanged = 1; } } else { # print " thumbnail unchanged\n"; $error = 0; push(@clips, $frameLineStart . $thumb . CR); } } until (!$error); print "\n"; } # else keep all other clip lines else { push(@clips, $_); } } # dump all clips, compressing shelf items if requested my ($alreadyCompressedShelf, $x, $y); $alreadyCompressedShelf = $x = $y = 0; foreach (@clips) { if ($compressShelf && s/^( ShelfXY:) (\d+) (\d+)/$1 $x $y/) { if (!$alreadyCompressedShelf && (($x != $2) || ($y != $3))) { print "compressing shelf clips\n\n"; $alreadyCompressedShelf = 1; $somethingChanged = 1; } if (++$x > 2) { $x = 0; $y++; } } print TEMPFILE; } if ($compressShelf && !$alreadyCompressedShelf) { print "shelf clips don't need to be compressed\n\n"; } close(FILE) or die ("error: couldn't close '$file' ($!)\nproject file '$file' unaffected\n"); close(TEMPFILE) or die ("error: couldn't close '$tempfile' ($!)\nproject file '$file' unaffected\n"); if ($somethingChanged) { copy($file, $backfile) or die ("error: could not backup '$file' to '$backfile' ($!)\nproject file '$file' unaffected"); ($_) = (stat($file))[9]; utime $_, $_, $backfile; if (!copy($tempfile, $file)) { warn ("error: could not copy '$tempfile' to '$file' ($!)\n"); warn ("restoring project file '$file'\n"); rename($backfile, $file) or die ("error: could not restore '$backfile' to '$file' ($!)\nbackup still exists at '$backfile'"); } print "project file '$file' updated successfully\n"; } else { print "no edits made -- project file '$file' unchanged\n"; } unlink($tempfile) or die ("error: could not delete '$tempfile' ($!)\n"); ################## # end of program # ################## ############### # subroutines # ############### sub frames2time { my ($totalFrames) = @_; my $minutes = int($totalFrames / (30 * 60)); my $remainder = $totalFrames % (30 * 60); my $seconds = int($remainder / 30); my $frames = $remainder % 30; return(sprintf("%02d:%02d.%02d", $minutes, $seconds, $frames)); } sub time2frames { my ($time) = @_; my ($minutes, $seconds, $frames); $minutes = $seconds = $frames = 0; if ($time =~ /([\d\.:]+)\s*([-+])\s*([\d\.:]+)/) { my ($firstTime) = $1; my ($operator) = $2; my ($secondTime) = $3; if ($operator eq "-") { return(&time2frames($firstTime) - &time2frames($secondTime)); } else { return(&time2frames($firstTime) + &time2frames($secondTime)); } } # 2 delimiters = min:sec:frames or min:sec.frames or min:sec: or min:sec. if ($time =~ /(\d+):(\d{1,2})[\.:](\d{1,2})?/) { $minutes = $1; $seconds = $2; $frames = $3 || 0; # 1 delimiter = sec:frames or sec.frames or sec. or sec: } elsif ($time =~ /(\d{1,2})[\.:](\d{1,2})?/) { $seconds = $1; $frames = $2 || 0; # 0 delimiters (lone integers) = frames } elsif ($time =~ /(\d+)/) { $frames = $1; } else { # print " warning: no number found in thumbnail entered\n"; return(undef); } # print sprintf(" interpreting as %02d:%02d.%02d\n", # $minutes, $seconds, $frames); return((($minutes*60) + $seconds) * 30 + $frames); }