From 0f1e362ad8a2d032898725c264175b402247f566 Mon Sep 17 00:00:00 2001 From: Puneet Rai Date: Sun, 8 Feb 2026 00:48:21 +0530 Subject: [PATCH] fix(exif): decode ComponentsConfiguration to human-readable format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api_integration_test.go | 4 +- internal/parser/tiff/values.go | 62 ++++++++++++++++++++++++++++ internal/parser/tiff/values_test.go | 63 +++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/api_integration_test.go b/api_integration_test.go index b74818a..8633756 100644 --- a/api_integration_test.go +++ b/api_integration_test.go @@ -21,7 +21,7 @@ func TestIntegration_JPEG(t *testing.T) { {Name: "ApertureValue", Value: "54823/32325"}, {Name: "BrightnessValue", Value: "40874/4739"}, {Name: "ColorSpace", Value: "Uncalibrated"}, - {Name: "ComponentsConfiguration", Value: []byte{1, 2, 3, 0}}, + {Name: "ComponentsConfiguration", Value: "Y, Cb, Cr, -"}, {Name: "CustomRendered", Value: "Portrait HDR"}, {Name: "DateTimeDigitized", Value: "2019:09:21 14:43:51"}, {Name: "DateTimeOriginal", Value: "2019:09:21 14:43:51"}, @@ -275,7 +275,7 @@ func TestIntegration_CR2(t *testing.T) { {Name: "ExifVersion", Value: "0221"}, {Name: "DateTimeOriginal", Value: "2004:11:13 23:02:21"}, {Name: "DateTimeDigitized", Value: "2004:11:13 23:02:21"}, - {Name: "ComponentsConfiguration", Value: []byte{1, 2, 3, 0}}, // Y, Cb, Cr, - + {Name: "ComponentsConfiguration", Value: "Y, Cb, Cr, -"}, // Y, Cb, Cr, - {Name: "ShutterSpeedValue", Value: "434176/65536"}, {Name: "ApertureValue", Value: "65536/65536"}, {Name: "ExposureBiasValue", Value: "0/1"}, diff --git a/internal/parser/tiff/values.go b/internal/parser/tiff/values.go index 98071c1..b8e1f15 100644 --- a/internal/parser/tiff/values.go +++ b/internal/parser/tiff/values.go @@ -5,6 +5,11 @@ import "fmt" // decodeEnumValue returns a human-readable string for enum tag values. // Returns empty string if the tag is not an enum or value is unknown. func decodeEnumValue(tag uint16, _ string, value any) string { + // Handle ComponentsConfiguration (0x9101) specially - it's a 4-byte array + if tag == 0x9101 { + return decodeComponentsConfiguration(value) + } + // Handle uint16 values (most common for enums) var v uint16 switch val := value.(type) { @@ -417,3 +422,60 @@ func decodeFlashValue(value uint16) string { } return result } + +// decodeComponentsConfiguration decodes the ComponentsConfiguration tag (0x9101). +// The value is 4 bytes where each byte represents a component: +// - 0 = does not exist (displayed as "-") +// - 1 = Y (luminance) +// - 2 = Cb (blue chrominance) +// - 3 = Cr (red chrominance) +// - 4 = R (red) +// - 5 = G (green) +// - 6 = B (blue) +func decodeComponentsConfiguration(value any) string { + var bytes []byte + + switch v := value.(type) { + case []byte: + bytes = v + case string: + // Handle hex string like "01020300" + if len(v) == 8 { + bytes = make([]byte, 4) + for i := 0; i < 4; i++ { + var b byte + fmt.Sscanf(v[i*2:i*2+2], "%02x", &b) + bytes[i] = b + } + } else { + return "" + } + default: + return "" + } + + if len(bytes) < 4 { + return "" + } + + componentNames := map[byte]string{ + 0: "-", + 1: "Y", + 2: "Cb", + 3: "Cr", + 4: "R", + 5: "G", + 6: "B", + } + + parts := make([]string, 4) + for i := 0; i < 4; i++ { + if name, ok := componentNames[bytes[i]]; ok { + parts[i] = name + } else { + parts[i] = fmt.Sprintf("%d", bytes[i]) + } + } + + return parts[0] + ", " + parts[1] + ", " + parts[2] + ", " + parts[3] +} diff --git a/internal/parser/tiff/values_test.go b/internal/parser/tiff/values_test.go index 030ada0..1b05e36 100644 --- a/internal/parser/tiff/values_test.go +++ b/internal/parser/tiff/values_test.go @@ -336,3 +336,66 @@ func TestEnumMappingsComplete(t *testing.T) { t.Errorf("ResolutionUnit should have 3 values, got %d", len(tiffEnumValues[0x0128])) } } + +func TestDecodeComponentsConfiguration(t *testing.T) { + tests := []struct { + name string + value any + expected string + }{ + { + name: "YCbCr standard", + value: []byte{1, 2, 3, 0}, + expected: "Y, Cb, Cr, -", + }, + { + name: "RGB", + value: []byte{4, 5, 6, 0}, + expected: "R, G, B, -", + }, + { + name: "hex string YCbCr", + value: "01020300", + expected: "Y, Cb, Cr, -", + }, + { + name: "hex string RGB", + value: "04050600", + expected: "R, G, B, -", + }, + { + name: "empty bytes", + value: []byte{0, 0, 0, 0}, + expected: "-, -, -, -", + }, + { + name: "too short", + value: []byte{1, 2}, + expected: "", + }, + { + name: "invalid type", + value: 123, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := decodeComponentsConfiguration(tt.value) + if result != tt.expected { + t.Errorf("decodeComponentsConfiguration(%v) = %q, want %q", + tt.value, result, tt.expected) + } + }) + } +} + +func TestDecodeEnumValue_ComponentsConfiguration(t *testing.T) { + // Test that decodeEnumValue handles ComponentsConfiguration (0x9101) + result := decodeEnumValue(0x9101, "ExifIFD", []byte{1, 2, 3, 0}) + expected := "Y, Cb, Cr, -" + if result != expected { + t.Errorf("decodeEnumValue for ComponentsConfiguration = %q, want %q", result, expected) + } +}