Skip to content

Commit b1e3c80

Browse files
committed
Merge origin/master into imagesuite
2 parents 3ccdbdc + 0fc4ef0 commit b1e3c80

10 files changed

Lines changed: 236 additions & 23 deletions

File tree

bindings/bindings.nim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ exportRefObject Image:
112112
newImage(int, int)
113113
procs:
114114
writeFile(Image, string)
115+
encodeBase64
115116
copy(Image)
116117
getColor
117118
setColor
@@ -291,6 +292,7 @@ exportRefObject Context:
291292
isPointInStroke(Context, Path, float32, float32)
292293

293294
exportProcs:
295+
decodeBase64
294296
decodeImage
295297
decodeImageDimensions
296298
readImage

src/pixie.nim

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import bumpy, chroma, flatty/binny, os, pixie/common, pixie/contexts,
2-
pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpeg,
3-
pixie/fileformats/png, pixie/fileformats/ppm, pixie/fileformats/qoi,
4-
pixie/fileformats/svg, pixie/fonts, pixie/images, pixie/internal,
5-
pixie/paints, pixie/paths, strutils, vmath
1+
import
2+
std/[os, strutils],
3+
bumpy, chroma, flatty/binny, vmath,
4+
pixie/[common, contexts, fonts, imagebase64, images, internal, paints, paths],
5+
pixie/fileformats/[bmp, gif, jpeg, png, ppm, qoi, svg]
66

7-
export bumpy, chroma, common, contexts, fonts, images, paints, paths, vmath
7+
export bumpy, chroma, common, contexts, fonts, imagebase64, images, paints,
8+
paths, vmath
89

910
type
1011
FileFormat* = enum
@@ -85,7 +86,9 @@ proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
8586
except IOError as e:
8687
raise newException(PixieError, e.msg, e)
8788

88-
proc encodeImage*(image: Image, fileFormat: FileFormat): string {.raises: [PixieError].} =
89+
proc encodeImage*(
90+
image: Image, fileFormat: FileFormat
91+
): string {.raises: [PixieError].} =
8992
## Encodes an image into memory.
9093
case fileFormat:
9194
of PngFormat:

src/pixie/fileformats/ppm.nim

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,20 @@ proc decodeHeader(
5353

5454
result.dataOffset = i
5555

56-
proc decodeP6Data(data: string, maxVal: int): seq[ColorRGBX] {.raises: [].} =
56+
proc decodeP6Data(
57+
data: string, maxVal: int
58+
): seq[ColorRGBX] {.raises: [PixieError].} =
5759
let needsUint16 = maxVal > 0xFF
58-
59-
result = newSeq[ColorRGBX](
60+
let bytesPerPixel =
6061
if needsUint16:
61-
data.len div 6
62+
6
6263
else:
63-
data.len div 3
64-
)
64+
3
65+
66+
if data.len mod bytesPerPixel != 0:
67+
failInvalid()
68+
69+
result = newSeq[ColorRGBX](data.len div bytesPerPixel)
6570

6671
# Let's calculate the real maximum value multiplier.
6772
# rgbx() accepts a maximum value of 255. Most of the time,
@@ -129,15 +134,18 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} =
129134
if not (header.version in ppmSignatures):
130135
failInvalid()
131136

132-
if 0 > header.maxVal or header.maxVal > 0xFFFF:
137+
if header.maxVal <= 0 or header.maxVal > 0xFFFF:
133138
failInvalid()
134139

135140
result = newImage(header.width, header.height)
136-
result.data =
141+
let pixels =
137142
if header.version == "P3":
138143
decodeP3Data(data[header.dataOffset .. ^1], header.maxVal)
139144
else:
140145
decodeP6Data(data[header.dataOffset .. ^1], header.maxVal)
146+
if pixels.len != result.data.len:
147+
failInvalid()
148+
result.data = pixels
141149

