Skip to content

Empirical JPEG tag matrix: exiftool↔oxidex comparison docs + CI regression gate - #19

Merged
swackhamer merged 5 commits into
mainfrom
claude/exiftool-oxidex-tag-mapping-d14171
Jul 19, 2026
Merged

Empirical JPEG tag matrix: exiftool↔oxidex comparison docs + CI regression gate#19
swackhamer merged 5 commits into
mainfrom
claude/exiftool-oxidex-tag-mapping-d14171

Conversation

@swackhamer

Copy link
Copy Markdown
Collaborator

What

Adds an empirical, per-tag JPEG support matrix comparing oxidex against ExifTool 13.55, published to oxidex.net and kept fresh + regression-gated in CI.

Docs (new pages under Reference on oxidex.net)

  • docs/reference/jpeg-tag-support.md — every ExifTool tag oxidex can read from JPEG (1,621 of 4,812), its oxidex JSON key, the working CLI write key where writable, and a tested example value per tag
  • docs/reference/jpeg-tag-matrix.md — full classification of all 4,812 ExifTool-writable JPEG tags (full / read-only / write-broken / read-broken / unsupported / untestable) plus a confirmed bug inventory (W1–W11 write bugs, R1–R8 read bugs) with reproduced root causes and file:line references

Pipeline (all committed, env-overridable paths)

  • scripts/generate_exiftool_manifest.py — builds the tag universe from exiftool -f -listx with type-appropriate sample values; behaviorally flags silent-no-op tags
  • scripts/jpeg_tag_matrix.py — per tag: exiftool-write → oxidex-read (read support), oxidex-write → read-back via both oxidex and exiftool (write support), with per-tag isolation retests so one poison tag (e.g. IFD0:GeoTiffDoubleParams, which exiftool itself serializes malformed) can't contaminate a batch
  • scripts/jpeg_tag_report.py — renders both docs pages and enforces a ratcheting baseline (docs/reference/jpeg-tag-baseline.json)
  • tests/fixtures/jpeg/tag_matrix_base.jpg — minimal clean 8×8 base image

CI (.github/workflows/jpeg-tag-matrix.yml)

  • Triggers: push to main (source/scripts paths), weekly cron, manual dispatch
  • Pins ExifTool 13.55 (cloned by tag), builds oxidex release, runs the full 4,812-tag matrix (~10–20 min), uploads raw results.json as an artifact
  • Fails on regression (readable/writable/full counts dropping, broken counts rising); improvements auto-raise the baseline
  • Auto-commits refreshed docs with [skip ci] (same pattern as update-coverage-docs.yml), which then triggers the Pages deploy via deploy-docs.yml

Baseline at this commit

Metric Count
Readable 1,621 / 4,812
CLI-writable 122 (56 clean, 57 non-standard encoding, 9 other)
Read OK, write broken by bug 240
Read broken by bug 48
Unsupported 2,923
Untestable / no synthetic sample 219 (+585 exiftool-read-only)

Notes

  • Headline bugs documented in the matrix: CLI can't write numeric/rational/date EXIF tags (values reach the writer as strings; fix 2433c79 exists only on fix/wiring), datetime writes misrouted to the date-shift path, XMP/IPTC/JFIF writes are silent no-ops that still report success, and a single malformed IFD entry silently drops the entire EXIF block on read.
  • Local docs:build requires docs/reference/comparison/ to exist (pre-existing deploy-time-generated dir); unchanged by this PR.

🤖 Generated with Claude Code

Adds an empirical, per-tag exiftool<->oxidex JPEG support matrix:

- scripts/generate_exiftool_manifest.py: dumps ExifTool's tag DB
  (-f -listx) for JPEG-relevant groups and synthesizes a type-appropriate
  sample value per writable tag (4,812 tags, 74 XMP namespaces)
