#!/usr/pkg/bin/perl
# ====================================================================
# make_album
#
# Copyright (c) 2000 David Burren.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#
# 3. The end-user documentation included with the redistribution,
#    if any, must include the following acknowledgment:
#       "This product includes software developed by David Burren."
#    Alternately, this acknowledgment may appear in the software itself,
#    if and wherever such third-party acknowledgments normally appear.
#
# 4. The name "David Burren" must not be used to endorse or promote
#    products derived from this software without prior written
#    permission. For written permission, please contact david@burren.cx.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED.  IN NO EVENT SHALL DAVID BURREN BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ====================================================================
# $Id: make_album,v 1.32 2000/09/12 23:09:51 davidb Exp $

package exif;

my $debug = 0;

my ($buf, $long, $short, $makerformat);

my %bytes_per_component = (
	1 => 1,	 # BYTE
	2 => 1,	 # ASCII
	3 => 2,	 # SHORT
	4 => 4,	 # LONG
	5 => 8,	 # RATIONAL
	6 => 1,	 # SBYTE
	7 => 1,	 # UNDEFINED
	8 => 2,	 # SSHORT
	9 => 4,	 # SLONG
	10 => 8, # SRATIONAL
	11 => 4, # FLOAT
	12 => 8	 # DOUBLE
);

# Known EXIF/TIFF tags
#
my %tag_name = (
	0x0001 => "InteroperabilityIndex",
	0x0002 => "InteroperabilityVersion",
	0x00fe => "NewSubfileType",
	0x00ff => "SubfileType",
	0x0100 => "ImageWidth",
	0x0101 => "ImageLength",
	0x0102 => "BitsPerSample",
	0x0103 => "Compression",
	0x0106 => "PhotometricInterpretation",
	0x010e => "ImageDescription",
	0x010f => "Make",
	0x0110 => "Model",
	0x0111 => "StripOffsets",
	0x0112 => "Orientation",
	0x0115 => "SamplesPerPixel",
	0x0116 => "RowsPerStrip",
	0x0117 => "StripByteCounts",
	0x011a => "XResolution",
	0x011b => "YResolution",
	0x011c => "PlanarConfiguration",
	0x0128 => "ResolutionUnit",
	0x012d => "TransferFunction",
	0x0131 => "Software",
	0x0132 => "DateTime",
	0x013b => "Artist",
	0x013d => "Predictor",
	0x013e => "WhitePoint",
	0x013f => "PrimaryChromaticities",
	0x0142 => "TileWidth",
	0x0143 => "TileLength",
	0x0144 => "TileOffsets",
	0x0145 => "TileByteCounts",
	0x014a => "SubIFDs",
	0x015b => "JPEGTables",
	0x0201 => "JpegIFOffset",
	0x0202 => "JpegIFByteCount",
	0x0211 => "YCbCrCoefficients",
	0x0213 => "YCbCrPositioning",
	0x0214 => "ReferenceBlackWhite",
	0x1000 => "RelatedImageFileFormat",
	0x1001 => "RelatedImageWidth",
	0x1002 => "RelatedImageLength?",
	0x828d => "CFARepeatPatternDim",
	0x828e => "CFAPattern?",
	0x828f => "BatteryLevel",
	0x8298 => "Copyright",
	0x829a => "ExposureTime",
	0x829d => "FNumber",
	0x83bb => "IPTC/NAA",
	0x8769 => "ExifOffset",
	0x8773 => "InterColorProfile",
	0x8822 => "ExposureProgram",
	0x8824 => "SpectralSensitivity",
	0x8825 => "GPSInfo",
	0x8827 => "ISOSpeedRatings",
	0x8828 => "OECF",
	0x8829 => "Interlace",
	0x882a => "TimeZoneOffset",
	0x882b => "SelfTimerMode",
	0x9000 => "ExifVersion",
	0x9003 => "DateTimeOriginal",
	0x9004 => "DateTimeDigitized",
	0x9101 => "ComponentConfiguration",
	0x9102 => "CompressedBitsPerPixel",
	0x9201 => "ShutterSpeedValue",
	0x9202 => "ApertureValue",
	0x9203 => "BrightnessValue",
	0x9204 => "ExposureBiasValue",
	0x9205 => "MaxApertureValue",
	0x9206 => "SubjectDistance",
	0x9207 => "MeteringMode",
	0x9208 => "LightSource",
	0x9209 => "Flash",
	0x920a => "FocalLength",
	0x920b => "FlashEnergy",
	0x920c => "SpatialFrequencyResponse?",
	0x920d => "Noise",
	0x920e => "FocalPlaneXResolution",
	0x920f => "FocalPlaneYResolution",
	0x9210 => "FocalPlaneResolutionUnit",
	0x9211 => "ImageNumber",
	0x9212 => "SecurityClassification",
	0x9213 => "ImageHistory",
	0x9214 => "SubjectLocation",
	0x9215 => "ExposureIndex",
	0x9216 => "TIFF/EPStandardID",
	0x9217 => "SensingMethod",
	0x927c => "MakerNote",
	0x9286 => "UserComment",
	0x9290 => "SubSecTime",
	0x9291 => "SubSecTimeOriginal",
	0x9292 => "SubSecTimeDigitized",
	0xa000 => "FlashPixVersion",
	0xa001 => "ColorSpace",
	0xa002 => "ExifImageWidth",
	0xa003 => "ExifImageHeight",
	0xa004 => "RelatedSoundFile",
	0xa005 => "ExifInteroperabilityOffset",
	0xa20b => "FlashEnergy",
	0xa20c => "SpatialFrequencyResponse?",
	0xa20e => "ExifFocalPlaneXResolution",
	0xa20f => "ExifFocalPlaneYResolution",
	0xa210 => "ExifFocalPlaneResolutionUnit",
	0xa214 => "ExifSubjectLocation",
	0xa215 => "ExifExposureIndex",
	0xa217 => "ExifSensingMethod",
	0xa300 => "FileSource",
	0xa301 => "SceneType",
	0xa302 => "CFAPattern?",
);

my %quality;
{
	my $e900_qual = {
		1 => "VGA BASIC",
		2 => "VGA NORMAL",
		3 => "VGA FINE",
		4 => "SXGA BASIC",
		5 => "SXGA NORMAL",
		6 => "SXGA FINE"
	};
	my $e950_qual = {
		1 => "VGA BASIC",
		2 => "VGA NORMAL",
		3 => "VGA FINE",
		4 => "XGA BASIC",
		5 => "XGA NORMAL",
		6 => "XGA FINE",
		10 => "BASIC",
		11 => "NORMAL",
		12 => "FINE",
		20 => "HI"
	};
	%quality = (
		"E900" => $e900_qual,
		"E900S" => $e900_qual,
		"E910" => $e900_qual,
		"E700" => $e950_qual,
		"E800" => $e950_qual,
		"E950" => $e950_qual,
	);
}

sub signed_long {
	local($unsigned) = shift;

	scalar unpack("l", pack("L", $unsigned));
}

sub signed_short {
	local($unsigned) = shift;

	scalar unpack("s", pack("S", $unsigned));
}

sub rational {
	local($num, $denom) = @_;

	if ($denom == 0) {
		"infinite";
	} else {
		$num / $denom;
	}
}

