Merge pull request #541 from treeform/guzba
improved image dimension proc functionality
This commit is contained in:
commit
523b364fca
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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.}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue