# Blosxom Plugin: Pixom # Author: Buzz Andersen (pixom@scifihifi.com) # Version: 1.0+3i (June 22, 2004) # Documentation: See the bottom of this file or type: perldoc pixom package pixom; # --- User Configurable Variables --- my($useImageMagick) = 1; #This is where pixom caches processed images my($cachedImageDirectory) = "/home/user/web/pixom-cache"; #This is the publicly accessible URL of the cache directory my($cachedImageURL) = "/pixom-cache"; # This uses strftime() tokens (see http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html) my($displayExifDateFormat) = "%A, %D, %I:%M %p"; # The number of images per row my($imagesPerRow) = 2; # Default image processing preferences (only valid if using ImageMagick) my(%defaultImageWidth) = ( thumb => 150, large => 300, full => 640 ); my(%defaultQualityPercent) = ( thumb => 100, large => 90, full => 90 ); my(%defaultSharpenLevel) = ( thumb => 1, large => 1, full => 1 ); my(%defaultShouldNormalize) = ( thumb => 0, large => 0, full => 0 ); my(%defaultShouldEqualize) = ( thumb => 0, large => 0, full => 0 ); # --- Default, on-board templates --- my($galleryHeadTemplate) = <<'GALLERY_HEAD_TEMPLATE';

GALLERY_HEAD_TEMPLATE my($galleryImageTemplate) = <<'GALLERY_IMAGE_TEMPLATE'; GALLERY_IMAGE_TEMPLATE my($galleryRowEndTemplate) = <<'GALLERY_ROW_END_TEMPLATE'; GALLERY_ROW_END_TEMPLATE my($galleryFootTemplate) = <<'GALLERY_FOOT_TEMPLATE';
$pixom::originalImageName

GALLERY_FOOT_TEMPLATE my($singleImageTemplate) = <<'SINGLE_IMAGE_TEMPLATE';

$pixom::previousImageLink   gallery   $pixom::nextImageLink

$pixom::originalImageName

Camera: $pixom::exifInfo{'Model'}
Exposure Date: $pixom::exifInfo{'Exposure Date & Time'}
Aperture: f$pixom::exifInfo{'Aperture'}
Exposure: $pixom::exifInfo{'Exposure Time'} sec
Focal Length: $pixom::exifInfo{'Focal Length'} mm
Metering Mode: $pixom::exifInfo{'Metering Mode'}
Flash: $pixom::exifInfo{'Flash'}

