mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-13 06:09:21 -04:00
feat(thumbnails): support BigTIFF containers for raw previews (DNG 1.7)
This commit is contained in:
1 parent
83c1dc8d49
commit
47a7136c46
2 files changed
+152
-33
No files matched your search
@@ -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])
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user