started tiff
This commit is contained in:
parent
d662111f58
commit
9ed2ad32ab
266
src/pixie/fileformats/tiff.nim
Normal file
266
src/pixie/fileformats/tiff.nim
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
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()
|
BIN
tests/fileformats/tiff/pc260001.tif
Normal file
BIN
tests/fileformats/tiff/pc260001.tif
Normal file
Binary file not shown.
6
tests/test_tiff.nim
Normal file
6
tests/test_tiff.nim
Normal file
|
@ -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")
|
Loading…
Reference in a new issue