#!/usr/bin/perl -w
# ----------------------------------------------------------------------
# Copyright (C) 2001, 2002, 2003 by Bodo Bauer <bb@bb-zone.com>
#
# 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. 
#
# You should have received a copy of the GNU General Public License 
# along with this program; if not, write to the Free Software 
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, 
# MA 02111-1307, USA. 
#
# ----------------------------------------------------------------------
# Simple script to extract exif info
# ----------------------------------------------------------------------
# $Revision: 1.2 $
#
# $Log: exif.pl,v $
# Revision 1.2  2004/06/27 11:38:48  bb42
# * Allow to extract only single tag
# * Added usgae info
# * Added ability to list all valid tags
#
# ----------------------------------------------------------------------
use strict;
sub conv ( $@ );
sub read_ifd ( $$$ );
sub get_data ( $$$$$ );
sub raw_read ( $ );
sub parse_commandline ();
sub usage ();
sub print_tags ();

my $P = {
    fname => '',
    tag   => '',
};
my %P = %{$P};

my @Out;

my $EXIF_TAGS = {
  '0x0100' =>	"Image Width",
  '0x0101' =>	"Image Length",
  '0x0102' =>	"Bits Per Sample",
  '0x0103' =>	"Compression",
  '0x0106' =>	"Photometric Interpretation",
  '0x010a' =>	"Fill Order",
  '0x010d' =>	"Document Name",
  '0x010e' =>	"Image Description",
  '0x010f' =>	"Make",
  '0x0110' =>	"Model",
  '0x0111' =>	"Strip Offsets",
  '0x0112' =>	"Orientation",
  '0x0115' =>	"Samples Per Pixel",
  '0x0116' =>	"Rows Per Strip",
  '0x0117' =>	"Strip Byte Counts",
  '0x011a' =>	"X Resolution",
  '0x011b' =>	"Y Resolution",
  '0x011c' =>	"Planar Configuration",
  '0x0128' =>	"Resolution Unit",
  '0x012d' =>	"Transfer Function",
  '0x0131' =>	"Software",
  '0x0132' =>	"Date Time",
  '0x013b' =>	"Artist",
  '0x013e' =>	"White Point",
  '0x013f' =>	"Primary Chromaticities",
  '0x0156' =>	"Transfer Range",
  '0x0200' =>	"JPEG Proc",
  '0x0201' =>	"JPEG Interchange",
  '0x0202' =>	"JPEG Interchange Length",
  '0x0211' =>	"YCbCr Coefficients",
  '0x0212' =>	"YCbCr SubSampling",
  '0x0213' =>	"YCbCr Positioning",
  '0x0214' =>	"Reference Black White",
  '0x828d' =>	"CFA Repeat Pattern Dim",
  '0x828e' =>	"CFA Pattern",
  '0x828f' =>	"Battery Level",
  '0x8298' =>	"Copyright",
  '0x829a' =>	"Exposure Time",
  '0x829d' =>	"F Number",
  '0x83bb' =>	"IPTC/NAA",
  '0x8769' =>	"Exif Offset",
  '0x8773' =>	"Inter Color Profile",
  '0x8822' =>	"Exposure Program",
  '0x8824' =>	"Spectral Sensitivity",
  '0x8825' =>	"GPS Info",
  '0x8827' =>	"ISO Speed Ratings",
  '0x8828' =>	"OECF",
  '0x9000' =>	"Exif Version",
  '0x9003' =>	"Date Time Original",
  '0x9004' =>	"Date Time Digitized",
  '0x9101' =>	"Components Configuration",
  '0x9102' =>	"Compressed Bits Per Pixel",
  '0x9201' =>	"Shutter Speed Value",
  '0x9202' =>	"Aperture Value",
  '0x9203' =>	"Brightness 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",
  '0x9290' =>	"Sub Sec Time",
  '0x9291' =>	"Sub Sec TimeOriginal",
  '0x9292' =>	"Sub Sec TimeDigitized",
  '0xa000' =>	"Flash Pix Version",
  '0xa001' =>	"Color Space",
  '0xa002' =>	"Exif Image Width",
  '0xa003' =>	"Exif Image Length",
  '0xa005' =>	"InteroperabilityOffset",
  '0xa20b' =>	"Flash Energy",	                # 0x920B in TIFF/EP
  '0xa20c' =>	"Spatial Frequency Response",	# 0x920C    -  -
  '0xa20e' =>	"Focal Plane X Resolution",	# 0x920E    -  -
  '0xa20f' =>	"Focal Plane Y Resolution",	# 0x920F    -  -
  '0xa210' =>	"Focal Plane Resolution Unit",	# 0x9210    -  -
  '0xa214' =>	"SubjectLocation",	# 0x9214    -  -
  '0xa215' =>	"ExposureIndex",	# 0x9215    -  -
  '0xa217' =>	"SensingMethod",	# 0x9217    -  -
  '0xa300' =>	"FileSource",
  '0xa301' =>	"SceneType",
  '0x0001' =>	"Interoperability Index",
  '0x0002' =>	"Interoperability Version",
  '0x1000' =>	"Related Image File Format",
  '0x1001' =>	"Related Image Width",
  '0x1002' =>	"Related Image Length",
};

my @IFD_DATA_FORMAT = ( 
			{
			    TYPE => 'undefined', 
			    SIZE => 0,
			},
			{
			    TYPE => 'unsigned byte', 			    
			    SIZE => 1,
			},
			{
			    TYPE => 'ascii string', 
			    SIZE => 1,
			},
			{
			    TYPE => 'unsigned short',	
			    SIZE => 2,
			},
			{
			    TYPE => 'unsigned long', 
			    SIZE => 4,
			},
			{
			    TYPE => 'unsigned rational',
			    SIZE => 8,
			},
			{
			    TYPE => 'signed byte', 
			    SIZE => 1,
			},
			{
			    TYPE => 'undefined', 
			    SIZE => 1,
			},
			{
			    TYPE => 'signed short', 
			    SIZE => 2,
			},
			{
			    TYPE => 'signed long', 
			    SIZE => 4,
			},
			{
			    TYPE => 'signed rational', 
			    SIZE => 8,
			},
			{
			    TYPE => 'single float', 
			    SIZE => 4,
			},
			{
			    TYPE => 'double float' ,
			    SIZE => 8,
			},
);

my $item_nr=0;
parse_commandline();
if ( defined ( $P{fname} ) && open ( IMG, "<$P{fname}" ) ) {
    my $data;
    my $align = 'X';

    # check for JPEG image
    $data = raw_read ( 12 );
    if ( $data->[0] != 0xff || $data->[1] != 0xd8 ) {
	print "$ARGV[0] is not a JPEG file\n";
	exit ( 1 );
    }

    # check for EXIF marker
    if ( $data->[2] != 0xff || $data->[3] != 0xe1 ) {
	print "No EXIF marker found, sorry...\n";
	exit ( 1 );
    }

    # remember this position, all offsets are counted from here
    my $seek_set = sysseek ( IMG, 0, 1 );

    # read TIFF header
    $data = raw_read ( 8 );
    $align = 'INTEL'    if ( $data->[0] == 0x49 && $data->[1] == 0x49 );
    $align = 'MOTOROLA' if ( $data->[0] == 0x4d && $data->[1] == 0x4d );
    if ( $align eq 'X' ) {
	print "Unknown TIFF alignment, sorry...\n";
	exit ( 1 );
    } 
    my $offset = conv ( $align, $data->[4], $data->[5], $data->[6], $data->[7] );
    sysseek ( IMG, $offset-8, 1 );

    # read IFDs
    my $ifd_nr = 0;
    while ( $offset ) {
	$offset = read_ifd( $align, $seek_set, 'IFD' . $ifd_nr++ );
	sysseek ( IMG, $seek_set, 0 );
	sysseek ( IMG, $offset, 1 );
    }
    close ( IMG );

    if ( $P{tag} ne'' ) {
	foreach my $l ( sort @Out ) {
	    my @f = split ( /\s*:\s*/, $l, 6 );
	    if ( $f[2] eq $P{tag} ) {
		print $f[5] ."\n";
		exit;
	    }
	}	
    } else {
	foreach my $l ( sort @Out ) {
	    print $l . "\n";
	}
    }

} else {
    print "Cant open image file ($!)\n\n";

    usage();
}

