package Image::TIFF; # Copyright 1999-2001, Gisle Aas. # Copyright 2006, 2007 Tels # # This library is free software; you can redistribute it and/or # modify it under the same terms as Perl v5.8.8 itself. use strict; use vars qw($VERSION); $VERSION = '1.07'; my @types = ( [ "BYTE", "C1", 1], [ "ASCII", "A1", 1], [ "SHORT", "n1", 2], [ "LONG", "N1", 4], [ "RATIONAL", "N2", 8], [ "SBYTE", "c1", 1], [ "UNDEFINED", "a1", 1], [ "BINARY", "a1", 1], # treat binary data as UNDEFINED [ "SSHORT", "n1", 2], [ "SLONG", "N1", 4], [ "SRATIONAL", "N2", 8], [ "FLOAT", "f1", 4], # XXX 4-byte IEEE format [ "DOUBLE", "d1", 8], # XXX 8-byte IEEE format # XXX TODO: # [ "IFD", "??", ?], # See ExifTool ); my %nikon1_tags = ( 0x0003 => "Quality", 0x0004 => "ColorMode", 0x0005 => "ImageAdjustment", 0x0006 => "CCDSensitivity", 0x0007 => "Whitebalance", 0x0008 => "Focus", 0x000A => "DigitalZoom", 0x000B => "Converter", ); my %nikon2_tags = ( 0x0001 => "NikonVersion", 0x0002 => "ISOSetting", 0x0003 => "ColorMode", 0x0004 => "Quality", 0x0005 => "Whitebalance", 0x0006 => "ImageSharpening", 0x0007 => "FocusMode", 0x0008 => "FlashSetting", 0x0009 => "FlashMetering", 0x000B => "WBAdjustment", 0x000F => "ISOSelection", 0x0080 => "ImageAdjustment", 0x0082 => "AuxiliaryLens", 0x0084 => "Lens", 0x0085 => "ManualFocusDistance", 0x0086 => "DigitalZoom", 0x0088 => { __TAG__ => "AFFocusPosition", pack("xCxx",0) => "Center", pack("xCxx",1) => "Top", pack("xCxx",2) => "Bottom", pack("xCxx",3) => "Left", pack("xCxx",4) => "Right", }, 0x008d => "ColorMode", 0x0090 => "FlashType", 0x0095 => "NoiseReduction", 0x0010 => "DataDump", ); my %olympus_tags = ( 0x0200 => "SpecialMode", 0x0201 => { __TAG__ => "JpegQual", 0 => "SQ", 1 => "HQ", 2 => "SHQ" }, 0x0202 => { __TAG__ => "Macro", 0 => "Normal", 1 => "Macro" }, 0x0204 => "DigiZoom", 0x0207 => "SoftwareRelease", 0x0208 => "PictInfo", 0x0209 => "CameraID", 0x0f00 => "DataDump", ); my %fujifilm_tags = ( 0x0000 => "Version", 0x1000 => "Quality", 0x1001 => { __TAG__ => "Sharpness", 1 => "Very Soft", 2 => "Soft", 3 => "Normal", 4 => "Hard", 5 => "Very Hard", }, 0x1002 => { __TAG__ => "WhiteBalance", 0 => "Auto", 256 => "Daylight", 512 => "Cloudy", 768 => "DaylightColor-fluorescence", 769 => "DaywhiteColor-fluorescence", 770 => "White-fluorescence", 1024 => "Incandenscense", 3840 => "Custom white balance", }, 0x1003 => { __TAG__ => "Color", 0 => "Normal", 256 => "High", 512 => "Low" }, 0x1004 => { __TAG__ => "Tone" , 0 => "Normal", 256 => "High", 512 => "Low" }, 0x1010 => { __TAG__ => "FlashMode", 0 => "Auto", 1 => "On", 2 => "Off", 3 => "Red-eye reduction" }, 0x1011 => "FlashStrength", 0x1020 => { __TAG__ => "Macro", 0 => "Off", 1 => "On"}, 0x1021 => { __TAG__ => "FocusMode", 0 => "Auto", 1 => "Manual" }, 0x1030 => { __TAG__ => "SlowSync", 0 => "Off", 1 => "On"}, 0x1031 => { __TAG__ => "PictureMode", 0 => "Auto", 1 => "Portrait", 2 => "Landscape", 4 => "Sports", 5 => "Night", 6 => "Program AE", 256 => "Aperture priority", 512 => "Shutter priority", 768 => "Manual", }, 0x1100 => { __TAG__ => "AutoBracketing", 0 => "Off", 1 => "On"}, 0x1300 => { __TAG__ => "BlurWarning", 0 => "No", 1 => "Yes"}, 0x1301 => { __TAG__ => "FocusWarning", 0 => "No", 1 => "Yes"}, 0x1302 => { __TAG__ => "AEWarning", 0 => "No", 1 => "Yes"}, ); my %casio_tags = ( 0x0001 => { __TAG__ => "RecordingMode", 1 => "SingleShutter", 2 => "Panorama", 3 => "Night scene", 4 => "Portrait", 5 => "Landscape", }, 0x0002 => { __TAG__ => "Quality", 1 => "Economy", 2 => "Normal", 3 => "Fine" }, 0x0003 => { __TAG__ => "FocusingMode", 2 => "Macro", 3 => "Auto", 4 => "Manual", 5 => "Infinity", }, 0x0004 => { __TAG__ => "FlashMode", 1 => "Auto", 2 => "On", 3 => "Off", 4 => "Red-eye reduction" }, 0x0005 => { __TAG__ => "FlashIntensity", 11 => "Weak", 13 => "Normal", 15 => "Strong" }, 0x0006 => "ObjectDistance", 0x0007 => { __TAG__ => "WhiteBalance", 1 => "Auto", 2 => "Tungsten", 3 => "Daylight", 4 => "Fluorescent", 5 => "Shade", 129 => "Manual", }, 0x000a => { __TAG__ => "DigitalZoom", 65536 => "Off", 65537 => "2X" }, 0x000b => { __TAG__ => "Sharpness", 0 => "Normal", 1 => "Soft", 2 => "Hard" }, 0x000c => { __TAG__ => "Contrast" , 0 => "Normal", 1 => "Low", 2 => "High" }, 0x000d => { __TAG__ => "Saturation", 0 => "Normal", 1 => "Low", 2 => "High" }, 0x0014 => { __TAG__ => "CCDSensitivity", 64 => "Normal", 125 => "+1.0", 250 => "+2.0", 244 => "+3.0", 80 => "Normal", 100 => "High", }, ); my %canon_0x0001_tags = ( 0 => { __TAG__ => "MacroMode", 1 => "Macro", 2 => "Normal" }, 1 => "SelfTimer", 2 => { __TAG__ => "Quality", 2 => "Normal", 3 => "Fine", 5 => "SuperFine" }, 3 => 'Tag-0x0001-03', 4 => { __TAG__ => 'FlashMode', 0 => 'Flash Not Fired', 1 => 'Auto', 2 => 'On', 3 => 'Red-Eye Reduction', 4 => 'Slow Synchro', 5 => 'Auto + Red-Eye Reduction', 6 => 'On + Red-Eye Reduction', 16 => 'External Flash' }, 5 => { __TAG__ => 'ContinuousDriveMode', 0 => 'Single Or Timer', 1 => 'Continuous' }, 6 => 'Tag-0x0001-06', 7 => { __TAG__ => 'FocusMode', 0 => 'One-Shot', 1 => 'AI Servo', 2 => 'AI Focus', 3 => 'MF', 4 => 'Single', 5 => 'Continuous', 6 => 'MF' }, 8 => 'Tag-0x0001-08', 9 => 'Tag-0x0001-09', 10 => { __TAG__ => 'ImageSize', 0 => 'Large', 1 => 'Medium', 2 => 'Small' }, 11 => { __TAG__ => 'EasyShootingMode', 0 => 'Full Auto', 1 => 'Manual', 2 => 'Landscape', 3 => 'Fast Shutter', 4 => 'Slow Shutter', 5 => 'Night', 6 => 'B&W', 7 => 'Sepia', 8 => 'Portrait', 9 => 'Sports', 10 => 'Macro/Close-Up', 11 => 'Pan Focus' }, 12 => { __TAG__ => 'DigitalZoom', 0 => 'None', 1 => '2x', 2 => '4x' }, 13 => { __TAG__ => 'Contrast', 0xFFFF => 'Low', 0 => 'Normal', 1 => 'High' }, 14 => { __TAG__ => 'Saturation', 0xFFFF => 'Low', 0 => 'Normal', 1 => 'High' }, 15 => { __TAG__ => 'Sharpness', 0xFFFF => 'Low', 0 => 'Normal', 1 => 'High' }, 16 => { __TAG__ => 'ISO', 0 => 'See ISOSpeedRatings Tag', 15 => 'Auto', 16 => '50', 17 => '100', 18 => '200', 19 => '400' }, 17 => { __TAG__ => 'MeteringMode', 3 => 'Evaluative', 4 => 'Partial', 5 => 'Center-Weighted' }, 18 => { __TAG__ => 'FocusType', 0 => 'Manual', 1 => 'Auto', 3 => 'Close-Up (Macro)', 8 => 'Locked (Pan Mode)' }, 19 => { __TAG__ => 'AFPointSelected', 0x3000 => 'None { __TAG__ => MF)', 0x3001 => 'Auto-Selected', 0x3002 => 'Right', 0x3003 => 'Center', 0x3004 => 'Left' }, 20 => { __TAG__ => 'ExposureMode', 0 => 'Easy Shooting', 1 => 'Program', 2 => 'Tv-priority', 3 => 'Av-priority', 4 => 'Manual', 5 => 'A-DEP' }, 21 => 'Tag-0x0001-21', 22 => 'Tag-0x0001-22', 23 => 'LongFocalLengthOfLensInFocalUnits', 24 => 'ShortFocalLengthOfLensInFocalUnits', 25 => 'FocalUnitsPerMM', 26 => 'Tag-0x0001-26', 27 => 'Tag-0x0001-27', 28 => { __TAG__ => 'FlashActivity', 0 => 'Did Not Fire', 1 => 'Fired' }, 29 => { __TAG__ => 'FlashDetails', 14 => 'External E-TTL', 13 => 'Internal Flash', 11 => 'FP Sync Used', 7 => '2nd ("Rear")-Curtain Sync Used', 4 => 'FP Sync Enabled' }, 30 => 'Tag-0x0001-30', 31 => 'Tag-0x0001-31', 32 => { __TAG__ => 'FocusMode', 0 => 'Single', 1 => 'Continuous' }, ); my %canon_0x0004_tags = ( 7 => { __TAG__ => 'WhiteBalance', 0 => 'Auto', 1 => 'Sunny', 2 => 'Cloudy', 3 => 'Tungsten', 4 => 'Fluorescent', 5 => 'Flash', 6 => 'Custom' }, 9 => 'SequenceNumber', 14 => 'AFPointUsed', 15 => { __TAG__ => 'FlashBias', 0xFFC0 => '-2 EV', 0xFFCC => '-1.67 EV', 0xFFD0 => '-1.50 EV', 0xFFD4 => '-1.33 EV', 0xFFE0 => '-1 EV', 0xFFEC => '-0.67 EV', 0xFFF0 => '-0.50 EV', 0xFFF4 => '-0.33 EV', 0x0000 => '0 EV', 0x000C => '0.33 EV', 0x0010 => '0.50 EV', 0x0014 => '0.67 EV', 0x0020 => '1 EV', 0x002C => '1.33 EV', 0x0030 => '1.50 EV', 0x0034 => '1.67 EV', 0x0040 => '2 EV', }, 19 => 'SubjectDistance' ); my %canon_tags = ( 0x0001 => { __TAG__ => "Custom_0x0001", __ARRAYOFFSET__ => \%canon_0x0001_tags }, 0x0004 => { __TAG__ => "Custom_0x0004", __ARRAYOFFSET__ => \%canon_0x0004_tags }, 0x0006 => "ImageType", 0x0007 => "FirmwareVersion", 0x0008 => "ImageNumber", 0x0009 => "OwnerName", 0x000c => "SerialNumber", ); # see http://www.compton.nu/panasonic.html my %panasonic_tags = ( 0x0001 => { __TAG__ => "ImageQuality", 2 => 'High', 3 => 'Normal', 6 => 'Very High', #3 (Leica) 7 => 'Raw', #3 (Leica) }, 0x0002 => "FirmwareVersion", 0x0003 => { __TAG__ => "WhiteBalance", 1 => 'Auto', 2 => 'Daylight', 3 => 'Cloudy', 4 => 'Halogen', 5 => 'Manual', 8 => 'Flash', 10 => 'Black & White', #3 (Leica) }, 0x0007 => { __TAG__ => "FocusMode", 1 => 'Auto', 2 => 'Manual', 5 => 'Auto, Continuous', 4 => 'Auto, Focus button', }, 0x000f => { __TAG__ => "SpotMode", # XXX TODO: does not decode properly "0,1" => 'On', "0,16" => 'Off', }, 0x001a => { __TAG__ => "ImageStabilizer", 2 => 'On, Mode 1', 3 => 'Off', 4 => 'On, Mode 2', }, 0x001c => { __TAG__ => "MacroMode", 1 => 'On', 2 => 'Off', }, 0x001f => { __TAG__ => "ShootingMode", 1 => 'Normal', 2 => 'Portrait', 3 => 'Scenery', 4 => 'Sports', 5 => 'Night Portrait', 6 => 'Program', 7 => 'Aperture Priority', 8 => 'Shutter Priority', 9 => 'Macro', 11 => 'Manual', 13 => 'Panning', 18 => 'Fireworks', 19 => 'Party', 20 => 'Snow', 21 => 'Night Scenery', }, 0x0020 => { __TAG__ => "Audio", 1 => 'Yes', 2 => 'No', }, 0x0021 => "DataDump", 0x0022 => "Panasonic 0x0022", 0x0023 => "WhiteBalanceBias", 0x0024 => "FlashBias", 0x0025 => "SerialNumber", 0x0026 => "Panasonic 0x0026", 0x0027 => "Panasonic 0x0027", 0x0028 => { __TAG__ => "ColorEffect", 1 => 'Off', 2 => 'Warm', 3 => 'Cool', 4 => 'Black & White', 5 => 'Sepia', }, 0x0029 => "Panasonic 0x0029", 0x002a => { __TAG__ => "BurstMode", 0 => 'Off', 1 => 'Low/High Quality', 2 => 'Infinite', }, 0x002b => "ImageSequenceNumber", 0x002c => { __TAG__ => "Contrast", 0 => 'Normal', 1 => 'Low', 2 => 'High', 0x100 => 'Low', # Leica 0x110 => 'Normal', # Leica 0x120 => 'High', # Leica }, 0x002d => { __TAG__ => "NoiseReduction", 0 => 'Standard', 1 => 'Low', 2 => 'High', }, 0x002e => { __TAG__ => "SelfTimer", 1 => 'Off', 2 => '10s', 3 => '2s', }, 0x002f => "Panasonic 0x002f", 0x0030 => "Panasonic 0x0030", 0x0031 => "Panasonic 0x0031", 0x0032 => "Panasonic 0x0032", ); # format: # "Make Model" => [ Offset, 'Tag_prefix', ptr to tags ] # Offset is either 0, or a positive number of Bytes. # Offset -1 or -2 means a kludge for Fuji or Nikon my %makernotes = ( "NIKON CORPORATION NIKON D1" => [0, 'NikonD1', \%nikon2_tags], "NIKON CORPORATION NIKON D70" => [-2, 'NikonD1', \%nikon2_tags], "NIKON CORPORATION NIKON D100" => [-2, 'NikonD1', \%nikon2_tags], # For the following manufacturers we simple discard the model and always # decode the MakerNote in the same manner. This makes it work with all # models, even yet unreleased ones. (That's better than the very limited # list of models we previously had) "CANON" => [0, 'Canon', \%canon_tags], 'PANASONIC' => [12, 'Panasonic', \%panasonic_tags], "FUJIFILM" => [-1, 'FinePix', \%fujifilm_tags], "CASIO" => [0, 'Casio', \%casio_tags], "OLYMPUS" => [8, 'Olympus', \%olympus_tags], ); BEGIN { # add some Nikon cameras for my $model (qw/E700 E800 E900 E900S E910 E950/) { $makernotes{'NIKON ' . $model} = [8, 'CoolPix', \%nikon1_tags]; } for my $model (qw/E880 E990 E995/) { $makernotes{'NIKON ' . $model} = [0, 'CoolPix', \%nikon2_tags]; } } my %exif_intr_tags = ( 0x1 => "InteroperabilityIndex", 0x2 => "InteroperabilityVersion", 0x1000 => "RelatedImageFileFormat", 0x1001 => "RelatedImageWidth", 0x1002 => "RelatedImageLength", ); # Tag decode helpers sub components_configuration_decoder; sub file_source_decoder; sub scene_type_decoder; my %exif_tags = ( 0x828D => "CFARepeatPatternDim", 0x828E => "CFAPattern", 0x828F => "BatteryLevel", 0x8298 => "Copyright", 0x829A => "ExposureTime", 0x829D => "FNumber", 0x83BB => "IPTC/NAA", 0x8769 => "ExifOffset", 0x8773 => "InterColorProfile", 0x8822 => { __TAG__ => "ExposureProgram", 0 => "unknown", 1 => "Manual", 2 => "Program", 3 => "Aperture priority", 4 => "Shutter priority", 5 => "Program creative", 6 => "Program action", 7 => "Portrait", 8 => "Landscape", # 9 .. 255 reserved }, 0x8824 => "SpectralSensitivity", 0x8827 => "ISOSpeedRatings", 0x8828 => "OECF", 0x9000 => "ExifVersion", 0x9003 => "DateTimeOriginal", 0x9004 => "DateTimeDigitized", 0x9101 => { __TAG__ => "ComponentsConfiguration", DECODER => \&components_configuration_decoder, }, 0x9102 => "CompressedBitsPerPixel", 0x9201 => "ShutterSpeedValue", 0x9202 => "ApertureValue", 0x9203 => "BrightnessValue", 0x9204 => "ExposureBiasValue", 0x9205 => "MaxApertureValue", 0x9206 => "SubjectDistance", 0x9207 => { __TAG__ => "MeteringMode", 0 => "unknown", 1 => "Average", 2 => "CenterWeightedAverage", 3 => "Spot", 4 => "MultiSpot", 5 => "Pattern", 6 => "Partial", # 7 .. 254 reserved in EXIF 1.2 255 => "other", }, 0x9208 => { __TAG__ => "LightSource", 0 => "unknown", 1 => "Daylight", 2 => "Fluorescent", 3 => "Tungsten", 4 => "Flash", # 5 .. 8 reserveed in EXIF 2.2 9 => "Fine weather", 10 => "Cloudy weather", 11 => "Shade", 12 => "Daylight fluorescent (D 5700-7100K)", 13 => "Day white fluorescent (N 4600-5400K)", 14 => "Cool white fluorescent (W 3900-4500K)", 15 => "White fluorescent (WW 3200-3700K)", 17 => "Standard light A", 18 => "Standard light B", 19 => "Standard light C", 20 => "D55", 21 => "D65", 22 => "D75", 23 => "D50", 24 => "ISO studio tungesten", # 25 .. 254 reserved in EXIF 2.2 255 => "other light source", }, 0x9209 => { __TAG__ => "Flash", 0x0000 => "Flash did not fire", 0x0001 => "Flash fired", 0x0005 => "Strobe return light not detected", 0x0007 => "Strobe return light detected", 0x0009 => "Flash fired, compulsory flash mode", 0x000D => "Flash fired, compulsory flash mode, return light not detected", 0x000F => "Flash fired, compulsory flash mode, return light detected", 0x0010 => "Flash did not fire, compulsory flash mode", 0x0018 => "Flash did not fire, auto mode", 0x0019 => "Flash fired, auto mode", 0x001D => "Flash fired, auto mode, return light not detected", 0x001F => "Flash fired, auto mode, return light detected", 0x0020 => "No flash function", 0x0041 => "Flash fired, red-eye reduction mode", 0x0045 => "Flash fired, red-eye reduction mode, return light not detected", 0x0047 => "Flash fired, red-eye reduction mode, return light detected", 0x0049 => "Flash fired, compulsory flash mode, red-eye reduction mode", 0x004D => "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 0x004F => "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 0x0059 => "Flash fired, auto mode, red-eye reduction mode", 0x005D => "Flash fired, auto mode, return light not detected, red-eye reduction mode", 0x005F => "Flash fired, auto mode, return light detected, red-eye reduction mode" }, 0x920A => "FocalLength", 0x9214 => "SubjectArea", 0x927C => "MakerNote", 0x9286 => "UserComment", 0x9290 => "SubSecTime", 0x9291 => "SubSecTimeOriginal", 0x9292 => "SubSecTimeDigitized", 0xA000 => "FlashPixVersion", 0xA001 => "ColorSpace", 0xA002 => "ExifImageWidth", 0xA003 => "ExifImageLength", 0xA004 => "RelatedAudioFile", 0xA005 => {__TAG__ => "InteroperabilityOffset", __SUBIFD__ => \%exif_intr_tags, }, 0xA20B => "FlashEnergy", # 0x920B in TIFF/EP 0xA20C => "SpatialFrequencyResponse", # 0x920C - - 0xA20E => "FocalPlaneXResolution", # 0x920E - - 0xA20F => "FocalPlaneYResolution", # 0x920F - - 0xA210 => { __TAG__ => "FocalPlaneResolutionUnit", # 0x9210 - - 1 => "pixels", 2 => "dpi", 3 => "dpcm", }, 0xA214 => "SubjectLocation", # 0x9214 - - 0xA215 => "ExposureIndex", # 0x9215 - - 0xA217 => {__TAG__ => "SensingMethod", 1 => "Not defined", 2 => "One-chip color area sensor", 3 => "Two-chip color area sensor", 4 => "Three-chip color area sensor", 5 => "Color sequential area sensor", 7 => "Trilinear sensor", 8 => "Color sequential linear sensor" }, 0xA300 => {__TAG__ => "FileSource", DECODER => \&file_source_decoder, }, 0xA301 => {__TAG__ => "SceneType", DECODER => \&scene_type_decoder, }, 0xA302 => "CFAPattern", 0xA401 => {__TAG__ => "CustomRendered", 0 => "Normal process", 1 => "Custom process" }, 0xA402 => {__TAG__ => "ExposureMode", 0 => "Auto exposure", 1 => "Manual exposure", 2 => "Auto bracket" }, 0xA403 => {__TAG__ => "WhiteBalance", 0 => "Auto white balance", 1 => "Manual white balance" }, 0xA404 => "DigitalZoomRatio", 0xA405 => "FocalLengthIn35mmFilm", 0xA406 => {__TAG__ => "SceneCaptureType", 0 => "Standard", 1 => "Landscape", 2 => "Portrait", 3 => "Night Scene" }, 0xA407 => {__TAG__ => "GainControl", 0 => "None", 1 => "Low gain up", 2 => "High gain up", 3 => "Low gain down", 4 => "High gain down" }, 0xA408 => {__TAG__ => "Contrast", 0 => "Normal", 1 => "Soft", 2 => "Hard" }, 0xA409 => {__TAG__ => "Saturation", 0 => "Normal", 1 => "Low saturation", 2 => "High saturation" }, 0xA40A => {__TAG__ => "Sharpness", 0 => "Normal", 1 => "Soft", 2 => "Hard" }, 0xA40B => "DeviceSettingDescription", 0xA40C => {__TAG__ => "SubjectDistanceRange", 0 => "Unknown", 1 => "Macro", 2 => "Close view", 3 => "Distant view" }, 0xA420 => "ImageUniqueID", ); my %gps_tags = ( 0x0000 => 'GPSVersionID', 0x0001 => 'GPSLatitudeRef', 0x0002 => 'GPSLatitude', 0x0003 => 'GPSLongitudeRef', 0x0004 => 'GPSLongitude', 0x0005 => 'GPSAltitudeRef', 0x0006 => 'GPSAltitude', 0x0007 => 'GPSTimeStamp', 0x0008 => 'GPSSatellites', 0x0009 => 'GPSStatus', 0x000A => 'GPSMeasureMode', 0x000B => 'GPSDOP', 0x000C => 'GPSSpeedRef', 0x000D => 'GPSSpeed', 0x000E => 'GPSTrackRef', 0x000F => 'GPSTrack', 0x0010 => 'GPSImgDirectionRef', 0x0011 => 'GPSImgDirection', 0x0012 => 'GPSMapDatum', 0x0013 => 'GPSDestLatitudeRef', 0x0014 => 'GPSDestLatitude', 0x0015 => 'GPSDestLongitudeRef', 0x0016 => 'GPSDestLongitude', 0x0017 => 'GPSDestBearingRef', 0x0018 => 'GPSDestBearing', 0x0019 => 'GPSDestDistanceRef', 0x001A => 'GPSDestDistance', 0x001B => 'GPSProcessingMethod', 0x001C => 'GPSAreaInformation', 0x001D => 'GPSDateStamp', 0x001E => 'GPSDifferential', ); my %tiff_tags = ( 254 => { __TAG__ => "NewSubfileType", 1 => "ReducedResolution", 2 => "SinglePage", 4 => "TransparencyMask", }, 255 => { __TAG__ => "SubfileType", 1 => "FullResolution", 2 => "ReducedResolution", 3 => "SinglePage", }, 256 => "width", 257 => "height", 258 => "BitsPerSample", 259 => { __TAG__ => "Compression", 1 => "PackBytes", 2 => "CCITT Group3", 3 => "CCITT T4", 4 => "CCITT T6", 5 => "LZW", 6 => "JPEG", 7 => "JPEG DCT", 8 => "Deflate (Adobe)", 32766 => "NeXT 2-bit RLE", 32771 => "#1 w/ word alignment", 32773 => "PackBits (Macintosh RLE)", 32809 => "ThunderScan RLE", 32895 => "IT8 CT w/padding", 32896 => "IT8 Linework RLE", 32897 => "IT8 Monochrome picture", 32898 => "IT8 Binary line art", 32908 => "Pixar companded 10bit LZW", 32909 => "Pixar companded 11bit ZIP", 32946 => "Deflate", }, 262 => { __TAG__ => "PhotometricInterpretation", 0 => "WhiteIsZero", 1 => "BlackIsZero", 2 => "RGB", 3 => "RGB Palette", 4 => "Transparency Mask", 5 => "CMYK", 6 => "YCbCr", 8 => "CIELab", }, 263 => { __TAG__ => "Threshholding", 1 => "NoDithering", 2 => "OrderedDither", 3 => "Randomized", }, 266 => { __TAG__ => "FillOrder", 1 => "LowInHigh", 2 => "HighInLow", }, 269 => "DocumentName", 270 => "ImageDescription", 271 => "Make", 272 => "Model", 273 => "StripOffsets", 274 => { __TAG__ => "Orientation", 1 => "top_left", 2 => "top_right", 3 => "bot_right", 4 => "bot_left", 5 => "left_top", 6 => "right_top", 7 => "right_bot", 8 => "left_bot", }, 277 => "SamplesPerPixel", 278 => "RowsPerStrip", 279 => "StripByteCounts", 282 => "XResolution", 283 => "YResolution", 284 => {__TAG__ => "PlanarConfiguration", 1 => "Chunky", 2 => "Planar", }, 296 => {__TAG__ => "ResolutionUnit", 1 => "pixels", 2 => "dpi", 3 => "dpcm", }, 297 => "PageNumber", 301 => "TransferFunction", 305 => "Software", 306 => "DateTime", 315 => "Artist", 316 => "Host", 318 => "WhitePoint", 319 => "PrimaryChromaticities", 320 => "ColorMap", 513 => "JPEGInterchangeFormat", 514 => "JPEGInterchangeFormatLength", 529 => "YCbCrCoefficients", 530 => "YCbCrSubSampling", 531 => "YCbCrPositioning", 532 => "ReferenceBlackWhite", 33432 => "Copyright", 34665 => { __TAG__ => "ExifOffset", __SUBIFD__ => \%exif_tags, }, 34853 => { __TAG__ => "GPSInfo", __SUBIFD__ => \%gps_tags, }, ); sub new { my $class = shift; my $source = shift; if (!ref($source)) { local(*F); open(F, $source) || return; binmode(F); $source = \*F; } if (ref($source) ne "SCALAR") { # XXX should really only read the file on demand local($/); # slurp mode my $data = <$source>; $source = \$data; } my $self = bless { source => $source }, $class; for ($$source) { my $byte_order = substr($_, 0, 2); $self->{little_endian} = ($byte_order eq "II"); $self->{version} = $self->unpack("n", substr($_, 2, 2)); my $ifd = $self->unpack("N", substr($_, 4, 4)); while ($ifd) { push(@{$self->{ifd}}, $ifd); my($num_fields) = $self->unpack("x$ifd n", $_); my $next_ifd = $self->unpack("N", substr($_, $ifd + 2 + $num_fields*12, 4)); # guard against (bug #26127) $next_ifd = 0 if $next_ifd > length($_); # guard against looping ifd (bug #26130) if ($next_ifd <= $ifd) { # bad TIFF header - would cause a loop or strange results last; } $ifd = $next_ifd; } } $self; } sub unpack { my $self = shift; my $template = shift; if ($self->{little_endian}) { $template =~ tr/nN/vV/; } #print "UNPACK $template\n"; CORE::unpack($template, $_[0]); } sub num_ifds { my $self = shift; scalar @{$self->{ifd}}; } sub ifd { my $self = shift; my $num = shift || 0; my @ifd; $self->add_fields($self->{ifd}[$num], \@ifd); } sub tagname { $tiff_tags{$_[1]} || sprintf "Tag-0x%04x",$_[1]; } sub exif_tagname { $tiff_tags{$_[1]} || $exif_tags{$_[1]} || sprintf "Tag-0x%04x",$_[1]; } sub add_fields { my($self, $offset, $ifds, $tags, $voff_plus) = @_; return unless $offset; # guard against finding the same offset twice (bug #29088) return if $self->{seen_offset}{$offset}++; $tags ||= \%tiff_tags; for (${$self->{source}}) { # alias as $_ last if $offset > length($_) - 2; # bad offset my $entries = $self->unpack("x$offset n", $_); my $max_entries = int((length($_) - $offset - 2) / 12); # print "ENTRIES $entries $max_entries\n"; if ($entries > $max_entries) { # Hmmm, something smells bad here... parsing garbage $entries = $max_entries; last; } FIELD: for my $i (0 .. $entries-1) { my $entry_offset = 2 + $offset + $i*12; my($tag, $type, $count, $voff) = $self->unpack("nnNN", substr($_, $entry_offset, 12)); #print STDERR "TAG $tag $type $count $voff\n"; if ($type == 0 || $type > @types) { # unknown type code might indicate that we are parsing garbage print STDERR "# Ignoring unknown type code $type\n"; next; } # extract type information my($tmpl, $vlen); ($type, $tmpl, $vlen) = @{$types[$type-1]}; die "Undefined type while parsing" unless $type; if ($count * $vlen <= 4) { $voff = $entry_offset + 8; } elsif ($voff + $count * $vlen > length($_)) { # offset points outside of string, corrupt entry ignore print STDERR "# ignoring offset pointer outside of string\n"; next; } else { $voff += $voff_plus || 0; } $tmpl =~ s/(\d+)$/$count*$1/e; my @v = $self->unpack("x$voff $tmpl", $_); if ($type =~ /^S(SHORT|LONG|RATIONAL)/) { my $max = 2 ** ($1 eq "SHORT" ? 15 : 31); $v[0] -= ($max * 2) if $v[0] >= $max; } my $val = (@v > 1) ? \@v : $v[0]; bless $val, "Image::TIFF::Rational" if $type =~ /^S?RATIONAL$/; if ($type eq 'ASCII' || $type eq 'UNDEFINED') { if ($val =~ /\0[^\0]+\z/) { # cut out the first "ASCII\0" and take the remaining text # (fix bug #29243 parsing of UserComment) # XXX TODO: this needs to handle UNICODE, too: $val =~ /\0([^\0]+)/; $val = '' . ($1 || ''); } else { # avoid things like "Foo\x00\x00\x00" by removing trailing nulls # this needs to handle UNICODE, too: $val =~ /^([^\0]*)/; $val = '' . ($1 || ''); } } $tag = $tags->{$tag} || $self->tagname($tag); if ($tag eq 'MakerNote') { my $maker = uc($self->{Make} || ''); $maker =~ /^([A-Z]+)/; $maker = $1 || ''; # "OLYMPUS ..." > "OLYMPUS" # if 'Panasonic' doesn't exist, try 'Panasonic DMC-FZ5' $maker = $self->{Make}.' '.$self->{Model} unless exists $makernotes{$maker}; if (exists $makernotes{$maker}) { my ($ifd_off, $tag_prefix, $sub) = @{$makernotes{$maker}}; #print STDERR "# Decoding Makernotes from $maker\n"; $self->{tag_prefix} = $tag_prefix; if ($ifd_off == -1 && length($val) >= 12) { # fuji kludge - http://www.butaman.ne.jp/~tsuruzoh/Computer/Digicams/exif-e.html#APP4 my $save_endian = $self->{little_endian}; $self->{little_endian} = 1; $ifd_off = $self->unpack("N", substr($val, 8, 4)); $self->add_fields($voff+$ifd_off, $ifds, $sub, $voff); $self->{little_endian} = $save_endian; } elsif ($ifd_off == -2) { # Nikon D70/D100 kludge -- word "Nikon" and 5 # bytes of data is tacked to the front of MakerNote; # all EXIF offsets are relative to MakerNote section my ($nikon_voff); $nikon_voff = 0; if (substr($val, 0, 5) eq 'Nikon') { $nikon_voff = $voff+10; } #print "IFD_OFF $ifd_off NIKON_VOFF $nikon_voff\n"; $self->add_fields($voff+18, $ifds, $sub, $nikon_voff); } else { $self->add_fields($voff+$ifd_off, $ifds, $sub) } delete $self->{tag_prefix}; next FIELD; } } if (ref($tag)) { die "Assert" unless ref($tag) eq "HASH"; if (my $sub = $tag->{__SUBIFD__}) { next if $val < 0 || $val > length($_); #print "SUBIFD $tag->{__TAG__} $val ", length($_), "\n"; $self->add_fields($val, $ifds, $sub); next FIELD; } if (my $sub = $tag->{__ARRAYOFFSET__}) { my $prefix = $tag = $self->{tag_prefix} . '-' if $self->{tag_prefix}; for (my $i=0; $i < @$val; $i++) { if ( exists($sub->{$i}) ) { if ( ref($sub->{$i}) eq "HASH" && exists($sub->{$i}->{__TAG__}) ) { if ( exists($sub->{$i}->{$val->[$i]}) ) { push @$ifds, [ $prefix . $sub->{$i}->{__TAG__}, $type, $count, $sub->{$i}->{$val->[$i]} ]; } else { push @$ifds, [ $prefix . $sub->{$i}->{__TAG__}, $type, $count, "Unknown (" . $val->[$i] . ")" ]; } } else { push @$ifds, [ $prefix . $sub->{$i}, $type, $count, $val->[$i] ]; } } } next FIELD; } #hack for UNDEFINED values, they all have different #meanings depending on tag $val = &{$tag->{DECODER}}($self,$val) if defined($tag->{DECODER}); $val = $tag->{$val} if exists $tag->{$val}; $tag = $tag->{__TAG__}; } $tag = $self->{tag_prefix} . '-' . $tag if $self->{tag_prefix}; #if ( $val =~ m/ARRAY/ ) { $val = join(', ',@$val); } push @$ifds, [ $tag, $type, $count, $val ]; $self->{$tag} = $val if ($tag eq 'Make' or $tag eq 'Model'); } } $ifds; } sub components_configuration_decoder { my $self = shift; my $val = shift; my $rv = ""; my %v = ( 0 => undef, 1 => 'Y', 2 => 'Cb', 3 => 'Cr', 4 => 'R', 5 => 'G', 6 => 'B', ); return join ( '', map { $v{$_} if defined($v{$_}) } $self->unpack('c4',$val) ); } sub file_source_decoder { my $self = shift; my $val = shift; my %v = ( 3 => "(DSC) Digital Still Camera", ); $val = $self->unpack('c',$val); return $v{$val} if $v{$val}; "Other"; } sub scene_type_decoder { my $self = shift; my $val = shift; my %v = ( 1 => "Directly Photographed Image", ); $val = $self->unpack('c',$val); return $v{$val} if $v{$val}; "Other"; } package Image::TIFF::Rational; use overload '""' => \&as_string, '0+' => \&as_float, fallback => 1; sub new { my($class, $a, $b) = @_; bless [$a, $b], $class; } sub as_string { my $self = shift; #warn "@$self"; "$self->[0]/$self->[1]"; } sub as_float { my $self = shift; # We check here because some stupid cameras (Samsung Digimax 200) # use rationals with 0 denominator (found in thumbnail resolution spec). if ($self->[1]) { return $self->[0] / $self->[1]; } else { return $self->[0]; } } 1;