ljr/wcmtools/lib/Danga-EXIF/EXIF.pm

1101 lines
29 KiB
Perl
Executable File

#!/usr/bin/perl
#
# Danga::EXIF - An all-perl EXIF extraction library
# Brad Whitaker (whitaker@danga.com) - Danga Interactive
#
# TODO:
# - MakerNotes/UserNotes
# - GPS Tags
# - Handle more SubIFD/SubSubIFDs
# - fix bugs?
package Danga::EXIF;
use strict;
use Carp;
use IO::File;
# print debugging information?
use constant DEBUG => 0;
use constant MARK_SOI => "\xFF\xD8"; # start of image
use constant MARK_EOI => "\xFF\xD9"; # end of image
use constant MARK_SOS => "\xFF\xDA"; # start of stream
use constant MARK_APP0_JFIF => "\xFF\xE0"; # App0 (JFIF) marker
use constant MARK_APP1_EXIF => "\xFF\xE1"; # App1 (EXIF) marker
use constant EXIF_HEADER => "Exif\x00\x00"; # EXIF header
use constant EXIF_SUBIFD => 0x8769; # EXIF SubIFD Tag (numeric)
use constant TIFF_HEADER => 0x002A; # TIFF header marker
use constant ORDER_INTEL => "II"; # Intel (little-endian) byte-order
use constant ORDER_MOTOROLA => "MM"; # Motorola (big-endian) byte-order
use fields qw(byte_order src filename buff offset tags);
sub debug { print STDERR "$_[0]\n" if DEBUG; }
sub new {
my Danga::EXIF $self = shift;
$self = fields::new($self) unless ref $self;
my %args = @_;
$self->{byte_order} = undef;
$self->{src} = $args{src};
$self->{filename} = undef;
$self->{buff} = undef;
$self->{offset} = 0;
$self->{tags} = [];
# this is most of the work
$self->process_file;
return $self;
}
sub open_src
{
my Danga::EXIF $self = shift;
my $filename = shift;
return length(${$self->{src}}) if ref $self->{src} eq 'SCALAR';
return -s $self->{src} if ref $self->{src};
# it's probably a scalar, open the file
if ($self->{filename} = $self->{src}) {
debug("open_src: file: $self->{filename}");
$self->{src} = new IO::File $self->{filename}
or croak "open_src: couldn't open file $self->{filename}: $!";
return -s $self->{src};
}
return undef;
}
sub close_src
{
my Danga::EXIF $self = shift;
# close filehandle if we were the ones who opened it
return close $self->{src}
if $self->{filename} && ref $self->{src} eq 'IO::Handle';
return 1;
}
# read a specified number of bytes from $self->{src}
sub read_src
{
my Danga::EXIF $self = shift;
my $len = shift;
# src is scalarref
if (ref $self->{src} eq 'SCALAR') {
return substr(${$self->{src}}, 0, $len, '');
}
# src is probably filehandle
my $n = read($self->{src}, $self->{buff}, $len);
die "read failed: $!" unless defined $n;
die "short read ($len/$n)" unless $n == $len;
return $self->{buff};
}
# read the next n bytes from $self->{buff} and increment offset
sub read_buff
{
my Danga::EXIF $self = shift;
my $len = shift;
my $rval = substr($self->{buff}, $self->{offset}, $len);
$self->seek_buff($len);
return $rval;
}
# move the offset pointer n bytes forward
sub seek_buff
{
my Danga::EXIF $self = shift;
return $self->{offset} += $_[0];
}
# return n bytes from $self->{buff} starting at a given offset
sub peek_buff
{
my Danga::EXIF $self = shift;
my ($offset, $len) = @_;
return substr($self->{buff}, $offset, $len);
}
# unpacking mechanism which respects byte ordering
sub unpack
{
my Danga::EXIF $self = shift;
my ($tmpl, $data) = @_;
croak "undefined byte order"
unless defined $self->{byte_order};
# flip order to little endian if necessary
$tmpl =~ tr/vV/nN/
unless $self->{byte_order} eq ORDER_INTEL;
return CORE::unpack($tmpl, $data);
}
sub process_file
{
my Danga::EXIF $self = shift;
my $filesize = $self->open_src
or croak "Couldn't open source file";
debug("filesize: $filesize");
debug("src: $self->{src}");
my $pos = 0;
my $do_read = sub {
my $len = shift;
debug("received len: $len");
if ($pos + $len > $filesize) {
$len = $filesize - $pos;
debug("adjusing len: $len");
}
$pos += $len;
debug("do_read, $pos > $filesize?");
debug("yes, done") if $pos > $filesize;
#last if $pos > $filesize;
my $buf = $self->read_src($len);
debug("nope, buf=" . length($buf));
return $buf;
};
# need an SOI to make sure this file is valid
my $soi = $self->read_src(2);
$pos += 2;
croak "Error reading EXIF header: SOI missing"
unless $soi eq MARK_SOI;
# read until end of header
while ($pos + 4 <= $filesize) {
debug("in loop");
my ($mark, $len) = CORE::unpack("a2v", $do_read->(4));
debug("mark=" . CORE::unpack("H4", $mark) . ", len=" . length($self->{buff}));
last if $mark eq MARK_EOI || $mark eq MARK_SOS;
last if $len < 2;
# length contains the 2-byte data length descriptor,
# so strip that to know how much to actually read
$self->{buff} = $do_read->($len - 2);
if ($mark eq MARK_APP0_JFIF) {
debug("JFIF marker");
# TODO: get info from here?
next;
}
if ($mark eq MARK_APP1_EXIF) {
debug("EXIF marker");
$self->process_app1_exif;
last;
}
# don't care about other markers
}
$self->close_src;
return 1;
}
sub process_app1_exif
{
my Danga::EXIF $self = shift;
my $hdr = substr($self->{buff}, 0, 6, '');
croak "invalid exif header"
unless $hdr eq EXIF_HEADER;
# Offset 0 is now beginning of TIFF header
# determine byte order
$self->{byte_order} = $self->read_buff(2);
debug("byte order: $self->{byte_order}");
croak "process_app1_exif: unknown byte order"
unless ($self->{byte_order} eq ORDER_INTEL ||
$self->{byte_order} eq ORDER_MOTOROLA);
croak "process_app1_exif: invalid TIFF header"
unless $self->unpack("v", $self->read_buff(2)) == TIFF_HEADER;
my $ifd_offset = $self->unpack("V", $self->read_buff(4));
# subtract 8 to account for the 8 bytes of tiff header
# already read, but included in the offset
$ifd_offset -= 8;
# if the first ifd starts at an offset, skip to there
# and throw away anything in between.
debug("ifd offset: $ifd_offset");
if ($ifd_offset > 0) {
debug("seeking");
$self->seek_buff($ifd_offset);
}
$self->process_ifds;
}
sub process_ifds
{
my Danga::EXIF $self = shift;
debug("processing ifd at offset: $self->{offset}");
my $ifd_size = $self->unpack("v", $self->read_buff(2))
or croak "process_ifds: empty ifd";
debug("ifd_size: $ifd_size");
foreach (1..$ifd_size) {
my ($tagid, $type, $count) =
$self->unpack("vvV", $self->read_buff(8));
# next 4 bytes are either a value or an offset,
# read them raw at first until we know how they
# should be interpretted
my $val = $self->read_buff(4);
# if the tagid is the EXIF_SUBIFD tag, then the value
# is a pointer to that IFD, which needs processing
if ($tagid == EXIF_SUBIFD) {
# FIXME: ghetto, do something smarter
my $save_offset = $self->{offset};
$self->{offset} = $self->unpack("V", $val);
$self->process_ifds;
$self->{offset} = $save_offset;
next;
}
my $typeinf = Danga::EXIF::get_type_info($type);
my $len = $count * $typeinf->{bytes};
# if length is supposed to be less than 4 bytes, it'll be
# left-aligned inside the value field
if ($len < 4) {
$val = substr($val, 0, $len);
# if length is more than 4 bytes, then val is an offset
# to the real value
} elsif ($len > 4) {
$val = $self->peek_buff($self->unpack("V", $val), $len);
}
# apply template if defined
my $template = $typeinf->{template} || "a*";
$template x= $count unless index($template, '*') >= 0;
my @val = $self->unpack($template, $val);
# register this tag
if (Danga::EXIF::is_known_tag($tagid)) {
push @{$self->{tags}}, Danga::EXIF::Tag->new
( tagid => $tagid, type => $type, value => \@val );
}
}
}
sub tags
{
my Danga::EXIF $self = shift;
# array context returns array of tags
return @{$self->{tags}} if wantarray;
# scalar context returns hashref of tag => value
return { map { $_->tag => $_->value } grep { $_->value } @{$self->{tags}} };
}
sub is_known_tag
{
my $tagid = shift;
return exists $Danga::EXIF::TAG_INFO{$tagid} ? 1 : 0;
}
sub get_tag_info
{
my $tagid = shift;
return $Danga::EXIF::TAG_INFO{$tagid} || {
tag => $tagid,
name => "Tag-" . sprintf("%4x", $tagid),
disp => "none",
};
}
sub get_type_info
{
return $Danga::EXIF::TYPE_INFO{$_[0]} || {};
}
###############################################################################
package Danga::EXIF::Tag;
use strict;
use Carp;
use fields qw(tagid type value);
*debug = *Danga::EXIF::debug;
sub new
{
my Danga::EXIF::Tag $self = shift;
$self = fields::new($self) unless ref $self;
my %args = @_;
$self->{tagid} = $args{tagid} or croak "no tagid";
$self->{type} = $args{type} or croak "no data type";;
$self->{value} = $args{value} or croak "no value";
return $self;
}
sub tag
{
my Danga::EXIF::Tag $self = shift;
return Danga::EXIF::get_tag_info($self->{tagid})->{tag};
}
sub name
{
my Danga::EXIF::Tag $self = shift;
return Danga::EXIF::get_tag_info($self->{tagid})->{name};
}
sub value
{
my Danga::EXIF::Tag $self = shift;
my $taginf = Danga::EXIF::get_tag_info($self->{tagid});
my $disp = $taginf->{disp} || 'literal';
if ($disp eq 'none') {
return ''; # MakerNote, UserComment (for now)
}
if (ref $disp eq 'HASH') {
my $key = join('', @{$self->{value}});
return $disp->{$key};
}
if (ref $disp eq 'CODE') {
return $disp->(@{$self->{value}});
}
my $typeinf = Danga::EXIF::get_type_info($self->{type});
if (my $literal = $typeinf->{literal}) {
return $literal->(@{$self->{value}});
}
# default literal behavior
return join('', @{$self->{value}});
}
###############################################################################
package Danga::EXIF;
use strict;
use vars qw(%TYPE_INFO %TAG_INFO);
%TYPE_INFO =
(
# An 8-bit unsigned integer.
0x0001 => {
type => "byte",
bytes => 1,
template => "a",
},
# An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
0x0002 => {
type => "ascii",
bytes => 1,
template => "A*",
},
# A 16-bit (2-byte) unsigned integer.
0x0003 => {
type => "short",
bytes => 2,
template => "v",
},
# A 32-bit (4-byte) unsigned integer.
0x0004 => {
type => "long",
bytes => 4,
template => "V",
},
# Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator.
0x0005 => {
type => "rational",
bytes => 8,
template => "VV",
literal => sub { join("/", map { $_+0 } @_[0,1]) },
},
# An 8-bit byte that can take any value depending on the field definition.
0x0007 => {
type => "undefined",
bytes => 1,
template => "a*",
},
# A 32-bit (4-byte) signed integer (2's complement notation).
0x0009 => {
type => "slong",
bytes => 4,
template => "l",
},
# Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
0x000a => {
type => "srational",
bytes => 8,
template => "ll",
literal => sub { join("/", map { $_+0 } @_[0,1]) },
},
);
#
# TagID => { tag => "TagName",
# name => "Display name",
# disp => literal|none|hashref|subref,
# }
%TAG_INFO =
(
0x927c => {
tag => "MakerNote",
name => "Manufacturer Notes",
disp => "none",
},
0x9286 => {
tag => "UserComment",
name => "User comments",
disp => "none",
},
# FILE INFO
0x0100 => {
tag => "ImageWidth",
name => "Image width",
disp => "literal",
},
0x0101 => {
tag => "ImageLength",
name => "Image height",
disp => "literal",
},
0x0102 => {
tag => "BitsPerSample",
name => "Number of bits per component",
disp => "literal",
},
0x0103 => {
tag => "Compression",
name => "Compression scheme",
disp => {
1 => "Uncompressed",
6 => "JPEG compression (thumbnails only)",
},
},
0x0106 => {
tag => "PhotometricInterpretation",
name => "Pixel composition",
disp => {
2 => "RGB",
6 => "YCbCr",
},
},
0x0112 => {
tag => "Orientation",
name => "Orientation of image",
disp => {
1 => "The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side",
2 => "The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side",
3 => "The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side",
4 => "The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side",
5 => "The 0th row is the visual left-hand side of the image, and the 0th column is the visual top",
6 => "The 0th row is the visual right-hand side of the image, and the 0th column is the visual top",
7 => "The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom",
8 => "The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom",
},
},
0x0115 => {
tag => "SamplesPerPixel",
name => "Number of components",
disp => "literal",
},
0x011c => {
tag => "PlanarConfiguration",
name => "Image data arrangement",
disp => {
1 => "Chunky format",
2 => "Planar format",
},
},
0x0212 => {
tag => "YCbCrSubSampling",
name => "Subsampling ratio of Y to C",
disp => {
21 => "YCbCr4:2:2",
22 => "YCbCr4:2:0",
},
},
0x0213 => {
tag => "YCbCrPositioning",
name => "Y and C positioning",
disp => {
1 => "Centered",
2 => "Co-sited",
},
},
0x011a => {
tag => "XResolution",
name => "Image resolution in width direction",
disp => "literal",
},
0x011b => {
tag => "YResolution",
name => "Image resolution in height direction",
disp => "literal",
},
0x0128 => {
tag => "ResolutionUnit",
name => "Unit of X and Y resolution",
disp => {
1 => "Unspecified",
2 => "Pixels/Inch",
3 => "Pixels/Centimeter",
},
},
0x0111 => {
tag => "StripOffsets",
name => "Image data location",
disp => "literal",
},
0x0116 => {
tag => "RowsPerStrip",
name => "Number of rows per strip",
disp => "literal",
},
0x0117 => {
tag => "StripByteCounts",
name => "Bytes per compressed strip",
disp => "literal",
},
0x0201 => {
tag => "JPEGInterchangeFormat",
name => "Offset to JPEG SOI",
disp => "literal",
},
0x0202 => {
tag => "JPEGInterchangeFormatLength",
name => "Bytes of JPEG data",
disp => "literal",
},
0x012d => {
tag => "TransferFunction",
name => "Transfer function",
disp => "literal",
},
0x013e => {
tag => "WhitePoint",
name => "White point chromaticity",
disp => "literal",
},
0x013f => {
tag => "PrimaryChromaticities",
name => "Chromaticities of primaries",
disp => "literal",
},
0x0211 => {
tag => "YCbCrCoefficients",
name => "Color space transformation matrix coefficients",
disp => "literal",
},
0x0214 => {
tag => "ReferenceBlackWhite",
name => "Pair of black and white reference values",
disp => "literal",
},
0xa001 => {
tag => "ColorSpace",
name => "Color space information",
disp => {
1 => "sRGB",
65535 => "Uncalibrated",
},
},
0x9000 => {
tag => "ExifVersion",
name => "Exif version",
disp => "literal",
},
0xa000 => {
tag => "FlashpixVersion",
name => "Supported Flashpix version",
disp => {
0100 => "Flashpix Format Version 1.0",
},
},
0x0132 => {
tag => "DateTime",
name => "File change date and time",
disp => "literal",
},
0x010e => {
tag => "ImageDescription",
name => "Image title",
disp => "literal",
},
0x010f => {
tag => "Make",
name => "Image input equipment manufacturer",
disp => "literal",
},
0x0110 => {
tag => "Model",
name => "Image input equipment model",
disp => "literal",
},
0x0131 => {
tag => "Software",
name => "Software used",
disp => "literal",
},
0x013b => {
tag => "Artist",
name => "Person who created the image",
disp => "literal",
},
0x8298 => {
tag => "Copyright",
name => "Copyright holder",
disp => "literal",
},
0xa420 => {
tag => "ImageUniqueID",
name => "Unique image ID",
disp => "literal",
},
0x9101 => {
tag => "ComponentsConfiguration",
name => "Meaning of each component",
disp => sub {
my $val = shift;
my $map = {
1 => "Y",
2 => "Cb",
3 => "Cr",
4 => "R",
5 => "G",
6 => "B",
};
return join('', map { $map->{$_} } split('', $val));
},
},
0x9102 => {
tag => "CompressedBitsPerPixel",
name => "Image compression mode",
disp => "literal",
},
0xa002 => {
tag => "PixelXDimension",
name => "Valid image width",
disp => "literal",
},
0xa003 => {
tag => "PixelYDimension",
name => "Valid image height",
disp => "literal",
},
0xa004 => {
tag => "RelatedSoundFile",
name => "Related audio file",
disp => "literal",
},
0xa005 => {
tag => "ExifInteroperabilityOffset",
name => "Exif interoperability offset",
disp => "literal",
},
0x9003 => {
tag => "DateTimeOriginal",
name => "Date and time of original data generation",
disp => "literal",
},
0x9004 => {
tag => "DateTimeDigitized",
name => "Date and time of digital data generation",
disp => "literal",
},
0x9290 => {
tag => "SubSecTime",
name => "DateTime subseconds",
disp => "literal",
},
0x9291 => {
tag => "SubSecTimeOriginal",
name => "DateTimeOriginal subseconds",
disp => "literal",
},
0x9292 => {
tag => "SubSecTimeDigitized",
name => "DateTimeDigitized subseconds",
disp => "literal",
},
0x829a => {
tag => "ExposureTime",
name => "Exposure time",
disp => "literal",
},
0x829d => {
tag => "FNumber",
name => "FNumber",
disp => "literal",
},
0x8822 => {
tag => "ExposureProgram",
name => "Exposure program",
disp => {
1 => "Manual",
2 => "Normal program",
3 => "Aperture priority",
4 => "Shutter priority",
5 => "Creative program (biased toward depth of field)",
6 => "Action program (biased toward fast shutter speed)",
7 => "Portrait mode (for closeup photos with the background out of focus)",
8 => "Landscape mode (for landscape photos with the background in focus)",
},
},
0x8824 => {
tag => "SpectralSensitivity",
name => "Spectral sensitivity",
disp => "literal",
},
0x8827 => {
tag => "ISOSpeedRatings",
name => "ISO speed rating",
disp => "literal",
},
0x8828 => {
tag => "OECF",
name => "Optoelectric conversion factor",
disp => "literal",
},
0x9201 => {
tag => "ShutterSpeedValue",
name => "Shutter speed",
disp => "literal",
},
0x9202 => {
tag => "ApertureValue",
name => "Aperture",
disp => "literal",
},
0x9203 => {
tag => "BrightnessValue",
name => "Brightness",
disp => "literal",
},
0x9204 => {
tag => "ExposureBiasValue",
name => "Exposure bias",
disp => "literal",
},
0x9205 => {
tag => "MaxApertureValue",
name => "Maximum lens aperture",
disp => "literal",
},
0x9206 => {
tag => "SubjectDistance",
name => "Subject distance",
disp => "literal",
},
0x9207 => {
tag => "MeteringMode",
name => "Metering mode",
disp => {
1 => "Average",
2 => "CenterWeightedAverage",
3 => "Spot",
4 => "MultiSpot",
5 => "Pattern",
6 => "Partial",
255 => "Other",
},
},
0x9208 => {
tag => "LightSource",
name => "Light source",
disp => {
1 => "Daylight",
2 => "Fluorescent",
3 => "Tungsten (incandescent light)",
4 => "Flash",
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 tungsten",
255 => "Other light source",
},
},
0x9209 => {
tag => "Flash",
name => "Flash",
disp => sub {
my $val = shift;
my $bit = sub {
return $val & (1 << $_[0]);
};
# bit 0
my @ret = $bit->(0) ? ("Flash fired") : ("Flash did not fire");
# bits 1-2
if (! $bit->(1) && ! $bit->(2)) {
push @ret, "No strobe return detection function";
} elsif ($bit->(1)) {
push @ret, "Strobe return light " . ($bit->(2) ? "" : "not") . " detected";
}
# bits 3-4
if (! $bit->(3) && $bit->(4)) {
push @ret, "Compulsory flash firing";
} elsif ($bit->(3) && ! $bit->(4)) {
push @ret, "Compulsory flash suppression";
} elsif ($bit->(3) && $bit->(4)) {
push @ret, "Auto mode";
}
# bit 5
push @ret, ($bit->(5) ? "No flash function" : "Flash function present");
# bit 6
push @ret, ($bit->(6) ? "Red-eye reduction supported" : "No red-eye reduction mode or unknown");
return join("; ", @ret);
},
},
0x920a => {
tag => "FocalLength",
name => "Lens focal length",
disp => "literal",
},
0x9214 => {
tag => "SubjectArea",
name => "Subject area",
disp => "literal",
},
0xa20b => {
tag => "FlashEnergy",
name => "Flash energy",
disp => "literal",
},
0xa20c => {
tag => "SpatialFrequencyResponse",
name => "Spatial frequency response",
disp => "literal",
},
0xa20e => {
tag => "FocalPlaneXResolution",
name => "Focal plane X resolution",
disp => "literal",
},
0xa20f => {
tag => "FocalPlaneYResolution",
name => "Focal plane Y resolution",
disp => "literal",
},
0xa210 => {
tag => "FocalPlaneResolutionUnit",
name => "Focal plane resolution unit",
disp => sub {
return "$_[0] inch";
},
},
0xa214 => {
tag => "SubjectLocation",
name => "Subject location",
disp => "literal",
},
0xa215 => {
tag => "ExposureIndex",
name => "Exposure index",
disp => "literal",
},
0xa217 => {
tag => "SensingMethod",
name => "Sensing method",
disp => {
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",
name => "File source",
disp => {
3 => "DSC",
},
},
0xa301 => {
tag => "SceneType",
name => "Scene type",
disp => {
1 => "A directly photographed image",
},
},
0xa302 => {
tag => "CFAPattern",
name => "CFA pattern",
disp => "literal",
},
0xa401 => {
tag => "CustomRendered",
name => "Custom rendered",
disp => {
0 => "Normal process",
1 => "Custom process",
},
},
0xa402 => {
tag => "ExposureMode",
name => "Exposure mode",
disp => {
0 => "Auto exposure",
1 => "Manual exposure",
2 => "Auto bracket",
},
},
0xa403 => {
tag => "WhiteBalance",
name => "White balance",
disp => {
0 => "Auto white balance",
1 => "Manual white balance",
},
},
0xa404 => {
tag => "DigitalZoomRatio",
name => "Digital zoom ratio",
disp => "literal",
},
0xa405 => {
tag => "FocalLengthIn35mmFilm",
name => "Focal length in 35 mm film",
disp => "literal",
},
0xa406 => {
tag => "SceneCaptureType",
name => "Scene capture type",
disp => {
0 => "Standard",
1 => "Landscape",
2 => "Portrait",
3 => "Night scene",
},
},
0xa407 => {
tag => "GainControl",
name => "Gain control",
disp => {
0 => "None",
1 => "Low gain up",
2 => "High gain up",
3 => "Low gain down",
4 => "High gain down",
},
},
0xa408 => {
tag => "Contrast",
name => "Contrast",
disp => {
0 => "Normal",
1 => "Soft",
2 => "Hard",
},
},
0xa409 => {
tag => "Saturation",
name => "Saturation",
disp => {
0 => "Normal",
1 => "Low saturation",
2 => "High saturation",
},
},
0xa40a => {
tag => "Sharpness",
name => "Sharpness",
disp => {
0 => "Normal",
1 => "Soft",
2 => "Hard",
},
},
0xa40b => {
tag => "DeviceSettingDescription",
name => "Device settings description",
disp => "literal",
},
0xa40c => {
tag => "SubjectDistanceRange",
name => "Subject distance range",
disp => {
0 => "Unknown",
1 => "Macro",
2 => "Close view",
3 => "Distant view",
},
},
# TODO: GPS INFO
);