decodeImageDimensions(data) and readImageDimensions(path)

This commit is contained in:
Ryan Oldenburg 2022-06-08 19:53:22 -05:00
parent a039c3681b
commit 5e6103fbc0
15 changed files with 235 additions and 22 deletions

View file

@ -93,6 +93,9 @@ exportObject ColorStop:
exportObject TextMetrics: exportObject TextMetrics:
discard discard
exportObject ImageDimensions:
discard
exportSeq seq[float32]: exportSeq seq[float32]:
discard discard
@ -310,7 +313,10 @@ exportRefObject Context:
isPointInStroke(Context, Path, float32, float32) isPointInStroke(Context, Path, float32, float32)
exportProcs: exportProcs:
decodeImage
decodeImageDimensions
readImage readImage
readImageDimensions
readmask readmask
readTypeface readTypeface
readFont readFont

View file

@ -18,6 +18,25 @@ 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: 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")
proc decodeImage*(data: string): Image {.raises: [PixieError].} = proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory. ## Loads an image from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature): if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
@ -45,6 +64,15 @@ proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
else: else:
raise newException(PixieError, "Unsupported mask file format") raise newException(PixieError, "Unsupported mask file format")
proc readImageDimensions*(
filePath: string
): ImageDimensions {.inline, raises: [PixieError].} =
## Loads an image from a file.
try:
decodeImageDimensions(readFile(filePath))
except IOError as e:
raise newException(PixieError, e.msg, e)
proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} = proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
## Loads an image from a file. ## Loads an image from a file.
try: try:

View file

@ -28,6 +28,9 @@ type
SubtractMaskBlend ## Inverse mask SubtractMaskBlend ## Inverse mask
ExcludeMaskBlend ExcludeMaskBlend
ImageDimensions* = object
width*, height*: int
proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} = proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} =
## Linearly interpolate between a and b using t. ## Linearly interpolate between a and b using t.
let t = round(t * 255).uint32 let t = round(t * 255).uint32

View file

@ -227,6 +227,21 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
decodeDib(data[14].unsafeAddr, data.len - 14) decodeDib(data[14].unsafeAddr, data.len - 14)
proc decodeBmpDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions.
if data.len < 26:
failInvalid()
# BMP Header
if data[0 .. 1] != "BM":
failInvalid()
result.width = data.readInt32(18).int
result.height = abs(data.readInt32(22)).int
proc encodeBmp*(image: Image): string {.raises: [].} = proc encodeBmp*(image: Image): string {.raises: [].} =
## Encodes an image into the BMP file format. ## Encodes an image into the BMP file format.

View file

@ -178,5 +178,18 @@ proc decodeGif*(data: string): Image {.raises: [PixieError].} =
else: else:
raise newException(PixieError, "Invalid GIF block type") raise newException(PixieError, "Invalid GIF block type")
proc decodeGifDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the GIF dimensions.
if data.len < 10:
failInvalid()
if data[0 .. 5] notin gifSignatures:
raise newException(PixieError, "Invalid GIF file signature")
result.width = data.readInt16(6).int
result.height = data.readInt16(8).int
when defined(release): when defined(release):
{.pop.} {.pop.}

View file

