From 2ffa449d9d9a022ea0fe0b6ba6024b9decee13da Mon Sep 17 00:00:00 2001 From: KAMADA Ken'ichi Date: Sun, 7 Apr 2019 21:11:14 +0900 Subject: [PATCH] Support displaying a value with its unit. - Field::display_value and DisplayValue::with_unit were introduced. - The string "centimeter" was changed to "cm" for consistency with "m", "mm", and "km". - F-number was changed to be prefixed by "f/" only when with_unit() is called, though it is actually not a unit. --- examples/dumpexif.rs | 3 +- examples/reading.rs | 8 +- src/lib.rs | 2 +- src/reader.rs | 44 ++++++++- src/tag.rs | 198 ++++++++++++++++++++++++++++++++++++++--- src/tiff.rs | 206 ++++++++++++++++++++++++++++++++++++++++++- src/value.rs | 5 +- tests/unit.tif | Bin 0 -> 691 bytes 8 files changed, 448 insertions(+), 18 deletions(-) create mode 100644 tests/unit.tif diff --git a/examples/dumpexif.rs b/examples/dumpexif.rs index 312c3eb..3ca6f3b 100644 --- a/examples/dumpexif.rs +++ b/examples/dumpexif.rs @@ -47,7 +47,8 @@ fn dump_file(path: &Path) -> Result<(), exif::Error> { println!("{}", path.display()); for f in reader.fields() { let thumb = if f.thumbnail { "1/" } else { "0/" }; - println!(" {}{}: {}", thumb, f.tag, f.value.display_as(f.tag)); + println!(" {}{}: {}", + thumb, f.tag, f.display_value().with_unit(&reader)); if let exif::Value::Ascii(ref s) = f.value { println!(" Ascii({:?})", s.iter().map(escape).collect::>()); diff --git a/examples/reading.rs b/examples/reading.rs index 65ab9b3..1df19ab 100644 --- a/examples/reading.rs +++ b/examples/reading.rs @@ -35,8 +35,9 @@ fn main() { let file = File::open("tests/exif.jpg").unwrap(); let reader = Reader::new(&mut BufReader::new(&file)).unwrap(); - // To obtain a string representation, `Value::display_as` can be used - // for any tag. + // To obtain a string representation, `Value::display_as` + // or `Field::display_value` can be used. To display a value with its + // unit, call `with_unit` on the return value of `Field::display_value`. let tag_list = [Tag::ExifVersion, Tag::PixelXDimension, Tag::XResolution, @@ -44,7 +45,8 @@ fn main() { Tag::DateTime]; for &tag in tag_list.iter() { if let Some(field) = reader.get_field(tag, false) { - println!("{}: {}", field.tag, field.value.display_as(field.tag)); + println!("{}: {}", + field.tag, field.display_value().with_unit(&reader)); } } diff --git a/src/lib.rs b/src/lib.rs index 47c72d0..3ebae76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ //! &mut std::io::BufReader::new(&file)).unwrap(); //! for f in reader.fields() { //! println!("{} {} {}", -//! f.tag, f.thumbnail, f.value.display_as(f.tag)); +//! f.tag, f.thumbnail, f.display_value().with_unit(&reader)); //! } //! } //! ``` diff --git a/src/reader.rs b/src/reader.rs index 73a501b..9ad1b91 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -33,10 +33,20 @@ use crate::error::Error; use crate::jpeg; use crate::tag::Tag; use crate::tiff; -use crate::tiff::Field; +use crate::tiff::{Field, ProvideUnit}; /// The `Reader` struct reads a JPEG or TIFF image, /// parses the Exif attributes in it, and holds the results. +/// +/// # Examples +/// ``` +/// let file = std::fs::File::open("tests/exif.jpg").unwrap(); +/// let reader = exif::Reader::new( +/// &mut std::io::BufReader::new(&file)).unwrap(); +/// let xres = reader.get_field(exif::Tag::XResolution, false).unwrap(); +/// assert_eq!(format!("{}", xres.display_value().with_unit(&reader)), +/// "72 pixels per inch"); +/// ``` // // The struct Reader is self-contained, which means that it does not // have any external reference. The `fields` field actually refers to @@ -126,6 +136,12 @@ impl Reader { } } +impl<'a> ProvideUnit<'a> for &'a Reader { + fn get_field(self, tag: Tag, thumbnail: bool) -> Option<&'a Field<'a>> { + self.get_field(tag, thumbnail) + } +} + #[cfg(test)] mod tests { use std::fs::File; @@ -188,4 +204,30 @@ mod tests { assert_pat!(reader.get_field(Tag::ExifVersion, false).unwrap().value, Value::Undefined(b"0230", _)); } + + #[test] + fn display_value_with_unit() { + let file = File::open("tests/unit.tif").unwrap(); + let reader = Reader::new(&mut BufReader::new(&file)).unwrap(); + // No unit. + let exifver = reader.get_field(Tag::ExifVersion, false).unwrap(); + assert_eq!(format!("{}", exifver.display_value().with_unit(&reader)), + "2.31"); + // Fixed string. + let width = reader.get_field(Tag::ImageWidth, false).unwrap(); + assert_eq!(format!("{}", width.display_value().with_unit(&reader)), + "15 pixels"); + // Unit tag (with a non-default value). + let gpsalt = reader.get_field(Tag::GPSAltitude, false).unwrap(); + assert_eq!(format!("{}", gpsalt.display_value().with_unit(&reader)), + "0.5 meters below sea level"); + // Unit tag is missing but the default is specified. + let xres = reader.get_field(Tag::XResolution, false).unwrap(); + assert_eq!(format!("{}", xres.display_value().with_unit(&reader)), + "72 pixels per inch"); + // Unit tag is missing and the default is not specified. + let gpslat = reader.get_field(Tag::GPSLatitude, false).unwrap(); + assert_eq!(format!("{}", gpslat.display_value().with_unit(&reader)), + "10 deg 0 min 0 sec [GPSLatitudeRef missing]"); + } } diff --git a/src/tag.rs b/src/tag.rs index 11f6c95..d91efa3 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -92,6 +92,11 @@ impl Tag { pub fn default_value(&self) -> Option { get_tag_info(*self).and_then(|ti| (&ti.default).into()) } + + #[inline] + pub(crate) fn unit(self) -> Option<&'static [UnitPiece]> { + get_tag_info(self).and_then(|ti| ti.unit) + } } impl fmt::Display for Tag { @@ -116,12 +121,40 @@ pub enum Context { Interop, // 0th/1st IFD -- Exif IFD -- Interoperability IFD } +#[derive(Debug)] +pub enum UnitPiece { + Value, + Str(&'static str), + Tag(Tag), +} + +macro_rules! unit { + () => ( None ); + ( $str:literal ) => ( unit![V, concat!(" ", $str)] ); + ( Tag::$tag:ident ) => ( unit![V, " ", Tag::$tag] ); + ( $($tokens:tt)* ) => ( Some(unit_expand!( ; $($tokens)* , )) ); +} + +macro_rules! unit_expand { + ( $($built:expr),* ; ) => ( &[$($built),*] ); + ( $($built:expr),* ; , ) => ( &[$($built),*] ); + ( $($built:expr),* ; V, $($rest:tt)* ) => ( + unit_expand!($($built,)* UnitPiece::Value ; $($rest)*) ); + ( $($built:expr),* ; $str:literal, $($rest:tt)* ) => ( + unit_expand!($($built,)* UnitPiece::Str($str) ; $($rest)*) ); + ( $($built:expr),* ; concat!($($strs:literal),*), $($rest:tt)* ) => ( + unit_expand!($($built,)* UnitPiece::Str(concat!($($strs),*)) ; $($rest)*) ); + ( $($built:expr),* ; Tag::$tag:ident, $($rest:tt)* ) => ( + unit_expand!($($built,)* UnitPiece::Tag(Tag::$tag) ; $($rest)*) ); +} + macro_rules! generate_well_known_tag_constants { ( $( |$ctx:path| $( // Copy the doc attribute to the actual definition. $( #[$attr:meta] )* - ($name:ident, $num:expr, $defval:expr, $dispval:ident, $desc:expr) + ($name:ident, $num:expr, $defval:expr, $dispval:ident, $unit:expr, + $desc:expr) ),+, )+ ) => ( // This is not relevant for associated constants, because @@ -146,12 +179,14 @@ macro_rules! generate_well_known_tag_constants { use std::fmt; use crate::value::Value; use crate::value::DefaultValue; + use super::{Tag, UnitPiece}; pub struct TagInfo { pub name: &'static str, pub desc: &'static str, pub default: DefaultValue, pub dispval: fn(&mut fmt::Write, &Value) -> fmt::Result, + pub unit: Option<&'static [UnitPiece]>, } $($( @@ -161,6 +196,7 @@ macro_rules! generate_well_known_tag_constants { desc: $desc, default: $defval, dispval: super::$dispval, + unit: $unit, }; )+)+ } @@ -185,10 +221,12 @@ generate_well_known_tag_constants!( /// A pointer to the Exif IFD. This is used for the internal structure /// of Exif data and will not be returned to the user. (ExifIFDPointer, 0x8769, DefaultValue::None, d_default, + unit![], "Exif IFD pointer"), /// A pointer to the GPS IFD. This is used for the internal structure /// of Exif data and will not be returned to the user. (GPSInfoIFDPointer, 0x8825, DefaultValue::None, d_default, + unit![], "GPS Info IFD pointer"), |Context::Exif| @@ -196,6 +234,7 @@ generate_well_known_tag_constants!( /// A pointer to the interoperability IFD. This is used for the internal /// structure of Exif data and will not be returned to the user. (InteropIFDPointer, 0xa005, DefaultValue::None, d_default, + unit![], "Interoperability IFD pointer"), // TIFF primary and thumbnail attributes [EXIF23 4.6.4 Table 4, @@ -203,232 +242,344 @@ generate_well_known_tag_constants!( |Context::Tiff| (ImageWidth, 0x100, DefaultValue::None, d_default, + unit!["pixels"], "Image width"), (ImageLength, 0x101, DefaultValue::None, d_default, + unit!["pixels"], "Image height"), (BitsPerSample, 0x102, DefaultValue::Short(&[8, 8, 8]), d_default, + unit![], "Number of bits per component"), (Compression, 0x103, DefaultValue::None, d_compression, + unit![], "Compression scheme"), (PhotometricInterpretation, 0x106, DefaultValue::None, d_photointp, + unit![], "Pixel composition"), (ImageDescription, 0x10e, DefaultValue::None, d_default, + unit![], "Image title"), (Make, 0x10f, DefaultValue::None, d_default, + unit![], "Manufacturer of image input equipment"), (Model, 0x110, DefaultValue::None, d_default, + unit![], "Model of image input equipment"), (StripOffsets, 0x111, DefaultValue::None, d_default, + unit![], "Image data location"), (Orientation, 0x112, DefaultValue::Short(&[1]), d_orientation, + unit![], "Orientation of image"), (SamplesPerPixel, 0x115, DefaultValue::Short(&[3]), d_default, + unit![], "Number of components"), (RowsPerStrip, 0x116, DefaultValue::None, d_default, + unit![], "Number of rows per strip"), (StripByteCounts, 0x117, DefaultValue::None, d_default, + unit![], "Bytes per compressed strip"), (XResolution, 0x11a, DefaultValue::Rational(&[(72, 1)]), d_decimal, + unit![V, " pixels per ", Tag::ResolutionUnit], "Image resolution in width direction"), (YResolution, 0x11b, DefaultValue::Rational(&[(72, 1)]), d_decimal, + unit![V, " pixels per ", Tag::ResolutionUnit], "Image resolution in height direction"), (PlanarConfiguration, 0x11c, DefaultValue::Short(&[1]), d_planarcfg, + unit![], "Image data arrangement"), (ResolutionUnit, 0x128, DefaultValue::Short(&[2]), d_resunit, + unit![], "Unit of X and Y resolution"), (TransferFunction, 0x12d, DefaultValue::None, d_default, + unit![], "Transfer function"), (Software, 0x131, DefaultValue::None, d_default, + unit![], "Software used"), (DateTime, 0x132, DefaultValue::None, d_datetime, + unit![], "File change date and time"), (Artist, 0x13b, DefaultValue::None, d_default, + unit![], "Person who created the image"), (WhitePoint, 0x13e, DefaultValue::None, d_decimal, + unit![], "White point chromaticity"), (PrimaryChromaticities, 0x13f, DefaultValue::None, d_decimal, + unit![], "Chromaticities of primaries"), // Not referenced in Exif. (TileOffsets, 0x144, DefaultValue::None, d_default, + unit![], "Tiled image data location"), // Not referenced in Exif. (TileByteCounts, 0x145, DefaultValue::None, d_default, + unit![], "Bytes per compressed tile"), (JPEGInterchangeFormat, 0x201, DefaultValue::None, d_default, + unit![], "Offset to JPEG SOI"), (JPEGInterchangeFormatLength, 0x202, DefaultValue::None, d_default, + unit![], "Bytes of JPEG data"), (YCbCrCoefficients, 0x211, DefaultValue::Unspecified, d_decimal, + unit![], "Color space transformation matrix coefficients"), (YCbCrSubSampling, 0x212, DefaultValue::None, d_ycbcrsubsamp, + unit![], "Subsampling ratio of Y to C"), (YCbCrPositioning, 0x213, DefaultValue::Short(&[1]), d_ycbcrpos, + unit![], "Y and C positioning"), (ReferenceBlackWhite, 0x214, DefaultValue::ContextDependent, d_decimal, + unit![], "Pair of black and white reference values"), (Copyright, 0x8298, DefaultValue::None, d_default, + unit![], "Copyright holder"), // Exif IFD attributes [EXIF23 4.6.5 Table 7 and 4.6.8 Table 18]. |Context::Exif| (ExposureTime, 0x829a, DefaultValue::None, d_exptime, + unit!["s"], "Exposure time"), - (FNumber, 0x829d, DefaultValue::None, d_fnumber, + (FNumber, 0x829d, DefaultValue::None, d_decimal, + // F-number is dimensionless, but usually prefixed with "F" in Japan, + // "f/" in the U.S., and so on. + unit!["f/", V], "F number"), (ExposureProgram, 0x8822, DefaultValue::None, d_expprog, + unit![], "Exposure program"), (SpectralSensitivity, 0x8824, DefaultValue::None, d_default, + unit![], "Spectral sensitivity"), (PhotographicSensitivity, 0x8827, DefaultValue::None, d_default, + unit![], "Photographic sensitivity"), (OECF, 0x8828, DefaultValue::None, d_default, + unit![], "Optoelectric conversion factor"), (SensitivityType, 0x8830, DefaultValue::None, d_sensitivitytype, + unit![], "Sensitivity type"), (StandardOutputSensitivity, 0x8831, DefaultValue::None, d_default, + unit![], "Standard output sensitivity"), (RecommendedExposureIndex, 0x8832, DefaultValue::None, d_default, + unit![], "Recommended exposure index"), (ISOSpeed, 0x8833, DefaultValue::None, d_default, + unit![], "ISO speed"), (ISOSpeedLatitudeyyy, 0x8834, DefaultValue::None, d_default, + unit![], "ISO speed latitude yyy"), (ISOSpeedLatitudezzz, 0x8835, DefaultValue::None, d_default, + unit![], "ISO speed latitude zzz"), // The absence of this field means non-conformance to Exif, so the default // value specified in the standard (e.g., "0231") should not apply. (ExifVersion, 0x9000, DefaultValue::None, d_exifver, + unit![], "Exif version"), (DateTimeOriginal, 0x9003, DefaultValue::None, d_datetime, + unit![], "Date and time of original data generation"), (DateTimeDigitized, 0x9004, DefaultValue::None, d_datetime, + unit![], "Date and time of digital data generation"), (OffsetTime, 0x9010, DefaultValue::None, d_default, + unit![], "Offset data of DateTime"), (OffsetTimeOriginal, 0x9011, DefaultValue::None, d_default, + unit![], "Offset data of DateTimeOriginal"), (OffsetTimeDigitized, 0x9012, DefaultValue::None, d_default, + unit![], "Offset data of DateTimeDigitized"), (ComponentsConfiguration, 0x9101, DefaultValue::ContextDependent, d_cpntcfg, + unit![], "Meaning of each component"), (CompressedBitsPerPixel, 0x9102, DefaultValue::None, d_decimal, + unit![], "Image compression mode"), (ShutterSpeedValue, 0x9201, DefaultValue::None, d_decimal, + unit!["EV"], "Shutter speed"), (ApertureValue, 0x9202, DefaultValue::None, d_decimal, + unit!["EV"], "Aperture"), (BrightnessValue, 0x9203, DefaultValue::None, d_decimal, + unit!["EV"], "Brightness"), (ExposureBiasValue, 0x9204, DefaultValue::None, d_decimal, + unit!["EV"], "Exposure bias"), (MaxApertureValue, 0x9205, DefaultValue::None, d_decimal, + unit!["EV"], "Maximum lens aperture"), (SubjectDistance, 0x9206, DefaultValue::None, d_subjdist, + unit!["m"], "Subject distance"), (MeteringMode, 0x9207, DefaultValue::Short(&[0]), d_metering, + unit![], "Metering mode"), (LightSource, 0x9208, DefaultValue::Short(&[0]), d_lightsrc, + unit![], "Light source"), (Flash, 0x9209, DefaultValue::Unspecified, d_flash, + unit![], "Flash"), (FocalLength, 0x920a, DefaultValue::None, d_decimal, + unit!["mm"], "Lens focal length"), (SubjectArea, 0x9214, DefaultValue::None, d_subjarea, + unit![], "Subject area"), (MakerNote, 0x927c, DefaultValue::None, d_default, + unit![], "Manufacturer notes"), (UserComment, 0x9286, DefaultValue::None, d_default, + unit![], "User comments"), (SubSecTime, 0x9290, DefaultValue::None, d_default, + unit![], "DateTime subseconds"), (SubSecTimeOriginal, 0x9291, DefaultValue::None, d_default, + unit![], "DateTimeOriginal subseconds"), (SubSecTimeDigitized, 0x9292, DefaultValue::None, d_default, + unit![], "DateTimeDigitized subseconds"), (Temperature, 0x9400, DefaultValue::None, d_optdecimal, + unit!["degC"], "Temperature"), (Humidity, 0x9401, DefaultValue::None, d_optdecimal, + unit!["%"], "Humidity"), (Pressure, 0x9402, DefaultValue::None, d_optdecimal, + unit!["hPa"], "Pressure"), (WaterDepth, 0x9403, DefaultValue::None, d_optdecimal, + unit!["m"], "Water depth"), (Acceleration, 0x9404, DefaultValue::None, d_optdecimal, + unit!["mGal"], "Acceleration"), (CameraElevationAngle, 0x9405, DefaultValue::None, d_optdecimal, + unit!["deg"], "Camera elevation angle"), (FlashpixVersion, 0xa000, DefaultValue::Undefined(b"0100"), d_exifver, + unit![], "Supported Flashpix version"), (ColorSpace, 0xa001, DefaultValue::Unspecified, d_cspace, + unit![], "Color space information"), (PixelXDimension, 0xa002, DefaultValue::None, d_default, + unit!["pixels"], "Valid image width"), (PixelYDimension, 0xa003, DefaultValue::Unspecified, d_default, + unit!["pixels"], "Valid image height"), (RelatedSoundFile, 0xa004, DefaultValue::None, d_default, + unit![], "Related audio file"), (FlashEnergy, 0xa20b, DefaultValue::None, d_decimal, + unit!["BCPS"], "Flash energy"), (SpatialFrequencyResponse, 0xa20c, DefaultValue::None, d_default, + unit![], "Spatial frequency response"), (FocalPlaneXResolution, 0xa20e, DefaultValue::None, d_decimal, + unit![V, " pixels per ", Tag::FocalPlaneResolutionUnit], "Focal plane X resolution"), (FocalPlaneYResolution, 0xa20f, DefaultValue::None, d_decimal, + unit![V, " pixels per ", Tag::FocalPlaneResolutionUnit], "Focal plane Y resolution"), (FocalPlaneResolutionUnit, 0xa210, DefaultValue::Short(&[2]), d_resunit, + unit![], "Focal plane resolution unit"), (SubjectLocation, 0xa214, DefaultValue::None, d_subjarea, + unit![], "Subject location"), (ExposureIndex, 0xa215, DefaultValue::None, d_decimal, + unit![], "Exposure index"), (SensingMethod, 0xa217, DefaultValue::None, d_sensingmethod, + unit![], "Sensing method"), (FileSource, 0xa300, DefaultValue::Undefined(&[3]), d_filesrc, + unit![], "File source"), (SceneType, 0xa301, DefaultValue::Undefined(&[1]), d_scenetype, + unit![], "Scene type"), (CFAPattern, 0xa302, DefaultValue::None, d_default, + unit![], "CFA pattern"), (CustomRendered, 0xa401, DefaultValue::Short(&[0]), d_customrendered, + unit![], "Custom image processing"), (ExposureMode, 0xa402, DefaultValue::None, d_expmode, + unit![], "Exposure mode"), (WhiteBalance, 0xa403, DefaultValue::None, d_whitebalance, + unit![], "White balance"), (DigitalZoomRatio, 0xa404, DefaultValue::None, d_dzoomratio, + unit![], "Digital zoom ratio"), (FocalLengthIn35mmFilm, 0xa405, DefaultValue::None, d_focallen35, + unit!["mm"], "Focal length in 35 mm film"), (SceneCaptureType, 0xa406, DefaultValue::Short(&[0]), d_scenecaptype, + unit![], "Scene capture type"), (GainControl, 0xa407, DefaultValue::None, d_gainctrl, + unit![], "Gain control"), (Contrast, 0xa408, DefaultValue::Short(&[0]), d_contrast, + unit![], "Contrast"), (Saturation, 0xa409, DefaultValue::Short(&[0]), d_saturation, + unit![], "Saturation"), (Sharpness, 0xa40a, DefaultValue::Short(&[0]), d_sharpness, + unit![], "Sharpness"), (DeviceSettingDescription, 0xa40b, DefaultValue::None, d_default, + unit![], "Device settings description"), (SubjectDistanceRange, 0xa40c, DefaultValue::None, d_subjdistrange, + unit![], "Subject distance range"), (ImageUniqueID, 0xa420, DefaultValue::None, d_default, + unit![], "Unique image ID"), (CameraOwnerName, 0xa430, DefaultValue::None, d_default, + unit![], "Camera owner name"), (BodySerialNumber, 0xa431, DefaultValue::None, d_default, + unit![], "Body serial number"), (LensSpecification, 0xa432, DefaultValue::None, d_lensspec, + unit![], "Lens specification"), (LensMake, 0xa433, DefaultValue::None, d_default, + unit![], "Lens make"), (LensModel, 0xa434, DefaultValue::None, d_default, + unit![], "Lens model"), (LensSerialNumber, 0xa435, DefaultValue::None, d_default, + unit![], "Lens serial number"), (Gamma, 0xa500, DefaultValue::None, d_decimal, + unit![], "Gamma"), // GPS attributes [EXIF23 4.6.6 Table 15 and 4.6.8 Table 19]. @@ -436,74 +587,107 @@ generate_well_known_tag_constants!( // Depends on the Exif version. (GPSVersionID, 0x0, DefaultValue::ContextDependent, d_gpsver, + unit![], "GPS tag version"), (GPSLatitudeRef, 0x1, DefaultValue::None, d_gpslatlongref, + unit![], "North or south latitude"), (GPSLatitude, 0x2, DefaultValue::None, d_gpsdms, + unit![Tag::GPSLatitudeRef], "Latitude"), (GPSLongitudeRef, 0x3, DefaultValue::None, d_gpslatlongref, + unit![], "East or West Longitude"), (GPSLongitude, 0x4, DefaultValue::None, d_gpsdms, + unit![Tag::GPSLongitudeRef], "Longitude"), (GPSAltitudeRef, 0x5, DefaultValue::Byte(&[0]), d_gpsaltref, + unit![], "Altitude reference"), (GPSAltitude, 0x6, DefaultValue::None, d_decimal, + unit![V, " meters ", Tag::GPSAltitudeRef], "Altitude"), (GPSTimeStamp, 0x7, DefaultValue::None, d_gpstimestamp, + unit![], "GPS time (atomic clock)"), (GPSSatellites, 0x8, DefaultValue::None, d_default, + unit![], "GPS satellites used for measurement"), (GPSStatus, 0x9, DefaultValue::None, d_gpsstatus, + unit![], "GPS receiver status"), (GPSMeasureMode, 0xa, DefaultValue::None, d_gpsmeasuremode, + unit![], "GPS measurement mode"), (GPSDOP, 0xb, DefaultValue::None, d_decimal, + unit![], "Measurement precision"), (GPSSpeedRef, 0xc, DefaultValue::Ascii(&[b"K"]), d_gpsspeedref, + unit![], "Speed unit"), (GPSSpeed, 0xd, DefaultValue::None, d_decimal, + unit![Tag::GPSSpeedRef], "Speed of GPS receiver"), (GPSTrackRef, 0xe, DefaultValue::Ascii(&[b"T"]), d_gpsdirref, + unit![], "Reference for direction of movement"), (GPSTrack, 0xf, DefaultValue::None, d_decimal, + unit![V, " degrees in ", Tag::GPSTrackRef], "Direction of movement"), (GPSImgDirectionRef, 0x10, DefaultValue::Ascii(&[b"T"]), d_gpsdirref, + unit![], "Reference for direction of image"), (GPSImgDirection, 0x11, DefaultValue::None, d_decimal, + unit![V, " degrees in ", Tag::GPSImgDirectionRef], "Direction of image"), (GPSMapDatum, 0x12, DefaultValue::None, d_default, + unit![], "Geodetic survey data used"), (GPSDestLatitudeRef, 0x13, DefaultValue::None, d_gpslatlongref, + unit![], "Reference for latitude of destination"), (GPSDestLatitude, 0x14, DefaultValue::None, d_gpsdms, + unit![Tag::GPSDestLatitudeRef], "Latitude of destination"), (GPSDestLongitudeRef, 0x15, DefaultValue::None, d_gpslatlongref, + unit![], "Reference for longitude of destination"), (GPSDestLongitude, 0x16, DefaultValue::None, d_gpsdms, + unit![Tag::GPSDestLongitudeRef], "Longitude of destination"), (GPSDestBearingRef, 0x17, DefaultValue::Ascii(&[b"T"]), d_gpsdirref, + unit![], "Reference for bearing of destination"), (GPSDestBearing, 0x18, DefaultValue::None, d_decimal, + unit![V, " degrees in ", Tag::GPSDestBearingRef], "Bearing of destination"), (GPSDestDistanceRef, 0x19, DefaultValue::Ascii(&[b"K"]), d_gpsdistref, + unit![], "Reference for distance to destination"), (GPSDestDistance, 0x1a, DefaultValue::None, d_decimal, + unit![Tag::GPSDestDistanceRef], "Distance to destination"), (GPSProcessingMethod, 0x1b, DefaultValue::None, d_ascii_in_undef, + unit![], "Name of GPS processing method"), (GPSAreaInformation, 0x1c, DefaultValue::None, d_default, + unit![], "Name of GPS area"), (GPSDateStamp, 0x1d, DefaultValue::None, d_gpsdatestamp, + unit![], "GPS date"), (GPSDifferential, 0x1e, DefaultValue::None, d_gpsdifferential, + unit![], "GPS differential correction"), (GPSHPositioningError, 0x1f, DefaultValue::None, d_decimal, + unit!["m"], "Horizontal positioning error"), // Interoperability attributes [EXIF23 4.6.7 Table 16 and 4.6.8 Table 20]. |Context::Interop| (InteroperabilityIndex, 0x1, DefaultValue::None, d_default, + unit![], "Interoperability identification"), ); @@ -571,8 +755,8 @@ fn d_planarcfg(w: &mut fmt::Write, value: &Value) -> fmt::Result { fn d_resunit(w: &mut fmt::Write, value: &Value) -> fmt::Result { let s = match value.get_uint(0) { Some(1) => "no absolute unit", - Some(2) => "pixels per inch", - Some(3) => "pixels per centimeter", + Some(2) => "inch", + Some(3) => "cm", _ => return d_unknown(w, value, "unknown unit "), }; w.write_str(s) @@ -634,12 +818,6 @@ fn d_exptime(w: &mut fmt::Write, value: &Value) -> fmt::Result { d_default(w, value) } -// FNumber (Exif 0x829d) -fn d_fnumber(w: &mut fmt::Write, value: &Value) -> fmt::Result { - w.write_str("f/")?; - d_decimal(w, value) -} - // ExposureProgram (Exif 0x8822) fn d_expprog(w: &mut fmt::Write, value: &Value) -> fmt::Result { let s = match value.get_uint(0) { diff --git a/src/tiff.rs b/src/tiff.rs index 9660b53..02b420f 100644 --- a/src/tiff.rs +++ b/src/tiff.rs @@ -28,7 +28,8 @@ use std::fmt; use crate::endian::{Endian, BigEndian, LittleEndian}; use crate::error::Error; -use crate::tag::{Context, Tag}; +use crate::tag::{Context, Tag, UnitPiece}; +use crate::value; use crate::value::Value; use crate::value::get_type_info; use crate::util::{atou16, ctou32}; @@ -264,9 +265,143 @@ impl fmt::Display for DateTime { } } +impl<'a> Field<'a> { + /// Returns an object that implements `std::fmt::Display` for + /// printing the value of this field in a tag-specific format. + /// + /// To print the value with the unit, call `with_unit` method on the + /// returned object. It takes a parameter, which is either `()`, + /// `&Field`, or `&Reader` and provides the unit information. + /// If the unit does not depend on another field, `()` can be used. + /// Otherwise, `&Field` or `&Reader` should be used. + /// + /// # Examples + /// + /// ``` + /// use exif::{Field, Rational, Tag, Value}; + /// + /// let xres = Field { + /// tag: Tag::XResolution, + /// thumbnail: false, + /// value: Value::Rational(vec![Rational { num: 72, denom: 1 }]), + /// }; + /// let cm = Field { + /// tag: Tag::ResolutionUnit, + /// thumbnail: false, + /// value: Value::Short(vec![3]), + /// }; + /// assert_eq!(format!("{}", xres.display_value()), "72"); + /// assert_eq!(format!("{}", cm.display_value()), "cm"); + /// // The unit of XResolution is indicated by ResolutionUnit. + /// assert_eq!(format!("{}", xres.display_value().with_unit(&cm)), + /// "72 pixels per cm"); + /// // If ResolutionUnit is not given, the default value is used. + /// assert_eq!(format!("{}", xres.display_value().with_unit(())), + /// "72 pixels per inch"); + /// assert_eq!(format!("{}", xres.display_value().with_unit(&xres)), + /// "72 pixels per inch"); + /// + /// let flen = Field { + /// tag: Tag::FocalLengthIn35mmFilm, + /// thumbnail: false, + /// value: Value::Short(vec![24]), + /// }; + /// // The unit of the focal length is always mm, so the argument + /// // has nothing to do with the result. + /// assert_eq!(format!("{}", flen.display_value().with_unit(())), "24 mm"); + /// assert_eq!(format!("{}", flen.display_value().with_unit(&cm)), "24 mm"); + /// ``` + #[inline] + pub fn display_value(&self) -> DisplayValue { + DisplayValue { + tag: self.tag, + thumbnail: self.thumbnail, + value_display: self.value.display_as(self.tag), + } + } +} + +/// Helper struct for printing a value in a tag-specific format. +pub struct DisplayValue<'a> { + tag: Tag, + thumbnail: bool, + value_display: value::Display<'a>, +} + +impl<'a> DisplayValue<'a> { + #[inline] + pub fn with_unit<'b, T>(&'b self, unit_provider: T) + -> DisplayValueUnit where T: ProvideUnit<'b> { + DisplayValueUnit { + thumbnail: self.thumbnail, + value_display: self.value_display, + unit: self.tag.unit(), + unit_provider: unit_provider, + } + } +} + +impl<'a> fmt::Display for DisplayValue<'a> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value_display.fmt(f) + } +} + +/// Helper struct for printing a value with its unit. +pub struct DisplayValueUnit<'a, T> where T: ProvideUnit<'a> { + thumbnail: bool, + value_display: value::Display<'a>, + unit: Option<&'static [UnitPiece]>, + unit_provider: T, +} + +impl<'a, T> fmt::Display for DisplayValueUnit<'a, T> where T: ProvideUnit<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(unit) = self.unit { + assert!(!unit.is_empty()); + for piece in unit { + match *piece { + UnitPiece::Value => self.value_display.fmt(f), + UnitPiece::Str(s) => f.write_str(s), + UnitPiece::Tag(tag) => + if let Some(x) = self.unit_provider.get_field( + tag, self.thumbnail) { + x.value.display_as(tag).fmt(f) + } else if let Some(x) = tag.default_value() { + x.display_as(tag).fmt(f) + } else { + write!(f, "[{} missing]", tag) + }, + }? + } + Ok(()) + } else { + self.value_display.fmt(f) + } + } +} + +pub trait ProvideUnit<'a>: Copy { + fn get_field(self, tag: Tag, thumbnail: bool) -> Option<&'a Field<'a>>; +} + +impl<'a> ProvideUnit<'a> for () { + fn get_field(self, _tag: Tag, _thumbnail: bool) -> Option<&'a Field<'a>> { + None + } +} + +impl<'a> ProvideUnit<'a> for &'a Field<'a> { + fn get_field(self, tag: Tag, thumbnail: bool) -> Option<&'a Field<'a>> { + Some(self).filter(|x| x.tag == tag && x.thumbnail == thumbnail) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::value::Rational; // Before the error is returned, the IFD is parsed twice as the // 0th and 1st IFDs. @@ -325,4 +460,73 @@ mod tests { assert_err_pat!(dt.parse_offset(b" : "), Error::BlankValue(_)); assert_err_pat!(dt.parse_offset(b" "), Error::BlankValue(_)); } + + #[test] + fn display_value_with_unit() { + let cm = Field { + tag: Tag::ResolutionUnit, + thumbnail: false, + value: Value::Short(vec![3]), + }; + let cm_tn = Field { + tag: Tag::ResolutionUnit, + thumbnail: true, + value: Value::Short(vec![3]), + }; + // No unit. + let exifver = Field { + tag: Tag::ExifVersion, + thumbnail: false, + value: Value::Undefined(b"0231", 0), + }; + assert_eq!(format!("{}", exifver.display_value()), + "2.31"); + assert_eq!(format!("{}", exifver.display_value().with_unit(())), + "2.31"); + assert_eq!(format!("{}", exifver.display_value().with_unit(&cm)), + "2.31"); + // Fixed string. + let width = Field { + tag: Tag::ImageWidth, + thumbnail: false, + value: Value::Short(vec![257]), + }; + assert_eq!(format!("{}", width.display_value()), + "257"); + assert_eq!(format!("{}", width.display_value().with_unit(())), + "257 pixels"); + assert_eq!(format!("{}", width.display_value().with_unit(&cm)), + "257 pixels"); + // Unit tag (with a non-default value). + // Unit tag is missing but the default is specified. + let xres = Field { + tag: Tag::XResolution, + thumbnail: false, + value: Value::Rational(vec![Rational { num: 300, denom: 1 }]), + }; + assert_eq!(format!("{}", xres.display_value()), + "300"); + assert_eq!(format!("{}", xres.display_value().with_unit(())), + "300 pixels per inch"); + assert_eq!(format!("{}", xres.display_value().with_unit(&cm)), + "300 pixels per cm"); + assert_eq!(format!("{}", xres.display_value().with_unit(&cm_tn)), + "300 pixels per inch"); + // Unit tag is missing and the default is not specified. + let gpslat = Field { + tag: Tag::GPSLatitude, + thumbnail: false, + value: Value::Rational(vec![ + Rational { num: 10, denom: 1 }, + Rational { num: 0, denom: 1 }, + Rational { num: 1, denom: 10 }, + ]), + }; + assert_eq!(format!("{}", gpslat.display_value()), + "10 deg 0 min 0.1 sec"); + assert_eq!(format!("{}", gpslat.display_value().with_unit(())), + "10 deg 0 min 0.1 sec [GPSLatitudeRef missing]"); + assert_eq!(format!("{}", gpslat.display_value().with_unit(&cm)), + "10 deg 0 min 0.1 sec [GPSLatitudeRef missing]"); + } } diff --git a/src/value.rs b/src/value.rs index 70a242a..9f3bf94 100644 --- a/src/value.rs +++ b/src/value.rs @@ -77,6 +77,8 @@ impl<'a> Value<'a> { /// printing a value in a tag-specific format. /// The tag of the value is specified as the argument. /// + /// If you want to display with the unit, use `Field::display_value`. + /// /// # Examples /// /// ``` @@ -86,7 +88,7 @@ impl<'a> Value<'a> { /// "2.31"); /// let val = Value::Short(vec![2]); /// assert_eq!(format!("{}", val.display_as(Tag::ResolutionUnit)), - /// "pixels per inch"); + /// "inch"); /// ``` #[inline] pub fn display_as(&self, tag: crate::tag::Tag) -> Display { @@ -146,6 +148,7 @@ impl<'a> Iterator for UIntIter<'a> { impl<'a> ExactSizeIterator for UIntIter<'a> {} /// Helper struct for printing a value in a tag-specific format. +#[derive(Copy, Clone)] pub struct Display<'a> { pub fmt: fn(&mut fmt::Write, &Value) -> fmt::Result, pub value: &'a Value<'a>, diff --git a/tests/unit.tif b/tests/unit.tif new file mode 100644 index 0000000000000000000000000000000000000000..4fbf6df679f10d50b72c81dcb8e90f278247f295 GIT binary patch literal 691 zcmb7=yAAu6h2nM;9&%NA%(Yja!TQbcYBzde2OK8