Skip to content

fix(exif): FNumber prints through PrintFNumber, not the raw quotient - #408

Merged
swackhamer merged 1 commit into
mainfrom
fix/exif-fnumber-printconv
Aug 2, 2026
Merged

fix(exif): FNumber prints through PrintFNumber, not the raw quotient#408
swackhamer merged 1 commit into
mainfrom
fix/exif-fnumber-printconv

Conversation

@swackhamer

Copy link
Copy Markdown
Collaborator

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.

Root cause

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.
  • $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.

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 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 (composite::lookup_key), and FNumber is deliberately not in apex_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 with parse_float=str; files keyed on full relative path.

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 test

Removing the arm fails fnumber_reaches_print_fnumber_from_its_rational with 4 for 4/1. 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).

🤖 Generated with Claude Code

@swackhamer
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
swackhamer force-pushed the fix/exif-fnumber-printconv branch from 2ffc5b1 to 9c065d2 Compare August 2, 2026 08:20
@swackhamer
swackhamer merged commit 3f7be90 into main Aug 2, 2026
6 of 7 checks passed
@swackhamer
swackhamer deleted the fix/exif-fnumber-printconv branch August 2, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant