Skip to content
Merged
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
264 changes: 264 additions & 0 deletions src/pixie/fileformats/tiff.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import chroma, flatty/binny, pixie/common, pixie/images, pixie/internal

const
tiffSignatures* = [
[0x4d.uint8, 0x4d, 0x00, 0x2a],
[0x49.uint8, 0x49, 0x2a, 0x00]
]
knownTags = [
0x0100.uint16, # ImageWidth
0x0101, # ImageLength
0x0102, # BitsPerSample
0x0103, # Compression
0x0106, # PhotometricInterpretation
0x0111, # StripOffsets
0x0116, # RowsPerStrip
0x0117, # StripByteCounts
0x0140, # ColorMap
]

type
Tiff* = ref object
width*, height*: int
data*: seq[ColorRGBA]

template failInvalid() =
raise newException(PixieError, "Invalid TIFF buffer, unable to load")

proc decodeTiff*(data: string): Tiff =
if data.len < 8:
failInvalid()

result = Tiff()

var
pos: int
isBigEndian: bool
bitsPerSample: seq[int]
compression: int
photometricInterpretation: int
stripOffsets, stripByteCounts: seq[int]
rowsPerStrip: int
colorMap: seq[ColorRGBA]

let signature = cast[array[4, uint8]](data.readUint32(0))
if signature == tiffSignatures[0]:
isBigEndian = true
elif signature == tiffSignatures[1]:
discard
else:
failInvalid()

pos = 4

let ifdOffset = data.readUint32(pos).maybeSwap(isBigEndian).int
pos = ifdOffset # Move to the first IFD offset

if pos + 2 > data.len:
failInvalid()

let numEntries = data.readUint16(pos).maybeSwap(isBigEndian).int
pos += 2

for _ in 0 ..< numEntries:
if pos + 12 > data.len:
failInvalid()

let
tag = data.readUint16(pos + 0).maybeSwap(isBigEndian)
fieldType = data.readUint16(pos + 2).maybeSwap(isBigEndian)
numValues = data.readUint32(pos + 4).maybeSwap(isBigEndian).int
valueOrOffset = pos + 8

pos += 12

if tag notin knownTags:
continue

let bytesPerValue =
case fieldType:
of 1:
1
of 2:
1
of 3:
2
of 4:
4
else:
raise newException(PixieError, "Unsupported field type " & $fieldType)

var valueOffset =
if numValues * bytesPerValue <= 4:
valueOrOffset
else:
data.readUint32(valueOrOffset).maybeSwap(isBigEndian).int

proc readValue(offset: int): int =
case fieldType:
of 1:
if offset + 1 > data.len:
failInvalid()
data.readUint8(offset).maybeSwap(isBigEndian).int
of 3:
if offset + 2 > data.len:
failInvalid()
data.readUint16(offset).maybeSwap(isBigEndian).int
of 4:
if offset + 4 > data.len:
failInvalid()
data.readUint32(offset).maybeSwap(isBigEndian).int
else:
raise newException(PixieError, "Unsupported field type " & $fieldType)

case tag:
of knownTags[0]:
if numValues != 1:
failInvalid()
result.width = readValue(valueOffset)
of knownTags[1]:
if numValues != 1:
failInvalid()
result.height = readValue(valueOffset)
of knownTags[2]:
for _ in 0 ..< numValues:
bitsPerSample.add(readValue(valueOffset))
valueOffset += bytesPerValue
of knownTags[3]:
if numValues != 1:
failInvalid()
compression = readValue(valueOffset)
of knownTags[4]:
if numValues != 1:
failInvalid()
photometricInterpretation = readValue(valueOffset)
of knownTags[5]:
for _ in 0 ..< numValues:
stripOffsets.add(readValue(valueOffset))
valueOffset += bytesPerValue
of knownTags[6]:
if numValues != 1:
failInvalid()
rowsPerStrip = readValue(valueOffset)
of knownTags[7]:
for _ in 0 ..< numValues:
stripByteCounts.add(readValue(valueOffset))
valueOffset += bytesPerValue
of knownTags[8]:
if fieldType != 3:
failInvalid()
var values: seq[int]
for _ in 0 ..< numValues:
values.add(readValue(valueOffset))
valueOffset += bytesPerValue
colorMap.setLen(numValues div 3)
for i in 0 ..< colorMap.len:
colorMap[i] = rgba(
((values[i].float32 / 65535) * 255).uint8,
((values[i + colorMap.len].float32 / 65535) * 255).uint8,
((values[i + 2 * colorMap.len].float32 / 65535) * 255).uint8,
255
)
else:
discard

