diff --git a/bindings/bindings.nim b/bindings/bindings.nim index 83e0758..b47c7f6 100644 --- a/bindings/bindings.nim +++ b/bindings/bindings.nim @@ -93,6 +93,9 @@ exportObject ColorStop: exportObject TextMetrics: discard +exportObject ImageDimensions: + discard + exportSeq seq[float32]: discard @@ -310,7 +313,10 @@ exportRefObject Context: isPointInStroke(Context, Path, float32, float32) exportProcs: + decodeImage + decodeImageDimensions readImage + readImageDimensions readmask readTypeface readFont diff --git a/src/pixie.nim b/src/pixie.nim index 6fc8608..2b31dfb 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -18,6 +18,25 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: []. ## Convert a straight alpha RGBA to a premultiplied alpha RGBA. 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].} = ## Loads an image from memory. if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature): @@ -45,6 +64,15 @@ proc decodeMask*(data: string): Mask {.raises: [PixieError].} = else: 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].} = ## Loads an image from a file. try: diff --git a/src/pixie/common.nim b/src/pixie/common.nim index 73c7e7a..7441461 100644 --- a/src/pixie/common.nim +++ b/src/pixie/common.nim @@ -28,6 +28,9 @@ type SubtractMaskBlend ## Inverse mask ExcludeMaskBlend + ImageDimensions* = object + width*, height*: int + proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} = ## Linearly interpolate between a and b using t. let t = round(t * 255).uint32 diff --git a/src/pixie/fileformats/bmp.nim b/src/pixie/fileformats/bmp.nim index 9f7cc52..44e7f7c 100644 --- a/src/pixie/fileformats/bmp.nim +++ b/src/pixie/fileformats/bmp.nim @@ -227,6 +227,21 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} = 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: [].} = ## Encodes an image into the BMP file format. diff --git a/src/pixie/fileformats/gif.nim b/src/pixie/fileformats/gif.nim index 1b89302..a8dcd8e 100644 --- a/src/pixie/fileformats/gif.nim +++ b/src/pixie/fileformats/gif.nim @@ -178,5 +178,18 @@ proc decodeGif*(data: string): Image {.raises: [PixieError].} = else: 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): {.pop.} diff --git a/src/pixie/fileformats/jpeg.nim b/src/pixie/fileformats/jpeg.nim index 22bf277..442661a 100644 --- a/src/pixie/fileformats/jpeg.nim +++ b/src/pixie/fileformats/jpeg.nim @@ -1056,7 +1056,7 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = state.decodeDHT() of 0xD8: # SOI - Start of Image - continue + discard of 0xD9: # EOI - End of Image break @@ -1094,5 +1094,63 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} = 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): {.pop.} diff --git a/src/pixie/fileformats/png.nim b/src/pixie/fileformats/png.nim index 242aaf3..eb2abc9 100644 --- a/src/pixie/fileformats/png.nim +++ b/src/pixie/fileformats/png.nim @@ -341,6 +341,26 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} = copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4) 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].} = ## Decodes the PNG data. if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND diff --git a/src/pixie/fileformats/ppm.nim b/src/pixie/fileformats/ppm.nim index 23c849f..104ca3c 100644 --- a/src/pixie/fileformats/ppm.nim +++ b/src/pixie/fileformats/ppm.nim @@ -135,6 +135,14 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} = else: 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: [].} = ## Encodes an image into the PPM file format (version P6). diff --git a/src/pixie/fileformats/qoi.nim b/src/pixie/fileformats/qoi.nim index bc6c7f9..1d6fa4e 100644 --- a/src/pixie/fileformats/qoi.nim +++ b/src/pixie/fileformats/qoi.nim @@ -121,6 +121,16 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} = raise newException(PixieError, "Invalid QOI padding") 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].} = ## Encodes raw QOI pixels to the QOI file format. diff --git a/tests/test_bmp.nim b/tests/test_bmp.nim index 6eeeaef..157d6b6 100644 --- a/tests/test_bmp.nim +++ b/tests/test_bmp.nim @@ -1,4 +1,4 @@ -import chroma, os, pixie, pixie/fileformats/bmp, strutils +import os, pixie/fileformats/bmp # block: # var image = newImage(4, 2) @@ -32,9 +32,9 @@ import chroma, os, pixie, pixie/fileformats/bmp, strutils block: for bits in [32, 24]: - let image = decodeBmp(readFile( - "tests/fileformats/bmp/knight." & $bits & ".master.bmp" - )) + let + path = "tests/fileformats/bmp/knight." & $bits & ".master.bmp" + image = decodeBmp(readFile(path)) writeFile("tests/fileformats/bmp/knight." & $bits & ".bmp", encodeBmp(image)) block: @@ -46,5 +46,9 @@ block: block: for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"): # echo file - let image = decodeBmp(readFile(file)) + let + image = decodeBmp(readFile(file)) + dimensions = decodeBmpDimensions(readFile(file)) #image.writeFile(file.replace("bmpsuite", "output") & ".png") + doAssert image.width == dimensions.width + doAssert image.height == dimensions.height diff --git a/tests/test_gif.nim b/tests/test_gif.nim index ceb811b..7d0581e 100644 --- a/tests/test_gif.nim +++ b/tests/test_gif.nim @@ -1,13 +1,37 @@ import pixie, pixie/fileformats/gif -var img = decodeGIF(readFile("tests/fileformats/gif/3x5.gif")) -img.writeFile("tests/fileformats/gif/3x5.png") +block: + 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")) -img2.writeFile("tests/fileformats/gif/audrey.png") +block: + 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")) -img3.writeFile("tests/fileformats/gif/sunflower.png") +block: + 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") -doAssert img3.data == img4.data +block: + 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 diff --git a/tests/test_jpeg.nim b/tests/test_jpeg.nim index d8750c6..680e404 100644 --- a/tests/test_jpeg.nim +++ b/tests/test_jpeg.nim @@ -1,4 +1,8 @@ -import jpegsuite, pixie +import jpegsuite, pixie, pixie/fileformats/jpeg 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 diff --git a/tests/test_png.nim b/tests/test_png.nim index 0b61060..9e158f9 100644 --- a/tests/test_png.nim +++ b/tests/test_png.nim @@ -33,3 +33,9 @@ block: block: 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 diff --git a/tests/test_ppm.nim b/tests/test_ppm.nim index 578b85d..f668edf 100644 --- a/tests/test_ppm.nim +++ b/tests/test_ppm.nim @@ -2,13 +2,22 @@ import pixie/fileformats/ppm block: for format in @["p3", "p6"]: - let image = decodePpm(readFile( - "tests/fileformats/ppm/feep." & $format & ".master.ppm" - )) + let + 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)) + 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)) + doAssert image.width == dimensions.width + doAssert image.height == dimensions.height # produced output should be identical to P6 master let p6Master = readFile("tests/fileformats/ppm/feep.p6.master.ppm") diff --git a/tests/test_qoi.nim b/tests/test_qoi.nim index b8acd0d..8949fd0 100644 --- a/tests/test_qoi.nim +++ b/tests/test_qoi.nim @@ -4,14 +4,19 @@ const tests = ["testcard", "testcard_rgba"] for name in tests: let - input = readImage("tests/fileformats/qoi/" & name & ".qoi") + path = "tests/fileformats/qoi/" & name & ".qoi" + input = readImage(path) control = readImage("tests/fileformats/qoi/" & name & ".png") + dimensions = decodeQoiDimensions(readFile(path)) doAssert input.data == control.data, "input mismatch of " & name + doAssert input.width == dimensions.width + doAssert input.height == dimensions.height discard encodeQoi(control) for name in tests: let - input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi")) + path = "tests/fileformats/qoi/" & name & ".qoi" + input = decodeQoi(readFile(path)) output = decodeQoi(encodeQoi(input)) doAssert output.data.len == input.data.len doAssert output.data == input.data