FA-79021 / Image orientation metadata / Open access
XMP orientation with surrounding whitespace is ignored · case 01
Sidecar-edited photos whose XMP value is serialised with a newline lose their rotation.
ROOT CAUSE
The XMP text is compared without trimming whitespace.
VERIFIED REPAIR
Strip all surrounding whitespace before matching the digit.
Unsuccessful approach: strip(' ') removes spaces but not tabs or newlines from pretty-printed XMP.
Case contract
Resolve the displayed orientation tag from metadata sources. For HEIF, the container transforms are authoritative and EXIF/XMP are ignored: irot is anticlockwise quarter turns (default 0) applied first, then imir mirrors about the vertical axis (0, left-right) or the horizontal axis (1, top-bottom); the result is mapped to a tag through (k clockwise quarter turns after m mirrors) = {(0,0):1, (0,1):2, (2,0):3, (2,1):4, (3,1):5, (1,0):6, (1,1):7, (3,0):8}. For other containers a valid integer EXIF value 1..8 wins; otherwise an XMP string that is a single digit 1..8 after trimming whitespace; otherwise 1.
Why this case matters
Camera, phone and scanner images carry an orientation hint separately from the stored pixels; galleries, thumbnailers, editors and upload pipelines must interpret it consistently or photos appear sideways, mirrored or doubly rotated.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(meta):
from_km = {(0, 0): 1, (0, 1): 2, (2, 0): 3, (2, 1): 4, (3, 1): 5, (1, 0): 6, (1, 1): 7, (3, 0): 8}
c = meta.get('container')
if c == 'heif':
rot = meta.get('irot')
mir = meta.get('imir')
rot = 0 if rot is None else rot % 4
if mir is None:
return from_km[((-rot) % 4, 0)]
if mir == 0:
return from_km[(rot, 1)]
return from_km[((rot + 2) % 4, 1)]
e = meta.get('exif')
if isinstance(e, int) and not isinstance(e, bool) and 1 <= e <= 8:
return e
x = meta.get('xmp')
if isinstance(x, str):
s = x
if s in ('1', '2', '3', '4', '5', '6', '7', '8'):
return int(s)
return 1
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[{'container': 'tiff', 'exif': True, 'xmp': '7 '}, 7], [{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'png'}, 1], [{'container': 'jpeg', 'xmp': '2'}, 2], [{'container': 'heif', 'xmp': '\t5', 'irot': 3}, 6], [{'container': 'tiff'}, 1], [{'container': 'tiff', 'exif': True}, 1], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 5, 'xmp': ' 8', 'imir': 0}, 2], [{'container': 'heif', 'exif': 3, 'xmp': 'x', 'irot': 0}, 1], [{'container': 'heif', 'xmp': '\t5', 'irot': 0, 'imir': 1}, 4], [{'container': 'png'}, 1], [{'container': 'heif', 'exif': 3, 'irot': 3, 'imir': 1}, 7], [{'container': 'jpeg', 'xmp': '3\n'}, 3]], [[{'container': 'png', 'xmp': '\t5'}, 5], [{'container': 'tiff', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'tiff'}, 1], [{'container': 'jpeg'}, 1], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 6, 'xmp': 'x', 'irot': 2}, 3], [{'container': 'png', 'exif': 1}, 1], [{'container': 'jpeg', 'exif': 9, 'xmp': ' 8'}, 8]], [[{'container': 'png', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5', 'irot': 3, 'imir': 0}, 5], [{'container': 'png', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 1}, 1], [{'container': 'heif', 'exif': 1, 'xmp': '7 ', 'irot': 2}, 3], [{'container': 'heif', 'xmp': '\t5'}, 1], [{'container': 'tiff', 'exif': 5}, 5], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5'}, 5], [{'container': 'heif', 'irot': 2}, 3], [{'container': 'heif', 'xmp': ' 8', 'irot': 1}, 8], [{'container': 'jpeg', 'xmp': 'x'}, 1], [{'container': 'png', 'exif': 7}, 7], [{'container': 'jpeg', 'exif': 7}, 7], [{'container': 'tiff', 'exif': 9, 'xmp': '\t5'}, 5]]]
labels = ["regression: XMP value trimming", "repair trap", "combined fault", "control", "control", "boundary", "boundary", "control"]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("%s %d" % (labels[i % len(labels)], i), solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: XMP value trimming 0 | 1 | 7 | Failed |
| repair trap 1 | 1 | 5 | Failed |
| combined fault 2 | 1 | 1 | Passed |
| control 3 | 2 | 2 | Passed |
| control 4 | 6 | 6 | Passed |
| boundary 5 | 1 | 1 | Passed |
| boundary 6 | 1 | 1 | Passed |
| control 7 | 1 | 7 | Failed |
SHA-256 / 38ef02d669ecd521bcc8b36403c17c4097c5f39b765228e6a639b5d675aae430
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(meta):
from_km = {(0, 0): 1, (0, 1): 2, (2, 0): 3, (2, 1): 4, (3, 1): 5, (1, 0): 6, (1, 1): 7, (3, 0): 8}
c = meta.get('container')
if c == 'heif':
rot = meta.get('irot')
mir = meta.get('imir')
rot = 0 if rot is None else rot % 4
if mir is None:
return from_km[((-rot) % 4, 0)]
if mir == 0:
return from_km[(rot, 1)]
return from_km[((rot + 2) % 4, 1)]
e = meta.get('exif')
if isinstance(e, int) and not isinstance(e, bool) and 1 <= e <= 8:
return e
x = meta.get('xmp')
if isinstance(x, str):
s = x.strip(' ')
if s in ('1', '2', '3', '4', '5', '6', '7', '8'):
return int(s)
return 1
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[{'container': 'tiff', 'exif': True, 'xmp': '7 '}, 7], [{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'png'}, 1], [{'container': 'jpeg', 'xmp': '2'}, 2], [{'container': 'heif', 'xmp': '\t5', 'irot': 3}, 6], [{'container': 'tiff'}, 1], [{'container': 'tiff', 'exif': True}, 1], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 5, 'xmp': ' 8', 'imir': 0}, 2], [{'container': 'heif', 'exif': 3, 'xmp': 'x', 'irot': 0}, 1], [{'container': 'heif', 'xmp': '\t5', 'irot': 0, 'imir': 1}, 4], [{'container': 'png'}, 1], [{'container': 'heif', 'exif': 3, 'irot': 3, 'imir': 1}, 7], [{'container': 'jpeg', 'xmp': '3\n'}, 3]], [[{'container': 'png', 'xmp': '\t5'}, 5], [{'container': 'tiff', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'tiff'}, 1], [{'container': 'jpeg'}, 1], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 6, 'xmp': 'x', 'irot': 2}, 3], [{'container': 'png', 'exif': 1}, 1], [{'container': 'jpeg', 'exif': 9, 'xmp': ' 8'}, 8]], [[{'container': 'png', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5', 'irot': 3, 'imir': 0}, 5], [{'container': 'png', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 1}, 1], [{'container': 'heif', 'exif': 1, 'xmp': '7 ', 'irot': 2}, 3], [{'container': 'heif', 'xmp': '\t5'}, 1], [{'container': 'tiff', 'exif': 5}, 5], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5'}, 5], [{'container': 'heif', 'irot': 2}, 3], [{'container': 'heif', 'xmp': ' 8', 'irot': 1}, 8], [{'container': 'jpeg', 'xmp': 'x'}, 1], [{'container': 'png', 'exif': 7}, 7], [{'container': 'jpeg', 'exif': 7}, 7], [{'container': 'tiff', 'exif': 9, 'xmp': '\t5'}, 5]]]
labels = ["regression: XMP value trimming", "repair trap", "combined fault", "control", "control", "boundary", "boundary", "control"]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("%s %d" % (labels[i % len(labels)], i), solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: XMP value trimming 0 | 7 | 7 | Passed |
| repair trap 1 | 1 | 5 | Failed |
| combined fault 2 | 1 | 1 | Passed |
| control 3 | 2 | 2 | Passed |
| control 4 | 6 | 6 | Passed |
| boundary 5 | 1 | 1 | Passed |
| boundary 6 | 1 | 1 | Passed |
| control 7 | 7 | 7 | Passed |
SHA-256 / 0046017f40d5c1a36f10a2ab42a2a99439d878536ae8e6cb7cc844c9f616dc4b
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(meta):
from_km = {(0, 0): 1, (0, 1): 2, (2, 0): 3, (2, 1): 4, (3, 1): 5, (1, 0): 6, (1, 1): 7, (3, 0): 8}
c = meta.get('container')
if c == 'heif':
rot = meta.get('irot')
mir = meta.get('imir')
rot = 0 if rot is None else rot % 4
if mir is None:
return from_km[((-rot) % 4, 0)]
if mir == 0:
return from_km[(rot, 1)]
return from_km[((rot + 2) % 4, 1)]
e = meta.get('exif')
if isinstance(e, int) and not isinstance(e, bool) and 1 <= e <= 8:
return e
x = meta.get('xmp')
if isinstance(x, str):
s = x.strip()
if s in ('1', '2', '3', '4', '5', '6', '7', '8'):
return int(s)
return 1
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[{'container': 'tiff', 'exif': True, 'xmp': '7 '}, 7], [{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'png'}, 1], [{'container': 'jpeg', 'xmp': '2'}, 2], [{'container': 'heif', 'xmp': '\t5', 'irot': 3}, 6], [{'container': 'tiff'}, 1], [{'container': 'tiff', 'exif': True}, 1], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'exif': 0, 'xmp': '\t5'}, 5], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 5, 'xmp': ' 8', 'imir': 0}, 2], [{'container': 'heif', 'exif': 3, 'xmp': 'x', 'irot': 0}, 1], [{'container': 'heif', 'xmp': '\t5', 'irot': 0, 'imir': 1}, 4], [{'container': 'png'}, 1], [{'container': 'heif', 'exif': 3, 'irot': 3, 'imir': 1}, 7], [{'container': 'jpeg', 'xmp': '3\n'}, 3]], [[{'container': 'png', 'xmp': '\t5'}, 5], [{'container': 'tiff', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'tiff'}, 1], [{'container': 'jpeg'}, 1], [{'container': 'jpeg', 'exif': 0}, 1], [{'container': 'heif', 'exif': 6, 'xmp': 'x', 'irot': 2}, 3], [{'container': 'png', 'exif': 1}, 1], [{'container': 'jpeg', 'exif': 9, 'xmp': ' 8'}, 8]], [[{'container': 'png', 'exif': 9, 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5', 'irot': 3, 'imir': 0}, 5], [{'container': 'png', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 1}, 1], [{'container': 'heif', 'exif': 1, 'xmp': '7 ', 'irot': 2}, 3], [{'container': 'heif', 'xmp': '\t5'}, 1], [{'container': 'tiff', 'exif': 5}, 5], [{'container': 'jpeg', 'xmp': '7 '}, 7]], [[{'container': 'jpeg', 'xmp': '3\n'}, 3], [{'container': 'jpeg', 'exif': 9, 'xmp': '\t5'}, 5], [{'container': 'heif', 'irot': 2}, 3], [{'container': 'heif', 'xmp': ' 8', 'irot': 1}, 8], [{'container': 'jpeg', 'xmp': 'x'}, 1], [{'container': 'png', 'exif': 7}, 7], [{'container': 'jpeg', 'exif': 7}, 7], [{'container': 'tiff', 'exif': 9, 'xmp': '\t5'}, 5]]]
labels = ["regression: XMP value trimming", "repair trap", "combined fault", "control", "control", "boundary", "boundary", "control"]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("%s %d" % (labels[i % len(labels)], i), solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: XMP value trimming 0 | 7 | 7 | Passed |
| repair trap 1 | 5 | 5 | Passed |
| combined fault 2 | 1 | 1 | Passed |
| control 3 | 2 | 2 | Passed |
| control 4 | 6 | 6 | Passed |
| boundary 5 | 1 | 1 | Passed |
| boundary 6 | 1 | 1 | Passed |
| control 7 | 7 | 7 | Passed |
SHA-256 / 196bc76e9ec2e477d61d6e61ffa24dbfb70776f3f0b386078b744828b104b753
Verification & scope
A deterministic bounded teaching model with a stipulated contract; it makes no claim of conformance to any published specification. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:49:40.558431+00:00.
Case digest / dbba2eb2158a82185049f88ab652edc911a1d1a8a7e57f43fc07752fc0722600