if result.width == 0 or result.height == 0:
failInvalid()

if stripOffsets.len != stripByteCounts.len:
failInvalid()

if bitsPerSample.len == 0:
failInvalid()

for i, bits in bitsPerSample:
if bits notin {8}:
raise newException(
PixieError,
"TIFF bits per sample of " & $bits & " not supported yet"
)

# Check the bits per sample are all equal
for i in 0 ..< bitsPerSample.len:
for j in 0 ..< bitsPerSample.len:
if bitsPerSample[i] != bitsPerSample[j]:
failInvalid()

var decompressed: string
case compression:
of 1: # No compression
var stripDataLen: int
for byteCount in stripByteCounts:
stripDataLen += byteCount

decompressed.setLen(stripDataLen)

var at: int
for i, offset in stripOffsets:
let byteCount = stripByteCounts[i]
if offset + byteCount > data.len:
failInvalid()
copyMem(decompressed[at].addr, data[offset].unsafeAddr, byteCount)
at += byteCount

# of 5: # LZW

else:
raise newException(
PixieError,
"TIFF compression " & $compression & " not supported yet"
)

result.data.setLen(result.width * result.height)

case photometricInterpretation:
of 2: # RGB
if bitsPerSample.len == 4: # 32 bit RGBA
raise newException(PixieError, "RGBA TIFF not supported yet")
elif bitsPerSample.len == 3: # 24 bit RGB
if decompressed.len div 3 != result.data.len:
failInvalid()
for i in 0 ..< result.data.len:
let decompressedIdx = i * 3
result.data[i] = rgba(
decompressed[decompressedIdx + 0].uint8,
decompressed[decompressedIdx + 1].uint8,
decompressed[decompressedIdx + 2].uint8,
255
)
else:
failInvalid()

of 3: # Color Map
if decompressed.len != result.data.len:
failInvalid()
for i in 0 ..< result.data.len:
let colorMapIndex = decompressed[i].int
if colorMapIndex > colorMap.len:
failInvalid()
result.data[i] = colorMap[colorMapIndex]

else:
raise newException(
PixieError,
"TIFF photometric interpretation " & $photometricInterpretation &
" not supported yet"
)

proc newImage*(tiff: Tiff): Image =
result = newImage(tiff.width, tiff.height)
copyMem(result.data[0].addr, tiff.data[0].addr, tiff.data.len * 4)
result.data.toPremultipliedAlpha()

proc convertToImage*(tiff: Tiff): Image {.raises: [].} =
## Converts a PNG into an Image by moving the data. This is faster but can
## only be done once.
type Movable = ref object
width, height, channels: int
data: seq[ColorRGBX]

result = Image()
result.width = tiff.width
result.height = tiff.height
result.data = move cast[Movable](tiff).data
result.data.toPremultipliedAlpha()
Binary file added tests/fileformats/tiff/pc260001.tif
Binary file not shown.
6 changes: 6 additions & 0 deletions tests/test_tiff.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import pixie, pixie/fileformats/tiff

let
t = decodeTiff(readFile("tests/fileformats/tiff/pc260001.tif"))
image = newImage(t)
# image.writeFile("tests/fileformats/tiff/pc260001.png")