Skip to content

Commit d996fd1

Browse files
rpuneetclaude
andauthored
fix(exif): decode ComponentsConfiguration to human-readable format (#35)
Fixes #18 ComponentsConfiguration tag (0x9101) was displaying as hex bytes (e.g., "01020300") instead of the standard component notation. Now decodes to human-readable format: - 0 = "-" (does not exist) - 1 = "Y" (luminance) - 2 = "Cb" (blue chrominance) - 3 = "Cr" (red chrominance) - 4 = "R", 5 = "G", 6 = "B" Example: [1,2,3,0] → "Y, Cb, Cr, -" Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 7ff0b3e commit d996fd1

File tree

3 files changed

+127
-2
lines changed

3 files changed

+127
-2
lines changed

api_integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestIntegration_JPEG(t *testing.T) {
2121
{Name: "ApertureValue", Value: "54823/32325"},
2222
{Name: "BrightnessValue", Value: "40874/4739"},
2323
{Name: "ColorSpace", Value: "Uncalibrated"},
24-
{Name: "ComponentsConfiguration", Value: []byte{1, 2, 3, 0}},
24+
{Name: "ComponentsConfiguration", Value: "Y, Cb, Cr, -"},
2525
{Name: "CustomRendered", Value: "Portrait HDR"},
2626
{Name: "DateTimeDigitized", Value: "2019:09:21 14:43:51"},
2727
{Name: "DateTimeOriginal", Value: "2019:09:21 14:43:51"},
@@ -275,7 +275,7 @@ func TestIntegration_CR2(t *testing.T) {
275275
{Name: "ExifVersion", Value: "0221"},
276276
{Name: "DateTimeOriginal", Value: "2004:11:13 23:02:21"},
277277
{Name: "DateTimeDigitized", Value: "2004:11:13 23:02:21"},
278-
{Name: "ComponentsConfiguration", Value: []byte{1, 2, 3, 0}}, // Y, Cb, Cr, -
278+
{Name: "ComponentsConfiguration", Value: "Y, Cb, Cr, -"}, // Y, Cb, Cr, -
279279
{Name: "ShutterSpeedValue", Value: "434176/65536"},
280280
{Name: "ApertureValue", Value: "65536/65536"},
281281
{Name: "ExposureBiasValue", Value: "0/1"},

internal/parser/tiff/values.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import "fmt"
55
// decodeEnumValue returns a human-readable string for enum tag values.
66
// Returns empty string if the tag is not an enum or value is unknown.
77
func decodeEnumValue(tag uint16, _ string, value any) string {
8+
// Handle ComponentsConfiguration (0x9101) specially - it's a 4-byte array
9+
if tag == 0x9101 {
10+
return decodeComponentsConfiguration(value)
11+
}
12+
813
// Handle uint16 values (most common for enums)
914
var v uint16
1015
switch val := value.(type) {
@@ -417,3 +422,60 @@ func decodeFlashValue(value uint16) string {
417422
}
418423
return result
419424
}
425+
426+
// decodeComponentsConfiguration decodes the ComponentsConfiguration tag (0x9101).
427+
// The value is 4 bytes where each byte represents a component:
428+
// - 0 = does not exist (displayed as "-")
429+
// - 1 = Y (luminance)
430+
// - 2 = Cb (blue chrominance)
431+
// - 3 = Cr (red chrominance)
432+
// - 4 = R (red)
433+
// - 5 = G (green)
434+
// - 6 = B (blue)
435+
func decodeComponentsConfiguration(value any) string {
436+
var bytes []byte
437+
438+
switch v := value.(type) {
439+
case []byte:
440+
bytes = v
441+
case string:
442+
// Handle hex string like "01020300"
443+
if len(v) == 8 {
444+
bytes = make([]byte, 4)
445+
for i := 0; i < 4; i++ {
446+
var b byte
447+
fmt.Sscanf(v[i*2:i*2+2], "%02x", &b)
448+
bytes[i] = b
449+
}
450+
} else {
451+
return ""
452+
}
453+
default:
454+
return ""
455+
}
456+
457+
if len(bytes) < 4 {
458+
return ""
459+
}
460+
461+
componentNames := map[byte]string{
462+
0: "-",
463+
1: "Y",
464+
2: "Cb",
465+
3: "Cr",
466+
4: "R",
467+
5: "G",
468+
6: "B",
469+
}
470+
471+
parts := make([]string, 4)
472+
for i := 0; i < 4; i++ {
473+
if name, ok := componentNames[bytes[i]]; ok {
474+
parts[i] = name
475+
} else {
476+
parts[i] = fmt.Sprintf("%d", bytes[i])
477+
}
478+
}
479+
480+
return parts[0] + ", " + parts[1] + ", " + parts[2] + ", " + parts[3]
481+
}

internal/parser/tiff/values_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,66 @@ func TestEnumMappingsComplete(t *testing.T) {
336336
t.Errorf("ResolutionUnit should have 3 values, got %d", len(tiffEnumValues[0x0128]))
337337
}
338338
}
339+
340+
func TestDecodeComponentsConfiguration(t *testing.T) {
341+
tests := []struct {
342+
name string
343+
value any
344+
expected string
345+
}{
346+
{
347+
name: "YCbCr standard",
348+
value: []byte{1, 2, 3, 0},
349+
expected: "Y, Cb, Cr, -",
350+
},
351+
{
352+
name: "RGB",
353+
value: []byte{4, 5, 6, 0},
354+
expected: "R, G, B, -",
355+
},
356+
{
357+
name: "hex string YCbCr",
358+
value: "01020300",
359+
expected: "Y, Cb, Cr, -",
360+
},
361+
{
362+
name: "hex string RGB",
363+
value: "04050600",
364+
expected: "R, G, B, -",
365+
},
366+
{
367+
name: "empty bytes",
368+
value: []byte{0, 0, 0, 0},
369+
expected: "-, -, -, -",
370+
},
371+
{
372+
name: "too short",
373+
value: []byte{1, 2},
374+
expected: "",
375+
},
376+
{
377+
name: "invalid type",
378+
value: 123,
379+
expected: "",
380+
},
381+
}
382+
383+
for _, tt := range tests {
384+
t.Run(tt.name, func(t *testing.T) {
385+
result := decodeComponentsConfiguration(tt.value)
386+
if result != tt.expected {
387+
t.Errorf("decodeComponentsConfiguration(%v) = %q, want %q",
388+
tt.value, result, tt.expected)
389+
}
390+
})
391+
}
392+
}
393+
394+
func TestDecodeEnumValue_ComponentsConfiguration(t *testing.T) {
395+
// Test that decodeEnumValue handles ComponentsConfiguration (0x9101)
396+
result := decodeEnumValue(0x9101, "ExifIFD", []byte{1, 2, 3, 0})
397+
expected := "Y, Cb, Cr, -"
398+
if result != expected {
399+
t.Errorf("decodeEnumValue for ComponentsConfiguration = %q, want %q", result, expected)
400+
}
401+
}

0 commit comments

Comments
 (0)