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. ## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx() 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*( proc decodeImageDimensions*(
data: string data: string
): ImageDimensions {.raises: [PixieError].} = ): ImageDimensions {.raises: [PixieError].} =
## Decodes an image's dimensions from memory. ## Decodes an image's dimensions from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature): decodeImageDimensions(data.cstring, data.len)
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")
proc decodeImage*(data: string): Image {.raises: [PixieError].} = proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory. ## 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) decodeDib(data[14].unsafeAddr, data.len - 14)
proc decodeBmpDimensions*( proc decodeBmpDimensions*(
data: string data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} = ): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions. ## Decodes the BMP dimensions.
if data.len < 26: if len < 26:
failInvalid() failInvalid()
let data = cast[ptr UncheckedArray[uint8]](data)
# BMP Header # BMP Header
if data[0 .. 1] != "BM": if data[0].char != 'B' or data[1].char != 'M': # Must start with BM
failInvalid() failInvalid()
result.width = data.readInt32(18).int result.width = data.readInt32(18).int
result.height = abs(data.readInt32(22)).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: [].} = proc encodeDib*(image: Image): string {.raises: [].} =
## Encodes an image into a DIB. ## Encodes an image into a DIB.

View file

@ -379,18 +379,29 @@ proc decodeGif*(data: string): Gif {.raises: [PixieError].} =
result.duration += interval result.duration += interval
proc decodeGifDimensions*( proc decodeGifDimensions*(
data: string data: pointer, len: int
): ImageDimensions {.raises: [PixieError].} = ): ImageDimensions {.raises: [PixieError].} =
## Decodes the GIF dimensions. ## Decodes the GIF dimensions.
if data.len < 10: if len < 10:
failInvalid() 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") raise newException(PixieError, "Invalid GIF file signature")
result.width = data.readInt16(6).int result.width = data.readInt16(6).int
result.height = data.readInt16(8).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: [].} = proc newImage*(gif: Gif): Image {.raises: [].} =
gif.frames[0].copy() gif.frames[0].copy()

View file

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

View file

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