#!/usr/bin/perl # Filename: img2iphone # Author: David Ljung Madison # See License: http://MarginalHacks.com/License/ # Description: Copy images to an iphone album # # WARNING: This may erase all your images and destroy your phone # and burn down your house. Not responsible for any lost data # or damage caused. (Though we did fix that in the last release) # # Issues: # - If using -mount, then the files may end up being owned by root, # and not eraseable from the iPhone "Camera" or "Photos" application. # - Can only transfer image to "Camera Roll" - if anyone knows how # to create other albums, let me know! # # 2009/12/26: Now handles v3.0+ firmware (with .MISC thumbnail directory) # (Thanks to troy/tdr!) # use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my ($BASENAME,$PROGNAME) = ($0 =~ m|(.*)/(.+)|) ? ($1?$1:'/',$2) : ('.',$0); ################################################## # Settings ################################################## # iPod Convenience settings my $DEFAULT_OPTIONS = { cp_cmd => 'cp', identify_cmd => 'identify', # Optional (for speed) convert_cmd => 'convert', mount_cmd => 'iphone-mount', umount_cmd => 'iphone-umount', conv_conf => '/etc/default/ipod-convenience', mount_dcim => 'DCIM', thumbX => 55, thumbY => 55, num_hashes => 60, }; ################################################## # Usage ################################################## sub doSys { my ($opt,@cmd) = @_; debug("CMD: @cmd"); system(@cmd); fatal("Command failed: @cmd") if $?; } sub fatal { foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR: $msg\n"; } exit(-1); } sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR < Add images to an iPhone/iPod touch -d Set debug mode -mount Use iPod Convenience mount/umount commands and mount points -dcim Specify the DCIM directory (such as "Media/DCIM") -album Specify the album (such as "101APPLE") -v3 Force v3 firmware (autodetected if photos have been taken) -q No hashes -v Verbose (instead of hashes) WARNING: This may erase all your images and destroy your phone and burn down your house. Not responsible for any lost data or damage caused. (Though we did fix that in the last release) USAGE exit -1; } sub getFiles { my ($opt,$arg) = @_; return $arg if -f $arg; usage("Unknown file/dir [$arg]") unless -d $arg; opendir(DIR,$arg) || usage("Couldn't read directory [$arg]"); my @dir = readdir(DIR); closedir(DIR); @dir = grep(-f $_, map { "$arg/$_" } @dir); @dir; } sub parseArgs { my $opt = $DEFAULT_OPTIONS; while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-q$/) { $opt->{q}=1; next; } if ($arg =~ /^-v$/) { $opt->{v}=1; next; } if ($arg =~ /^-mount$/) { $opt->{mount}=1; next; } if ($arg =~ /^-dcim$/) { $opt->{dcim}=shift @ARGV; next; } if ($arg =~ /^-album$/) { $opt->{album}=shift @ARGV; next; } if ($arg =~ /^-v3$/) { $opt->{v3}=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } push(@{$opt->{files}},getFiles($opt,$arg)); } usage("No images/directories specified") unless $opt->{files}; $opt->{v}=0 if $opt->{q}; mount($opt) if $opt->{mount}; getDestination($opt); print "Destination: $opt->{dest}\n"; # Make v3 thumbnail directory if it doesn't exist yet $opt->{misc} = "$opt->{dest}/.MISC/"; if ($opt->{v3} && ! -d $opt->{misc}) { print "Creating $opt->{misc}\n" if $opt->{v}; mkdir("$opt->{misc}", 0755); } $opt; } sub debug { return unless $MAIN::DEBUG; foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; } } ################################################## # Handling files/directories ################################################## sub getDestination { my ($opt) = @_; # Figure out final destination if ($opt->{dcim} && !-d $opt->{dcim}) { warn("[WARNING] DCIM directory not found:\n $opt->{dcim}\n"); undef $opt->{dcim}; } $opt->{dcim} ||= '.'; $opt->{dest} = $opt->{album} ? "$opt->{dcim}/$opt->{album}" : nextPath($opt,$opt->{dcim},"%0.3dAPPLE",101); -d $opt->{dest} || mkdir($opt->{dest},0755) || usage("Couldn't create directory:\n $opt->{dest}\n"); $opt->{dest}; } sub nextPath { my ($opt,$dir,$fmt,$start) = @_; opendir(DIR,$dir) || usage("Couldn't read directory [$dir]"); my @dir = readdir(DIR); closedir(DIR); my %dir; map {$dir{$_}++} @dir; $start ||= 1; while (1) { my $tst = sprintf($fmt,$start++); return "$dir/$tst" unless $dir{$tst}; } } # iPod Convenience sub mount { my ($opt) = @_; return unless $opt->{mount}; if (!$opt->{dcim} && open(CONF,"<$opt->{conv_conf}")) { while () { s/#.*//; next unless /\S/; next unless /(\S+)="([^"]+)"/ || /(\S+)=(\S+)/; my ($var,$val) = ($1,$2); $opt->{mountpoint} = $val if $var =~ /mountpoint/i; $opt->{dcim} = "$opt->{mountpoint}/$opt->{mount_dcim}"; } close CONF; } else { warn("WARNING: Couldn't read conf: [$opt->{conv_conf}]\n Specify -dest\n\n"); } doSys($opt,$opt->{mount_cmd}); } sub umount { my ($opt) = @_; return unless $opt->{mount}; doSys($opt,$opt->{umount_cmd}); } ################################################## # Thumbnails ################################################## sub thumb { my ($opt,$img,$thm) = @_; my ($x,$y) = getSize($opt,$img); # Scale my ($newX,$newY) = ($opt->{thumbX},$opt->{thumbY}); ($x/$newX < $y/$newY) ? ($newY=$y) : ($newX=$x); ($x,$y) = scale($opt,$img,$newX,$newY,$thm); return warn("Couldn't scale $img\n") unless $x; # Now crop my ($offX,$offY) = (0,0); $offX = int(($x-$opt->{thumbX})/2) if $x>$opt->{thumbX}; $offY = int(($y-$opt->{thumbY})/2) if $y>$opt->{thumbY}; crop($opt,$thm,$opt->{thumbX},$opt->{thumbY},$offX,$offY,$thm) unless $x==$opt->{thumbX} && $y==$opt->{thumbY}; } sub getSize { my ($opt,$img) = @_; my ($qimg) = "\Q$img\E"; return (0,0) unless (-f $img); # Try to use identify if we have it if ($opt->{identify_cmd}) { print STDERR "getSize() run: $opt->{identify_cmd} -ping $img\n" if ($MAIN::DEBUG); if (open(SIZE,"$opt->{identify_cmd} -ping $qimg 2>&1 |")) { while() { print STDERR "getSize(): $_" if ($MAIN::DEBUG); if(/\s(\d+)x(\d+)(\s|\+)/) { close(SIZE); return ($1,$2); } } } else { undef $opt->{identify_cmd}; } } # Kludgy way to get size, but works with all images that convert reads print STDERR "getSize() run: $opt->{convert_cmd} -verbose $img /dev/null\n" if ($MAIN::DEBUG); open(SIZE,"$opt->{convert_cmd} -verbose $qimg /dev/null 2>&1 |") || die("[$PROGNAME] Couldn't run convert! [$opt->{convert_cmd}]\n"); while() { print STDERR "getSize(): $_" if ($MAIN::DEBUG); if(/\s(\d+)x(\d+)(\s|\+)/) { close(SIZE); return ($1,$2); } } die("[$PROGNAME] Can't get [$img] size from 'convert -verbose' output\n"); } sub scale { my ($opt,$img,$x,$y,$new) = @_; my $qimg = "\Q$img\E"; my $qnew = "\Q$new\E"; print STDERR "scale() run: $opt->{convert_cmd} -verbose $img -geometry ${x}x${y} $new\n" if ($MAIN::DEBUG); open(SIZE,"$opt->{convert_cmd} -verbose $qimg -geometry ${x}x${y} $qnew 2>&1 |") || die("[$PROGNAME] Couldn't run convert! [$opt->{convert_cmd}]\n"); while() { print STDERR "scale(): $_" if ($MAIN::DEBUG); if(/=>(\d+)x(\d+)\s/) { close(SIZE); return ($1,$2); } } close(SIZE); # Sometimes convert doesn't give us the new size information getSize($opt,$new); } sub crop { my ($opt,$img,$x,$y,$offX,$offY,$new) = @_; my $qimg = "\Q$img\E"; my $qnew = "\Q$new\E"; print STDERR "crop() run: $opt->{convert_cmd} $img -crop ${x}x${y}+${offX}+${offY} $new\n" if ($MAIN::DEBUG); system("$opt->{convert_cmd} $qimg -crop ${x}x${y}+${offX}+${offY} $qnew"); return unless ($?); print STDERR "[$PROGNAME] Error cropping $img\n"; } ################################################## # Hashes ################################################## sub start_hashes { my ($opt) =@_; return if $opt->{q}; $opt->{hashes_done} = 0; print STDERR "["," "x$opt->{num_hashes},"]\b","\b"x$opt->{num_hashes}; } sub show_hashes { my ($opt,$done,$outof) = @_; return if $opt->{q}; return unless $outof; my $needed = int($opt->{num_hashes}*($done/$outof)); print STDERR "X"x($needed-$opt->{hashes_done}); $opt->{hashes_done} = $needed; } sub stop_hashes { my ($opt) = @_; return if $opt->{q}; show_hashes($opt,1,1); undef $opt->{hashes_done}; print STDERR "]\n"; } ################################################## # Main code ################################################## sub main { my $opt = parseArgs(); my ($done,$tot) = (0,scalar @{$opt->{files}}); start_hashes($opt); foreach my $img ( @{$opt->{files}} ) { my $to = nextPath($opt,$opt->{dest},"IMG_%0.4d.JPG"); print "Copy: $img -> $to\n" if $opt->{v}; if ($img =~ /\.jpe?g$/i) { doSys($opt,$opt->{cp_cmd},$img,$to); } else { doSys($opt,$opt->{convert_cmd},$img,$to); } # Thumbnail my $thm = $to; $thm =~ s/JPG$/THM/; #print "THM $thm\n"; thumb($opt,$img,$thm); # Move .thm's to .MISC/ if desired, for firmware >3.0 compatibility if (-d $opt->{misc}) { my $new = $thm; $new =~ s|.*/([^/]+)$|$opt->{misc}/$1|; rename($thm,$new); } show_hashes($opt,++$done,$tot); } stop_hashes($opt); umount($opt); } main();