fix(exif): FNumber prints through PrintFNumber, not the raw quotient - #408
Merged
Conversation
swackhamer
enabled auto-merge (squash)
August 2, 2026 03:58
`ExifIFD:FNumber` disagreed with ExifTool in 1,035 of the 4,238
sample-corpus files: `4` where ExifTool prints `4.0`, and
`0.640234375` where it prints `0.64`.
Exif.pm 0x829d (line 1853) is
`PrintConv => 'Image::ExifTool::Exif::PrintFNumber($val)'`, and
`format_tag_value` had no arm for it. The rational therefore fell through
to the PrintConv-less-rational rule, which prints the quotient the
rational reader already rounded -- `RoundFloat($val, 10)`, i.e. `%.10g`.
That is the right rule for a tag with no PrintConv and the wrong one
here, in both directions: it drops the decimal place `%.1f` keeps (964
files), and prints the full stored expansion where ExifTool rounds
(another 71).
PrintFNumber, Exif.pm:5715, reproduced exactly:
my $val = shift;
if (Image::ExifTool::IsFloat($val) and $val > 0) {
# round to 1 decimal place, or 2 for values < 1.0
$val = sprintf(($val<1 ? "%.2f" : "%.1f"), $val);
}
return $val;
Both halves of that condition are load-bearing. Below 1.0 it is two
decimal places, not one -- GPS.jpg stores 0.640234375 and ExifTool prints
`0.64`. And `$val > 0` guards the sprintf, so a zero is returned
unformatted: CanonEOS20Da.jpg, NikonSUPER_COOLSCAN9000ED.jpg and
SonyNEX-VG900.jpg store a zero FNumber and ExifTool prints `0`, never
`0.0`.
This is a *display* conversion and 0x829d has no ValueConv, so the one
Composite that consumes FNumber -- `Aperture` (Exif.pm:4782,
`ValueConv => '$val[0] || $val[1]'`, which applies PrintFNumber itself)
-- must keep receiving the raw quotient. It does: composites are derived
from the stored `TagValue` before `format_tag_value` runs at all, and
FNumber is deliberately not in `apex_value_conv`.
Measured per file, keyed `Group1:Name`, against ExifTool 13.59 over the
4,238-file corpus (oxidex reads 4,104 of them):
matched set +1,030 / -0
files 1,030 improved, 0 regressed
keys changed ExifIFD:FNumber, and nothing else -- no maker-note
`*:FNumber` moved, because the arm is gated on the
rational form the EXIF reader produces
Composite:* 0 values changed, across all 35 Composite keys and the
3,719 files carrying Composite:Aperture
The five files still short of ExifTool are pre-existing gaps this does
not touch: CanonRaw.cr3 does not emit the tag, and four Samsung Anycall
files store FNumber as a two-element rational array whose second element
has a zero denominator (`3 0` vs ExifTool's `3 undef`) -- the array path,
not this one.
Mutation-tested: removing the arm fails
`fnumber_reaches_print_fnumber_from_its_rational` with `4` for `4/1`,
and replacing the two-branch sprintf with an unconditional `%.1f` fails
`quotients_round_to_one_place_or_two_below_one` (`0.6` for `0.64`) and
`non_positive_values_are_returned_unformatted` (`0.0` for `0`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
swackhamer
force-pushed
the
fix/exif-fnumber-printconv
branch
from
August 2, 2026 08:20
2ffc5b1 to
9c065d2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ExifIFD:FNumberdisagreed with ExifTool in 1,035 of the 4,238 sample-corpus files:4where ExifTool prints4.0, and0.640234375where it prints0.64.Root cause
Exif.pm 0x829d (line 1853) is
PrintConv => 'Image::ExifTool::Exif::PrintFNumber($val)', andformat_tag_valuehad no arm for it. The rational therefore fell through to the PrintConv-less-rational rule, which prints the quotient the rational reader already rounded --RoundFloat($val, 10), i.e.%.10g. That is the right rule for a tag with no PrintConv and the wrong one here, in both directions: it drops the decimal place%.1fkeeps (964 files), and prints the full stored expansion where ExifTool rounds (another 71).PrintFNumber, Exif.pm:5715, reproduced exactly:Both halves of that condition are load-bearing:
GPS.jpgstores0.640234375and ExifTool prints0.64.$val > 0guards the sprintf, so a zero is returned unformatted.CanonEOS20Da.jpg,NikonSUPER_COOLSCAN9000ED.jpgandSonyNEX-VG900.jpgstore a zero FNumber and ExifTool prints0, never0.0.Composites are untouched, by construction and by measurement
This is a display conversion and 0x829d has no ValueConv, so the one Composite that consumes FNumber --
Aperture(Exif.pm:4782,Desire => { 0 => 'FNumber', 1 => 'ApertureValue' },ValueConv => '$val[0] || $val[1]', which appliesPrintFNumberitself) -- must keep receiving the raw quotient.It does. Composites are derived from the stored
TagValuebeforeformat_tag_valueruns at all (composite::lookup_key), and FNumber is deliberately not inapex_value_conv, the list of tags whose stored value is not what a reader wants. A test pins both halves of that.Measurement
Per file, keyed
Group1:Name, against ExifTool 13.59 over the 4,238-file corpus (oxidex reads 4,104 of them). JSON parsed withparse_float=str; files keyed on full relative path.ExifIFD:FNumber, and nothing else -- no maker-note*:FNumbermoved, because the arm is gated on the rational form the EXIF reader producesComposite:*Composite:ApertureThe five files still short of ExifTool are pre-existing gaps this does not touch:
CanonRaw.cr3does not emit the tag, and four Samsung Anycall files store FNumber as a two-element rational array whose second element has a zero denominator (3 0vs ExifTool's3 undef) -- the array path, not this one.Mutation test
Removing the arm fails
fnumber_reaches_print_fnumber_from_its_rationalwith4for4/1. Replacing the two-branch sprintf with an unconditional%.1ffailsquotients_round_to_one_place_or_two_below_one(0.6for0.64) andnon_positive_values_are_returned_unformatted(0.0for0).🤖 Generated with Claude Code