diff --git a/bindings/bindings.nim b/bindings/bindings.nim index 68537e0b..ebdc4c19 100644 --- a/bindings/bindings.nim +++ b/bindings/bindings.nim @@ -112,6 +112,7 @@ exportRefObject Image: newImage(int, int) procs: writeFile(Image, string) + encodeBase64 copy(Image) getColor setColor @@ -291,6 +292,7 @@ exportRefObject Context: isPointInStroke(Context, Path, float32, float32) exportProcs: + decodeBase64 decodeImage decodeImageDimensions readImage diff --git a/src/pixie.nim b/src/pixie.nim index a3e4fe3e..f2f53e69 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -1,10 +1,11 @@ -import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts, - pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpeg, - pixie/fileformats/png, pixie/fileformats/ppm, pixie/fileformats/qoi, - pixie/fileformats/svg, pixie/fonts, pixie/images, pixie/internal, - pixie/paints, pixie/paths, strutils, vmath +import + std/[os, strutils], + bumpy, chroma, flatty/binny, vmath, + pixie/[common, contexts, fonts, imagebase64, images, internal, paints, paths], + pixie/fileformats/[bmp, gif, jpeg, png, ppm, qoi, svg] -export bumpy, chroma, common, contexts, fonts, images, paints, paths, vmath +export bumpy, chroma, common, contexts, fonts, imagebase64, images, paints, + paths, vmath type FileFormat* = enum @@ -85,7 +86,9 @@ proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} = except IOError as e: raise newException(PixieError, e.msg, e) -proc encodeImage*(image: Image, fileFormat: FileFormat): string {.raises: [PixieError].} = +proc encodeImage*( + image: Image, fileFormat: FileFormat +): string {.raises: [PixieError].} = ## Encodes an image into memory. case fileFormat: of PngFormat: diff --git a/src/pixie/imagebase64.nim b/src/pixie/imagebase64.nim new file mode 100644 index 00000000..ba72838d --- /dev/null +++ b/src/pixie/imagebase64.nim @@ -0,0 +1,52 @@ +import + std/base64 as stdbase64, + std/strutils, + flatty/binny, + common, + fileformats/[bmp, gif, jpeg, png, ppm, qoi, svg] + +proc decodeImageData(data: string): Image {.raises: [PixieError].} = + ## Decodes supported image bytes into an image. + if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature): + decodePng(data).convertToImage() + elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage): + decodeJpeg(data) + elif data.len > 2 and data.readStr(0, 2) == bmpSignature: + decodeBmp(data) + elif data.len > 5 and ( + data.readStr(0, 5) == xmlSignature or + data.readStr(0, 4) == svgSignature + ): + newImage(parseSvg(data)) + elif data.len > 6 and data.readStr(0, 6) in gifSignatures: + newImage(decodeGif(data)) + elif data.len > (14 + 8) and data.readStr(0, 4) == qoiSignature: + decodeQoi(data).convertToImage() + elif data.len > 9 and data.readStr(0, 2) in ppmSignatures: + decodePpm(data) + else: + raise newException(PixieError, "Unsupported image file format") + +proc getPayload(data: string): string {.raises: [PixieError].} = + ## Gets the base64 payload from plain base64 data or a data URL. + if data.startsWith("data:"): + let comma = data.find(',') + if comma == -1: + raise newException(PixieError, "Invalid data URL") + if comma + 1 < data.len: + data[comma + 1 .. ^1] + else: + "" + else: + data + +proc encodeBase64*(image: Image): string {.raises: [PixieError].} = + ## Encodes an image into PNG format and returns it as base64. + stdbase64.encode(image.encodePng()) + +proc decodeBase64*(data: string): Image {.raises: [PixieError].} = + ## Decodes a base64 image string into an image. + try: + decodeImageData(stdbase64.decode(data.getPayload())) + except ValueError as e: + raise newException(PixieError, e.msg, e) diff --git a/tests/test_base64.nim b/tests/test_base64.nim new file mode 100644 index 00000000..9b88c74d --- /dev/null +++ b/tests/test_base64.nim @@ -0,0 +1,35 @@ +import + std/base64, + chroma, pixie, pixie/fileformats/png + +block: + let image = newImage(2, 1) + image[0, 0] = rgba(255, 0, 0, 255) + image[1, 0] = rgba(0, 0, 255, 128) + + let encoded = image.encodeBase64() + doAssert encoded == base64.encode(image.encodePng()) + + let decoded = decodeBase64(encoded) + doAssert decoded.width == image.width + doAssert decoded.height == image.height + doAssert decoded.data == image.data + +block: + let image = newImage(1, 1) + image[0, 0] = rgba(0, 255, 0, 255) + + let + encoded = image.encodeBase64() + decoded = decodeBase64("data:image/png;base64," & encoded) + + doAssert decoded.width == image.width + doAssert decoded.height == image.height + doAssert decoded.data == image.data + +block: + try: + discard decodeBase64("nope") + doAssert false + except PixieError: + discard diff --git a/tests/tests.nim b/tests/tests.nim index da0f5e62..74b443a8 100644 --- a/tests/tests.nim +++ b/tests/tests.nim @@ -1,4 +1,5 @@ import + test_base64, test_bmp, test_contexts, test_fonts,