sub conv ( $@ ) {
    my ( $align, @d ) = @_;
    my $value = 0;

    if ( $#d == 3 ) {
	for ( $align ) {
	    /^INTEL$/ && do {
		$value += ( $d[0] <<  0 );
		$value += ( $d[1] <<  8 );
		$value += ( $d[2] << 16 );
		$value += ( $d[3] << 24 );
		last;
	    };
	    /^MOTOROLA$/ && do {
		$value += ( $d[3] <<  0 );
		$value += ( $d[2] <<  8 );
		$value += ( $d[1] << 16 );
		$value += ( $d[0] << 24 );
		last;
	    };
	    print "Unknown Alignement: $align\n";
	    exit ( 1 );
	}
    } elsif ( $#d == 1 ) {
	for ( $align ) {
	    /^INTEL$/ && do {
		$value += ( $d[0] <<  0 );
		$value += ( $d[1] <<  8 );
		last;
	    };
	    /^MOTOROLA$/ && do {
		$value += ( $d[1] << 0 );
		$value += ( $d[0] << 8 );
		last;
	    };
	    print "Unknown Alignement: $align\n";
	    exit ( 1 );
	}	
    }
    return ( $value );
}

sub read_ifd ( $$$ ) {
    my $align      = shift;
    my $seek_set   = shift;
    my $title      = shift;
    my $ifd_tag_nr = 0;
    my $data;
    my $out        = '';

    $data = raw_read ( 2 );
    my $entries = conv ( $align, $data->[0], $data->[1] );
    for ( my $j=0; $j<$entries; $j++ ) {
	$out .= sprintf ( "%04d : %-16s : ", $item_nr++, $title . '-' . sprintf ( "%04d", $ifd_tag_nr++ ) );

	$data = raw_read ( 8 );
	# Get Tag Number
	my $tag_nr = sprintf ( "0x%02x%02x", $data->[1], $data->[0] );
	$out .= sprintf ( "%-6s : %-35s : ", $tag_nr, ( defined ( $EXIF_TAGS->{$tag_nr} ) ) ? $EXIF_TAGS->{$tag_nr} : $tag_nr );

	# Format
	my $format = conv ( $align, $data->[2], $data->[3] );
	$out .= sprintf ( "%-20s : ", (defined ($IFD_DATA_FORMAT[$format]->{TYPE}))? $IFD_DATA_FORMAT[$format]->{TYPE} : $format );
	
	# Number of Components (length)
	my $components_nr = conv ( $align, $data->[4], $data->[5], $data->[6], $data->[7] );
	
	# Data value or offset
	my $dat;
	if ( $components_nr * $IFD_DATA_FORMAT[$format]->{SIZE} <= 4 ) {
	    # data is right here!
	    $dat = get_data ( $align, $seek_set, $format, $components_nr, -1 );
	    raw_read ( 4 );
	} else {
	    # read data at given offset
	    $data = raw_read ( 4 );
	    my $value = conv ( $align, $data->[0], $data->[1], $data->[2], $data->[3] );
	    $dat = get_data ( $align, $seek_set, $format, $components_nr, $value );
	}
	$out .=  "$dat\n";
	if ( $tag_nr eq '0x8769' || $tag_nr eq '0xa005' ) {
	    # Found SubIFD
	    my $pos = sysseek ( IMG, 0, 1 );
	    sysseek ( IMG, $seek_set+$dat, 0 );
	    read_ifd ( $align, $seek_set, 'Sub' . $title );
	    sysseek ( IMG, $pos, 0 );
	} 
    }
    # return offset to next IFD
    $data = raw_read ( 4 );
    my $offset = conv ( $align , $data->[0], $data->[1], $data->[2], $data->[3] );
    push ( @Out, split ( /\n/, $out ));

    return ( $offset );
}