SINGLE_IMAGE_TEMPLATE my(%nextImageLinkTemplate) = ( enabled => <<'NEXT_IMAGE_ENABLED_TEMPLATE', next >> NEXT_IMAGE_ENABLED_TEMPLATE disabled => <<'NEXT_IMAGE_DISABLED_TEMPLATE' next >> NEXT_IMAGE_DISABLED_TEMPLATE ); my(%previousImageLinkTemplate) = ( enabled => <<'PREVIOUS_IMAGE_ENABLED_TEMPLATE', << prev PREVIOUS_IMAGE_ENABLED_TEMPLATE disabled => <<'PREVIOUS_IMAGE_DISABLED_TEMPLATE' << prev PREVIOUS_IMAGE_DISABLED_TEMPLATE ); # --- EXIF Constants --- my(%exifTag) = ( 0x010e => "Description", 0x010f => "Make", 0x0110 => "Model", 0x0112 => "Orientation", 0x011a => "X Resolution", 0x011b => "Y Resolution", 0x0128 => "Resolution Unit", 0x0132 => "Modification Date & Time", 0x0213 => "YCbCr Positioning", 0x8769 => "Offset to EXIF Sub IFD", 0x829a => "Exposure Time", 0x829d => "F-Stop", 0x882a => "Time Zone Offset", 0x9003 => "Exposure Date & Time", 0x9000 => "EXIF Version", 0x9004 => "Digitized Date & Time", 0x9101 => "Components Configuration", 0x9102 => "Compressed Bits Per Pixel", 0x9201 => "Shutter Speed", 0x9202 => "Aperture", 0x9204 => "Exposure Bias", 0x9205 => "Maximum Aperture", 0x9207 => "Metering Mode", 0x9209 => "Flash", 0x920a => "Focal Length", 0xa001 => "Color Space", 0xa002 => "Image Width", 0xa003 => "Image Height", 0x0131 => "Software", 0x8822 => "Exposure Program", 0x8827 => "ISO", 0x9208 => "Light Source", 0x0103 => "Thumbnail Compression Type", 0x0201 => "JPEG Thumbnail Offset", 0x0202 => "JPEG Thumbnail Length", 0x9286 => "User Comment" ); my(%lightSource) = ( 1 => "Daylight", 2 => "Flourescent", 3 => "Tungsten", 10 => "Flash", 17 => "Standard Light A", 18 => "Standard Light B", 19 => "Standard Light C", 20 => "D55", 21 => "D65", 22 => "D75", 255 => "Other" ); my(%meteringMode) = ( 0 => "Unknown", 1 => "Average", 2 => "Center-Weighted Average", 3 => "Spot", 4 => "Multi-Spot", 5 => "Multi-Segment", 6 => "Partial", 255 => "Other" ); my(%exposureProgram) = ( 1 => "Manual", 2 => "Program Normal", 3 => "Aperture Priority", 4 => "Shutter Priority", 5 => "Program Creative", 6 => "Program Action", 7 => "Portrait", 8 => "Landscape" ); my(%flashStatus) = ( 0 => "Off", 1 => "Fired", 5 => "Fired, strobe return light not detected", 7 => "Fired, strobe return light detected", 9 => "Fill fired", 13 => "Fill fired, strobe return light not detected", 15 => "Fill fired, strobe return light detected", 16 => "Off", 24 => "Auto Off", 25 => "Auto fired", 29 => "Auto fired, strobe return light not detected", 31 => "Auto fired, strobe return light detected", 32 => "Not Available" ); my(%yCbCrPosition) = ( 1 => "Centered", 2 => "Cosited" ); my(%resolutionUnit) = ( 2 => "Inches", 3 => "Centimeters" ); my(%compressionType) = ( "Uncompressed" => 1, "JPEG" => 6 ); my(%dataTypeLength) = ( 1 => 1, 2 => 1, 3 => 2, 4 => 4, 5 => 8, 6 => 1, 7 => 1, 8 => 2, 9 => 4, 10 => 8, 11 => 4, 12 => 8 ); my(%dataTypeTemplate) = ( 1 => "C", 2 => "A", 3 => "S", 4 => "L", 6 => "c", 8 => "s", 9 => "l", 11 => "f", 12 => "d" ); my(%errorCode) = ( 0 => "Image file not found or is not readable", -1 => "Invalid start of image (file does not seem to be a valid JPEG)", -2 => "Start of application data not found (file does not seem to contain EXIF data)", -3 => "EXIF identifier not found (file does not seem to contain EXIF data)", -4 => "Unable to determine EXIF data byte order", -5 => "EXIF data doesn't begin with a valid TIFF header" ); my($unsignedRationalType) = 5; my($signedRationalType) = 10; my($undefinedType) = 7; my($exifThumbnailDomain) = "Thumbnail"; # --- Other Constants --- my(@contentTypes) = ("jpg", "jpeg"); my(%viewTypeLabels) = ( thumb => "thumb", large => "large", full => "full" ); my($imageTypes) = join('|', @contentTypes); my($viewTypes) = join('|', keys(%viewTypeLabels)); my($pixomGalleryExtension) = "pixom"; # --- Library Management --- use strict; use CGI; use POSIX qw(strftime); use Time::Local; use File::Copy; use File::Path; use File::Basename; use vars qw/$galleryPageURL $galleryCacheURL $originalImageName $fullImageName $fullImageWidth $fullImageHeight $largeImageName $largeImageWidth $largeImageHeight $thumbImageName $thumbImageWidth $thumbImageHeight %exifInfo $previousImageName $nextImageName $previousImageLink $nextImageLink $gallery/; sub loadOptionalModules { if ($useImageMagick && eval "require Image::Magick") { Image::Magick->import(); } else { $useImageMagick = 0; } } # --- Blosxom Hooks --- sub start { loadOptionalModules(); return $blosxom::static_or_dynamic eq 'dynamic' ? 1 : 0; } sub story { my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_; undef($pixom::gallery); my($absoluteGalleryPath) = $blosxom::datadir.$path."/".$filename.".".$pixomGalleryExtension; if (!-e $absoluteGalleryPath) { return 0; } if (CGI::param('image')) { $pixom::gallery = buildGalleryView(selectedImage=>CGI::param('image'), privateGalleryAbsolutePath=>$absoluteGalleryPath, publicGalleryURLRoot=>$blosxom::url, publicGalleryURLPath=>$path, publicGalleryURLFilename=>$filename.".".$blosxom::flavour); } else { $pixom::gallery = buildGalleryView(privateGalleryAbsolutePath=>$absoluteGalleryPath, publicGalleryURLRoot=>$blosxom::url, publicGalleryURLPath=>$path, publicGalleryURLFilename=>$filename.".".$blosxom::flavour); } return 1; } # --- Gallery Processing Functions --- sub buildGalleryView { my(%parameters) = @_; my($selectedImage) = $parameters{selectedImage}; my($galleryPath) = $parameters{privateGalleryAbsolutePath}; my($URLRoot) = $parameters{publicGalleryURLRoot}; my($URLPath) = $parameters{publicGalleryURLPath}; my($URLFilename) = $parameters{publicGalleryURLFilename}; $galleryPageURL = "$URLRoot$URLPath/$URLFilename"; my($galleryCode); if (!-w $cachedImageDirectory) { warn "Can't cache images to cached image directory $cachedImageDirectory"; return; } my(@images) = getImagesAtPath($galleryPath); if (defined($selectedImage)) { $originalImageName = $selectedImage; $previousImageName = getPreviousImage($originalImageName, @images); $nextImageName = getNextImage($originalImageName, @images); $previousImageLink = interpolateTemplate($previousImageLinkTemplate{defined($previousImageName) ? "enabled" : "disabled"}); $nextImageLink = interpolateTemplate($nextImageLinkTemplate{defined($nextImageName) ? "enabled" : "disabled"}); processGalleryImage($galleryPath, $selectedImage, $viewTypeLabels{large}); $galleryCode = interpolateTemplate($singleImageTemplate); } else { $galleryCode = $galleryHeadTemplate; # Prevent divide by zero if ($imagesPerRow < 1) { $imagesPerRow = 1; } my($i); for ($i = 0; $i <= $#images; $i++) { $originalImageName = $images[$i]; $previousImageName = getPreviousImage($originalImageName, @images); $nextImageName = getNextImage($originalImageName, @images); processGalleryImage($galleryPath, $originalImageName, $viewTypeLabels{thumb}); $galleryCode .= interpolateTemplate($galleryImageTemplate); if (($i < $#images) && ($i + 1) % $imagesPerRow == 0) { $galleryCode .= $galleryRowEndTemplate; } } $galleryCode .= $galleryFootTemplate; } return $galleryCode; } sub processGalleryImage { my($galleryPath, $image, $viewType) = @_; my($sourcePath) = "$galleryPath/$image"; my($name, $path, $extension) = fileparse($sourcePath, '\..*'); my($galleryName) = (fileparse($galleryPath))[0]; my($outputDirectory) = "$cachedImageDirectory/$galleryName"; $largeImageName = "$name.".$viewTypeLabels{large}.$extension; $thumbImageName = "$name.".$viewTypeLabels{thumb}.$extension; $fullImageName = "$name.".$viewTypeLabels{full}.$extension; $galleryCacheURL = "$cachedImageURL/$galleryName"; my($largeOutputPath) = "$outputDirectory/$largeImageName"; my($fullOutputPath) = "$outputDirectory/$fullImageName"; my($thumbOutputPath) = "$outputDirectory/$thumbImageName"; if (!cacheImage($sourcePath, $thumbOutputPath, $viewTypeLabels{thumb}, \$thumbImageWidth, \$thumbImageHeight)) { warn("Unable to cache thumbnail representation to $thumbOutputPath"); return 0; } if (!cacheImage($sourcePath, $largeOutputPath, $viewTypeLabels{large}, \$largeImageWidth, \$largeImageHeight)) { warn("Unable to cache large representation to $largeOutputPath--using thumbnail"); $largeImageName = $thumbImageName; } if (!cacheImage($sourcePath, $fullOutputPath, $viewTypeLabels{full}, \$fullImageWidth, \$fullImageHeight)) { warn("Unable to cache full representation to $fullOutputPath--using large"); } %exifInfo = getExifHashFromFile("$galleryPath/$originalImageName", 1); return 1; } sub cacheImage { my($sourcePath, $destinationPath, $viewType, $widthRef, $heightRef) = @_; my($destinationDirectory) = dirname($destinationPath); my($sourceDirectory) = dirname($sourcePath); my($outputName) = (fileparse($destinationPath))[0]; my($manualVersionPath) = "$sourceDirectory/$outputName"; my($status); if (!-e $destinationPath) { if (!-e $destinationDirectory) { $status = mkpath($destinationDirectory); if ($status == 0) { warn("Unable to create cache directory $destinationDirectory"); return 0; } } # If the user has provided a manually created version of this image, use that. if (-e $manualVersionPath) { if (!copy($manualVersionPath, $destinationPath)) { warn("Couldn't copy existing image from $sourcePath to $destinationDirectory"); return 0; } } # If we don't have manually created images, and we are allowed to use ImageMagick, automatically generate them. elsif ($useImageMagick) { my($imageFile) = new Image::Magick(); if (!$imageFile) { warn("Couldn't instantiate new ImageMagick image"); return 0; } $status = $imageFile->Read($sourcePath); if (!$status == 0) { warn("Couldn't read image from path: $sourcePath"); return 0; } resizeImageRef($imageFile, $defaultImageWidth{$viewType}); processImageRef($imageFile, $defaultQualityPercent{$viewType}, $defaultSharpenLevel{$viewType}, $defaultShouldNormalize{$viewType}, $defaultShouldEqualize{$viewType}); $status = $imageFile->Write($destinationPath); ${$widthRef} = $imageFile->Get('width'); ${$heightRef} = $imageFile->Get('height'); if ($status == 0) { warn("Couldn't write image to path: $destinationPath"); return 0; } undef($imageFile); } # If we don't have a manual image or the ability to use ImageMagick, copy the original image to use as the full representationtry getting the EXIF thumbnail for the thumb or large view. elsif ($viewType eq $viewTypeLabels{full}) { if (!copy($sourcePath, $destinationPath)) { warn("Couldn't copy existing image from $sourcePath to $destinationDirectory"); return 0; } } # If we don't have a manual image or the ability to use ImageMagick, try getting the EXIF thumbnail to use as the large and thumbnail representations. else { my($thumbnailData) = getExifThumbnailFromFile($sourcePath); if ($thumbnailData) { open(THUMB, "> $destinationPath"); binmode(THUMB); print THUMB $thumbnailData; close(THUMB); } else { warn "Exif error: ".$errorCode{$status}."\n"; return 0; } } } if (!${$widthRef} || !${$heightRef}) { if ($useImageMagick) { getImageSizeAtPath($destinationPath, $widthRef, $heightRef); } } return 1; } sub getPreviousImage { my($image, @images) = @_; my($prevImage); my($currentImage); foreach $currentImage (@images) { last if ($image eq $currentImage); $prevImage = $currentImage; } return $prevImage; } sub getNextImage { my($image, @images) = @_; return getPreviousImage($image, reverse(@images)); } # --- Path Utilities --- sub getImagesAtPath { my($path) = @_; if (!opendir(DIR, $path)) { warn "Can't open gallery at path: $path"; return; } my(@images); my($file); foreach $file (readdir(DIR)) { if ($file !~ /\.($viewTypes)\./i && $file =~ /.($imageTypes)$/i && -r "$path/$file") { push(@images, "$file"); } } closedir(DIR); return @images; } # --- Template Utilities --- sub interpolateTemplate { my($template) = shift; # Special code to interpolate EXIF hashes only $template =~ s/(\$pixom::exifInfo{.*})/"defined $1 ? $1 : ''"/gee; # Also run the template through the interpolate implementation Blosxom is using return &$blosxom::interpolate($template); } # --- Image Processing --- sub resizeImageRef { my($image, $resizeWidth) = @_; my($originalWidth) = $image->Get('width'); my($originalHeight) = $image->Get('height'); if ($originalWidth > $resizeWidth) { my($resizeHeight) = (($resizeWidth * $originalHeight) / $originalWidth); $image->Scale(width => $resizeWidth, height => $resizeHeight); } return $image; } sub processImageRef { my($image, $qualityLevel, $sharpenLevel, $normalize, $equalize) = @_; $image->Set(quality=>$qualityLevel); if ($sharpenLevel > 0) { $image->Sharpen($sharpenLevel); } if ($normalize) { $image->Normalize(); } if ($equalize) { $image->Equalize(); } } sub getImageSizeAtPath { my($sourcePath) = $_[0]; my($widthRef) = $_[1]; my($heightRef) = $_[2]; my($status); my($imageFile) = new Image::Magick(); if (!$imageFile) { warn("Couldn't instantiate new ImageMagick image"); return; } $status = $imageFile->Read($sourcePath); if (!$status == 0) { warn("Couldn't read image from path: $sourcePath"); } else { ${$widthRef} = $imageFile->Get('width'); ${$heightRef} = $imageFile->Get('height'); } undef($imageFile); } # --- EXIF Parsing Functions --- sub getExifHashFromFile { my(%exifHash); my($format) = $_[1]; my($thumbnailData); my($status) = extractExifDataFromFile($_[0], \%exifHash, \$thumbnailData); if ($status > 0) { if (defined($format) && $format) { formatExifHash(\%exifHash); } } else { warn("EXIF error: ".$errorCode{$status}."\n"); } return %exifHash; } sub getExifThumbnailFromFile { my(%exifHash); my($thumbnailData); my($status) = extractExifDataFromFile($_[0], \%exifHash, \$thumbnailData); if ($status < 0) { warn("EXIF error: ".$errorCode{$status}."\n"); } return $thumbnailData; } sub extractExifDataFromFile { my($file) = $_[0]; my($exifHashRef) = $_[1]; my($thumbnailDataRef) = $_[2]; my $filehandle = do { local(*EXIFDATA) }; my($buffer); if (!(-e $file && -r $file)) { return 0; } open($filehandle, $file); binmode($filehandle); # Check for JPEG start of image marker read($filehandle, $buffer, 2); my($soiMarker) = get16BitsFromBuffer($buffer, 1); if (unpack("S", $soiMarker) != 0xFFD8) { return -1; } my($foundExif) = 0; # Find application data marker 1 (0xFFE1) while (read($filehandle, $buffer, 1)) { last if (!(unpack("C", $buffer) == 0xFF)); read($filehandle, $buffer, 1); my($marker) = unpack("C", $buffer); read($filehandle, $buffer, 2); my($appDataSize) = unpack("S", get16BitsFromBuffer($buffer, 1)); if ($marker == 0xE1) { read($filehandle, $buffer, 6); if (unpack("A*", $buffer) eq "Exif") { $foundExif = 1; last; } } else { seek($filehandle, $appDataSize - 2, 1); } } if (!$foundExif) { return -3; } # Check for byte order marker read($filehandle, $buffer, 2); my($bigEndian); if ($buffer eq "MM") { $bigEndian = 1; } elsif ($buffer eq "II") { $bigEndian = 0; } else { return -4; } my($start) = tell($filehandle) - 2; # Check for TIFF header marker read($filehandle, $buffer, 2); my($tiffMarker) = get16BitsFromBuffer($buffer, $bigEndian); # Magic number is 42 (0x2a00) (possible "hitchhiker's guide" reference?) if (unpack("S", $tiffMarker) != 0x2a) { return -5; } # Get offset of first image file directory read($filehandle, $buffer, 4); my($firstDirectoryOffset) = unpack("l", get32BitsFromBuffer($buffer, $bigEndian)); # Process IFD0 (main image) processIFD($filehandle, $start, $firstDirectoryOffset, $bigEndian, $exifHashRef); # Process EXIF sub IFD if (defined(${$exifHashRef}{"Offset to EXIF Sub IFD"})) { processIFD($filehandle, $start, ${$exifHashRef}{"Offset to EXIF Sub IFD"}, $bigEndian, $exifHashRef); } # Process IFD1 (thumbnail image) if it exists if (defined(${$exifHashRef}{"Next IFD Offset"}) && defined($thumbnailDataRef)) { processIFD($filehandle, $start, ${$exifHashRef}{"Next IFD Offset"}, $bigEndian, $exifHashRef, $exifThumbnailDomain); if (${$exifHashRef}{$exifThumbnailDomain.".Thumbnail Compression Type"} == $compressionType{"JPEG"}) { seek($filehandle, $start + ${$exifHashRef}{$exifThumbnailDomain.".JPEG Thumbnail Offset"}, 0); read($filehandle, $buffer, ${$exifHashRef}{$exifThumbnailDomain.".JPEG Thumbnail Length"}); ${$thumbnailDataRef} = $buffer; } } close($filehandle); return 1; } sub processIFD { my($filehandle) = $_[0]; my($base) = $_[1]; my($directoryOffset) = $_[2]; my($bigEndian) = $_[3]; my($exifHashRef) = $_[4]; my($domain) = $_[5]; my($buffer); seek($filehandle, $base + $directoryOffset, 0); read($filehandle, $buffer, 2); my($numberOfEntries) = unpack("S", get16BitsFromBuffer($buffer, $bigEndian)); $directoryOffset += 2; my($i); for ($i = 1; $i <= $numberOfEntries; $i++) { read($filehandle, $buffer, 2); my($tag) = unpack("S", get16BitsFromBuffer($buffer, $bigEndian)); read($filehandle, $buffer, 2); my($dataFormat) = unpack("S", get16BitsFromBuffer($buffer, $bigEndian)); read($filehandle, $buffer, 4); my($fieldLength) = unpack("l", get32BitsFromBuffer($buffer, $bigEndian)); read($filehandle, $buffer, 4); if ($dataTypeLength{$dataFormat} * $fieldLength > 4) { my($fieldOffset) = unpack("l", get32BitsFromBuffer($buffer, $bigEndian)); seek($filehandle, $base + $fieldOffset, 0); read($filehandle, $buffer, $fieldLength * $dataTypeLength{$dataFormat}); seek($filehandle, $base + $directoryOffset + ($i * 12), 0); } my($fieldValue) = getValueForExifField($buffer, $dataFormat, $fieldLength, $bigEndian); if (defined($fieldValue) && defined($exifTag{$tag})) { my($exifKey); if (defined($domain)) { $exifKey = "$domain.".$exifTag{$tag}; } else { $exifKey = $exifTag{$tag}; } ${$exifHashRef}{$exifKey} = $fieldValue; } else { next; } } read($filehandle, $buffer, 4); my($nextOffset) = unpack("l", get32BitsFromBuffer($buffer, $bigEndian)); if ($nextOffset > 0) { ${$exifHashRef}{"Next IFD Offset"} = $nextOffset; } } sub getValueForExifField { my($rawValue) = @_[0]; my($dataFormat) = @_[1]; my($fieldLength) = @_[2]; my($bigEndian) = @_[3]; my($dataTypeLength); my($swappedValue); my($fieldValue); if ($dataTypeLength{$dataFormat} == 2) { # 16 bit value $swappedValue = get16BitsFromBuffer($rawValue, $bigEndian); } elsif ($dataTypeLength{$dataFormat} == 4) { # 32 bit value $swappedValue = get32BitsFromBuffer($rawValue, $bigEndian); } elsif ($dataTypeLength{$dataFormat} == 8) { # 64 bit value $swappedValue = get64BitsFromBuffer($rawValue, $bigEndian); } else { $swappedValue = $rawValue; } if (defined($dataTypeTemplate{$dataFormat})) { $fieldValue = unpack($dataTypeTemplate{$dataFormat}.$fieldLength, $swappedValue); } elsif ($dataFormat == $undefinedType) { undef($fieldValue); } elsif ($dataFormat == $unsignedRationalType) { my($numerator, $denominator) = unpack('L2', $swappedValue); $fieldValue = $numerator."/".$denominator; } elsif ($dataFormat == $signedRationalType) { my($numerator, $denominator) = unpack('l2', $swappedValue); $fieldValue = $numerator."/".$denominator; } else { $fieldValue = unpack("a", $swappedValue); } return $fieldValue; } # --- Formatting Functions --- sub formatExifHash { my($exifHashRef) = $_[0]; my(@keys) = keys(%{$exifHashRef}); my($tag); foreach $tag (@keys) { my($fieldValue) = ${$exifHashRef}{$tag}; if ($tag eq "X Resolution" || $tag eq "$exifThumbnailDomain.X Resolution") { ${$exifHashRef}{$tag} = convertRationalToFloat($fieldValue); } elsif ($tag eq "Y Resolution" || $tag eq "$exifThumbnailDomain.Y Resolution") { ${$exifHashRef}{$tag} = convertRationalToFloat($fieldValue); } elsif ($tag eq "Resolution Unit" || $tag eq "$exifThumbnailDomain.Resolution Unit") { ${$exifHashRef}{$tag} = $resolutionUnit{$fieldValue}; } elsif ($tag eq "YCbCr Positioning" || $tag eq "$exifThumbnailDomain.YCbCr Positioning") { ${$exifHashRef}{$tag} = $yCbCrPosition{$fieldValue}; } elsif ($tag eq "Exposure Time") { ${$exifHashRef}{$tag} = reduceRational($fieldValue); } elsif ($tag eq "Exposure Date & Time") { ${$exifHashRef}{$tag} = formatExifDate($fieldValue); } elsif ($tag eq "F-Stop") { ${$exifHashRef}{$tag} = convertRationalToFloat($fieldValue); } elsif ($tag eq "Shutter Speed") { ${$exifHashRef}{$tag} = convertAPEXToShutter($fieldValue); } elsif ($tag eq "Aperture") { ${$exifHashRef}{$tag} = convertRationalToFloat($fieldValue); } elsif ($tag eq "Exposure Bias") { ${$exifHashRef}{$tag}= convertRationalToFloat($fieldValue); } elsif ($tag eq "Maximum Aperture") { ${$exifHashRef}{$tag} = convertRationalToFloat($fieldValue); } elsif ($tag eq "Metering Mode") { ${$exifHashRef}{$tag} = $meteringMode{$fieldValue}; } elsif ($tag eq "Flash") { ${$exifHashRef}{$tag} = $flashStatus{$fi