142150
proc decodePpmDimensions*(
143151
data: pointer, len: int

src/pixie/fontformats/opentype.nim

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ type
394394
when defined(release):
395395
{.push checks: off.}
396396

397+
const maxCompositeGlyphRecursion = 64
398+
397399
template eofCheck(buf: string, readTo: int) =
398400
if readTo > buf.len:
399401
raise newException(PixieError, "Unexpected error reading font data, EOF")
@@ -2180,7 +2182,7 @@ proc hasGlyph*(opentype: OpenType, rune: Rune): bool =
21802182
rune in opentype.cmap.runeToGlyphId
21812183

21822184
proc parseGlyfGlyph(
2183-
opentype: OpenType, glyphId: uint16
2185+
opentype: OpenType, glyphId: uint16, recursionDepth = 0
21842186
): Path {.raises: [PixieError], gcsafe.}
21852187

21862188
proc parseGlyphPath(
@@ -2318,7 +2320,9 @@ proc parseGlyphPath(
23182320

23192321
result.closePath()
23202322

2321-
proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
2323+
proc parseCompositeGlyph(
2324+
opentype: OpenType, offset, recursionDepth: int
2325+
): Path =
23222326
result = newPath()
23232327

23242328
var
@@ -2402,7 +2406,10 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
24022406
# elif (flags and 0b1000000000000) != 0: # UNSCALED_COMPONENT_OFFSET
24032407
# discard
24042408

2405-
var subPath = opentype.parseGlyfGlyph(component.glyphId)
2409+
var subPath = opentype.parseGlyfGlyph(
2410+
component.glyphId,
2411+
recursionDepth + 1
2412+
)
24062413
subPath.transform(mat3(
24072414
component.xScale, component.scale10, 0.0,
24082415
component.scale01, component.yScale, 0.0,
@@ -2413,7 +2420,11 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
24132420

24142421
moreComponents = (flags and 0b100000) != 0
24152422

2416-
proc parseGlyfGlyph(opentype: OpenType, glyphId: uint16): Path =
2423+
proc parseGlyfGlyph(
2424+
opentype: OpenType, glyphId: uint16, recursionDepth: int
2425+
): Path =
2426+
if recursionDepth > maxCompositeGlyphRecursion:
2427+
raise newException(PixieError, "Invalid composite glyph recursion")
24172428

24182429
if glyphId.int >= opentype.glyf.offsets.len:
24192430
raise newException(PixieError, "Invalid glyph ID " & $glyphId)
@@ -2434,7 +2445,7 @@ proc parseGlyfGlyph(opentype: OpenType, glyphId: uint16): Path =
24342445
i += 10
24352446

24362447
if numberOfContours < 0:
2437-
opentype.parseCompositeGlyph(i)
2448+
opentype.parseCompositeGlyph(i, recursionDepth)
24382449
else:
24392450
parseGlyphPath(opentype.buf, i, numberOfContours)
24402451

src/pixie/imagebase64.nim

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import
2+
std/base64 as stdbase64,
3+
std/strutils,
4+
flatty/binny,
5+
common,
6+
fileformats/[bmp, gif, jpeg, png, ppm, qoi, svg]
7+
8+
proc decodeImageData(data: string): Image {.raises: [PixieError].} =
9+
## Decodes supported image bytes into an image.
10+
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
11+
decodePng(data).convertToImage()
12+
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
13+
decodeJpeg(data)
14+
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
15+
decodeBmp(data)
16+
elif data.len > 5 and (
17+
data.readStr(0, 5) == xmlSignature or
18+
data.readStr(0, 4) == svgSignature
19+
):
20+
newImage(parseSvg(data))
21+
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
22+
newImage(decodeGif(data))
23+
elif data.len > (14 + 8) and data.readStr(0, 4) == qoiSignature:
24+
decodeQoi(data).convertToImage()
25+
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
26+
decodePpm(data)
27+
else:
28+
raise newException(PixieError, "Unsupported image file format")
29+
30+
proc getPayload(data: string): string {.raises: [PixieError].} =
31+
## Gets the base64 payload from plain base64 data or a data URL.
32+
if data.startsWith("data:"):
33+
let comma = data.find(',')
34+
if comma == -1:
35+
raise newException(PixieError, "Invalid data URL")
36+
if comma + 1 < data.len:
37+
data[comma + 1 .. ^1]
38+
else:
39+
""
40+
else:
41+
data
42+
43+
proc encodeBase64*(image: Image): string {.raises: [PixieError].} =
44+
## Encodes an image into PNG format and returns it as base64.
45+
stdbase64.encode(image.encodePng())
46+
47+
proc decodeBase64*(data: string): Image {.raises: [PixieError].} =
48+
## Decodes a base64 image string into an image.
49+
try:
50+
decodeImageData(stdbase64.decode(data.getPayload()))
51+
except ValueError as e:
52+
raise newException(PixieError, e.msg, e)

tests/test_base64.nim

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import
2+
std/base64,
3+
chroma, pixie, pixie/fileformats/png
4+
5+
block:
6+
let image = newImage(2, 1)
7+
image[0, 0] = rgba(255, 0, 0, 255)
8+
image[1, 0] = rgba(0, 0, 255, 128)
9+
10+
let encoded = image.encodeBase64()
11+
doAssert encoded == base64.encode(image.encodePng())
12+
13+
let decoded = decodeBase64(encoded)
14+
doAssert decoded.width == image.width
15+
doAssert decoded.height == image.height
16+
doAssert decoded.data == image.data
17+
18+
block:
19+
let image = newImage(1, 1)
20+
image[0, 0] = rgba(0, 255, 0, 255)
21+
22+
let
23+
encoded = image.encodeBase64()
24+
decoded = decodeBase64("data:image/png;base64," & encoded)
25+
26+
doAssert decoded.width == image.width
27+
doAssert decoded.height == image.height
28+
doAssert decoded.data == image.data
29+
30+
block:
31+
try:
32+
discard decodeBase64("nope")
33+
doAssert false
34+
except PixieError:
35+
discard

tests/test_fonts.nim

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os, pixie, strformat, unicode, xrays
1+
import os, pixie, pixie/fontformats/opentype, strformat, tables, unicode, xrays
22

33
proc wh(image: Image): Vec2 =
44
## Return with and height as a size vector.
@@ -1048,6 +1048,52 @@ block:
10481048
var typeface = readTypeface("tests/fonts/Roboto-Regular_1.ttf")
10491049
doAssert typeface.getKerningAdjustment('T'.Rune, 'e'.Rune) == -99.0
10501050

1051+
block:
1052+
proc writeBe16(data: var string, offset, value: int) =
1053+
data[offset] = char((value shr 8) and 0xff)
1054+
data[offset + 1] = char(value and 0xff)
1055+
1056+
let originalData = readFile("tests/fonts/Roboto-Regular_1.ttf")
1057+
let original = parseOpenType(originalData)
1058+
1059+
var
1060+
targetRune: Rune
1061+
targetGlyphId = -1
1062+
glyphOffset = -1
1063+
1064+
for rune, glyphId in original.cmap.runeToGlyphId.pairs:
1065+
let glyphIndex = glyphId.int
1066+
if glyphIndex == 0 or glyphIndex + 1 >= original.glyf.offsets.len:
1067+
continue
1068+
1069+
let
1070+
startOffset = original.glyf.offsets[glyphIndex].int
1071+
endOffset = original.glyf.offsets[glyphIndex + 1].int
1072+
glyphLen = endOffset - startOffset
1073+
1074+
if glyphLen >= 16:
1075+
targetRune = rune
1076+
targetGlyphId = glyphIndex
1077+
glyphOffset = startOffset
1078+
break
1079+
1080+
doAssert targetGlyphId >= 0
1081+
1082+
var mutated = originalData
1083+
writeBe16(mutated, glyphOffset + 0, 0xffff) # Composite glyph.
1084+
writeBe16(mutated, glyphOffset + 2, 0)
1085+
writeBe16(mutated, glyphOffset + 4, 0)
1086+
writeBe16(mutated, glyphOffset + 6, 0)
1087+
writeBe16(mutated, glyphOffset + 8, 0)
1088+
writeBe16(mutated, glyphOffset + 10, 0x0002) # ARGS_ARE_XY_VALUES.
1089+
writeBe16(mutated, glyphOffset + 12, targetGlyphId)
1090+
mutated[glyphOffset + 14] = '\0'
1091+
mutated[glyphOffset + 15] = '\0'
1092+
1093+
let font = parseOpenType(mutated)
1094+
doAssertRaises PixieError:
1095+
discard font.getGlyphPath(targetRune)
1096+
10511097
block:
10521098
var font = readFont("tests/fonts/Inter-Regular.ttf")
10531099
font.size = 26

tests/test_ppm.nim

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import pixie/fileformats/ppm
1+
import pixie, pixie/fileformats/ppm
22

33
block:
44
for format in @["p3", "p6"]:
@@ -23,3 +23,9 @@ block:
2323
let p6Master = readFile("tests/fileformats/ppm/feep.p6.master.ppm")
2424
for image in @["p3", "p6", "p3.hidepth"]:
2525
doAssert readFile("tests/fileformats/ppm/feep." & $image & ".ppm") == p6Master
26+
27+
block:
28+
let payload = "P6\n10 10\n255\n" & "\x12\x34\x56"
29+
30+
doAssertRaises PixieError:
31+
discard decodeImage(payload)

tests/test_tiff.nim

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import pixie/fileformats/tiff
1+
import pixie/common, pixie/fileformats/tiff
22

33
let
44
data = readFile("tests/fileformats/tiff/pc260001.tif")
@@ -50,3 +50,52 @@ doAssert grayInt16Image.data[2].r == 255
5050
doAssert floatImage.width == 1
5151
doAssert floatImage.height == 1
5252
doAssert floatImage.data[0].r == 128
53+
54+
block:
55+
proc addLe16(data: var string, value: int) =
56+
data.add(char(value and 0xff))
57+
data.add(char((value shr 8) and 0xff))
58+
59+
proc addLe32(data: var string, value: int) =
60+
data.add(char(value and 0xff))
61+
data.add(char((value shr 8) and 0xff))
62+
data.add(char((value shr 16) and 0xff))
63+
data.add(char((value shr 24) and 0xff))
64+
65+
proc addIfdEntry(
66+
data: var string,
67+
tag, fieldType, numValues, valueOrOffset: int
68+
) =
69+
data.addLe16(tag)
70+
data.addLe16(fieldType)
71+
data.addLe32(numValues)
72+
data.addLe32(valueOrOffset)
73+
74+
const
75+
ifdOffset = 8
76+
entryCount = 9
77+
colorMapOffset = ifdOffset + 2 + entryCount * 12 + 4
78+
pixelDataOffset = colorMapOffset + 6
79+
80+
var payload = ""
81+
payload.add("II")
82+
payload.addLe16(42)
83+
payload.addLe32(ifdOffset)
84+
payload.addLe16(entryCount)
85+
payload.addIfdEntry(0x0100, 4, 1, 1) # ImageWidth
86+
payload.addIfdEntry(0x0101, 4, 1, 1) # ImageLength
87+
payload.addIfdEntry(0x0102, 3, 1, 8) # BitsPerSample
88+
payload.addIfdEntry(0x0103, 3, 1, 1) # Compression = none
89+
payload.addIfdEntry(0x0106, 3, 1, 3) # PhotometricInterpretation = palette
90+
payload.addIfdEntry(0x0111, 4, 1, pixelDataOffset)
91+
payload.addIfdEntry(0x0116, 4, 1, 1) # RowsPerStrip
92+
payload.addIfdEntry(0x0117, 4, 1, 1) # StripByteCounts
93+
payload.addIfdEntry(0x0140, 3, 3, colorMapOffset)
94+
payload.addLe32(0)
95+
payload.addLe16(0)
96+
payload.addLe16(0)
97+
payload.addLe16(0)
98+
payload.add(char(0x01))
99+
100+
doAssertRaises PixieError:
101+
discard decodeTiff(payload)

tests/tests.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import
2+
test_base64,
23
test_bmp,
34
test_contexts,
45
test_fonts,

0 commit comments

Comments
 (0)