sub get_data ( $$$$$ ) {
    my ( $align, $seek_set, $type, $size, $offset ) = @_;
    my $retval = 0;
    # safe current file offset
    my $save_offset = sysseek ( IMG, 0, 1 );

    if ( defined ( $IFD_DATA_FORMAT[$type] ) ) {

	if ( $offset >= 0 ) {
	    sysseek ( IMG, $seek_set, 0 );
	    sysseek ( IMG, $offset, 1 );
	} 

	for ( $IFD_DATA_FORMAT[$type]->{TYPE} ) {
	    /^ascii string$/ && do {
		sysread ( IMG, $retval, $size-1, 0 );
		last;
	    };

	    /^unsigned byte$/ && do {
		sysread ( IMG, my $buffer, 1, 0 );
		$retval = ord ( $buffer );
		last;
	    };

	    /^unsigned short$/ && do {
		my $data = raw_read ( 2 );
		$retval = conv ( $align, $data->[0], $data->[1] );
		last;
	    };

	    /^unsigned long$/ && do {
		my $data = raw_read ( 4 );
		$retval = conv ( $align, $data->[0], $data->[1], $data->[2], $data->[3] );
		last;
	    };

	    /^signed rational$/ && do {
		return ( "can't handle more than one rational at a time" ) if ( $size != 1 );
		my $data = raw_read ( 8 );
		my $numerator   = conv ( $align, $data->[0], $data->[1], $data->[2], $data->[3] );
		my $denominator = conv ( $align, $data->[4], $data->[5], $data->[6], $data->[7] );
		$retval = "$numerator/$denominator";
		last;
	    };

	    /^unsigned rational$/ && do {
		return ( "can't handle more than one rational at a time" ) if ( $size != 1 );
		my $data = raw_read ( 8 );
		my $numerator   = conv ( $align, $data->[0], $data->[1], $data->[2], $data->[3] );
		my $denominator = conv ( $align, $data->[4], $data->[5], $data->[6], $data->[7] );
		$retval = "$numerator/$denominator";
		last;
	    };
	    # dump raw hex values for types we don't know anything about
	    my $count = $size * $IFD_DATA_FORMAT[$type]->{SIZE};
	    my $data = raw_read ( $count );
	    $retval = '';
	    for ( my $i=0; $i<$count; $i++ ) {
		$retval .= sprintf ( "%02x ", $data->[$i] );
	    }
	    $retval =~ s/ $//;
	    $retval = "0x[$retval]";
	}
    } else {
	print "Unknown data type: $type\n";
	exit ( 0 );
    }
    
    # restore file offset
    sysseek ( IMG, $save_offset, 0 );
    return ( $retval );
}

sub raw_read ( $ ) {
    my $bytes = shift;
    my @data;
    my $i = 0;
    sysread ( IMG, my $buffer, $bytes, 0 );
    foreach (  split ( //, $buffer, $bytes ) ) { 
	$data[$i++] = ord ( $_ ); 
    }
    return ( \@data );
}

sub print_tags () {
    print " Tag#\t| Description\n";
    print "----------------------------------------------------------\n";
    foreach my $k ( sort ( keys %{$EXIF_TAGS} ) ) {
	print "$k\t| $EXIF_TAGS->{$k}\n";
    }
}


sub parse_commandline () {
    usage() if ( $#ARGV < 0 );
    
    for ( my $i=0; $i<=$#ARGV; $i++ ) {
	for ( $ARGV[$i] ) {
	    ( /^-t$/ || /^--tag$/ ) && do {
		$P{tag} = $ARGV[++$i];
		last;
	    };
	    ( /^-f$/ || /^--file$/ )&& do {
		$P{fname} = $ARGV[++$i];
		last;
	    };
	    ( /^-h$/ || /^--help$/ ) && do {
		usage();
		exit ( 1 );
		last;
	    };
	    ( /^-l$/ || /^--list-tags$/ )&& do {
		print_tags();
		exit;
	    };

	    $P{fname} = $ARGV[$i];
	}
    }
}

sub usage () {
    print <<END;
Usage:
    $0 [Options] [<filename>]

Options:
    -t  --tag <t>     Print only value of tag ID <t> 
                      (use '-l' to get a list of vaid tag IDs)
    -l  --list-tags   Print list of valid tags IDs and their description
    -f  --file <f>    Print EXIF info for file <f>
END
    exit;
}










