Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,42 @@ func TestIntegration_CR2(t *testing.T) {
{Name: "JPEGInterchangeFormatLength", Value: uint32(13120)},
},
},
{
Name: "Canon",
ExactTagCount: 28, // Canon EOS-1Ds Mark II MakerNote
Tags: []imxtest.TagExpectation{
// Core Canon tags
{Name: "CameraSettings1"},
{Name: "FocalLength"},
{Name: "FlashInfo"},
{Name: "CameraSettings2"},
{Name: "ImageType"},
{Name: "FirmwareVersion"},
{Name: "OwnerName"},
{Name: "SerialNumber"},
{Name: "CameraInfo"},
{Name: "CustomFunctions"},
{Name: "ModelID"},
{Name: "AFInfo"},
{Name: "ColorInfo"},
{Name: "VRDOffset"},
{Name: "SensorInfo"},
{Name: "ColorData"},
{Name: "CRWParam"},
{Name: "ColorInfo2"},
// Unknown/undocumented tags (identified by hex code)
{Name: "0x0013"},
{Name: "0x0015"},
{Name: "0x0019"},
{Name: "0x0083"},
{Name: "0x0091"},
{Name: "0x0092"},
{Name: "0x0093"},
{Name: "0x0094"},
{Name: "0x00AA"},
{Name: "0x4004"},
},
},
})
if result.Failed() {
for _, err := range result.Errors {
Expand Down
58 changes: 57 additions & 1 deletion internal/parser/tiff/ifd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tiff
import (
"bytes"
"fmt"
"io"
"strings"

imxbin "github.com/gomantics/imx/internal/binary"
Expand All @@ -11,7 +12,7 @@ import (
)

// parseIFD parses an IFD at the given offset
func (p *Parser) parseIFD(r *imxbin.Reader, offset int64, dirName string, iccDirs, iptcDirs, xmpDirs *[]parser.Directory, sharedParseErr *parser.ParseError) (*parser.Directory, *parser.ParseError, []SubIFD, uint16) {
func (p *Parser) parseIFD(r *imxbin.Reader, fileReader io.ReaderAt, offset int64, dirName string, iccDirs, iptcDirs, xmpDirs, makernoteDirs *[]parser.Directory, sharedParseErr *parser.ParseError) (*parser.Directory, *parser.ParseError, []SubIFD, uint16) {
var parseErr *parser.ParseError
if sharedParseErr != nil {
// Use shared error accumulator for multi-IFD parsing
Expand Down Expand Up @@ -60,6 +61,8 @@ func (p *Parser) parseIFD(r *imxbin.Reader, offset int64, dirName string, iccDir
p.handleIPTC(r, entry, parseErr, iptcDirs)
case TagXMP:
p.handleXMP(r, entry, parseErr, xmpDirs)
case TagMakerNote:
p.handleMakerNote(r, fileReader, entry, &dir.Tags, parseErr, makernoteDirs)
default:
// Regular tag
tag, err := p.parseTag(r, entry, dirName)
Expand Down Expand Up @@ -516,6 +519,59 @@ func (p *Parser) handleXMP(r *imxbin.Reader, entry *IFDEntry, parseErr *parser.P
}
}

// handleMakerNote handles MakerNote tag (tag 0x927C)
// MakerNote contains manufacturer-specific metadata in various formats.
// The raw MakerNote data is always returned as a tag in ExifIFD.
// When a manufacturer handler is registered and parses successfully,
// the parsed tags are also returned in a separate manufacturer directory.
func (p *Parser) handleMakerNote(r *imxbin.Reader, fileReader io.ReaderAt, entry *IFDEntry, dirTags *[]parser.Tag, parseErr *parser.ParseError, makernoteDirs *[]parser.Directory) {
// Read MakerNote data
makerNoteOffset := int64(entry.ValueOffset)
data, err := r.ReadBytes(makerNoteOffset, int(entry.Count))
if err != nil {
parseErr.Add(fmt.Errorf("failed to read MakerNote data at offset %d: %w", makerNoteOffset, err))
return
}

// Always add raw MakerNote tag for backward compatibility
*dirTags = append(*dirTags, parser.Tag{
ID: parser.TagID("ExifIFD:0x927C"),
Name: "MakerNote",
Value: data,
DataType: "UNDEFINED",
})

// If no registry, we're done
if p.makernote == nil {
return
}

// Try to detect and parse manufacturer-specific format
handler, cfg := p.makernote.Detect(data)
if handler == nil {
// Unknown manufacturer - raw tag already added above
return
}

// Parse manufacturer-specific tags
// exifBase is 0 for standard TIFF files (TIFF header at file start)
// TODO: For JPEG files, this would need to be the APP1 EXIF header offset
exifBase := int64(0)

tags, mnErr := handler.Parse(fileReader, makerNoteOffset, exifBase, cfg)
if mnErr != nil {
parseErr.Merge(mnErr)
}

// Create directory for MakerNote tags if any were parsed
if len(tags) > 0 {
*makernoteDirs = append(*makernoteDirs, parser.Directory{
Name: handler.Manufacturer(),
Tags: tags,
})
}
}

// handleSubIFDs handles SubIFDs tag (tag 0x014A)
// SubIFDs contain an array of offsets to sub-IFDs for preview/RAW image data
func (p *Parser) handleSubIFDs(r *imxbin.Reader, entry *IFDEntry, subIFDs *[]SubIFD, parseErr *parser.ParseError) {
Expand Down
7 changes: 4 additions & 3 deletions internal/parser/tiff/ifd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ func TestParser_parseIFD(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := imxbin.NewReader(bytes.NewReader(tt.data), order)
var iccDirs, iptcDirs, xmpDirs []parser.Directory
fileReader := bytes.NewReader(tt.data)
reader := imxbin.NewReader(fileReader, order)
var iccDirs, iptcDirs, xmpDirs, makernoteDirs []parser.Directory

// Test both with shared error and without
var parseErr *parser.ParseError
Expand All @@ -174,7 +175,7 @@ func TestParser_parseIFD(t *testing.T) {
} else {
parseErr = parser.NewParseError()
}
dir, _, subIFDs, numEntries := p.parseIFD(reader, 0, "IFD0", &iccDirs, &iptcDirs, &xmpDirs, parseErr)
dir, _, subIFDs, numEntries := p.parseIFD(reader, fileReader, 0, "IFD0", &iccDirs, &iptcDirs, &xmpDirs, &makernoteDirs, parseErr)

if (dir != nil) != tt.wantDir {
t.Errorf("dir = %v, wantDir %v", dir != nil, tt.wantDir)
Expand Down
Loading