feat(thumbnails): support BigTIFF containers for raw previews (DNG 1.7)

This commit is contained in:
Dominik Schmidt committed 2026-08-19 07:30:53 +02:00
1 parent 83c1dc8d49
commit 47a7136c46
2 files changed
+152 -33

No files matched your search

+90 -33
View File
@@ -43,13 +43,23 @@ const (
tiffTagJPEGLength = 0x0202 // JPEGInterchangeFormatLength
tiffTypeShort = 3
tiffTypeLong = 4
tiffTypeLong8 = 16 // BigTIFF 64-bit offset
tiffMagic = 42
// bound work on untrusted input: cap processed IFDs (and the queue) and
// the JPEG header segments walked before the SOF marker
bigTiffMagic = 43
// bound work on untrusted input: cap processed IFDs (and the queue), the
// entries per IFD (BigTIFF counts are 64-bit) and the JPEG header segments
// walked before the SOF marker
maxIFDs = 64
maxIFDEntries = 4096
maxJPEGSegments = 32
)
// isLongType reports whether a tag type carries a 32-bit (or, in BigTIFF, a
// 64-bit) integer offset we can follow.
func isLongType(typ uint16, bigTiff bool) bool {
return typ == tiffTypeLong || (bigTiff && typ == tiffTypeLong8)
}
// maxPreviewLength bounds the served preview: previews are camera-generated
// JPEGs, so tens of MB is already generous and an oversized declared length is
// rejected rather than served. A var so tests can lower it.
@@ -70,23 +80,57 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
default:
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
}
if order.Uint16(data[2:4]) != tiffMagic {
// classic TIFF (magic 42, 32-bit offsets) and BigTIFF (magic 43, 64-bit
// offsets, allowed for DNG since spec 1.7) share the IFD layout, only with
// wider entry-count, per-entry count and offset/value fields.
var (
bigTiff bool
firstIFD uint64
countSize int // width of the IFD entry-count field
entrySize int // width of one IFD entry
offW int // width of an offset / inline value field
)
switch order.Uint16(data[2:4]) {
case tiffMagic:
countSize, entrySize, offW = 2, 12, 4
firstIFD = uint64(order.Uint32(data[4:8]))
case bigTiffMagic:
// header carries the offset size (always 8) and a constant 0 before
// the 8-byte IFD0 offset
if len(data) < 16 || order.Uint16(data[4:6]) != 8 || order.Uint16(data[6:8]) != 0 {
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
}
bigTiff = true
countSize, entrySize, offW = 8, 20, 8
firstIFD = order.Uint64(data[8:16])
default:
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
}
type candidate struct{ offset, length uint32 }
// readOff reads a 4- or 8-byte offset/value depending on the container.
readOff := func(b []byte) uint64 {
if bigTiff {
return order.Uint64(b[:8])
}
return uint64(order.Uint32(b[:4]))
}
valAt := 4 + offW // inline value field: after tag(2) + type(2) + count(offW)
type candidate struct{ offset, length uint64 }
var candidates []candidate
var orientation uint16
queue := []uint32{order.Uint32(data[4:8])}
seen := map[uint32]struct{}{}
queue := []uint64{firstIFD}
seen := map[uint64]struct{}{}
// push queues an IFD offset unless the queue is already full; a crafted
// file with millions of SubIFD pointers must not grow it without bound
push := func(offset uint32) {
push := func(offset uint64) {
if len(queue) < maxIFDs {
queue = append(queue, offset)
}
}
dlen := uint64(len(data))
first := true
for len(queue) > 0 && len(seen) < maxIFDs {
ifdOffset := queue[0]
@@ -95,59 +139,72 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
continue
}
seen[ifdOffset] = struct{}{}
if ifdOffset == 0 || int(ifdOffset)+2 > len(data) {
if ifdOffset == 0 || ifdOffset+uint64(countSize) > dlen {
continue
}
entryCount := int(order.Uint16(data[ifdOffset : ifdOffset+2]))
entriesEnd := int(ifdOffset) + 2 + entryCount*12
if entriesEnd+4 > len(data) {
var entryCount int
if bigTiff {
entryCount = int(order.Uint64(data[ifdOffset : ifdOffset+8]))
} else {
entryCount = int(order.Uint16(data[ifdOffset : ifdOffset+2]))
}
if entryCount < 0 || entryCount > maxIFDEntries {
continue
}
entriesEnd := ifdOffset + uint64(countSize) + uint64(entryCount)*uint64(entrySize)
if entriesEnd+uint64(offW) > dlen {
continue
}
var jpegOffset, jpegLength, stripOffset, stripLength uint32
var jpegOffset, jpegLength, stripOffset, stripLength uint64
for i := 0; i < entryCount; i++ {
entry := data[int(ifdOffset)+2+i*12:]
entry := data[ifdOffset+uint64(countSize)+uint64(i*entrySize):]
tag := order.Uint16(entry[0:2])
typ := order.Uint16(entry[2:4])
count := order.Uint32(entry[4:8])
var count uint64
if bigTiff {
count = order.Uint64(entry[4:12])
} else {
count = uint64(order.Uint32(entry[4:8]))
}
switch tag {
case tiffTagOrientation:
// only IFD0's orientation applies
if first && typ == tiffTypeShort && count == 1 {
orientation = order.Uint16(entry[8:10])
orientation = order.Uint16(entry[valAt : valAt+2])
}
case tiffTagJPEGOffset:
if typ == tiffTypeLong && count == 1 {
jpegOffset = order.Uint32(entry[8:12])
if isLongType(typ, bigTiff) && count == 1 {
jpegOffset = readOff(entry[valAt:])
}
case tiffTagJPEGLength:
if typ == tiffTypeLong && count == 1 {
jpegLength = order.Uint32(entry[8:12])
if isLongType(typ, bigTiff) && count == 1 {
jpegLength = readOff(entry[valAt:])
}
case tiffTagStripOffsets:
// only single-strip images can be a contiguous JPEG stream
if typ == tiffTypeLong && count == 1 {
stripOffset = order.Uint32(entry[8:12])
if isLongType(typ, bigTiff) && count == 1 {
stripOffset = readOff(entry[valAt:])
}
case tiffTagStripByteCounts:
if typ == tiffTypeLong && count == 1 {
stripLength = order.Uint32(entry[8:12])
if isLongType(typ, bigTiff) && count == 1 {
stripLength = readOff(entry[valAt:])
}
case tiffTagSubIFDs:
if typ != tiffTypeLong {
if !isLongType(typ, bigTiff) {
continue
}
if count == 1 {
push(order.Uint32(entry[8:12]))
push(readOff(entry[valAt:]))
continue
}
arrayOffset := order.Uint32(entry[8:12])
for j := uint32(0); j < count && j < maxIFDs; j++ {
pos := int(arrayOffset) + int(j)*4
if pos+4 > len(data) || len(queue) >= maxIFDs {
arrayOffset := readOff(entry[valAt:])
for j := uint64(0); j < count && j < uint64(maxIFDs); j++ {
pos := arrayOffset + j*uint64(offW)
if pos+uint64(offW) > dlen || len(queue) >= maxIFDs {
break
}
push(order.Uint32(data[pos : pos+4]))
push(readOff(data[pos:]))
}
}
}
@@ -157,14 +214,14 @@ func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
if stripOffset > 0 && stripLength > 0 {
candidates = append(candidates, candidate{stripOffset, stripLength})
}
push(order.Uint32(data[entriesEnd : entriesEnd+4]))
push(readOff(data[entriesEnd:]))
first = false
}
var best []byte
for _, c := range candidates {
end := int64(c.offset) + int64(c.length)
if end > int64(len(data)) || int64(c.length) > maxPreviewLength || int(c.length) <= len(best) {
end := c.offset + c.length
if c.length == 0 || end < c.offset || end > dlen || int64(c.length) > maxPreviewLength || int(c.length) <= len(best) {
continue
}
jpg := data[c.offset:end]
@@ -391,3 +391,65 @@ var _ = Describe("RawTiffDecoder orientation direction", func() {
})
}
})
// buildBigTiffRawFile assembles a minimal little-endian BigTIFF (magic 43,
// 8-byte counts/offsets, 20-byte entries) as DNG allows since spec 1.7: IFD0
// carries the orientation and a LONG8 JpegInterchange preview.
func buildBigTiffRawFile(orientation uint16, preview []byte) []byte {
le := binary.LittleEndian
// header: II, magic 43, offset size 8, constant 0, then the 8-byte IFD0 offset
buf := []byte{'I', 'I', 43, 0, 8, 0, 0, 0}
buf = le.AppendUint64(buf, 16)
entry := func(tag, typ uint16, value uint64) []byte {
e := le.AppendUint16(nil, tag)
e = le.AppendUint16(e, typ)
e = le.AppendUint64(e, 1) // count
return le.AppendUint64(e, value)
}
// IFD0: entryCount(8) + 3 entries(20 each) + next(8)
previewStart := uint64(16) + 8 + 3*20 + 8
ifd0 := le.AppendUint64(nil, 3)
ifd0 = append(ifd0, entry(tiffTagOrientation, tiffTypeShort, uint64(orientation))...)
ifd0 = append(ifd0, entry(tiffTagJPEGOffset, tiffTypeLong8, previewStart)...)
ifd0 = append(ifd0, entry(tiffTagJPEGLength, tiffTypeLong8, uint64(len(preview)))...)
ifd0 = le.AppendUint64(ifd0, 0)
buf = append(buf, ifd0...)
return append(buf, preview...)
}
var _ = Describe("RawTiffDecoder BigTIFF (DNG 1.7)", func() {
preview := encodeJPEG(64, 32)
It("extracts the embedded JPEG and orientation from a BigTIFF container", func() {
jpg, orientation, err := extractEmbeddedJPEG(buildBigTiffRawFile(6, preview))
Expect(err).ToNot(HaveOccurred())
Expect(jpg).To(Equal(preview))
Expect(orientation).To(Equal(uint16(6)))
})
It("rejects a BigTIFF header with an unexpected offset size", func() {
data := buildBigTiffRawFile(1, preview)
data[4] = 4 // offset size must be 8
_, _, err := extractEmbeddedJPEG(data)
Expect(err).To(MatchError(thumbnailerErrors.ErrNoImageFromRawFile))
})
It("decodes and orients a BigTIFF preview end to end", func() {
img, err := RawTiffDecoder{}.Convert(bytes.NewReader(buildBigTiffRawFile(6, preview)))
Expect(err).ToNot(HaveOccurred())
bounds := img.(image.Image).Bounds()
Expect(bounds.Dx()).To(Equal(32))
Expect(bounds.Dy()).To(Equal(64))
})
It("survives truncated BigTIFF files", func() {
full := buildBigTiffRawFile(1, preview)
for cut := 0; cut < len(full); cut += 7 {
_, _, _ = extractEmbeddedJPEG(full[:cut])
}
})
})