@ -1056,7 +1056,7 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.decodeDHT() state.decodeDHT()
of 0xD8: of 0xD8:
# SOI - Start of Image # SOI - Start of Image
continue discard
of 0xD9: of 0xD9:
# EOI - End of Image # EOI - End of Image
break break
@ -1094,5 +1094,63 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.buildImage() state.buildImage()
proc decodeJpegDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the JPEG dimensions.
var state = DecoderState()
state.buffer = cast[ptr UncheckedArray[uint8]](data.cstring)
state.len = data.len
while true:
if state.readUint8() != 0xFF:
failInvalid("invalid chunk marker")
let chunkId = state.readUint8()
case chunkId:
of 0xD8:
# SOI - Start of Image
discard
of 0xC0:
# Start Of Frame (Baseline DCT)
state.decodeSOF0()
break
of 0xC1:
# Start Of Frame (Extended sequential DCT)
state.decodeSOF1()
break
of 0xC2:
# Start Of Frame (Progressive DCT)
state.decodeSOF2()
break
of 0xDB:
# Define Quantization Table(s)
state.skipChunk()
of 0XE0:
# Application-specific
state.skipChunk()
of 0xE1:
# Exif/APP1
state.decodeExif()
of 0xE2..0xEF:
# Application-specific
state.skipChunk()
of 0xFE:
# Comment
state.skipChunk()
else:
failInvalid("invalid chunk " & chunkId.toHex())
case state.orientation:
of 0, 1, 2, 3, 4:
result.width = state.imageWidth
result.height = state.imageHeight
of 5, 6, 7, 8:
result.width = state.imageHeight
result.height = state.imageWidth
else:
failInvalid("invalid orientation")
when defined(release): when defined(release):
{.pop.} {.pop.}

View file

@ -341,6 +341,26 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} =
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4) copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
result.data.toPremultipliedAlpha() result.data.toPremultipliedAlpha()
proc decodePngDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PNG dimensions.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
failInvalid()
# PNG file signature
let signature = cast[array[8, uint8]](data.readUint64(0))
if signature != pngSignature:
failInvalid()
# First chunk must be IHDR
if data.readUint32(8).swap() != 13 or data.readStr(12, 4) != "IHDR":
failInvalid()
let header = decodeHeader(data[16 ..< 16 + 13])
result.width = header.width
result.height = header.height
proc decodePng*(data: string): Png {.raises: [PixieError].} = proc decodePng*(data: string): Png {.raises: [PixieError].} =
## Decodes the PNG data. ## Decodes the PNG data.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND

View file

@ -135,6 +135,14 @@ 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: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PPM dimensions.
let header = decodeHeader(data)
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

@ -121,6 +121,16 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
raise newException(PixieError, "Invalid QOI padding") raise newException(PixieError, "Invalid QOI padding")
inc(p) inc(p)
proc decodeQoiDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the QOI dimensions.
if data.len <= 12 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header")
result.width = data.readUint32(4).swap().int
result.height = data.readUint32(8).swap().int
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.

View file

@ -1,4 +1,4 @@
import chroma, os, pixie, pixie/fileformats/bmp, strutils import os, pixie/fileformats/bmp
# block: # block:
# var image = newImage(4, 2) # var image = newImage(4, 2)
@ -32,9 +32,9 @@ import chroma, os, pixie, pixie/fileformats/bmp, strutils
block: block:
for bits in [32, 24]: for bits in [32, 24]:
let image = decodeBmp(readFile( let
"tests/fileformats/bmp/knight." & $bits & ".master.bmp" path = "tests/fileformats/bmp/knight." & $bits & ".master.bmp"
)) image = decodeBmp(readFile(path))
writeFile("tests/fileformats/bmp/knight." & $bits & ".bmp", encodeBmp(image)) writeFile("tests/fileformats/bmp/knight." & $bits & ".bmp", encodeBmp(image))
block: block:
@ -46,5 +46,9 @@ block:
block: block:
for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"): for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"):
# echo file # echo file
let image = decodeBmp(readFile(file)) let
image = decodeBmp(readFile(file))
dimensions = decodeBmpDimensions(readFile(file))
#image.writeFile(file.replace("bmpsuite", "output") & ".png") #image.writeFile(file.replace("bmpsuite", "output") & ".png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -1,13 +1,37 @@
import pixie, pixie/fileformats/gif import pixie, pixie/fileformats/gif
var img = decodeGIF(readFile("tests/fileformats/gif/3x5.gif")) block:
img.writeFile("tests/fileformats/gif/3x5.png") let
path = "tests/fileformats/gif/3x5.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/3x5.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img2 = decodeGIF(readFile("tests/fileformats/gif/audrey.gif")) block:
img2.writeFile("tests/fileformats/gif/audrey.png") let
path = "tests/fileformats/gif/audrey.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/audrey.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img3 = decodeGIF(readFile("tests/fileformats/gif/sunflower.gif")) block:
img3.writeFile("tests/fileformats/gif/sunflower.png") let
path = "tests/fileformats/gif/sunflower.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/sunflower.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img4 = readImage("tests/fileformats/gif/sunflower.gif") block:
doAssert img3.data == img4.data let
path = "tests/fileformats/gif/sunflower.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/sunflower.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -1,4 +1,8 @@
import jpegsuite, pixie import jpegsuite, pixie, pixie/fileformats/jpeg
for file in jpegSuiteFiles: for file in jpegSuiteFiles:
let img = readImage(file) let
image = readImage(file)
dimensions = decodeJpegDimensions(readFile(file))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -33,3 +33,9 @@ block:
block: block:
discard readImage("tests/fileformats/png/trailing_data.png") discard readImage("tests/fileformats/png/trailing_data.png")
block:
let dimensions =
decodeImageDimensions(readFile("tests/fileformats/png/mandrill.png"))
doAssert dimensions.width == 512
doAssert dimensions.height == 512