sub read_value {
	local($tiff_start, $tag, $format, $components, $data) = @_;
	my @values = ();

	my $length = $components * $bytes_per_component{$format};
	if (!$length) {
		# Unknown format - ignore it.
		return;
	}
	if ($length > 4) {
		# Need to find the data.
		seek(IMG, $tiff_start + unpack("$long", $data), 0);
		if (!(my $bytes = read(IMG, $data, $length))) {
			return;
		}
	}
	if ($format == 1) {
		# BYTE
		@values = unpack("C$components", $data);
	} elsif ($format == 2) {
		# ASCII
		$components--; # we don't need no stinking NULs
		$values[0] = unpack("a$components", $data);
		$values[0] =~ s/\000+$//;
		$values[0] =~ s/\s+$//;
	} elsif ($format == 3) {
		# SHORT
		@values = unpack("$short$components", $data);
	} elsif ($format == 4) {
		# LONG
		@values = unpack("$long$components", $data);
	} elsif ($format == 5) {
		# RATIONAL
		while ($components) {
			($numerator, $denominator) = unpack("$long$long", $data);
			push(@values, rational($numerator, $denominator));
			$components--;
		}
	} elsif ($format == 6) {
		# SBYTE
		@values = unpack("c*", $data);
	} elsif ($format == 7) {
		# UNDEFINED
		@values = unpack("C*", $data);
	} elsif ($format == 8) {
		# SSHORT
		foreach $unsigned (unpack("$short*", $data)) {
			push(@values, signed_short($unsigned));
		}
	} elsif ($format == 9) {
		# SLONG
		foreach $unsigned (unpack("$long*", $data)) {
			push(@values, signed_long($unsigned));
		}
	} elsif ($format == 10) {
		# SRATIONAL
		while ($components) {
			($numerator, $denominator) = unpack("$long$long", $data);
			$numerator = signed_long($numerator);
			$denominator = signed_long($denominator);
			push(@values, rational($numerator, $denominator));
			$components--;
		}
	} elsif ($format == 11) {
		# FLOAT
		# How to do this in a byte-order-independent way?
		# Actually, no need for these values with digicams so far.
		return;
	} elsif ($format == 12) {
		# DOUBLE
		# How to do this in a byte-order-independent way?
		# Actually, no need for these values with digicams so far.
		return;
	}
	@values;
}


sub read_ifd {
	local($tiff_start, $ifd_offset) = @_;
	my ($bytes, $next_ifd, $ifd);

	seek(IMG, $tiff_start + $ifd_offset, 0);
	if (!($bytes = read(IMG, $buf, 2))) {
		return;
	}
	my $dir_entries = unpack("$short", $buf);
	print "IFD entries: $dir_entries\n" if ($debug);
	if (!($bytes = read(IMG, $ifd, 12 * $dir_entries))) {
		return;
	}
	if (!($bytes = read(IMG, $buf, 4))) {
		return;
	}
	$next_ifd = unpack("$long", $buf);
	($dir_entries, $next_ifd, $ifd);
}


sub reread_ifd {
	local($buff) = @_;

	my $dir_entries = unpack("$short", $buff);
	print "IFD entries: $dir_entries\n" if ($debug);
	my $ifd = substr($buff, 2, 12 * $dir_entries);
	($dir_entries, 0, $ifd);
}


sub subjectdistance {
	local($value) = shift;
	my $ret = $value."m";

	if ($value == 0 && $value ne "0") {
		return undef;
	}
	if ($value eq "0") {
		$ret = "unknown";
	}
	if ($value < 0
		|| $value == (2 ** 16 - 1) / 1000
		|| $value == (2 ** 16 - 1) / 100	# EOS D30
		|| $value == (2 ** 15 - 1) / 1000
		|| $value == (2 ** 32 - 1) / 100	# DC290
		|| $value eq "infinite") {
		$ret = "infinite";
	}
	$ret;
}

sub parse_ifd {
	local($tiff_start, $dir_entries, $ifd, $rtags, $name) = @_;

	print "IFD: $name\n" if ($debug);
	my $ent = 0;
	while ($ent < $dir_entries) {
		$dirent = substr($ifd, 12*($ent++), 12);
		($tag, $format, $components, $d0, $d1, $d2, $d3) =
			unpack("$short$short$long"."C4", $dirent);
		$data = pack("C4", $d0, $d1, $d2, $d3);
		my @results = read_value($tiff_start, $tag, $format, $components, $data);
		if ($debug) {
			printf "Tag %x, format %d, components %d", $tag, $format, $components;
			if ($name ne "makernote" && (my $label = $tag_name{$tag})) {
				print "\t$label";
			}
			my @res = ();
			if ($format == 1 || $format == 7) {
				foreach my $res (@results) {
					push(@res,
						sprintf("(0x%02x) $res", $res));
				}
			} elsif ($format == 3) {
				foreach my $res (@results) {
					push(@res,
						sprintf("(0x%04x) $res", $res));
				}
			} elsif ($format == 4) {
				foreach my $res (@results) {
					push(@res,
						sprintf("(0x%08x) $res", $res));
				}
			} else {
				@res = @results;
			}
			print "\n\t", join("\n\t", @res), "\n";
		}
		my $res;
		if ($name eq "makernote") {
			if ($makerformat eq "NIKON0") {
				if ($tag == 0x003) {
					my $qual =  $results[0];

					if (($rtags->{"quality"} = $quality{$rtags->{"model"}}{$qual}) eq "") {
						$rtags->{"quality"} = $qual;
					}
				} elsif ($tag == 0x004) {
					my $mode =  $results[0];

					$rtags->{"color_mode"} =
						{	1 => "COLOR",
							2 => "B&W"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x005) {
					my $mode =  $results[0];

					$rtags->{"image_adjustment"}{"Image Adjustment"} =
						{	0 => "Normal",
							1 => "Bright+",
							2 => "Bright-",
							3 => "Contrast+",
							4 => "Contrast-"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x006) {
					my $mode =  $results[0];

					$rtags->{"iso"} =
						{	0 => 80,
							2 => 160,
							4 => 320,
							5 => 100,
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x007) {
					my $mode =  $results[0];

					$rtags->{"white_balance"} =
						{	0 => "Auto",
							1 => "Preset",
							2 => "Daylight",
							3 => "Incandenscent",
							4 => "Flourescent",
							5 => "Cloudy",
							6 => "Flash"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x008) {
					my $mode =  $results[0];

					if ($mode eq "infinite") {
						$mode = "infinite/auto";
					} else {
						$mode = $mode."m";
					}
					$rtags->{"focus"} = $mode;
				} elsif ($tag == 0x00a) {
					my $mode =  $results[0];

					if ($mode != 0 && $mode != 1) {
						$rtags->{"digital_zoom"} = $mode;
					}
				} elsif ($tag == 0x00b) {
					my $mode =  $results[0];

					$val = {	0 => "",
							1 => "converter",
						}->{$mode};
					if (!defined $val) {
						$rtags->{"converter"} = "unknown ($mode)";
					} elsif ($val ne "") {
						$rtags->{"converter"} = $val;
					}
				}
			} elsif ($makerformat eq "CASIO") {
				if ($tag == 0x0002) {
					my $mode =  $results[0];

					$rtags->{"quality"} =
						{	1 => "Economy",
							2 => "Normal",
							3 => "Fine"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x0003) {
					my $mode =  $results[0];

					$rtags->{"focus_mode"} =
						{	2 => "macro",
							3 => "auto",
							4 => "manual",
							5 => "infinity"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x0004) {
					my $mode =  $results[0];

					$rtags->{"flash_mode"} =
						{	1 => "auto",
							2 => "on",
							3 => "off",
							4 => "red-eye"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x0006) {
					my $mode =  $results[0];

					if ($mode ne "infinite") {
						$mode = ($mode/1000)."m";
					}
					$rtags->{"focus"} = $mode;
				} elsif ($tag == 0x007) {
					my $mode =  $results[0];

					$rtags->{"white_balance"} =
						{	1 => "auto",
							2 => "tungsten",
							3 => "daylight",
							4 => "flourescent",
							5 => "shade",
							129 => "manual"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x000a) {
					my $mode =  $results[0];

					my $val =
						{	0x10000 => "1",
							0x10001 => "2",
						}->{$mode}
						||
						"unknown ($mode)";
					if ($val ne "0" && $val ne "1") {
						$rtags->{"digital_zoom"} = $val;
					}
				} elsif ($tag == 0x000b) {
					my $mode =  $results[0];

					$rtags->{"image_adjustment"}{"Sharpening"} =
						{	0 => "normal",
							1 => "soft",
							2 => "hard",
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x000c) {
					my $mode =  $results[0];

					$rtags->{"image_adjustment"}{"Contrast"} =
						{	0 => "normal",
							1 => "low",
							2 => "high",
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x000d) {
					my $mode =  $results[0];

					$rtags->{"image_adjustment"}{"Saturation"} =
						{	0 => "normal",
							1 => "low",
							2 => "high",
						}->{$mode}
						||
						"unknown ($mode)";
				}
			} elsif ($makerformat eq "OLYMPUS") {
				if ($tag == 0x0201) {
					my $mode =  $results[0];

					$rtags->{"quality"} =
						{	1 => "SQ",
							2 => "HQ",
							3 => "SHQ"
						}->{$mode}
						||
						"unknown ($mode)";
				} elsif ($tag == 0x0202 && $results[0]) {
					$rtags->{"focus_mode"} = "macro";
				} elsif ($tag == 0x0203) {
					my $mode =  $results[0];

					if ($mode != 0 && $mode != 1) {
						$rtags->{"digital_zoom"} = $mode;
					}
				} elsif ($tag == 0x0207) {
					$rtags->{"firmware"} = $results[0];
				}
			} elsif ($makerformat eq "CANON") {
				if ($tag == 0x0007) {
					$rtags->{"firmware"} = $results[0];
				} elsif ($tag == 0x0009) {
					$mode = $results[0];
					$mode =~ s/\s+$//;
					$mode =~ s/\000/ /;
					if ($mode ne "") {
						$rtags->{"owner"} = $mode;
					}
				} elsif ($tag == 0x000f) {
					my $num_fns = $results[0]/2-1;
					my @cfns = ();
					my $rcfns = {};
					foreach my $cf (@results[1..$num_fns]) {
						my $cfn = $cf >> 8;
						my $val = $cf & 0xff;
						if ($cfn == 1) {
							$description = "Long exposure noise reduction";
							$vdesc = 
							{	0 => "Off",
								1 => "On"
							}->{$val}
						} elsif ($cfn == 2) {
							$description = "Shutter/AE-lock buttons";
							$vdesc = 
							{	0 => "AF/AE lock",
								1 => "AE lock/AF",
								2 => "AF/AF lock",
								3 => "AE+release/AE+AF",
							}->{$val}
						} elsif ($cfn == 3) {
							$description = "Mirror lockup";
							$vdesc = 
							{	0 => "Disable",
								1 => "Enable"
							}->{$val}
						} elsif ($cfn == 4) {
							$description = "Tv/Av and exposure level";
							$vdesc = 
							{	0 => "1/2 stop",
								1 => "1/3 stop"
							}->{$val}
						} elsif ($cfn == 5) {
							$description = "AF-assist light";
							$vdesc = 
							{	0 => "On (auto)",
								1 => "Off"
							}->{$val}
						} elsif ($cfn == 6) {
							$description = "Shutter speed in Av mode";
							$vdesc = 
							{	0 => "Automatic",
								1 => "1/200 (fixed)",
							}->{$val}
						} elsif ($cfn == 7) {
							$description = "AEB sequence/auto cancellation";
							$vdesc = 
							{	0 => "0, -, + / Enabled",
								1 => "0, -, + / Disabled",
								2 => "-, 0, + / Enabled",
								3 => "-, 0, + / Disabled",
							}->{$val}
						} elsif ($cfn == 8) {
							$description = "Shutter curtain sync";
							$vdesc = 
							{	0 => "1st-curtain sync",
								1 => "2nd-curtain sync",
							}->{$val}
						} elsif ($cfn == 9) {
							$description = "Lens AF stop button Fn. Switch";
							$vdesc = 
							{	0 => "AF stop",
								1 => "Operate AF",
								2 => "Lock AE and start timer"
							}->{$val}
						} elsif ($cfn == 10) {
							$description = "Auto reduction of fill flash";
							$vdesc = 
							{	0 => "Enable",
								1 => "Disable"
							}->{$val}
						} elsif ($cfn == 11) {
							$description = "Menu button return position";
							$vdesc = 
							{	0 => "Start",
								1 => "Previous (volatile)",
								2 => "Previous"
							}->{$val}
						} elsif ($cfn == 12) {
							$description = "SET button func. when shooting";
							$vdesc = 
							{	0 => "Not assigned",
								1 => "Change quality",
								2 => "Change ISO speed",
								3 => "Select parameters"
							}->{$val}
						} elsif ($cfn == 13) {
							$description = "Sensor cleaning";
							$vdesc = 
							{	0 => "Disable",
								1 => "Enable"
							}->{$val}
						} else {
							$description = "unknown";
							$vdesc = $val;
						}
						if (!defined $vdesc) {
							$vdesc = $val;
						}
						$rcfns->{$cfn}{"val"} = $val;
						$rcfns->{$cfn}{"vdesc"} = $vdesc;
						$rcfns->{$cfn}{"description"} = 
							$description;
						push(@cfns, "cf".$cfn.":".$val);
					}
					$rtags->{"cfn"} = join(",", @cfns);
					$rtags->{"cfns"} = $rcfns;
				}
			} elsif ($makerformat eq "NIKON1") {
				if ($tag == 0x002 && !$rtags->{"iso"}) {
					my ($mode1, $mode2) = @results;
					if ($mode1 == 0) {
						$rtags->{"iso"} = $mode2;
					}
				} elsif ($tag == 0x0003) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"color_mode"} = $mode;
				} elsif ($tag == 0x0004) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"quality"} = $mode;
				} elsif ($tag == 0x0005) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"white_balance"} = $mode;
				} elsif ($tag == 0x0006) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"image_adjustment"}{"Sharpening"} = $mode;
				} elsif ($tag == 0x0007) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"focus_mode"} = $mode;
				} elsif ($tag == 0x0008) {
					my $mode =  $results[0];

					$mode =~ s/\000+$//;
					$mode =~ s/\s+$//;
					if ($mode ne "") {
						$rtags->{"flash_mode"} = $mode;
					}
				} elsif ($tag == 0x000f) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"iso_mode"} = $mode;
				} elsif ($tag == 0x0080) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"image_adjustment"}{"Image Adjustment"} = $mode;
				} elsif ($tag == 0x0082) {
					my $mode =  $results[0];

					$mode =~ s/\s+$//;
					$rtags->{"converter"} = $mode;
				} elsif ($tag == 0x0085) {
					if ($mode = $results[0]) {
						if ($mode ne "infinite") {
							$mode = $mode."m";
						}
						if ($mode eq "0m") {
							$mode = "unknown";
						}
						$rtags->{"focus"} = $mode;
					}
				} elsif ($tag == 0x0086) {
					my $mode =  $results[0];

					if ($mode != 0 && $mode != 1) {
						$rtags->{"digital_zoom"} = $mode;
					}
				}
			}
		} elsif ($tag == 0x8769) {
			my ($sub_entries, $next_ifd, $sifd) =
				read_ifd($tiff_start, $results[0]);
			parse_ifd($tiff_start, $sub_entries, $sifd, $rtags, "exif");
		} elsif ($tag == 0x927c) {
			my $start = -1;

			my $make = $rtags->{"make"};
			if ($make eq "CASIO") {
				$start = 0;
				$makerformat = "CASIO";
			} elsif ($make eq "Canon") {
				$start = 0;
				$makerformat = "CANON";
			} elsif ($make =~ /^OLYMPUS/) {
				if (pack("C5", @results[0..4]) eq "OLYMP") {
					$start = 8;
					$makerformat = "OLYMPUS";
				}
			} elsif ($make =~ /^NIKON/) {
				if (pack("C5", @results[0..4]) eq "Nikon") {
					$start = 8;
					$makerformat = "NIKON0";
				} else {
					$start = 0;
					$makerformat = "NIKON1";
				}
			}
			print "makerformat: $makerformat\n" if ($debug);
			if ($start >= 0) {
				while ($start--) {
					shift @results;
				}
				my ($sub_entries, $next_ifd, $sifd) =
					reread_ifd(pack("C*", @results));
				parse_ifd($tiff_start, $sub_entries, $sifd, $rtags, "makernote");
			}
			undef $makerformat;
		} elsif ($tag == 0x9003) {
			# DateTimeOriginal
			$rtags->{"date"} = $results[0];
		} elsif ($tag == 0x132) {
			# DateTime
			# Don't override results of 0x9003
			if (!defined $rtags->{"date"}) {
				$rtags->{"date"} = $results[0];
			}
		} elsif ($tag == 0x9102 && $name eq "exif") {
			$rtags->{"compressed_bpp"} = $results[0];
		} elsif ($tag == 0x010f && $name eq "IFD0") {
			if (($mode = $results[0]) ne "") {
				$rtags->{"make"} = $mode;
			}
		} elsif ($tag == 0x0110 && $name eq "IFD0") {
			$mode = $results[0];
			$mode =~ s/\s+$//;
			if ($mode ne "") {
				$rtags->{"model"} = $mode;
			}
		} elsif ($tag == 0x829a && $name eq "exif") {
			$rtags->{"shutter"} = $results[0];
		} elsif ($tag == 0x829d && $name eq "exif") {
			$rtags->{"fstop"} = $results[0];

#			my ($mode1, $mode2) = @results;
#			$mode1 = sprintf "F%.1f", $mode1;
#			if ($mode2 ne "") {
#				$mode2 = sprintf "F%.1f", $mode2;
#				$rtags->{"fstop"} = $mode1."-".$mode2;
#			} else {
#				$rtags->{"fstop"} = $mode1;
#			}
		} elsif ($tag == 0x8822 && $name eq "exif") {
			$mode = $results[0];

			$rtags->{"exposure"} =
				{	1 => "manual",
					2 => "program",
					3 => "aperture-priority",
					4 => "shutter-priority",
					5 => "creative",
					6 => "action",
					7 => "portrait",
					8 => "landscape",
				}->{$mode}
				||
				"unknown ($mode)";
		} elsif ($tag == 0x8827 && $name eq "exif") {
			$rtags->{"iso"} = $results[0];
		} elsif ($tag == 0x9201 && $name eq "exif") {
			$mode = $results[0];

			$rtags->{"shutter"} = 1.0/(2 ** $mode);
		} elsif ($tag == 0x9202 && $name eq "exif") {
			$mode = sqrt(2) ** $results[0];
			$rtags->{"fstop"} = $mode;
#			$rtags->{"fstop"} = sprintf "F%.1f", $mode;
		} elsif ($tag == 0x9203 && $name eq "exif") {
			$mode = sqrt(2) ** $results[0];
			$rtags->{"brightness"} = sprintf "EV%.1f", $mode;
		} elsif ($tag == 0x9204 && $name eq "exif") {
			$rtags->{"bias"} = $results[0];
		} elsif ($tag == 0x9205 && $name eq "exif") {
			$mode = sqrt(2) ** $results[0];
			$rtags->{"maxf"} = $mode;
		} elsif ($tag == 0x9206 && $name eq "exif") {
			my ($mode1, $mode2) = @results;

			if (defined $mode2) {
				$rtags->{"focus"} = subjectdistance($mode1)."-".subjectdistance($mode2);
			} else {
				$rtags->{"focus"} = subjectdistance($mode1);
			}
		} elsif ($tag == 0x9207 && $name eq "exif") {
			$mode = $results[0];

			$rtags->{"metering"} =
				{	1 => "average",
					2 => "centre-weighted",
					3 => "spot",
					4 => "multispot",
					5 => "matrix/evaluative",
					6 => "partial",
				}->{$mode}
				||
				"unknown ($mode)";
		} elsif ($tag == 0x9208 && $name eq "exif") {
			my $mode =  $results[0];

			if ($mode < 32768) {
				$rtags->{"white_balance"} =
					{	0 => "auto",
						1 => "daylight",
						2 => "flourescent",
						3 => "tungsten",
						10 => "flash",
						17 => "std illuminant A",
						18 => "std illuminant B",
						19 => "std illuminant C",
						20 => "D55 illuminant",
						21 => "D65 illuminant",
						22 => "D75 illuminant",
					}->{$mode}
					||
					"unknown ($mode)";
			} else {
				$mode &= 0x7fff;
				$rtags->{"white_balance"} = $mode."K";
			}
		} elsif ($tag == 0x9209 && $name eq "exif") {
			my $mode =  $results[0];

			$rtags->{"flash"} =
				{	0 => "no",
					1 => "fired",
					5 => "fired [return not sensed]",
					7 => "fired [return sensed]",
					9 => "fired [fill][no sensing]",
					13 => "fired [fill][return not sensed]",
					15 => "fired [fill][return sensed]",
					16 => "no [off]",
					24 => "no [auto]",
					25 => "fired [auto][no sensing]",
					29 => "fired [auto][return not sensed]",
					31 => "fired [auto][return sensed]",
					32 => "",
				}->{$mode}
				||
				"unknown ($mode)";
			if ($rtags->{"flash"} eq "") {
				delete $rtags->{"flash"};
			}
		} elsif ($tag == 0x920a && $name eq "exif") {
			$rtags->{"focal"} = $results[0];
		} elsif ($tag == 0x0112 && $name eq "IFD0") {
			$mode = undef;
			$mode = {
					# Many cameras report 1 when they
					# mean 9.  Just assume that for all
					# cameras.
					#1 => "landscape",
					3 => "landscape+180",
					6 => "portrait+90",
					8 => "portrait-90",
				}->{$results[0]};
			if (defined $mode) {
				$rtags->{"orientation"} = $mode;
			}
		} elsif (($tag == 0x920e && $name eq "IFD0") ||
			($tag == 0xa20e && $name eq "exif")) {
			$rtags->{"focalxres"} = $results[0];
		} elsif (($tag == 0x920f && $name eq "IFD0") ||
			($tag == 0xa20f && $name eq "exif")) {
			$rtags->{"focalyres"} = $results[0];
		} elsif ($tag == 0x9210 && $name eq "IFD0") {
			$mode = $results[0];

			$rtags->{"focalresmm"} =
				{
					1 => 25.4,
					2 => 1000,
					3 => 10,
					4 => 1,
					5 => 0.001,
				}->{$mode};
		} elsif ($tag == 0xa210 && $name eq "exif") {
			$mode = $results[0];

			$rtags->{"focalresmm"} =
				{
					2 => 25.4,
					#3 => 10,#8.3,
					# EXIF 2.1 standard says 3 => 10!!!
				}->{$mode};
		} elsif ($tag == 0xa001 && $name eq "exif") {
			$rtags->{"colorspace"} = $results[0];
		} elsif ($tag == 0x0100 && $name eq "IFD0") {
			$rtags->{"width"} = $results[0];
		} elsif ($tag == 0x0101 && $name eq "IFD0") {
			$rtags->{"length"} = $results[0];
		} elsif ($tag == 0xa002 && $name eq "exif") {
			$rtags->{"width"} = $results[0];
		} elsif ($tag == 0xa003 && $name eq "exif") {
			$rtags->{"length"} = $results[0];
		} elsif ($tag == 0x0131 && $name eq "IFD0") {
			$rtags->{"firmware"} = $results[0];
		}
	}
	print "end IFD: $name\n" if ($debug);
}