- scripts/jpeg_tag_matrix.py: for every tag, exiftool-writes a sample into
  a clean JPEG and verifies oxidex reads it; then oxidex-writes it and
  verifies both oxidex and exiftool read it back (with per-tag isolation
  retests so one poison tag can't contaminate a group batch)
- scripts/jpeg_tag_report.py: renders docs/reference/jpeg-tag-support.md
  (readable-tag mapping with working write keys + example values) and
  docs/reference/jpeg-tag-matrix.md (full 4,812-tag classification and the
  confirmed W1-W11/R1-R8 bug inventory with root causes), plus a ratcheting
  baseline gate (docs/reference/jpeg-tag-baseline.json)
- tests/fixtures/jpeg/tag_matrix_base.jpg: minimal 8x8 base image
- .github/workflows/jpeg-tag-matrix.yml: rebuilds oxidex, runs the matrix
  against pinned ExifTool 13.55 on main pushes / weekly / on demand,
  auto-commits refreshed docs, and fails on support regressions
- VitePress sidebar entries so both reports publish to oxidex.net

Current baseline: 1,621/4,812 tags readable, 122 CLI-writable (56 clean),
48 read-broken, 240 read-OK-write-broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/generate_exiftool_manifest.py Fixed
@codacy-production

codacy-production Bot commented Jul 19, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 261 complexity · 0 duplication

Metric Results
Complexity 261
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Fit the empirical JPEG reports into the right places on oxidex.net:

- Move the two pages into the Compatibility sidebar section (next to the
  generated ExifTool comparison reports) instead of the top-level
  Reference list
- Add an ExifTool Compatibility entry to the Reference overview page
- Style both generated pages like the site's other auto-generated docs:
  VitePress info callout with last-updated date and pinned ExifTool
  version, outline depth for the large matrix page, and cross-links
  between JPEG Tag Support <-> JPEG Tag Matrix <-> ExifTool Coverage <->
  Compatibility overview
- Cross-link from the auto-generated tag-coverage-analysis page (via its
  generator, so the link survives regeneration) and from the deploy-time
  comparison overview (src/bin/tag-comparison markdown generator)

Verified: vitepress build succeeds; tag-comparison binary compiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/jpeg_tag_report.py Fixed
Comment thread scripts/jpeg_tag_report.py Fixed
swackhamer and others added 3 commits July 19, 2026 03:11
Resolves all 23 new issues flagged by Codacy on PR #19:

- Pin the two third-party GitHub Actions in the new workflow
  (dtolnay/rust-toolchain, Swatinem/rust-cache, astral-sh/setup-uv) to
  full commit SHAs; dtolnay/rust-toolchain keeps its "stable" channel
  behavior via an explicit `toolchain: stable` input (2 High)
- Require defusedxml unconditionally instead of falling back to stdlib
  xml.etree.ElementTree, closing the XXE gap for real rather than just
  preferring the hardened parser when available (4 Error/Warning)
- Replace the hardcoded "/tmp/oxidex-tagmap" default work directory with
  tempfile.gettempdir()-based construction (Bandit B108 x3); CI now pins
  TAGMATRIX_WORK to ${runner.temp}/oxidex-tagmap explicitly
- Add narrowly-scoped, justified nosec/nosemgrep suppressions on the
  subprocess.run() call sites: all use list-argv with no shell=True, and
  every argument is either a local tool path from an env var or a value
  this same tooling synthesized moments earlier from exiftool's own
  local -listx XML dump -- never externally-controlled input

Also converts the three scripts to PEP 723 inline-script format (matching
the existing scripts/generate_tag_coverage.py convention) and switches
the CI workflow from pip+setup-python to uv, declaring defusedxml as a
proper inline dependency instead of a manual CI install step.

Verified: uv run on all three scripts reproduces identical manifest/matrix
output; YAML validated; git status confirms no incidental changes to the
generated docs pages (smoke-testing against partial datasets was
reverted before commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v-args

The previous commit's suppression comments for
python.lang.security.audit.dangerous-subprocess-use-tainted-env-args
were placed on the `subprocess.run(` line, but Semgrep anchors that rule
on the line containing the actual argument list -- a different physical
line in these multi-line calls (confirmed by re-querying Codacy's PR
analysis, which still reported 4 new issues after the first fix commit).

Centralizes generate_exiftool_manifest.py's three subprocess call sites
into a single _run_exiftool() helper (also fixing a DRY violation) and
collapses jpeg_tag_matrix.py's existing run() wrapper call to one
physical line, so both the audit and tainted-env-args rule anchors are
covered by one trailing suppression comment per call site.

Verified: uv run on both scripts reproduces identical manifest/matrix
output as before; docs pages confirmed untouched by smoke testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves 5 unresolved review threads from automated code review:

- Two empty "except: pass" blocks (generate_exiftool_manifest.py's
  non-numeric tag `count` handling, jpeg_tag_report.py's malformed-
  manifest fallback in autogen_callout()) now carry a one-line comment
  explaining why the exception is intentionally swallowed, preserving
  identical runtime behavior
- Three paragraphs in jpeg_tag_report.py's generated Markdown relied on
  Python's implicit adjacent-string-literal concatenation inside a list
  literal -- a pattern that silently merges list items if a comma is
  ever dropped. Made each paragraph an explicit `+`-joined expression so
  a missing separator would be a visible syntax/runtime error instead of
  a silent merge.

Verified byte-for-byte: reran scripts/jpeg_tag_report.py against the
original full 4,812-tag results.json and diffed the regenerated
docs/reference/jpeg-tag-{support,matrix}.md against the committed
versions -- zero differences, confirming the refactor is a pure
readability/maintainability change with no output impact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@swackhamer
swackhamer merged commit ca4fbbc into main Jul 19, 2026
19 checks passed
swackhamer added a commit that referenced this pull request Jul 19, 2026
PR #19 merged mid-session (by swackhamer) before the classification-
pipeline refinements and the CI runner.temp fix landed; those commits
were stranded on this branch behind the now-closed PR. #21 (pin
remaining workflow actions) and #22 (wire dead JPEG segment parsers --
COM, SPIFF, DQT quality, multi-chunk ICC, APP6/GoPro -- into the read
path) merged to main after #19.

