#!/usr/bin/perl # jpginfo 1/4/04 # (c) 2003-2004 by Stephen A. Ness # Usage: see $usage below. # Quick and dirty: # only supports littleendian (Intel) byte ordering in app1 data; # not careful about signed vs. unsigned values in output, etc. # References: # TsuruZoh Tachibanaya, "Description of Exif File Format": # http://www.media.mit.edu/pia/Research/deepview/exif.html # Adobe, "TIFF Revision 6.0": # ftp://ftp.adobe.com/pub/adobe/devrelations/devtechnotes/pdffiles/tiff6.pdf # FIX_ME This dies on the marker at the end of Dennis Minnick's Canon photos; # they contain an EOI record at EOF-3 (0xFF, 0xD9, 0x00) rather than at EOF-2 as expected. $usage = join("\n", "Usage: jpginfo [ -c ] [ +cComment ] [ -d ] [ -h ] [ -t ] [ -v ] [ -w ] file ...", "Options:", "-b save original file as file.bak with +C option (default: off)", "-c toggle comment info output (default: on)", "+cComment add Comment to each JPEG file; suppresses all non-debug info output", "-d toggle debug info output (default: off)", "-h print usage message to stderr", "-t toggle tag info output (default: on)", "-v toggle verbose info output (default: off)", "-w toggle width info output (default: on)\n" ); # Default flag values, toggled by command line switches. $bflag = 0; $cflag = 1; $debug = 0; $tflag = 1; $vflag = 0; $wflag = 1; # Tuneable globals. $bufsiz = 32 * 1024; # 32K read/write max buffer size # JPEG markers. %markername = ( 0xC0, "M_SOF0", 0xC1, "M_SOF1", 0xC2, "M_SOF2", 0xC3, "M_SOF3", 0xC5, "M_SOF5", 0xC6, "M_SOF6", 0xC7, "M_SOF7", 0xC8, "M_JPG", 0xC9, "M_SOF9", 0xCA, "M_SOF10", 0xCB, "M_SOF11", 0xCD, "M_SOF13", 0xCE, "M_SOF14", 0xCF, "M_SOF15", 0xC4, "M_DHT", 0xCC, "M_DAC", 0xD0, "M_RST0", 0xD1, "M_RST1", 0xD2, "M_RST2", 0xD3, "M_RST3", 0xD4, "M_RST4", 0xD5, "M_RST5", 0xD6, "M_RST6", 0xD7, "M_RST7", 0xD8, "M_SOI", 0xD9, "M_EOI", 0xDA, "M_SOS", 0xDB, "M_DQT", 0xDC, "M_DNL", 0xDD, "M_DRI", 0xDE, "M_DHP", 0xDF, "M_EXP", 0xE0, "M_APP0", 0xE1, "M_APP1", 0xE2, "M_APP2", 0xE3, "M_APP3", 0xE4, "M_APP4", 0xE5, "M_APP5", 0xE6, "M_APP6", 0xE7, "M_APP7", 0xE8, "M_APP8", 0xE9, "M_APP9", 0xEA, "M_APP10", 0xEB, "M_APP11", 0xEC, "M_APP12", 0xED, "M_APP13", 0xEE, "M_APP14", 0xEF, "M_APP15", 0xF0, "M_JPG0", 0xFD, "M_JPG13", 0xFE, "M_COM", ); # And again, inverted, as %marker. foreach $key (keys(%markername)) { $marker{$markername{$key}} = $key; } # Exif tags. %tiff_tagname = ( 0x0103, "Compression", 0x010E, "Image Description", 0x010F, "Make", 0x0110, "Model", 0x0112, "Orientation", 0x011A, "XResolution", 0x011B, "YResolution", 0x0128, "Resolution Unit", 0x0131, "Software", 0x0132, "Date/Time", 0x0201, "JPEG IF Offset", 0x0202, "JPEG IF Byte Count", 0x0213, "YCbCr Positioning", 0x829A, "Exposure Time", 0x829D, "F Number", 0x8769, "Exif Offset", 0x8822, "Exposure Program", 0x8827, "ISO Speed Rating", 0x9000, "Exif Version", 0x9003, "Date/Time Original", 0x9004, "Date/Time Digitized", 0x9101, "Component Configuration", 0x9102, "Compressed Bits per Pixel", 0x9201, "Shutter Speed Value", 0x9202, "Aperture Value", 0x9204, "Exposure Bias Value", 0x9205, "Max Aperture Value", 0x9206, "Subject Distance", 0x9207, "Metering Mode", 0x9208, "Light Source", 0x9209, "Flash", 0x920A, "Focal Length", 0x927C, "Maker Note", 0x9286, "User Comment", 0xA000, "Flash Pix Version", 0xA001, "Color Space", 0xA002, "Exif Image Width", 0xA003, "Exif Image Height", 0xA005, "Exif Interop Offset", 0xA20E, "Focal Plane X Res", 0xA20F, "Focal Plane Y Res", 0xA210, "Focal Plane Res Unit", 0xA217, "Sensing Method", 0xA300, "File Type", 0xA301, "Source Type", ); # %tagname is normally %tiff_tagname, but changed to %olympus_tagname # while processing Olympus Maker Note info. %tagname = %tiff_tagname; # Olympus Maker Note tags. %olympus_tagname = ( 0x0200, "Special Mode", 0x0201, "JPEG Quality", 0x0202, "Macro Mode", 0x0203, "Unknown_0x0203", 0x0204, "Digital Zoom", 0x0205, "Unknown_0x205", 0x0206, "Unknown_0x206", 0x0207, "Software Release", 0x0208, "Picture Info", 0x0209, "Camera ID", 0x0F00, "Data Dump", ); # Size of TIFF type n-1 (in bytes). @typesize = ( 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8); # Process APP1 data. sub app1 { local ($size) = @_; &xread(6, "Exif header"); # read EXIF header die "expected EXIF header\n" unless $c[0] == 0x45 && $c[1] == 0x78 && $c[2] == 0x69 && $c[3] == 0x66 && $c[4] == 0 && $c[5] == 0; $tiff_beg = tell(INF); $tiff_end = $tiff_beg + $size - 6; &xread(8, "TIFF header"); # read TIFF header die "expected littleendian\n" unless &le_short(0) == 0x4949 && &le_short(2) == 0x2A; $off = &le_long(4); while ($off != 0) { $off = &ifd($off, "IFD" . $ifd++); } # process IFDs seek(INF, $tiff_end, 0); # skip remainder for now } # Return a 2-byte quantity in bigendian format. sub be_short { local ($n) = @_; $c[$n] << 8 | $c[$n + 1]; } # Return a 4-byte quantity in bigendian format. sub be_long { local ($n) = @_; $c[$n] << 24 | $c[$n + 1] << 16 | $c[$n + 2] << 8 | $c[$n + 3]; } # Dump data bytes. # The tag is passed so that this can set Make information for subsequent use in MakerNote. sub dump { local ($tag, $nitems, $off, $fmt) = @_; local ($i, $old, $nbytes, $o); $old = tell(INF); seek(INF, $off, 0); $nbytes = $nitems * $typesize[$fmt - 1]; &xread($nbytes, "tag data"); if ($fmt == 2) { # dump as ASCII string print "\""; foreach $i (0 .. $nbytes - 1) { printf "%c", $c[$i] if $c[$i]; } print "\""; if ($tagname{$tag} eq "Make" && $nbytes == 6 && $c[0] == 0x43 && $c[1] == 0x61 && $c[2] == 0x6E && $c[3] == 0x6F && $c[4] == 0x6E) { ++$is_canon; } } elsif ($fmt == 4 || $fmt == 9) { # dump as ulong or slong foreach $i (0 .. $nitems - 1) { printf "%ld ", &le_long(4 * $i); } } elsif ($fmt == 5 || $fmt == 10) { # dump as rational or srational foreach $i (0 .. $nitems - 1) { printf "%ld/%ld ", &le_long(8 * $i), &le_long(8 * $i + 4); } } elsif ($fmt == 3 || $fmt == 8) { # dump as short or sshort foreach $i (0 .. $nitems - 1) { $l = &le_short(2 * $i); $l |= 0xFFFF0000 if $fmt == 8 && ($l & 0x8000); printf "%ld ", $l; } } else { # dump as raw hex data foreach $i (0 .. $nbytes - 1) { printf "%02x ", $c[$i]; } } print "\n"; seek(INF, $old, 0); } # Process IFD, return offset to next IFD. sub ifd { local ($off, $id) = @_; local ($old, $n, $i, $tag, $fmt, $nitems, $ndata); $old = tell(INF); seek(INF, $tiff_beg + $off, 0); # seek to IFD &xread(2, "nentries"); # read number of entries $n = &le_short(0); print "\n[$id ($n entries):]\n" if $tflag; foreach $i (1 .. $n) { print "$i: " if $debug; &xread(12, "IFD entry"); # read IFD entry $tag = &le_short(0); $fmt = &le_short(2); $nitems = &le_long(4); $off = &le_long(8); $ndata = $nitems * $typesize[$fmt - 1]; printf "tag = %x fmt=%x nitems=$nitems off=0x%x ndata=$ndata\n", $tag, $fmt, $off if $debug; if ($tflag) { if ($tagname{$tag}) { print "$tagname{$tag}: "; } else { printf "tag=0x%x: ", $tag; } } if ($tagname{$tag} eq "Exif Offset") { &ifd($off, "Exif SubIFD"); } elsif ($tagname{$tag} eq "Maker Note") { &maker_note($nitems, $off, $fmt); } elsif ($ndata > 4) { &dump($tag, $nitems, $tiff_beg + $off, $fmt) if $tflag; } else { printf "0x%x\n", $off if $tflag; } # data is in offset field } &xread(4, "next IFD"); $off = &le_long(0); printf "offset to next IFD=0x%x\n", $off if $debug; seek(INF, $old, 0); # restore previous file position print "[end of $id]\n" if $tflag; $off; } # Add/replace comment in a JPEG file. # Saves old file in file.bak if -b, otherwise overwrites it in place. sub jpgcomment { local ($file) = @_; print "$file:\n" if $vflag; open(INF, "<$file") || die "cannot open input file \"$file\"\n"; binmode(INF); open(OUTF, ">$file.tmp") || die "cannot open input file \"$file.tmp\"\n"; binmode(OUTF); while (!eof(INF)) { &jpgcopy; } # copy JPEG close(INF); close(OUTF); rename("$file", "$file.bak") || die "rename to .bak failed\n" if $bflag; unlink("$file") unless $bflag; rename("$file.tmp", "$file") || die "rename failed"; } # Copy a JPEG marker and following data, changing comment on the fly. sub jpgcopy { &xread(2, "marker"); # read marker die "expected 0xff, got 0x", sprintf("%x", $c[0]), " at start of marker\n" unless $c[0] == 0xFF; $m = $c[1]; printf "$markername{$m} offset=0x%x\n", tell(INF) - 2 if $debug; &xwrite(2, "marker") if $m != $marker{"M_COM"}; if ($m == $marker{"M_SOI"}) { # Write new comment following M_SOI marker. $len = length($comment) + 1; $buf = pack("CCCCa$len", 0xFF, $marker{"M_COM"}, (($len + 2) >> 8) & 0xFF, ($len + 2) & 0xFF, $comment); &xwrite($len + 4, "comment marker size"); } elsif ($m != $marker{"M_EOI"}) { if ($m == $marker{"M_SOS"}) { $old = tell(INF); seek(INF, -2, 2); # seek past image data $size = tell(INF) - $old; seek(INF, $old, 0); # restore file position } else { &xread(2, "marker size"); &xwrite(2, "marker size") if $m != $marker{"M_COM"}; $size = &be_short(0) - 2; } while ($size > $bufsiz) { # Avoid huge reads/writes, copy at most $bufsiz at a time. &xread($bufsiz, "marker data"); &xwrite($bufsiz, "marker data") if $m != $marker{"M_COM"}; $size -= $bufsiz; } &xread($size, "marker data"); &xwrite($size, "marker data") if $m != $marker{"M_COM"}; } } # Process a JPEG file. sub jpginfo { local ($file) = @_; print "$file:\n" if $vflag; $is_canon = 0; open(INF, "<$file") || die "cannot open input file \"$file\"\n"; binmode(INF); $ifd = 0; while (!eof(INF)) { &jpgmarker; } # process JPEG markers close(INF); } # Read a JPEG marker. sub jpgmarker { &xread(2, "marker"); # read marker die "expected 0xff, got 0x", sprintf("%x", $c[0]), " at start of marker\n" unless $c[0] == 0xFF; $m = $c[1]; printf "$markername{$m} offset=0x%x", tell(INF) - 2 if $debug; if ($m == $marker{"M_SOI"} || $m == $marker{"M_EOI"} || $m == $marker{"M_SOS"}) { seek(INF, -2, 2) if $m == $marker{"M_SOS"}; # seek past image data, presumably M_EOI follows print "\n" if $debug; return; } &xread(2, "marker size"); # read marker size $size = &be_short(0) - 2; # JPEG always bigendian (unlike TIFF) printf "\tsize=0x%x\n", $size if $debug; if ($m == $marker{"M_SOF0"}) { # read M_SOF0 info for height and width &xread(6, "SOF0"); print "w=", &be_short(3), " h=", &be_short(1), "\n" if $wflag; &xread(3 * $c[5], "component data"); } elsif ($m == $marker{"M_APP1"}) { &app1($size); # process APP1 } elsif ($m == $marker{"M_COM"} && $cflag) { print "Comment: "; &dump(0, $size, tell(INF), 2); seek(INF, $size, 1); } else { seek(INF, $size, 1); } # else skip contents } # Return a 2-byte quantity in littleendian format. sub le_short { local ($n) = @_; $c[$n + 1] << 8 | $c[$n]; } # Return a 4-byte quantity in littleendian format. sub le_long { local ($n) = @_; $c[$n + 3] << 24 | $c[$n + 2] << 16 | $c[$n + 1] << 8 | $c[$n]; } # Read a maker note, just dumped unless Olympus or Canon. sub maker_note { local ($nitems, $off, $fmt) = @_; local ($old); $old = tell(INF); seek(INF, $tiff_beg + $off, 0); &xread(6, "maker note"); if ($c[0] == 0x4F && $c[1] == 0x4C && $c[2] == 0x59 && $c[3] == 0x4D && $c[4] == 0x50 && $c[5] == 0) { # Olympus. %tagname = %olympus_tagname; # dump maker note with Olympus tagnames &ifd($off + 8, "MakerNote IFD"); # N.B. unclear to me what bytes 7 and 8 represent %tagname = %tiff_tagname; # restore TIFF tagnames } elsif ($is_canon) { # Make tag set $is_canon if Canon # Canon: IFD at $off. &ifd($off, "MakerNote IFD"); } else { # Neither Olympus nor Canon, just dump. &dump(0, $nitems, $off, $fmt); } seek(INF, $old, 0); # restore previous file position } # Read from INF, die on failure, unpack as characters. # Globals: INF, $buf, @c. sub xread { local ($n, $msg) = @_; die "$msg read failed\n" unless read(INF, $buf, $n) == $n; @c = unpack("C$n", $buf); } # Write to OUTF, die on failure. # Globals: OUTF, $buf, @c. sub xwrite { local ($n, $msg) = @_; die "$msg write failed\n" unless syswrite(OUTF, $buf, $n) == $n; } # Main. while(@ARGV) { $arg = shift @ARGV; if ($arg eq "-b") { $bflag = !$bflag; next; } if ($arg eq "-c") { $cflag = !$cflag; next; } if (substr($arg, 0, 2) eq "+c") { $comment = substr($arg, 2); next; } if ($arg eq "-d") { $debug = !$debug; next; } if ($arg eq "-h") { print STDERR $usage; next; } if ($arg eq "-t") { $tflag = !$tflag; next; } if ($arg eq "-v") { $vflag = !$vflag; next; } if ($arg eq "-w") { $wflag = !$wflag; next; } if ($comment) { &jpgcomment($arg);} # replace JPEG comment else { &jpginfo($arg); } # process a JPEG file } # end of jpginfo