sub parse_tiff {
	local($tiff_start, $header, $rtags) = @_;
	my ($bytes, @bytes);
	my ($offset, $position);

	$position = 2;
	if ($header eq "MM") {
		print "Motorola format\n" if ($debug);
		$long = "N";
		$short = "n";
	} elsif ($header eq "II") {
		print "Intel format\n" if ($debug);
		$long = "V";
		$short = "v";
	} else {
		print "Unknown TIFF format\n" if ($debug);
		return;
	}
	if (!($bytes = read(IMG, $buf, 2))) {
		return;
	}
	@bytes = unpack("$short", $buf);
	$tiff_version = $bytes[0];
	if ($tiff_version < 42) {
		print "TIFF version too old to handle\n" if ($debug);
		return;
	}
	if (!($bytes = read(IMG, $buf, 4))) {
		return;
	}
	@bytes = unpack("$long", $buf);
	$next_ifd = $bytes[0];

	my $ifd_no = 0;
	while ($next_ifd) {
		($dir_entries, $next_ifd, $ifd) =
			read_ifd($tiff_start, $next_ifd);
		parse_ifd($tiff_start, $dir_entries, $ifd, $rtags, "IFD$ifd_no");
		$rtags->{"ok"} = 1;
		$ifd_no++;
	}
	if (defined (my $imagewidth = $rtags->{"width"}) &&
		defined (my $imagelength = $rtags->{"length"}) &&
		defined (my $focalxres = $rtags->{"focalxres"}) &&
		defined (my $focalyres = $rtags->{"focalyres"}) &&
		defined (my $focalresmm = $rtags->{"focalresmm"})) {
		my $w = $imagewidth * $focalresmm / $focalxres;
		my $l = $imagelength * $focalresmm / $focalyres;

		$rtags->{"imagersize"} = sqrt($l * $l + $w * $w);
		$rtags->{"imagerwidth"} = $w;
		$rtags->{"imagerlength"} = $l;
		$rtags->{"focal_factor"} = sqrt(24 * 24 + 36 * 36) /
					$rtags->{"imagersize"};
		if ($l < $w) {
			$rtags->{"focal_factor_x"} = 36 / $w;
			$rtags->{"focal_factor_y"} = 24 / $l;
		} else {
			$rtags->{"focal_factor_x"} = 24 / $w;
			$rtags->{"focal_factor_y"} = 36 / $l;
		}
	}
	if ($debug) {
		foreach my $tag (sort keys %$rtags) {
			printf "\t%-20s%s\n", $tag.":", $rtags->{$tag};
		}
	}
	$rtags;
}


