kamadak / exif-rs

Exif parsing library written in pure Rust
BSD 2-Clause "Simplified" License
190 stars 42 forks source link

WIP: Support for SubIFD #11

Closed runfalk closed 4 years ago

runfalk commented 4 years ago

Hi!

I tried to use your library with Nikon RAW images (NEF) and it seemed to work but some values were quite a bit off. It turns out that Nikon uses SubIFDs to store data about the full image. I whipped together an implementation that adds SubIFD support for exif-rs but I am unsure on how to implement it properly

I used this example image (you can find more formats at https://raw.pixls.us/)

$ cargo run --example dumpexif test.NEF
test.NEF
  primary/SubfileType (0xfe): Reduced-resolution image
      "Long([1])"
  primary/ImageWidth (0x100): 160 pixels
      "Long([160])"
  primary/ImageLength (0x101): 120 pixels
      "Long([120])"
  primary/BitsPerSample (0x102): 8, 8, 8
      "Short([8, 8, 8])"
  primary/Compression (0x103): uncompressed
      "Short([1])"
  primary/PhotometricInterpretation (0x106): RGB
      "Short([2])"
  primary/Make (0x10f): "NIKON CORPORATION"
      "Ascii([\"NIKON CORPORATION\"])"
  primary/Model (0x110): "NIKON D7000"
      "Ascii([\"NIKON D7000\"])"
  primary/StripOffsets (0x111): 122184
      "Long([122184])"
  primary/Orientation (0x112): row 0 at top and column 0 at left
      "Short([1])"
  primary/SamplesPerPixel (0x115): 3
      "Short([3])"
  primary/RowsPerStrip (0x116): 120
      "Long([120])"
  primary/StripByteCounts (0x117): 57600
      "Long([57600])"
  primary/XResolution (0x11a): 300 pixels per inch
      "Rational([Rational(300/1)])"
  primary/YResolution (0x11b): 300 pixels per inch
      "Rational([Rational(300/1)])"
  primary/PlanarConfiguration (0x11c): chunky
      "Short([1])"
  primary/ResolutionUnit (0x128): inch
      "Short([2])"
  primary/Software (0x131): "Ver.1.05 "
      "Ascii([\"Ver.1.05 \"])"
  primary/DateTime (0x132): 2017-01-22 14:18:36
      "Ascii([\"2017:01:22 14:18:36\"])"
  primary/Artist (0x13b): "Thomas Ryan                         "
      "Ascii([\"Thomas Ryan                         \"])"
  IFD0-0/SubfileType (0xfe): Reduced-resolution image
      "Long([1])"
  IFD0-0/Compression (0x103): JPEG
      "Short([6])"
  IFD0-0/XResolution (0x11a): 300 pixels per inch
      "Rational([Rational(300/1)])"
  IFD0-0/YResolution (0x11b): 300 pixels per inch
      "Rational([Rational(300/1)])"
  IFD0-0/ResolutionUnit (0x128): inch
      "Short([2])"
  IFD0-0/JPEGInterchangeFormat (0x201): 180160
      "Long([180160])"
  IFD0-0/JPEGInterchangeFormatLength (0x202): 1168887
      "Long([1168887])"
  IFD0-0/YCbCrPositioning (0x213): co-sited
      "Short([2])"
  IFD0-1/SubfileType (0xfe): Full-resolution image
      "Long([0])"
  IFD0-1/ImageWidth (0x100): 4992 pixels
      "Long([4992])"
  IFD0-1/ImageLength (0x101): 3280 pixels
      "Long([3280])"
  IFD0-1/BitsPerSample (0x102): 14
      "Short([14])"
  IFD0-1/Compression (0x103): [reserved compression 34713]
      "Short([34713])"
  IFD0-1/PhotometricInterpretation (0x106): [reserved photometric interpretation 32803]
      "Short([32803])"
  IFD0-1/StripOffsets (0x111): 1349056
      "Long([1349056])"
  IFD0-1/SamplesPerPixel (0x115): 1
      "Short([1])"
  IFD0-1/RowsPerStrip (0x116): 3280
      "Long([3280])"
  IFD0-1/StripByteCounts (0x117): 17691645
      "Long([17691645])"
  IFD0-1/XResolution (0x11a): 300 pixels per inch
      "Rational([Rational(300/1)])"
  IFD0-1/YResolution (0x11b): 300 pixels per inch
      "Rational([Rational(300/1)])"
  IFD0-1/PlanarConfiguration (0x11c): chunky
      "Short([1])"
  IFD0-1/ResolutionUnit (0x128): inch
      "Short([2])"
  IFD0-1/Tag(Tiff, 33421) (0x828d): 2, 2
      "Short([2, 2])"
  IFD0-1/Tag(Tiff, 33422) (0x828e): 4 byte blob
      "[0, 1, 1, 2]"
  IFD0-1/Tag(Tiff, 37399) (0x9217): 2
      "Short([2])"
  primary/ReferenceBlackWhite (0x214): 0, 255, 0, 255, 0, 255
      "Rational([Rational(0/1), Rational(255/1), Rational(0/1), Rational(255/1), Rational(0/1), Rational(255/1)])"
  primary/Copyright (0x8298): "2016 All rights reserved.                             "
      "Ascii([\"2016 All rights reserved.                             \"])"
  primary/ExposureTime (0x829a): 1/200 s
      "Rational([Rational(10/2000)])"
  primary/FNumber (0x829d): f/1.8
      "Rational([Rational(18/10)])"
  primary/ExposureProgram (0x8822): aperture priority
      "Short([3])"
  primary/PhotographicSensitivity (0x8827): 100
      "Short([100])"
  primary/SensitivityType (0x8830): REI
      "Short([2])"
  primary/DateTimeOriginal (0x9003): 2017-01-22 14:18:36
      "Ascii([\"2017:01:22 14:18:36\"])"
  primary/DateTimeDigitized (0x9004): 2017-01-22 14:18:36
      "Ascii([\"2017:01:22 14:18:36\"])"
  primary/ExposureBiasValue (0x9204): 0 EV
      "SRational([SRational(0/6)])"
  primary/MaxApertureValue (0x9205): 1.6 EV
      "Rational([Rational(16/10)])"
  primary/MeteringMode (0x9207): pattern
      "Short([5])"
  primary/LightSource (0x9208): unknown
      "Short([0])"
  primary/Flash (0x9209): not fired, no return light detection function, suppressed
      "Short([16])"
  primary/FocalLength (0x920a): 50 mm
      "Rational([Rational(500/10)])"
  primary/MakerNote (0x927c): 121022 byte blob
  primary/UserComment (0x9286): 44 byte blob
  primary/SubSecTime (0x9290): "90"
      "Ascii([\"90\"])"
  primary/SubSecTimeOriginal (0x9291): "90"
      "Ascii([\"90\"])"
  primary/SubSecTimeDigitized (0x9292): "90"
      "Ascii([\"90\"])"
  primary/SensingMethod (0xa217): one-chip color area sensor
      "Short([2])"
  primary/FileSource (0xa300): 1 byte blob
      "[3]"
  primary/SceneType (0xa301): 1 byte blob
      "[1]"
  primary/CFAPattern (0xa302): 8 byte blob
      "[0, 2, 0, 2, 0, 1, 1, 2]"
  primary/CustomRendered (0xa401): normal process
      "Short([0])"
  primary/ExposureMode (0xa402): auto exposure
      "Short([0])"
  primary/WhiteBalance (0xa403): auto white balance
      "Short([0])"
  primary/DigitalZoomRatio (0xa404): 1
      "Rational([Rational(1/1)])"
  primary/FocalLengthIn35mmFilm (0xa405): 75 mm
      "Short([75])"
  primary/SceneCaptureType (0xa406): standard
      "Short([0])"
  primary/GainControl (0xa407): none
      "Short([0])"
  primary/Contrast (0xa408): normal
      "Short([0])"
  primary/Saturation (0xa409): normal
      "Short([0])"
  primary/Sharpness (0xa40a): normal
      "Short([0])"
  primary/SubjectDistanceRange (0xa40c): unknown
      "Short([0])"
  primary/GPSVersionID (0x0): 4 byte blob
      "[2, 3, 0, 0]"
  primary/Tag(Tiff, 36867) (0x9003): "2017:01:22 14:18:36"
      "Ascii([\"2017:01:22 14:18:36\"])"
  primary/Tag(Tiff, 37398) (0x9216): 4 byte blob
      "[1, 0, 0, 0]"

As you can see ImageWidth and ImageLength in primary is only 160x120. However, the real dimensions are 4992x3280 which can be found in SubIFD1 (IFD0-1/ImageWidth).

Do you think that supporting this is within scope for exif-rs?

kamadak commented 4 years ago

Could you elaborate your purpose? If you are interested in the fields in Exif IFD in NEF, this library can read them as it is. On the other hand, if you are interested in the fields in "SubIFDs", using this library to read them does not make sense to me because they are not Exif fields.

Random observations follow:

Wikipedia says Nikon NEF is based on TIFF/EP. TIFF/EP is a different standard than TIFF or Exif. TIFF/EP reuses the basic structure from TIFF and Exif, but also has several differences especially in tag name spaces and IFD handling. Considering the nature of these differences, I do not like to overload TIFF/EP parser on top of Exif parser as your PoC code does.

runfalk commented 4 years ago

So, my purpose was to extract correct width and height from the metadata in NEF files (since exif-rs gives the width and height of a built-in thumbnail currently). I'm writing an organization system for all my digital photos and videos and to me it would be convenient to use a single library for all image metadata parsing.

One interesting thing about the NEF SubIFDs is that it contains the camera's own full resolution JPEG version of the image. This means that it's possible to extract a good looking thumbnail quickly without resorting to RAW processing.

I didn't realize TIFF came in different flavors. I do agree TIFF/EP support may be out of scope.

kamadak commented 4 years ago

Thank you for the clarification. You want to extract the image size and JPEG byte stream from SubIFDs without RAW format library. I do not think it is in scope, but I will keep it in mind that there is such a demand. If the implementation does not interfere with the main Exif functionality, I will not object this library having such a feature.

I checked a draft of ISO 12234-2 (TIFF/EP) and found that the IFD structure is more complex than "IFD number + sub IFD number". It's a tree, where child IFDs are "two-dimensional", meaning that they form not only an array in SubIFDs but also chains using next IFD pointers from the array.

runfalk commented 4 years ago

Thank you for your response and helpful input. I'll close this PR.