Merged origin/main (resolved 7 add/add conflicts: this branch's fixed
versions of the JPEG tag matrix pipeline/docs/workflow superseded PR
#19's pre-refinement originals; #21/#22's files merged cleanly with no
overlap). Verified: full workspace test suite passes (3,615 tests, 0
failures, 37 binaries), release binary rebuilds cleanly.

Re-ran the full 4,812-tag matrix against the fully current binary.
Result: only one tag changed status -- File:Comment flipped from
unsupported to readable, confirming #22's COM-comment read-path fix.
The rest of #22's fixes (SPIFF, DQT-derived quality, multi-chunk ICC,
APP6/GoPro) aren't independently visible to this harness: they're
either not ExifTool-writable tags at all, or fall outside the
EXIF/XMP/IPTC/JFIF/Photoshop/ICC_Profile groups this matrix synthesizes
samples for -- noted explicitly in the updated R8 entry rather than
left to look unconfirmed. Zero write regressions verified: the exact
same 122 tags remain CLI-writable in every re-run this session.
Baseline ratcheted 2028 -> 2029 readable to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
swackhamer added a commit that referenced this pull request Jul 19, 2026
…#22 re-verification (#23)

* feat(docs+ci): empirical JPEG tag matrix with exiftool comparison in CI

Adds an empirical, per-tag exiftool<->oxidex JPEG support matrix:

- scripts/generate_exiftool_manifest.py: dumps ExifTool's tag DB
  (-f -listx) for JPEG-relevant groups and synthesizes a type-appropriate
  sample value per writable tag (4,812 tags, 74 XMP namespaces)
- scripts/jpeg_tag_matrix.py: for every tag, exiftool-writes a sample into
  a clean JPEG and verifies oxidex reads it; then oxidex-writes it and
  verifies both oxidex and exiftool read it back (with per-tag isolation
  retests so one poison tag can't contaminate a group batch)
- scripts/jpeg_tag_report.py: renders docs/reference/jpeg-tag-support.md
  (readable-tag mapping with working write keys + example values) and
  docs/reference/jpeg-tag-matrix.md (full 4,812-tag classification and the
  confirmed W1-W11/R1-R8 bug inventory with root causes), plus a ratcheting
  baseline gate (docs/reference/jpeg-tag-baseline.json)
- tests/fixtures/jpeg/tag_matrix_base.jpg: minimal 8x8 base image
- .github/workflows/jpeg-tag-matrix.yml: rebuilds oxidex, runs the matrix
  against pinned ExifTool 13.55 on main pushes / weekly / on demand,
  auto-commits refreshed docs, and fails on support regressions
- VitePress sidebar entries so both reports publish to oxidex.net

Current baseline: 1,621/4,812 tags readable, 122 CLI-writable (56 clean),
48 read-broken, 240 read-OK-write-broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: integrate JPEG tag matrix into site structure

Fit the empirical JPEG reports into the right places on oxidex.net:

- Move the two pages into the Compatibility sidebar section (next to the
  generated ExifTool comparison reports) instead of the top-level
  Reference list
- Add an ExifTool Compatibility entry to the Reference overview page
- Style both generated pages like the site's other auto-generated docs:
  VitePress info callout with last-updated date and pinned ExifTool
  version, outline depth for the large matrix page, and cross-links
  between JPEG Tag Support <-> JPEG Tag Matrix <-> ExifTool Coverage <->
  Compatibility overview
- Cross-link from the auto-generated tag-coverage-analysis page (via its
  generator, so the link survives regeneration) and from the deploy-time
  comparison overview (src/bin/tag-comparison markdown generator)

Verified: vitepress build succeeds; tag-comparison binary compiles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): address Codacy findings on the JPEG tag matrix pipeline