sub analyse_file {
	local($file, $rtags) = @_;
	my ($bytes, @bytes);

	print "Opening $file\n" if ($debug);
	undef $makerformat;
	if (!open(IMG, "<$file")) {
		print STDERR "$file: $!\n";
		return;
	}
	if (!($bytes = read(IMG, $buf, 2))) {
		close(IMG); return;
	}
	@bytes = unpack("n", $buf);
	if ($bytes[0] == 0xffd8) {
		print "JPEG data\n" if ($debug);
		my $ok = 1;
		while ($ok) {
			if (!($bytes = read(IMG, $buf, 2))) {
				$ok = 0; last;
			}
			@bytes = unpack("CC", $buf);
			if ($bytes[0] == 0xff) {
				$marker = 0xff00 | $bytes[1];
				if (!($bytes = read(IMG, $buf, 2))) {
					$ok = 0; last;
				}
				@bytes = unpack("n", $buf);
				$length = $bytes[0] - 2;
				if ($marker == 0xffe1) {
					print "Found APP1 marker\n" if ($debug);
					if (!($bytes = read(IMG, $buf, 6))) {
						$ok = 0; last;
					}
					$length -= 6;
					if ($buf eq pack("a6", "Exif")) {
						print "\tEXIF data\n" if ($debug);
						$tiff_offset = tell IMG;
						if (!($bytes = read(IMG, $buf, 2))) {
							$ok = 0; last;
						}
						print "-----------------\n" if ($debug);
						parse_tiff($tiff_offset, $buf, $rtags);
						print "-----------------\n" if ($debug);
						$ok = 0; last;
					} else {
						seek(IMG, $length, 1);
					}
				} elsif ($marker == 0xffda) {
					print "Found SOS marker\n" if ($debug);
					$ok = 0; last;
				} else {
					printf "Found %x marker\n", $marker if ($debug);
					seek(IMG, $length, 1);
				}
			} else {
				# not a marker
				# abort
				$ok = 0; last;
			}
		}
	} else {
		print "not JPEG\n" if ($debug);
		if ($buf eq "MM" || $buf eq "II") {
			print "Looks like TIFF\n" if ($debug);
			print "-----------------\n" if ($debug);
			parse_tiff(0, $buf, $rtags);
			print "-----------------\n" if ($debug);
		}
	}
	close(IMG);
	$rtags;
}

package main;

my $exif = {};

sub ev {
	local($shutter, $aperture, $iso, $bias) = @_;

	return undef if (!defined($shutter) || $shutter == 0);
	return undef if (!defined($aperture));
	return undef if (!defined($iso) || $iso == 0);
	$bias = 0 if (!defined($bias));
	if ($aperture =~ /^F([0-9\.]+)/) {
		$aperture = $1;
	}
	return undef if (!$aperture);
	sprintf("%.1f",
		log(1/$shutter) / log(2) +
		log($aperture) / log(sqrt(2)) +
		log($iso / 100) / log(2) +
		$bias);
}

sub dof {
	local($focal, $aperture, $distance, $focal_factor, $xres, $yres) = @_;

	$distance =~ s/m$//;
	$distance *= 1000;			# Convert to mm
	my $c = 0.033;				# Circle of Confusion => 0.033mm

	# Currently v/2 pixels in CofC (a complete guess - need to think more)
	#
	$c = sqrt(2) *
		43.2 / ($focal_factor * sqrt($xres * $xres + $yres * $yres));

	my $h = $focal * $focal / ($aperture * $c); # hyperfocal distance

	my $d = $distance + $focal;

	my $dnear = $h * $distance / ( $h + $d);
	$dnear = sprintf("%.3fm", $dnear / 1000);

	my $t = $h - $d;
	my $dfar;
	if ($t > 0) {
		$dfar = sprintf("%.3fm", ($h * $distance / $t ) / 1000);
	} else {
		$dfar = "infinity";
	}
	return($dnear, $dfar);
}

# Read the cameras.txt file (snarfed from Thumber)
#	http://tawba.tripod.com/thumber.htm
# This should let us interpret the EXIF data appropriately.
#
sub read_cameras {
	my ($model, $camfile);
	my (@camfiles) = (
		$ENV{"CAMERAS_TXT"},
		$ENV{"HOME"} . "/lib/cameras.txt",
		$0 . "/../../lib/cameras.txt",
	);

    foreach $camfile (@camfiles) {
	next if ($camfile eq "");
	if (open(CAMERAS, "<$camfile")) {
		$model = "";
		@camera_models = ();
		while (<CAMERAS>) {
			chomp;
			s/
$//;
			s/;.*$//;
			next if (/^$/);
			if (/^\[(.*)\]/) {
				$model = $1;
				push(@camera_models, $model);
			} elsif (/^\s*(.*)\s*=\s*(\S+)/) {
				($field, $v) = ($1, $2);
				$field =~ tr/A-Z/a-z/;
				if ($field eq "iso") {
					$cameras{$model}{"iso"}{"default"} = $v;
				} elsif ($field =~ /^iso(\d+)/) {
					$cameras{$model}{"iso"}{$1} = $v;
				} elsif ($field =~ /^actual\s+focal/) {
					$cameras{$model}{"actual"} = $v;
				} elsif ($field =~ /^equiv(\S*)\s+focal/) {
					$cameras{$model}{"equiv"} = $v;
				} else {
					$cameras{$model}{$field} = $v;
				}
			}
		}
		close(CAMERAS);
		return;
	}
    }
}

