Conversation
The convertToTimestampString converter used ratNum() (numerator only) for
all three time components. It had a follow-up hack that detected 10-char
results and inserted a decimal point:
4279/100 → numerator 4279 → "13:03:4279" (10 chars) → "13:03:42.79"
This only works when the seconds numerator is exactly 4 digits. A rational
like 181/5 (= 36.2 seconds) has a 3-digit numerator and falls through:
181/5 → numerator 181 → "22:22:181" → no decimal inserted
Fix: use toFloat64() and strconv.FormatFloat for the seconds component.
Integer seconds still produce "47"; fractional seconds produce "36.2".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two test infrastructure fixes:
1. The fuzzy float comparator regex for space-delimited fields (LensInfo,
PrimaryChromaticities, etc.) required both the first and second token to
contain a decimal point. Fields with integer tokens (e.g. LensInfo
"2.22 9 1.78 2.8" where 9 is the max focal length) fell through to exact
string comparison, failing on float64 precision differences.
Fix: allow subsequent tokens to be integers.
2. ExifTool with -n outputs numeric-looking software versions (e.g. iOS
"26.3") as JSON numbers rather than strings. After json.Unmarshal this
becomes float64(26.3) while imagemeta correctly returns string("26.3")
since EXIF Software is an ASCII field.
Fix: convert float64 back to string for Software in normalizeThem.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #58 +/- ##
==========================================
- Coverage 79.38% 78.19% -1.20%
==========================================
Files 14 15 +1
Lines 1606 1885 +279
==========================================
+ Hits 1275 1474 +199
- Misses 228 279 +51
- Partials 103 132 +29 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
|
||
| // ISOBMFF box types used in HEIF/AVIF containers. | ||
| var ( | ||
| boxTypeFtyp = "ftyp" |
There was a problem hiding this comment.
There's a fourCC type used elsewhere for the same purpose. I would prefer to use this. That also avoid the string conversions when comparing.
There was a problem hiding this comment.
Fixed — switched to fourCC type with fcc prefix naming, matching the WebP decoder pattern. The readBox helper now returns fourCC directly (reads into the named return value), eliminating the intermediate [4]byte and string() conversion. Same for infe item type comparison.
| // than tiles or thumbnails in standard HEIF/AVIF output). | ||
| for _, p := range ipcoProps { | ||
| if p.isIspe && p.width > 0 && p.height > 0 { | ||
| if int(p.width)*int(p.height) > int(cfgWidth)*int(cfgHeight) { |
There was a problem hiding this comment.
The above looks like it could overflow; are those int conversions really needed?
There was a problem hiding this comment.
Good catch — on 32-bit platforms int is 32 bits, so int(uint32) * int(uint32) can overflow. Changed to uint64 multiplication, which can hold the full range (uint32 × uint32 fits in uint64).
Closes #55. Add metadata decoding for HEIF/HEIC and AVIF images (ISOBMFF containers). Supports EXIF, XMP, and CONFIG sources. No pixel decoding, no new dependencies.
Closes #55.
Adds metadata decoding support for HEIF/HEIC and AVIF images. Both formats use the
ISO Base Media File Format (ISOBMFF) container. Metadata extraction only requires
parsing the container's box structure — no pixel decoding, no new dependencies.
Supported sources
EXIF— full EXIF/MakerNote decoding via the existingnewMetaDecoderEXIFpipelineXMP— raw XMP blob passed to the existingdecodeXMPpipelineCONFIG— image dimensions fromispeviapitm/ipma+ rotation applied fromirotIPTC— not part of the HEIF/AVIF spec; not supportedImplementation
One new file
imagedecoder_heif.goimplementing the existingdecoderinterface.ISOBMFF box types are defined as package-level variables, consistent with the
fourCCand
[]bytetag constants used in the WebP and PNG decoders. Item IDs are tracked asuint32throughout, supporting bothinfeversion 2 (16-bit) and version 3 (32-bit)without truncation.
The ISOBMFF parsing flow:
ftypbox (caller is responsible for the format hint —HEIForAVIF)metametachildren:pitm→ primary item ID (used for CONFIG dimension selection)iinf→ find item IDs for EXIF (itemType = "Exif") and XMP (itemType = "mime");both
infeversion 2 (16-bit item IDs) and version 3 (32-bit item IDs) are handlediloc→ map those item IDs to absolute file offsets (baseOffset + extent offset)and lengths;
ilocversions 0, 1, and 2 with variable field sizes (0/2/4/8 bytes);items using non-file-offset construction methods (idat/item) are skipped
iprp→ipcocollects all properties by index;ipmamaps item IDs toproperty indices;
ispeandirotare extracted for CONFIGilocoffsets, extract EXIF and XMP blobs, hand to existing decodersEXIF blobs in HEIF have a 4-byte big-endian prefix indicating bytes to skip before
the TIFF header. The TIFF byte-order mark ("MM"/"II") is then auto-detected by
newMetaDecoderEXIFas usual. EXIF decoding is wrapped in adefer/recoverhandler(matching the JPEG decoder's
handleEXIFpattern) so that XMP decoding can proceedeven if EXIF parsing panics.
Image dimensions use a two-tier strategy. The primary path resolves the
pitmitem ID through
ipmaassociations to find the exactispeandirotpropertiesbelonging to the primary image — this is the spec-correct approach and avoids
selecting tile or thumbnail dimensions. When
pitmoripmadata is absent (e.g.older encoders), the decoder falls back to selecting the
ispeentry with thelargest pixel area, which is reliable in practice since the main image is always
larger than tiles or thumbnails.
irotangle 1 or 3 (90°/270° rotation) swapswidth and height to give display-oriented dimensions — tested against both an
iPhone 15 Pro (no rotation) and a Sony A6700 (irot angle=3, swaps 6192×4128 to
4128×6192).
Buffered readers for EXIF and XMP are scoped via anonymous functions /
handleEXIFto ensure prompt
Close()calls that return pooled buffers, following the patternestablished by the WebP and PNG decoders.
source_string.gois regenerated — this also picks up the pre-existingCONFIGsource constant that was missing from the
Sourcestringer.Test images
Both images are provided under CC BY-SA 4.0 (see
testdata/images/license.txt):testdata/images/iphone.heic— iPhone 15 Pro, big-endian EXIF, ftyp brandheic,5712×4284 px, includes GPS with fractional seconds
testdata/images/sony.heif— Sony ILCE-6700, little-endian EXIF, ftyp brandheix,6192×4128 coded / 4128×6192 display (portrait via
irotangle=3)Golden files generated with ExifTool 13.50 (
go generate ./gen). Both images areincluded in the non-skipped golden tests (
TestGoldenEXIF,TestGoldenConfig, etc.).Tests
TestDecodeHEIF— EXIF tag assertions for both test images (Make, Model, Orientation)TestDecodeHEIFConfig— image dimension assertions for both test images, includingrotation swap verification for the Sony image
TestGoldenEXIF,TestGoldenConfig— full golden file comparison against ExifToolFuzzDecodeHEIF,FuzzDecodeAVIF— fuzz tests seeded with the test imagesgo test -race ./...— all passgo vet ./...— cleangofmt -d .— no diffPrerequisites
This PR includes the two bug fixes from the companion PRs
(
fix/gps-timestamp-fractional-secondsandfix/test-infra-numeric-normalization).If those land first, this PR can be rebased to drop the duplicate changes.