diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-24 10:36:25 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-24 10:36:25 +0300 |
| commit | 6dfc0f1a9521cc4afe4941fc1328eec014b760a7 (patch) | |
| tree | 4303307c104aa9dbe84e906eb9c0f1dd279469d9 /src | |
| parent | 99c5241d853daceb8cfaec91023730ff062858b7 (diff) | |
Promote single canonical identify-stream EXIF parser into metadata-cache (8r0)
The `identify -verbose` EXIF-line regex and its array-fill loop were
duplicated in three places that had already drifted: album-metadata's
photo_exif_details_html and _photo_exif_values_to (exif: only) and
stats-aggregate's _stats_parse_identify_stream (exif: plus a native
Geometry -> __geometry path).
Promote one canonical parser, photo_exif_values_to, into
metadata-cache.source.sh next to its sibling cache primitive
cached_photo_identify_output. It reads an identify stream from stdin and
fills a nameref associative array; it is a strict superset of all three
former sites (bare exif: tag keys plus the synthetic __geometry key).
- stats accumulate_photo_stats now calls photo_exif_values_to (stdin);
_stats_parse_identify_stream is removed.
- album _photo_exif_values_to is a thin wrapper that pipes
cached_photo_identify_output through the canonical parser.
- album photo_exif_details_html consumes the same parser and skips the
__geometry key it does not display.
metadata-cache is sourced before both consumers in LIB_SOURCES, so the
canonical parser is available at use time. Regenerated bin/shuriken via
`just build`. Added test_shared_identify_parser_returns_exif_and_geometry
asserting one parse yields both an exif: key and __geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/album-metadata.source.sh | 64 | ||||
| -rw-r--r-- | src/lib/metadata-cache.source.sh | 38 | ||||
| -rw-r--r-- | src/lib/stats-aggregate.source.sh | 28 |
3 files changed, 81 insertions, 49 deletions
diff --git a/src/lib/album-metadata.source.sh b/src/lib/album-metadata.source.sh index b74bf8f..767ec4c 100644 --- a/src/lib/album-metadata.source.sh +++ b/src/lib/album-metadata.source.sh @@ -8,31 +8,40 @@ photo_exif_details_html() { local -r photo="$1"; shift local -r photo_path="$1"; shift + # exif_values is filled via the shared parser and iterated below. + # shellcheck disable=SC2034 + local -A exif_values=() local key local key_html - local line local value local value_html local -i exif_count=0 - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then - key="exif:${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - key_html=$(_html_escape "$key") - value_html=$(_html_escape "$value") - - if (( exif_count == 0 )); then - printf '<table class="details">\n' - printf '<tbody>\n' - fi - - printf '<tr><th>%s</th><td>%s</td></tr>\n' \ - "$key_html" \ - "$value_html" - (( ++exif_count )) + # Parse via the single canonical reader/parser (task 8r0) instead of a + # private exif: regex loop. The parser also yields a synthetic __geometry key + # from the native Geometry line; the details table only shows real exif: tags + # (rendered with their historical "exif:" label prefix), so __geometry is + # skipped explicitly below. + _photo_exif_values_to exif_values "$photo" "$photo_path" + + for key in "${!exif_values[@]}"; do + if [ "$key" = '__geometry' ]; then + continue + fi + value="${exif_values[$key]}" + key_html=$(_html_escape "exif:$key") + value_html=$(_html_escape "$value") + + if (( exif_count == 0 )); then + printf '<table class="details">\n' + printf '<tbody>\n' fi - done < <(cached_photo_identify_output "$photo" "$photo_path") + + printf '<tr><th>%s</th><td>%s</td></tr>\n' \ + "$key_html" \ + "$value_html" + (( ++exif_count )) + done if (( exif_count == 0 )); then printf '<p class="details-empty">No EXIF details available.</p>\n' @@ -58,20 +67,19 @@ _first_exif_value_to() { done } +# Fill the caller's associative array with a photo's parsed EXIF values. Thin +# wrapper that pairs the shared cache reader with the shared stream parser +# (photo_exif_values_to, promoted to metadata-cache.source.sh in task 8r0): it +# reads the cached identify output for this photo and pipes it through the one +# canonical parser. The parser also captures a __geometry key, which album code +# simply ignores. _photo_exif_values_to() { - local -n output_ref="$1"; shift + local -r target_array="$1"; shift local -r photo="$1"; shift local -r photo_path="$1"; shift - local line - output_ref=() - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then - # output_ref writes to the caller-provided associative array. - # shellcheck disable=SC2034 - output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" - fi - done < <(cached_photo_identify_output "$photo" "$photo_path") + photo_exif_values_to "$target_array" \ + < <(cached_photo_identify_output "$photo" "$photo_path") } _photo_exif_tooltip_text_from_values() { diff --git a/src/lib/metadata-cache.source.sh b/src/lib/metadata-cache.source.sh index 45edd3c..ba40ca9 100644 --- a/src/lib/metadata-cache.source.sh +++ b/src/lib/metadata-cache.source.sh @@ -102,3 +102,41 @@ cached_photo_identify_output() { print_cached_photo_identify_output "$cache_file" } + +# Canonical `identify -verbose` stream parser. Reads an identify stream from +# stdin and fills the caller-provided associative array (by nameref) with one +# entry per recognised field. Promoted here (task 8r0) from three near-identical +# copies that had already drifted -- album-metadata had two exif:-only loops and +# stats-aggregate added a native Geometry path. This single definition is a +# strict superset of all three: it lives next to cached_photo_identify_output so +# both the album (tooltips, details tables) and the stats aggregator share one +# regex and one set of key conventions, isolating any future identify-format +# drift to one place. +# +# Keys produced: +# - exif:* lines -> stored under the bare tag name (e.g. "Make", "FNumber"), +# i.e. WITHOUT the leading "exif:" -- callers that want the prefixed label +# (the details table) re-add it. This matches the historical album and stats +# array keys exactly. +# - the native Geometry line ("WxH+x+y") -> stored under the synthetic key +# "__geometry"; stats reads this for dimension tallies, album ignores it. +# +# Reads from stdin (not from a photo path) so it composes with the cache layer: +# stats pipes a fixture or cache stream straight in, while album wraps it with +# `photo_exif_values_to ref < <(cached_photo_identify_output ...)`. +photo_exif_values_to() { + local -n output_ref="$1"; shift + local line + + output_ref=() + while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then + # output_ref writes to the caller-provided associative array. + # shellcheck disable=SC2034 + output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" + elif [[ "$line" =~ ^[[:space:]]*Geometry:[[:space:]]*(.*)$ ]]; then + # shellcheck disable=SC2034 + output_ref[__geometry]="${BASH_REMATCH[1]}" + fi + done +} diff --git a/src/lib/stats-aggregate.source.sh b/src/lib/stats-aggregate.source.sh index 6b30f62..77603c5 100644 --- a/src/lib/stats-aggregate.source.sh +++ b/src/lib/stats-aggregate.source.sh @@ -499,8 +499,8 @@ _stats_record_enums() { # Record dimension stats (megapixels, aspect ratio, orientation) from the native # Geometry field. Geometry is "WxH+x+y"; the leading WxH is what we need. These -# are native fields, not exif: lines, so they come from the separate native -# parser path in _stats_parse_identify_stream. +# are native fields, not exif: lines, so they come from the native Geometry +# (__geometry) path in the shared photo_exif_values_to parser. _stats_record_dimensions() { local -n values_ref="$1"; shift local -r photo="$1"; shift @@ -618,24 +618,6 @@ _stats_tally() { fi } -# Parse one photo's `identify -verbose` stream into an associative array. The -# current album.source.sh regex only captures exif: lines, but the audit needs -# the native Geometry field for dimensions, so this adds a second match path -# storing it under the synthetic key __geometry. -_stats_parse_identify_stream() { - local -n values_ref="$1"; shift - local line - - values_ref=() - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then - values_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" - elif [[ "$line" =~ ^[[:space:]]*Geometry:[[:space:]]*(.*)$ ]]; then - values_ref[__geometry]="${BASH_REMATCH[1]}" - fi - done -} - # Aggregate a single photo: parse its identify stream (from stdin) and update # every counter. Split out from collect_photo_exif_stats so tests can feed a # synthetic fixture without stubbing the cache layer. @@ -646,7 +628,11 @@ accumulate_photo_stats() { local -A exif_values=() local record_fn - _stats_parse_identify_stream exif_values + # Parse the identify stream from stdin via the single canonical parser + # (photo_exif_values_to, promoted to metadata-cache.source.sh in task 8r0). + # It captures both exif: tags and the native Geometry line under __geometry, + # which _stats_record_dimensions consumes. + photo_exif_values_to exif_values STATS_TOTALS[photos]=$(( STATS_TOTALS[photos] + 1 )) # Dispatch the EXIF-driven recorders from the registry list so categories are # not hardcoded here. Each takes the parsed values array plus the photo path. |