# Get EXIF/other info about the image "$tag"
#
# "original/$tag.inf" is then read in, allowing manual setting of
# data not in the EXIF header (e.g. tripod / teleconverter information)
#
sub get_info {
	local($tag) = @_;
	my ($model, $original, $file);
	local (*INFO);

	my $orig = $orig_file{$tag};
	$original = "";
	foreach $file (<original/$orig.*>) {
		next if ($file eq "original/$orig.exif");
		next if ($file eq "original/$orig.inf");
		$original = $file;
		last;
	}
	return if ($original eq "");
	exif::analyse_file($original, $exif{$tag} = {});
	if (open(INFO, "<original/$orig.inf")) {
		my ($field, $data) = ("", "");
		while (<INFO>) {
			chomp;
			if (/^(\S+)\s+(\S.*)$/) {
				($field, $data) = ($1, $2);
				$field =~ tr/A-Z/a-z/;
				$exif{$tag}{$field} = $data;
				next;
			}
			if (/^\s+(\S.*)$/) {
				$data = $2;
				$exif{$tag}{$field} = $data if ($field ne "");
				next;
			}
			$field = "" if (/^\s*$/);
		}
		close(INFO);
	}
	if ($exif{$tag}{"ok"}) {
		if (defined (my $w = $exif{$tag}{"width"}) &&
				defined (my $l = $exif{$tag}{"length"})) {
			$exif{$tag}{"mode"} = $w."x".$l;
		}
		$model = $exif{$tag}{"model"};
		if ($model =~ /^Canon EOS D30/) {
			my $comp =
				{	2 => "Normal",
					3 => "Fine",
				}->{$exif{$tag}{"compressed_bpp"}};
			if (defined $comp) {
				$exif{$tag}{"mode"} .= " $comp";
			}
		}
		undef $this_cam;
		foreach my $camera (@camera_models) {
			if ($model =~ /^$camera/) {
				if ($cameras{$camera}{"actual"} &&
					!defined $exif{$tag}{"focal_factor"}) {
					$exif{$tag}{"focal_factor"} =
						$cameras{$camera}{"equiv"} /
						$cameras{$camera}{"actual"};
				}
				$this_cam = $camera;
			}
		}
		my $iso;
		if (defined $this_cam) {
			if (defined ($iso = $exif{$tag}{"iso"})) {
				$exif{$tag}{"iso"} =
					$cameras{$this_cam}{"iso"}{$iso}
					||
					$iso
					||
					$cameras{$this_cam}{"iso"}{"default"};
			} else {
				$exif{$tag}{"iso"} = $cameras{$this_cam}{"iso"}{"default"};
			}
		}
		# Some model names repeat the manufacturer name.
		#
		$make = $exif{$tag}{"make"};
		if ($make =~ /^OLYMPUS/) {
			$make = "Olympus";
		} elsif ($make =~ /^NIKON/) {
			$make = "Nikon";
			$model =~ s/^NIKON\s+//;
		} elsif ($make =~ /^Canon/) {
			$model =~ s/^Canon\s+//;
		} elsif ($make =~ /^Eastman Kodak/i) {
			$make = "Kodak";
			$model =~ s/^KODAK\s+//;
		}
		$exif{$tag}{"camera"} = $make." ".$model;
	}
}

sub short_shutter {
	local($exposure) = $_[0];

	if ($exposure < 1 && $exposure != 0) {
		"1/".int(0.5+1/$exposure)."s";
	} elsif ($exposure == 0) {
		"";
	} else {
		int(0.5+$exposure)."s";
	}
}


sub make_image {
	local($tag, $dir, $vert_size) = @_;

	my $dest = "web/$dir/$tag.jpg";
	if ((! -f $dest) || -M $dest > $mtime{$tag}) {
		my ($x, $y);

		if ($vert_size) {
			$y = $vert_size;
			$x = int(($vert_size * $image{"master/$tag.tiff"}{"x"})/$image{"master/$tag.tiff"}{"y"});
		} else {
			$y = $image{"master/$tag.tiff"}{"y"};
			$x = $image{"master/$tag.tiff"}{"x"};
		}
		print "\t$dir/$tag.jpg\n";
		$source = "master/$tag.tiff";
		if (! -f $source) {
			$source = "master/$tag.tiff.gz";
		}
		if (system("convert", "-cache", $im_cache,
				"-quality", "80",
				"-geometry", $x."x".$y."!",
				#"-sharpen", "50",
				$source."[0]", $dest) != 0) {
			my $err = $?;
			unlink $dest;
			die "convert $source $dest: $err";
		}
	}
}

sub tour_file {
	local($tag, $dir, $vert_size, $prev, $next, $tour) = @_;
	local($x, $y, $section, $f, $g);

	if ($dir eq ".") {
		$home = "";
		$d = "";
	} else {
		$home = "../";
		$d = $dir."/";
	}
	open(FILE, ">web/".$d.$tag.".htm") || die;
	print FILE "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	print FILE "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"DTD/xhtml1-strict.dtd\">\n";
	print FILE "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n";
	print FILE "<head><title>".$tag."(".$tour.")";
	if ($notes{$tag} ne "") {
		print FILE " - ".$notes{$tag};
	}
	print FILE "</title></head>";
	print FILE "<body bgcolor=\"#000000\" text=\"#FFFFFF\" link=\"#BFBFFF\" vlink=\"#4F4FFF\" alink=\"#FFFFFF\">\n";
	$section = $section{$section_name{$tag}}{"name"};
	print FILE $section."<br />";
	if ($vert_size) {
		$y = $vert_size;
		$x = int(($vert_size * $image{"master/$tag.tiff"}{"x"})/$image{"master/$tag.tiff"}{"y"});
	} else {
		$y = $image{"master/$tag.tiff"}{"y"};
		$x = $image{"master/$tag.tiff"}{"x"};
	}
	print FILE "<TABLE border=\"0\" cellspacing=\"0\">";
	print FILE "<TR>";
	print FILE "<TD>";
	if ($prev) {
		print FILE "<a href=\"$prev.htm\">Prev</a>";
	} else {
		print FILE "&nbsp;";
	}
	print FILE "</TD>\n";
	print FILE "<TD ROWSPAN=3>\n";
	print FILE "<img src=\"$tag.jpg\" height=\"$y\" width=\"$x\" />";
	print FILE "</TD>";
	print FILE "</TR><TR><TD>";
	print FILE "This image:\n";
	foreach $t (@tours) {
		if ($tour{$t}{"dir"} eq ".") {
			$d2 = "";
		} else {
			$d2 = $tour{$t}{"dir"}."/";
		}
		print FILE "<br /><a href=\"";
		print FILE $home.$d2.$tag.".htm\">";
		print FILE "<strong>" if ($t eq $tour);
		print FILE $t;
		print FILE "</strong>" if ($t eq $tour);
		print FILE "</a>";
	}
	print FILE "<hr /><a href=\"index.htm#$tag\">Index</a>";
	print FILE "<hr /><a href=\"".($dir ne "." ? "../" : "")."../index.htm\">Parent directory</a>";
	print FILE "</TD></TR>\n";
	print FILE "<TR><TD>";
	if ($next) {
		print FILE "<a href=\"$next.htm\">Next</a>";
	} else {
		print FILE "&nbsp;";
	}
	print FILE "</TD></TR>\n";
	print FILE "<TR><TD COLSPAN=2>";
	if ($notes{$tag} ne "") {
		print FILE "<CENTER>$notes{$tag}</CENTER>";
	}
	print FILE "<br /><font size=\"-1\">".$x."x".$y." (best";
	if ($x != $image{"master/$tag.tiff"}{"x"} || $y != $image{"master/$tag.tiff"}{"y"}) {
		print FILE ": ".$image{"master/$tag.tiff"}{"x"}."x".$image{"master/$tag.tiff"}{"y"};
	}
	print FILE ")";
	my $rexif = $exif{$tag};
	if (defined $rexif->{"date"}) {
		print FILE " ".$exif{$tag}{"date"};
	}
	print FILE "<br />";
	$comma = 0;
	if (defined ($f = $rexif->{"shutter"})) {
		if (($g = &short_shutter($f)) ne "") {
			print FILE $g;
			$comma = 1;
		}
	}
	if (($g = $rexif->{"fstop"}) ne "") {
		print FILE ", " if ($comma); $comma = 1;
		printf FILE "F%.1f", $g;
	}
	if (defined ($f = $rexif->{"iso"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "ISO $f";
		if (defined ($f = $rexif->{"iso_mode"})) {
			print FILE " ($f)";
		}
	}
	if (defined ($f = ev($rexif->{"shutter"}, $rexif->{"fstop"}, $rexif->{"iso"}, $rexif->{"bias"}))) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE $f, "EV";
	}
	if ((defined ($f = $rexif->{"bias"})) && $f != 0) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Bias: ", sprintf("%.1f", $f), "EV";
	}
	if (defined ($f = $rexif->{"orientation"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Orientation: $f";
	}
	if (defined ($f = $rexif->{"focal"}) && $f ne "") {
		print FILE ", " if ($comma); $comma = 1;
		printf FILE "%.1fmm", $f;
		if (defined ($g = $rexif->{"maxf"}) && $g ne "") {
			printf FILE "/%.1f", $g;
		}
		printf FILE " (";
		if (($factor = $rexif->{"focal_factor"}) > 0) {
			print FILE int(0.5+$rexif->{"focal"}*$factor)."mm";
		} else {
			print FILE "unknown";
		}
		print FILE " equiv)";
	}
#	if (($g = $rexif->{"brightness"}) ne "") {
#		print FILE ", " if ($comma); $comma = 1;
#		print FILE "Brightness: ", $g;
#	}
	$f = $rexif->{"flash"};
	$m = $rexif->{"flash_mode"};
	if (defined $f || defined $m) {
		if (!defined $f) {
			$f = "";
		} elsif (defined $m) {
			$m = " ($m)";
		} else {
			$m = "";
		}
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Flash: $f$m";
	}
	if (($f = $rexif->{"digital_zoom"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Digital zoom: ", $f, "x";
	}
	$f = $rexif->{"focus"};
	$m = $rexif->{"focus_mode"};
	if (defined $f || defined $m) {
		$f = "" if (!defined $f);
		$m = "" if (!defined $m);
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Focus: $m $f";
		if ($f != 0
			&& $f ne "infinite"
			&& defined ($focal = $rexif->{"focal"})
			&& $focal ne ""
			&& ($aperture = $rexif->{"fstop"}) ne ""
			&& ($factor = $rexif->{"focal_factor"}) > 0) {

			# This assumes that all cameras include the digital
			# zoom factor in the reported focal length.
			# This is at least true for the Nikon CoolPix units.
			if (defined (my $zoom = $rexif->{"digital_zoom"})) {
				$focal /= $zoom;
			}

			my ($dn, $df) = dof($focal, $aperture, $f, $factor, $rexif->{"width"}, $rexif->{"length"});
			print FILE ", DOF: ", $dn, "-", $df;
		}
	}
	print FILE "<br />"; $comma = 0;
	if (defined ($f = $rexif->{"camera"})) {

		print FILE "Camera: ".$f;
		if (defined ($f = $rexif->{"mode"})) {
			print FILE " - $f mode";
		}
		$comma = 1;
	}
	if (defined ($f = $rexif->{"quality"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE " Quality: $f";
	}
	if (defined ($f = $rexif->{"firmware"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE " S/w: $f";
	}
	print FILE "<br />"; $comma = 0;
	if (defined ($f = $rexif->{"metering"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Metering: $f";
	}
	if (defined ($f = $rexif->{"exposure"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Exposure Mode: $f";
	}
	if (defined ($f = $rexif->{"white_balance"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "White Balance: $f";
	}
	if (defined ($f = $rexif->{"color_mode"})) {
		print FILE ", " if ($comma); $comma = 1;
		print FILE "Color Mode: $f";
	}
	if (defined ($r = $rexif->{"image_adjustment"})) {
		foreach $f (sort keys %$r) {
			print FILE ", " if ($comma); $comma = 1;
			print FILE $f, ": ", $r->{$f};
		}
	}
	print FILE "<br />" if ($comma); $comma = 0;
	if (defined ($f = $rexif->{"cfn"})) {
		print FILE "Custom Functions: $f";
		if (defined ($r = $rexif->{"cfns"})) {
			print FILE "<br />";
			print FILE "<table border=\"1\">";
			my $row = 0;
			foreach $cfn (sort {$a <=> $b} keys %$r) {
				if (!$row) {
					print FILE "<tr>";
					$row = 1;
				} else {
					$row = 0;
				}
				print FILE "<td>";
				print FILE "<font size=\"-1\">";
				print FILE $cfn, ": ", $r->{$cfn}{"description"};
				print FILE "</font>";
				print FILE "</td><td>";
				print FILE "<font size=\"-1\">";
				print FILE $r->{$cfn}{"vdesc"};
				print FILE "</font>";
				print FILE "</td>";
				print FILE "</tr>" if (!$row);
			}
			print FILE "</tr>" if ($row);
			print FILE "</table>";
		}
	}
	print FILE "</font>";
	if (defined $copyright{$tag}) {
		print FILE "<br />".$copyright{$tag};
	}
	print FILE "</TD></TR>\n";
	print FILE "</TABLE>\n";

	print FILE "</body></html>\n";
	close(FILE);
}

sub make_tour {
	local($dir, $vert_size, $tour) = @_;
	my ($first, $otag, $tag, %prev, %next, $last);

	$first = 0;
	$otag = 0;
	foreach $tag (@tags) {
		$prev{$tag} = $otag;
		$next{$otag} = $tag;
		$otag = $tag;
		$first = $tag if (!$first);
		$last = $tag;
	}
	$next{$tag} = 0;
	mkdir "web/$dir", 0777;
	foreach $tag (@tags) {
		&tour_file($tag, $dir, $vert_size, $prev{$tag}, $next{$tag}, $tour);
	}
	system("cp", "web/$dir/$first.htm", "web/$dir/start.htm") == 0
		or die "cp: $?";
}


if (($thumbs_only = ($ARGV[0] eq "thumbs"))) {
	print "Thumbnails only\n";
}

if (defined $ENV{"IM_CACHE"}) {
	$im_cache = $ENV{"IM_CACHE"};
	print "Limiting ImageMagick RAM cache to $im_cache MB\n";
} else {
	$im_cache = 80; # ImageMagick default
}

print "Parsing configurations...\n";

&read_cameras;
$count = 1;
if (open(ATTRIB, "<config/attrib")) {
	while (<ATTRIB>) {
		chomp;
		if (/^(\S+)\s+(.*)$/) {
			$attrib{$count}{"pattern"} = $1;
			$attrib{$count}{"copyright"} = $2;
			$count++;
		}
	}
	close(ATTRIB);
}
if (-r "config/setup") {
	$images_file = "config/setup";
} elsif (-r "config/notes") {
	$images_file = "config/notes";
}
$count = 1;
$scount = 1;
$section = "default";
if (open(NOTES, "<$images_file")) {
	while (<NOTES>) {
		chomp;
		s/^#.*$//;
		if (/^Title\s+(.*)$/) {
			$title = $1;
			next;
		}
		if (/^Section\s+(.*)$/) {
			$scount++;
			$section{$scount}{"name"} = $section = $1;
			next;
		}
		if (/^(\S+)\s+(.*)$/) {
			my ($file, $notes) = ($1, $2);
			my $orig;

			if ($file =~ /^(.+),(.+)$/) {
				($file, $orig) = ($1, $2);
			} else {
				$orig = $file;
			}
			$notes{$file} = $notes;
			$orig_file{$file} = $orig;
			$list{$file} = $count;
			$section{$scount}{"tags"}{$count} = $1;
			$section_name{$file} = $scount;
			$count++;
			next;
		}
		if (/^(\S+)\s*$/) {
			my $file = $1;
			my $orig;

			if ($file =~ /^(.+),(.+)$/) {
				($file, $orig) = ($1, $2);
			} else {
				$orig = $file;
			}
			$notes{$file} = "";
			$orig_file{$file} = $orig;
			$list{$file} = $count;
			$section{$scount}{"tags"}{$count} = $1;
			$section_name{$1} = $scount;
			$count++;
			next;
		}
	}
	close(NOTES);
}

@tours = ();

if (open(TOURS, "<config/tours")) {
	while (<TOURS>) {
		chomp;
		s/^#.*$//;
		if (/^(\S+)\s+(\S+)\s+(\d+)$/) {
			push @tours, $1;
			$tour{$1}{"dir"} = $2;
			$tour{$1}{"vert"} = $3;
		} elsif (/^(\S+)\s+(\S+)\s+(\d+)\s+(.*)$/) {
			push @tours, $1;
			$tour{$1}{"dir"} = $2;
			$tour{$1}{"vert"} = $3;
			$tour{$1}{"description"} = $4;
		}
	}
	close(TOURS);
}
if (! scalar @tours) {
	print STDERR "No tours defined\n";
	exit 1;
}
@tags = sort {$list{$a} <=> $list{$b}} keys %list;

if (! -d "web/.") {
	mkdir "web", 0777 || die "mkdir web: $?";
}
if (! -d "web/thumbs") {
	mkdir "web/thumbs", 0777 || die "mkdir web/thumbs: $?";
}
print "Checking thumbnails...\n";
foreach $tag (@tags) {
	$source = "master/$tag.tiff";
	if (! -f $source) {
		$source = "master/$tag.tiff.gz";
	}
	if (! -f $source) {
		die "$source nonexistant\n";
	}
	$mtime{$tag} = -M $source;
	my $dest = "web/thumbs/$tag.jpg";
	if ((! -f $dest) || -M $dest > $mtime{$tag}) {
		print "\tthumbs/$tag.jpg\n";
		if (system("convert", "-cache", $im_cache,
				"-geometry", "160x160",
				$source."[0]", $dest) != 0) {
			my $err = $?;
			unlink $dest;
			die "convert $source $dest: $err";
		}
	}
}

# Make an index page for the thumbnails.
# This is not for any browsing purpose, just to allow review of the
# thumbnails prior to rotations
#
open(THUMBS, ">web/thumbs/index.htm") || die;
print THUMBS "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
print THUMBS "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"DTD/xhtml1-strict.dtd\">\n";
print THUMBS "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n";
print THUMBS "<body bgcolor=\"#000000\" text=\"#FFFFFF\" link=\"#BFBFFF\" vlink=\"#4F4FFF\" alink=\"#FFFFFF\">\n";
foreach $tag (@tags) {
	print THUMBS "<a href=\"../$tag.jpg\"><img src=\"$tag.jpg\" alt=\"$tag\" border=\"0\" /></a>\n";
}
print THUMBS "</body></html>\n";
close(THUMBS);

exit 0 if ($thumbs_only);

print "Reading EXIF info...\n";

foreach $tag (@tags) {
	&get_info($tag);
	foreach $copy (sort {$a <=> $b} keys %attrib) {
		if ($tag =~ /$attrib{$copy}{"pattern"}/) {
			$copyright{$tag} = "<font size=-2>".$attrib{$copy}{"copyright"}."</font>";
			last;
		}
	}
}

print "Identifying image sizes...\n";

system("cp", $images_file, "web/setup") == 0
	or die "cp: $?";
chmod 0644, "web/setup"; # make sure we can overwrite it next time

%image = {};
foreach $tag (@tags) {
	if (open(IDENTIFY, "identify -cache $im_cache web/thumbs/$tag.jpg|")) {
		while (<IDENTIFY>) {
			if (/JPEG\s+(\d+)x(\d+)/) {
				$image{"thumbs/$tag.jpg"}{"x"} = $1;
				$image{"thumbs/$tag.jpg"}{"y"} = $2;
			}
		}
		close(IDENTIFY);
	}
	my $got_size = 0;
	if (-f "master/$tag.siz" && (-M "master/$tag.siz" < $mtime{$tag})) {
		if (open(SIZE_CACHE, "<master/$tag.siz")) {
			while (<SIZE_CACHE>) {
				if (/^(\S+)\s+(\d+)/) {
					my $axis = $1;

					$axis =~ tr/A-Z/a-z/;
					$image{"master/$tag.tiff"}{$axis} = $2;
				}
			}
			close(SIZE_CACHE);
			if ($image{"master/$tag.tiff"}{"x"} > 0 &&
			    $image{"master/$tag.tiff"}{"y"} > 0) {
				$got_size = 1;
			}
		}
	}
	if (!$got_size) {
		$file = "master/$tag.tiff";
		if ((! -f $file) && -f $file.".gz") {
			$file = $file.".gz";
		}
		print "\t$file\n";
		if (open(IDENTIFY, "identify $file|")) {
			while (<IDENTIFY>) {
				if (/TIFF\s+(\d+)x(\d+)/) {
					$image{"master/$tag.tiff"}{"x"} = $1;
					$image{"master/$tag.tiff"}{"y"} = $2;
					$got_size = 1;
				}
			}
			close(IDENTIFY);
		}
		if ($got_size) {
			# Don't error if we can't open it - it might be
			# on a read-only filesystem.
			if (open(SIZE_CACHE, ">master/$tag.siz")) {
				print SIZE_CACHE "# Cached size information\n";
				print SIZE_CACHE "x ",
					$image{"master/$tag.tiff"}{"x"}, "\n";
				print SIZE_CACHE "y ",
					$image{"master/$tag.tiff"}{"y"}, "\n";
				close(SIZE_CACHE);
			}
		}
	}
}

# Make all the HTML
foreach $tour (@tours) {
	print "Making HTML: $tour\n";
	$dir = $tour{$tour}{"dir"};
	&make_tour($dir, $tour{$tour}{"vert"}, $tour);
	open(INDEX, ">web/".$dir."/index.htm") || die;

	print INDEX "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	print INDEX "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"DTD/xhtml1-strict.dtd\">\n";
	print INDEX "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n";
	print INDEX "<head><title>";
	print INDEX $title." " if (defined $title);
	print INDEX "(".$tour.")</title></head>";
	print INDEX "<body bgcolor=\"#000000\" text=\"#FFFFFF\" link=\"#BFBFFF\" vlink=\"#4F4FFF\" alink=\"#FFFFFF\">\n";
	print INDEX "<h1>".$title."</h1>" if (defined $title);
	#print INDEX $tour;
	#print INDEX " - ".$tour{$tour}{"description"} if $tour{$tour}{"description"};

	print INDEX "<h3>Tours:</h3>\n";
	print INDEX "<table border=1><tr>";
	print INDEX "<th>Index</th>";
	print INDEX "<th>Start</th>";
	print INDEX "<th>Description</th>";
	print INDEX "</tr>";

	foreach $stour (@tours) {
		$sdir = $tour{$stour}{"dir"};
		$sdir = "../$sdir" if ($dir ne ".");
		print INDEX "<tr><td>";
		print INDEX "<a href=\"$sdir/index.htm\">";
		print INDEX "<strong>" if ($stour eq $tour);
		print INDEX $stour;
		print INDEX "</strong>" if ($stour eq $tour);
		print INDEX "</a>";
		print INDEX "</td><td>";
		print INDEX "<a href=\"$sdir/start.htm\">";
		print INDEX "<strong>" if ($stour eq $tour);
		print INDEX $stour;
		print INDEX "</strong>" if ($stour eq $tour);
		print INDEX "</a>";
		print INDEX "</td><td>";
		print INDEX "* " if ($stour eq $tour);
		print INDEX $tour{$stour}{"description"};
		print INDEX "</td></tr>";
	}
	print INDEX "</table>";
	print INDEX "<br><a href=\"".($dir ne "." ? "../" : "")."../index.htm\">Parent directory</a>";

	foreach $section (sort {$a <=> $b} keys %section) {
		print INDEX "<hr /><h2>".$section{$section}{"name"}."</h2>\n";
		foreach $tag (sort {$list{$a} <=> $list{$b}} values %{$section{$section}{"tags"}}) {
			$thumbf = $thumb = "thumbs/$tag.jpg";
			$thumbf = "../$thumb" if ($dir ne ".");

			print INDEX "<a name=\"$tag\"></a>";
			print INDEX "<a href=\"$tag.htm\">";
			print INDEX "<img src=\"$thumbf\" width=\"".$image{$thumb}{"x"};
			print INDEX "\" alt=\"";
			if ($notes{$tag} ne "") {
				$t = $notes{$tag};
				$t =~ s/"/&quot;/g;
				print INDEX $t;
			} else {
				print INDEX "$tag";
			}
			print INDEX "\"";
			print INDEX " border=\"1\"";
			print INDEX " height=\"".$image{$thumb}{"y"}."\" /></a>\n";
		}
	}
	print INDEX "<hr /><font size=\"-1\">";
	print INDEX "Index last updated: ".scalar localtime(time);
	print INDEX "</font>";
	print INDEX "</body></html>\n";
	close(INDEX);
}
# Make all the appropriate JPEGs
print "Making images...\n";
foreach $tag (@tags) {
	foreach $tour (@tours) {
		make_image($tag, $tour{$tour}{"dir"}, $tour{$tour}{"vert"});
	}
}