View file

@ -2,13 +2,22 @@ import pixie/fileformats/ppm
block: block:
for format in @["p3", "p6"]: for format in @["p3", "p6"]:
let image = decodePpm(readFile( let
"tests/fileformats/ppm/feep." & $format & ".master.ppm" path = "tests/fileformats/ppm/feep." & $format & ".master.ppm"
)) image = decodePpm(readFile(path))
dimensions = decodePpmDimensions(readFile(path))
writeFile("tests/fileformats/ppm/feep." & $format & ".ppm", encodePpm(image)) writeFile("tests/fileformats/ppm/feep." & $format & ".ppm", encodePpm(image))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
let image = decodePpm(readFile("tests/fileformats/ppm/feep.p3.hidepth.master.ppm")) block:
let
path = "tests/fileformats/ppm/feep.p3.hidepth.master.ppm"
image = decodePpm(readFile(path))
dimensions = decodePpmDimensions(readFile(path))
writeFile("tests/fileformats/ppm/feep.p3.hidepth.ppm", encodePpm(image)) writeFile("tests/fileformats/ppm/feep.p3.hidepth.ppm", encodePpm(image))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
# produced output should be identical to P6 master # produced output should be identical to P6 master
let p6Master = readFile("tests/fileformats/ppm/feep.p6.master.ppm") let p6Master = readFile("tests/fileformats/ppm/feep.p6.master.ppm")

View file

@ -4,14 +4,19 @@ const tests = ["testcard", "testcard_rgba"]
for name in tests: for name in tests:
let let
input = readImage("tests/fileformats/qoi/" & name & ".qoi") path = "tests/fileformats/qoi/" & name & ".qoi"
input = readImage(path)
control = readImage("tests/fileformats/qoi/" & name & ".png") control = readImage("tests/fileformats/qoi/" & name & ".png")
dimensions = decodeQoiDimensions(readFile(path))
doAssert input.data == control.data, "input mismatch of " & name doAssert input.data == control.data, "input mismatch of " & name
doAssert input.width == dimensions.width
doAssert input.height == dimensions.height
discard encodeQoi(control) discard encodeQoi(control)
for name in tests: for name in tests:
let let
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi")) path = "tests/fileformats/qoi/" & name & ".qoi"
input = decodeQoi(readFile(path))
output = decodeQoi(encodeQoi(input)) output = decodeQoi(encodeQoi(input))
doAssert output.data.len == input.data.len doAssert output.data.len == input.data.len
doAssert output.data == input.data doAssert output.data == input.data