Merge pull request #541 from treeform/guzba

improved image dimension proc functionality
This commit is contained in:
Andre von Houck 2023-06-22 16:35:26 -07:00 committed by GitHub
commit 523b364fca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 59 deletions

View file

@ -18,24 +18,36 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx()
proc decodeImageDimensions*(
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes an image's dimensions from memory.
if len > 8 and equalMem(data, pngSignature[0].unsafeAddr, 8):
decodePngDimensions(data, len)
elif len > 2 and equalMem(data, jpegStartOfImage[0].unsafeAddr, 2):
decodeJpegDimensions(data, len)
elif len > 2 and equalMem(data, bmpSignature.cstring, 2):
decodeBmpDimensions(data, len)
elif len > 6 and (
equalMem(data, gifSignatures[0].cstring, 6) or
equalMem(data, gifSignatures[1].cstring, 6)
):
decodeGifDimensions(data, len)
elif len > (14 + 8) and equalMem(data, qoiSignature.cstring, 4):
decodeQoiDimensions(data, len)
elif len > 9 and (
equalMem(data, ppmSignatures[0].cstring, 2) or
equalMem(data, ppmSignatures[1].cstring, 2)
):
decodePpmDimensions(data, len)
else:
raise newException(PixieError, "Unsupported image file format")
proc decodeImageDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes an image's dimensions from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngDimensions(data)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegDimensions(data)
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
decodeBmpDimensions(data)
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
decodeGifDimensions(data)
elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature:
decodeQoiDimensions(data)
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
decodePpmDimensions(data)
else:
raise newException(PixieError, "Unsupported image file format")
decodeImageDimensions(data.cstring, data.len)
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory.

View file

@ -228,19 +228,27 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
decodeDib(data[14].unsafeAddr, data.len - 14)
proc decodeBmpDimensions*(
data: string
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions.
if data.len < 26:
if len < 26:
failInvalid()
let data = cast[ptr UncheckedArray[uint8]](data)
# BMP Header
if data[0 .. 1] != "BM":
if data[0].char != 'B' or data[1].char != 'M': # Must start with BM
failInvalid()
result.width = data.readInt32(18).int
result.height = abs(data.readInt32(22)).int
proc decodeBmpDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions.
decodeBmpDimensions(data.cstring, data.len)
proc encodeDib*(image: Image): string {.raises: [].} =
## Encodes an image into a DIB.

View file

@ -379,18 +379,29 @@ proc decodeGif*(data: string): Gif {.raises: [PixieError].} =
result.duration += interval
proc decodeGifDimensions*(
data: string
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes the GIF dimensions.
if data.len < 10:
if len < 10:
failInvalid()
if data[0 .. 5] notin gifSignatures:
let data = cast[ptr UncheckedArray[uint8]](data)
let startsWithSignature =
equalMem(data, gifSignatures[0].cstring, 6) or
equalMem(data, gifSignatures[1].cstring, 6)
if not startsWithSignature:
raise newException(PixieError, "Invalid GIF file signature")
result.width = data.readInt16(6).int
result.height = data.readInt16(8).int
proc decodeGifDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
decodeGifDimensions(data.cstring, data.len)
proc newImage*(gif: Gif): Image {.raises: [].} =
gif.frames[0].copy()

View file

@ -24,7 +24,6 @@ import chroma, flatty/binny, ../common, ../images, ../internal,
const
fastBits = 9
jpegStartOfImage* = [0xFF.uint8, 0xD8]
deZigZag = [
uint8 00, 01, 08, 16, 09, 02, 03, 10,
uint8 17, 24, 32, 25, 18, 11, 04, 05,
@ -40,6 +39,9 @@ const
1023, 2047, 4095, 8191, 16383, 32767, 65535
]
let
jpegStartOfImage* = [0xFF.uint8, 0xD8]
type
Huffman = object
codes: array[256, uint16]
@ -380,15 +382,16 @@ proc decodeSOF2(state: var DecoderState) =
proc decodeExif(state: var DecoderState) =
## Decode Exif header
let
len = state.readUint16be().int - 2
endOffset = state.pos + len
var len = state.readUint16be().int - 2
let exifHeader = state.readStr(6)
len -= 6
if exifHeader != "Exif\0\0":
# Happens with progressive images, just ignore instead of error.
# Skip to the end.
state.pos = endOffset
state.skipBytes(len)
return
# Read the endianess of the exif header
@ -402,22 +405,40 @@ proc decodeExif(state: var DecoderState) =
else:
failInvalid("invalid Tiff header")
len -= 2
# Verify we got the endianess right.
if state.readUint16be().maybeSwap(littleEndian) != 0x002A.uint16:
failInvalid("invalid Tiff header endianess")
len -= 2
# Skip any other tiff header data.
let offsetToFirstIFD = state.readUint32be().maybeSwap(littleEndian).int
len -= 4
if offsetToFirstIFD < 8:
failInvalid("invalid Tiff offset")
state.skipBytes(offsetToFirstIFD - 8)
len -= (offsetToFirstIFD - 8)
# Read the IFD0 (main image) tags.
let numTags = state.readUint16be().maybeSwap(littleEndian).int
len -= 2
for i in 0 ..< numTags:
let
tagNumber = state.readUint16be().maybeSwap(littleEndian)
dataFormat = state.readUint16be().maybeSwap(littleEndian)
numberComponents = state.readUint32be().maybeSwap(littleEndian)
dataOffset = state.readUint32be().maybeSwap(littleEndian).int
len -= 12
# For now we only care about orientation tag.
case tagNumber:
of 0x0112: # Orientation
@ -426,7 +447,7 @@ proc decodeExif(state: var DecoderState) =
discard
# Skip all of the data we do not want to read, IFD1, thumbnail, etc.
state.pos = endOffset
state.skipBytes(len) # Skip any remaining len
proc reset(state: var DecoderState) =
## Rests the decoder state need for restart markers.
@ -1136,13 +1157,13 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.buildImage()
proc decodeJpegDimensions*(
data: string
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes the JPEG dimensions.
var state = DecoderState()
state.buffer = cast[ptr UncheckedArray[uint8]](data.cstring)
state.len = data.len
state.buffer = cast[ptr UncheckedArray[uint8]](data)
state.len = len
while true:
if state.readUint8() != 0xFF:
@ -1153,21 +1174,24 @@ proc decodeJpegDimensions*(
of 0xD8:
# SOI - Start of Image
discard
of 0xC0:
# Start Of Frame (Baseline DCT)
state.decodeSOF0()
of 0xC0, 0xC2:
# Start Of Frame (Baseline DCT or Progressive DCT)
discard state.readUint16be().int # Chunk len
discard state.readUint8() # Precision
state.imageHeight = state.readUint16be().int
state.imageWidth = state.readUint16be().int
break
of 0xC1:
# Start Of Frame (Extended sequential DCT)
state.decodeSOF1()
break
of 0xC2:
# Start Of Frame (Progressive DCT)
state.decodeSOF2()
break
failInvalid("unsupported extended sequential DCT format")
of 0xC4:
# Define Huffman Table
state.decodeDHT()
of 0xDB:
# Define Quantization Table(s)
state.skipChunk()
of 0xDD:
# Define Restart Interval
state.skipChunk()
of 0XE0:
# Application-specific
state.skipChunk()
@ -1193,5 +1217,11 @@ proc decodeJpegDimensions*(
else:
failInvalid("invalid orientation")
proc decodeJpegDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the JPEG dimensions.
decodeJpegDimensions(data.cstring, data.len)
when defined(release):
{.pop.}

View file

@ -3,7 +3,7 @@ import chroma, flatty/binny, math, ../common, ../images, ../internal,
# See http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
const
let
pngSignature* = [137.uint8, 80, 78, 71, 13, 10, 26, 10]
type
@ -76,14 +76,6 @@ proc decodeHeader(data: pointer): PngHeader =
if result.interlaceMethod notin [0.uint8, 1]:
raise newException(PixieError, "Invalid PNG interlace method")
# Not yet supported:
if result.bitDepth == 16:
raise newException(PixieError, "PNG 16 bit depth not supported yet")
if result.interlaceMethod != 0:
raise newException(PixieError, "Interlaced PNG not supported yet")
proc decodePalette(data: pointer, len: int): seq[ColorRGB] =
if len == 0 or len mod 3 != 0:
failInvalid()
@ -451,6 +443,12 @@ proc decodePng*(data: pointer, len: int): Png {.raises: [PixieError].} =
failCRC()
inc(pos, 4) # CRC
# Not yet supported:
if header.bitDepth == 16:
raise newException(PixieError, "PNG 16 bit depth not supported yet")
if header.interlaceMethod != 0:
raise newException(PixieError, "Interlaced PNG not supported yet")
while true:
if pos + 8 > len:
failInvalid()

View file

@ -12,16 +12,17 @@ type
template failInvalid() =
raise newException(PixieError, "Invalid PPM data")
proc decodeHeader(data: string): PpmHeader {.raises: [PixieError].} =
if data.len <= 10: # Each part + whitespace
raise newException(PixieError, "Invalid PPM file header")
proc decodeHeader(
data: ptr UncheckedArray[uint8], len: int
): PpmHeader {.raises: [PixieError].} =
var
commentMode, readWhitespace: bool
i, readFields: int
field: string
while readFields < 4:
let c = readUint8(data, i).char
if i >= len:
raise newException(PixieError, "Invalid PPM file header")
let c = data[i].char
if c == '#':
commentMode = true
elif c == '\n':
@ -120,7 +121,10 @@ proc decodeP3Data(data: string, maxVal: int): seq[ColorRGBX] {.raises: [PixieErr
proc decodePpm*(data: string): Image {.raises: [PixieError].} =
## Decodes Portable Pixel Map data into an Image.
let header = decodeHeader(data)
let header = decodeHeader(
cast[ptr UncheckedArray[uint8]](data.cstring),
data.len
)
if not (header.version in ppmSignatures):
failInvalid()
@ -135,13 +139,21 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} =
else:
decodeP6Data(data[header.dataOffset .. ^1], header.maxVal)
proc decodePpmDimensions*(
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PPM dimensions.
let
data = cast[ptr UncheckedArray[uint8]](data)
header = decodeHeader(data, len)
result.width = header.width
result.height = header.height
proc decodePpmDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PPM dimensions.
let header = decodeHeader(data)
result.width = header.width
result.height = header.height
decodePpmDimensions(data.cstring, data.len)
proc encodePpm*(image: Image): string {.raises: [].} =
## Encodes an image into the PPM file format (version P6).

View file

@ -136,15 +136,23 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
inc(p)
proc decodeQoiDimensions*(
data: string
data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} =
## Decodes the QOI dimensions.
if data.len <= 12 or data[0 .. 3] != qoiSignature:
if len <= 12 or not equalMem(data, qoiSignature.cstring, 4):
raise newException(PixieError, "Invalid QOI header")
let data = cast[ptr UncheckedArray[uint8]](data)
result.width = data.readUint32(4).swap().int
result.height = data.readUint32(8).swap().int
proc decodeQoiDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the QOI dimensions.
decodeQoiDimensions(data.cstring, data.len)
proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
## Encodes raw QOI pixels to the QOI file format.