Resolves all 23 new issues flagged by Codacy on PR #19:

- Pin the two third-party GitHub Actions in the new workflow
  (dtolnay/rust-toolchain, Swatinem/rust-cache, astral-sh/setup-uv) to
  full commit SHAs; dtolnay/rust-toolchain keeps its "stable" channel
  behavior via an explicit `toolchain: stable` input (2 High)
- Require defusedxml unconditionally instead of falling back to stdlib
  xml.etree.ElementTree, closing the XXE gap for real rather than just
  preferring the hardened parser when available (4 Error/Warning)
- Replace the hardcoded "/tmp/oxidex-tagmap" default work directory with
  tempfile.gettempdir()-based construction (Bandit B108 x3); CI now pins
  TAGMATRIX_WORK to ${runner.temp}/oxidex-tagmap explicitly
- Add narrowly-scoped, justified nosec/nosemgrep suppressions on the
  subprocess.run() call sites: all use list-argv with no shell=True, and
  every argument is either a local tool path from an env var or a value
  this same tooling synthesized moments earlier from exiftool's own
  local -listx XML dump -- never externally-controlled input

Also converts the three scripts to PEP 723 inline-script format (matching
the existing scripts/generate_tag_coverage.py convention) and switches
the CI workflow from pip+setup-python to uv, declaring defusedxml as a
proper inline dependency instead of a manual CI install step.

Verified: uv run on all three scripts reproduces identical manifest/matrix
output; YAML validated; git status confirms no incidental changes to the
generated docs pages (smoke-testing against partial datasets was
reverted before commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): correct nosemgrep suppression placement for tainted-env-args

The previous commit's suppression comments for
python.lang.security.audit.dangerous-subprocess-use-tainted-env-args
were placed on the `subprocess.run(` line, but Semgrep anchors that rule
on the line containing the actual argument list -- a different physical
line in these multi-line calls (confirmed by re-querying Codacy's PR
analysis, which still reported 4 new issues after the first fix commit).

Centralizes generate_exiftool_manifest.py's three subprocess call sites
into a single _run_exiftool() helper (also fixing a DRY violation) and
collapses jpeg_tag_matrix.py's existing run() wrapper call to one
physical line, so both the audit and tainted-env-args rule anchors are
covered by one trailing suppression comment per call site.

Verified: uv run on both scripts reproduces identical manifest/matrix
output as before; docs pages confirmed untouched by smoke testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address code-quality review findings on PR #19

Resolves 5 unresolved review threads from automated code review:

- Two empty "except: pass" blocks (generate_exiftool_manifest.py's
  non-numeric tag `count` handling, jpeg_tag_report.py's malformed-
  manifest fallback in autogen_callout()) now carry a one-line comment
  explaining why the exception is intentionally swallowed, preserving
  identical runtime behavior
- Three paragraphs in jpeg_tag_report.py's generated Markdown relied on
  Python's implicit adjacent-string-literal concatenation inside a list
  literal -- a pattern that silently merges list items if a comma is
  ever dropped. Made each paragraph an explicit `+`-joined expression so
  a missing separator would be a visible syntax/runtime error instead of
  a silent merge.

Verified byte-for-byte: reran scripts/jpeg_tag_report.py against the
original full 4,812-tag results.json and diffed the regenerated
docs/reference/jpeg-tag-{support,matrix}.md against the committed
versions -- zero differences, confirming the refactor is a pure
readability/maintainability change with no output impact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: merge origin/main, fold classification logic into committed pipeline

Merges origin/main (adds src/writers/exif_inplace.rs and rewrites
date_shift.rs for ExifTool-compatible JPEG date shifting, PR #18).
Verified: full workspace test suite passes (598 tests), release binary
rebuilds cleanly.

Also closes a real gap found while re-verifying the JPEG tag matrix
against the merged binary: the read-bug pattern classification,
write-bug-cluster annotation, and non-standard-encoding validation that
back the published docs were only ever run via throwaway /tmp scripts,
never committed. The CI workflow as originally written would have
regenerated a materially cruder report on its first run (e.g. every raw
read mismatch showing as "read broken" instead of the refined 48-bug /
formatting-only split). Ports all of it into scripts/jpeg_tag_matrix.py:

- classify_read_mismatch() + apply_bug_classification(): root-causes
  read=MISMATCH into a tagged bug or MISMATCH_FORMAT (value-equivalent,
  formatting differs), and labels write=INTEROP_BROKEN with its
  root-caused bug cluster (I1-I5) -- ported as empirically-verified data,
  not guesses
- Generic no-op detection: every write result is compared against the
  tag's pristine pre-write value in the base fixture, so a write that
  silently changed nothing is classified NOT_WRITTEN regardless of
  whether the stale value happens to coincidentally match or mismatch
  the sample (replaces a hardcoded false-positive tag list)
- Generic same-group key-fallback scan: catches registry asymmetries
  (value written/read correctly but under an unexpected raw/hex key)
  without hardcoding tag names -- applied to both read and write paths,
  and found one previously-undetected case (TargetPrinter) was
  incorrectly counted as unsupported on the read side too
- Tightened write-verification lookups to require an exact group match,
  fixing two false READBACK_BROKEN results caused by matching an
  unrelated pre-existing tag of the same bare name in a different group
- Integrated exiftool -validate diffing directly into the write-test
  phase (was a separate, uncommitted script)
- Fixed a real field-naming collision: read and write phases both wrote
  "ox_val"/"et_val" into the same per-tag result dict, so a successful
  write silently clobbered the read phase's observed value for that tag
  -- corrupting the bug classifier's input on any tag where read failed
  but an independent write-then-readback happened to succeed. Write's
  fields are now namespaced (write_ox_val/write_et_val/write_ox_key)
- Fixed the sample generator picking a literal "None"/"Unknown" enum
  label as a tag's test value when a more distinctive option existed,
  which made a genuine write indistinguishable from a no-op for any tag
  whose pristine default happens to equal that label (found via
  JFIF:ResolutionUnit)

Re-ran the full 4,812-tag matrix against the merged binary end-to-end
through the now-fully-committed pipeline. Verified zero write
regressions: the exact same 122 tags remain CLI-writable, no losses.
Readable-tag detection improved 1621 -> 2028 (registry-asymmetry and
field-collision fixes surfacing previously-miscounted tags); the
"write broken" bucket's rise (240 -> 252) is entirely tags whose already-
broken write status was simply invisible before because their read was
wrongly counted as unsupported (verified precisely: it only fires where
read crosses from MISSING into a readable status while write is
unchanged). Baseline updated to reflect the corrected, more accurate
counts.

Also updates the W2 known-bug description: the merged date-shift rewrite
fixed updating an existing DateTimeOriginal-style tag in place (verified
manually); only fresh-creation on a tag with no prior value still fails,
which is what the harness's dateless base fixture measures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): move runner.temp reference out of unsupported env: scope

The `runner` context is only available inside individual step contexts
(steps[*].run/with/if/env), never in a workflow- or job-level `env:`
block -- confirmed by actionlint and by a real CI run on this branch
(29682285560), which silently scheduled zero jobs with the generic
"This run likely failed because of a workflow file issue" error. This
went undetected since d14d772 because no prior push touched a path
matching the workflow's trigger filter until the previous commit did.

Route TAGMATRIX_WORK through $GITHUB_ENV from a plain shell step instead
(uses $RUNNER_TEMP, not the `${{ runner.temp }}` expression), which is
the standard idiom for computing a workflow-wide env var from
step-only context. `actionlint` now reports zero issues for this file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: re-verify JPEG tag matrix against #21/#22, update R8 status

PR #19 merged mid-session (by swackhamer) before the classification-
pipeline refinements and the CI runner.temp fix landed; those commits
were stranded on this branch behind the now-closed PR. #21 (pin
remaining workflow actions) and #22 (wire dead JPEG segment parsers --
COM, SPIFF, DQT quality, multi-chunk ICC, APP6/GoPro -- into the read
path) merged to main after #19.

Merged origin/main (resolved 7 add/add conflicts: this branch's fixed
versions of the JPEG tag matrix pipeline/docs/workflow superseded PR
#19's pre-refinement originals; #21/#22's files merged cleanly with no
overlap). Verified: full workspace test suite passes (3,615 tests, 0
failures, 37 binaries), release binary rebuilds cleanly.

Re-ran the full 4,812-tag matrix against the fully current binary.
Result: only one tag changed status -- File:Comment flipped from
unsupported to readable, confirming #22's COM-comment read-path fix.
The rest of #22's fixes (SPIFF, DQT-derived quality, multi-chunk ICC,
APP6/GoPro) aren't independently visible to this harness: they're
either not ExifTool-writable tags at all, or fall outside the
EXIF/XMP/IPTC/JFIF/Photoshop/ICC_Profile groups this matrix synthesizes
samples for -- noted explicitly in the updated R8 entry rather than
left to look unconfirmed. Zero write regressions verified: the exact
same 122 tags remain CLI-writable in every re-run this session.
Baseline ratcheted 2028 -> 2029 readable to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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