Merge pull request from guzba/master

raises pragma to generate binding error handling
This commit is contained in:
treeform 2021-08-18 20:19:01 -07:00 committed by GitHub
commit 5e97339725
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 689 additions and 492 deletions

View file

@ -10,15 +10,15 @@ type
FileFormat* = enum
ffPng, ffBmp, ffJpg, ffGif
converter autoStraightAlpha*(c: ColorRGBX): ColorRGBA {.inline.} =
## Convert a paremultiplied alpha RGBA to a straight alpha RGBA.
converter autoStraightAlpha*(c: ColorRGBX): ColorRGBA {.inline, raises: [].} =
## Convert a premultiplied alpha RGBA to a straight alpha RGBA.
c.rgba()
converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline.} =
converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].} =
## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx()
proc decodeImage*(data: string | seq[uint8]): Image =
proc decodeImage*(data: string | seq[uint8]): Image {.raises: [PixieError].} =
## Loads an image from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePng(data)
@ -34,22 +34,28 @@ proc decodeImage*(data: string | seq[uint8]): Image =
else:
raise newException(PixieError, "Unsupported image file format")
proc decodeMask*(data: string | seq[uint8]): Mask =
proc decodeMask*(data: string | seq[uint8]): Mask {.raises: [PixieError].} =
## Loads a mask from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
newMask(decodePng(data))
else:
raise newException(PixieError, "Unsupported mask file format")
proc readImage*(filePath: string): Image =
proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
## Loads an image from a file.
decodeImage(readFile(filePath))
try:
decodeImage(readFile(filePath))
except IOError as e:
raise newException(PixieError, e.msg, e)
proc readMask*(filePath: string): Mask =
proc readMask*(filePath: string): Mask {.raises: [PixieError].} =
## Loads a mask from a file.
decodeMask(readFile(filePath))
try:
decodeMask(readFile(filePath))
except IOError as e:
raise newException(PixieError, e.msg, e)
proc encodeImage*(image: Image, fileFormat: FileFormat): string =
proc encodeImage*(image: Image, fileFormat: FileFormat): string {.raises: [PixieError].} =
## Encodes an image into memory.
case fileFormat:
of ffPng:
@ -61,7 +67,7 @@ proc encodeImage*(image: Image, fileFormat: FileFormat): string =
of ffGif:
raise newException(PixieError, "Unsupported file format")
proc encodeMask*(mask: Mask, fileFormat: FileFormat): string =
proc encodeMask*(mask: Mask, fileFormat: FileFormat): string {.raises: [PixieError].} =
## Encodes a mask into memory.
case fileFormat:
of ffPng:
@ -69,7 +75,7 @@ proc encodeMask*(mask: Mask, fileFormat: FileFormat): string =
else:
raise newException(PixieError, "Unsupported file format")
proc writeFile*(image: Image, filePath: string) =
proc writeFile*(image: Image, filePath: string) {.raises: [PixieError].} =
## Writes an image to a file.
let fileFormat = case splitFile(filePath).ext.toLowerAscii():
of ".png": ffPng
@ -77,9 +83,13 @@ proc writeFile*(image: Image, filePath: string) =
of ".jpg", ".jpeg": ffJpg
else:
raise newException(PixieError, "Unsupported file extension")
writeFile(filePath, image.encodeImage(fileFormat))
proc writeFile*(mask: Mask, filePath: string) =
try:
writeFile(filePath, image.encodeImage(fileFormat))
except IOError as e:
raise newException(PixieError, e.msg, e)
proc writeFile*(mask: Mask, filePath: string) {.raises: [PixieError].} =
## Writes a mask to a file.
let fileFormat = case splitFile(filePath).ext.toLowerAscii():
of ".png": ffPng
@ -87,4 +97,8 @@ proc writeFile*(mask: Mask, filePath: string) =
of ".jpg", ".jpeg": ffJpg
else:
raise newException(PixieError, "Unsupported file extension")
writeFile(filePath, mask.encodeMask(fileFormat))
try:
writeFile(filePath, mask.encodeMask(fileFormat))
except IOError as e:
raise newException(PixieError, e.msg, e)

View file

@ -34,18 +34,18 @@ type
bmSubtractMask ## Inverse mask
bmExcludeMask
Blender* = proc(backdrop, source: ColorRGBX): ColorRGBX
Blender* = proc(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].}
## Function signature returned by blender.
Masker* = proc(backdrop, source: uint8): uint8
Masker* = proc(backdrop, source: uint8): uint8 {.raises: [].}
## Function signature returned by masker.
when defined(release):
{.push checks: off.}
proc min(a, b: uint32): uint32 {.inline.} =
proc min(a, b: uint32): uint32 {.inline, raises: [].} =
if a < b: a else: b
proc alphaFix(backdrop, source, mixed: ColorRGBA): ColorRGBA =
proc alphaFix(backdrop, source, mixed: ColorRGBA): ColorRGBA {.raises: [].} =
## After mixing an image, adjust its alpha value to be correct.
let
sa = source.a.uint32
@ -68,7 +68,7 @@ proc alphaFix(backdrop, source, mixed: ColorRGBA): ColorRGBA =
result.b = (b div a div 255).uint8
result.a = a.uint8
proc alphaFix(backdrop, source, mixed: Color): Color =
proc alphaFix(backdrop, source, mixed: Color): Color {.raises: [].} =
## After mixing an image, adjust its alpha value to be correct.
result.a = (source.a + backdrop.a * (1.0 - source.a))
if result.a == 0:
@ -87,16 +87,16 @@ proc alphaFix(backdrop, source, mixed: Color): Color =
result.g /= result.a
result.b /= result.a
proc blendAlpha*(backdrop, source: uint8): uint8 {.inline.} =
proc blendAlpha*(backdrop, source: uint8): uint8 {.inline, raises: [].} =
## Blends alphas of backdrop, source.
source + ((backdrop.uint32 * (255 - source)) div 255).uint8
proc screen(backdrop, source: uint32): uint8 {.inline.} =
proc screen(backdrop, source: uint32): uint8 {.inline, raises: [].} =
((backdrop + source).int32 - ((backdrop * source) div 255).int32).uint8
proc hardLight(
backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint32
): uint8 {.inline.} =
): uint8 {.inline, raises: [].} =
if sourceColor * 2 <= sourceAlpha:
((
2 * sourceColor * backdropColor +
@ -106,41 +106,41 @@ proc hardLight(
else:
screen(backdropColor, sourceColor)
proc softLight(backdrop, source: float32): float32 {.inline.} =
proc softLight(backdrop, source: float32): float32 {.inline, raises: [].} =
## Pegtop
(1 - 2 * source) * backdrop ^ 2 + 2 * source * backdrop
proc `+`(c: Color, v: float32): Color {.inline.} =
proc `+`(c: Color, v: float32): Color {.inline, raises: [].} =
result.r = c.r + v
result.g = c.g + v
result.b = c.b + v
result.a = c.a + v
proc `+`(v: float32, c: Color): Color {.inline.} =
proc `+`(v: float32, c: Color): Color {.inline, raises: [].} =
c + v
proc `*`(c: Color, v: float32): Color {.inline.} =
proc `*`(c: Color, v: float32): Color {.inline, raises: [].} =
result.r = c.r * v
result.g = c.g * v
result.b = c.b * v
result.a = c.a * v
proc `/`(c: Color, v: float32): Color {.inline.} =
proc `/`(c: Color, v: float32): Color {.inline, raises: [].} =
result.r = c.r / v
result.g = c.g / v
result.b = c.b / v
result.a = c.a / v
proc `-`(c: Color, v: float32): Color {.inline.} =
proc `-`(c: Color, v: float32): Color {.inline, raises: [].} =
result.r = c.r - v
result.g = c.g - v
result.b = c.b - v
result.a = c.a - v
proc Lum(C: Color): float32 {.inline.} =
proc Lum(C: Color): float32 {.inline, raises: [].} =
0.3 * C.r + 0.59 * C.g + 0.11 * C.b
proc ClipColor(C: var Color) {.inline.} =
proc ClipColor(C: var Color) {.inline, raises: [].} =
let
L = Lum(C)
n = min([C.r, C.g, C.b])
@ -150,22 +150,22 @@ proc ClipColor(C: var Color) {.inline.} =
if x > 1:
C = L + (((C - L) * (1 - L)) / (x - L))
proc SetLum(C: Color, l: float32): Color {.inline.} =
proc SetLum(C: Color, l: float32): Color {.inline, raises: [].} =
let d = l - Lum(C)
result.r = C.r + d
result.g = C.g + d
result.b = C.b + d
ClipColor(result)
proc Sat(C: Color): float32 {.inline.} =
proc Sat(C: Color): float32 {.inline, raises: [].} =
max([C.r, C.g, C.b]) - min([C.r, C.g, C.b])
proc SetSat(C: Color, s: float32): Color {.inline.} =
proc SetSat(C: Color, s: float32): Color {.inline, raises: [].} =
let satC = Sat(C)
if satC > 0:
result = (C - min([C.r, C.g, C.b])) * s / satC
proc blendNormal(backdrop, source: ColorRGBX): ColorRGBX =
proc blendNormal(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
if backdrop.a == 0:
return source
if source.a == 255:
@ -179,7 +179,7 @@ proc blendNormal(backdrop, source: ColorRGBX): ColorRGBX =
result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8
result.a = blendAlpha(backdrop.a, source.a)
proc blendDarken(backdrop, source: ColorRGBX): ColorRGBX =
proc blendDarken(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
proc blend(
backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8
): uint8 {.inline.} =
@ -193,7 +193,7 @@ proc blendDarken(backdrop, source: ColorRGBX): ColorRGBX =
result.b = blend(backdrop.b, backdrop.a, source.b, source.a)
result.a = blendAlpha(backdrop.a, source.a)
proc blendMultiply(backdrop, source: ColorRGBX): ColorRGBX =
proc blendMultiply(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
proc blend(
backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8
): uint8 {.inline.} =
@ -218,7 +218,7 @@ proc blendMultiply(backdrop, source: ColorRGBX): ColorRGBX =
# result = alphaFix(backdrop, source, result)
# result = result.toPremultipliedAlpha()
proc blendColorBurn(backdrop, source: ColorRGBX): ColorRGBX =
proc blendColorBurn(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba()
source = source.rgba()
@ -235,7 +235,7 @@ proc blendColorBurn(backdrop, source: ColorRGBX): ColorRGBX =
blended.b = blend(backdrop.b, source.b)
result = alphaFix(backdrop, source, blended).rgbx()
proc blendLighten(backdrop, source: ColorRGBX): ColorRGBX =
proc blendLighten(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
proc blend(
backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8
): uint8 {.inline.} =
@ -249,7 +249,7 @@ proc blendLighten(backdrop, source: ColorRGBX): ColorRGBX =
result.b = blend(backdrop.b, backdrop.a, source.b, source.a)
result.a = blendAlpha(backdrop.a, source.a)
proc blendScreen(backdrop, source: ColorRGBX): ColorRGBX =
proc blendScreen(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
result.r = screen(backdrop.r, source.r)
result.g = screen(backdrop.g, source.g)
result.b = screen(backdrop.b, source.b)
@ -265,7 +265,7 @@ proc blendScreen(backdrop, source: ColorRGBX): ColorRGBX =
# result = alphaFix(backdrop, source, result)
# result = result.toPremultipliedAlpha()
proc blendColorDodge(backdrop, source: ColorRGBX): ColorRGBX =
proc blendColorDodge(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba()
source = source.rgba()
@ -282,13 +282,13 @@ proc blendColorDodge(backdrop, source: ColorRGBX): ColorRGBX =
blended.b = blend(backdrop.b, source.b)
result = alphaFix(backdrop, source, blended).rgbx()
proc blendOverlay(backdrop, source: ColorRGBX): ColorRGBX =
proc blendOverlay(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
result.r = hardLight(source.r, source.a, backdrop.r, backdrop.a)
result.g = hardLight(source.g, source.a, backdrop.g, backdrop.a)
result.b = hardLight(source.b, source.a, backdrop.b, backdrop.a)
result.a = blendAlpha(backdrop.a, source.a)
proc blendSoftLight(backdrop, source: ColorRGBX): ColorRGBX =
proc blendSoftLight(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
# proc softLight(backdrop, source: int32): uint8 {.inline.} =
# ## Pegtop
# (
@ -362,13 +362,13 @@ proc blendSoftLight(backdrop, source: ColorRGBX): ColorRGBX =
result = rgba.rgbx()
proc blendHardLight(backdrop, source: ColorRGBX): ColorRGBX =
proc blendHardLight(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
result.r = hardLight(backdrop.r, backdrop.a, source.r, source.a)
result.g = hardLight(backdrop.g, backdrop.a, source.g, source.a)
result.b = hardLight(backdrop.b, backdrop.a, source.b, source.a)
result.a = blendAlpha(backdrop.a, source.a)
proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX =
proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
proc blend(
backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8
): uint8 {.inline.} =
@ -384,7 +384,7 @@ proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX =
result.b = blend(backdrop.b, backdrop.a, source.b, source.a)
result.a = blendAlpha(backdrop.a, source.a)
proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX =
proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
proc blend(backdrop, source: uint32): uint8 {.inline.} =
let v = (backdrop + source).int32 - ((2 * backdrop * source) div 255).int32
max(0, v).uint8
@ -393,63 +393,63 @@ proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX =
result.b = blend(backdrop.b.uint32, source.b.uint32)
result.a = blendAlpha(backdrop.a, source.a)
proc blendColor(backdrop, source: ColorRGBX): ColorRGBX =
proc blendColor(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba().color
source = source.rgba().color
blended = SetLum(source, Lum(backdrop))
result = alphaFix(backdrop, source, blended).rgba.rgbx()
proc blendLuminosity(backdrop, source: ColorRGBX): ColorRGBX =
proc blendLuminosity(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba().color
source = source.rgba().color
blended = SetLum(backdrop, Lum(source))
result = alphaFix(backdrop, source, blended).rgba.rgbx()
proc blendHue(backdrop, source: ColorRGBX): ColorRGBX =
proc blendHue(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba().color
source = source.rgba().color
blended = SetLum(SetSat(source, Sat(backdrop)), Lum(backdrop))
result = alphaFix(backdrop, source, blended).rgba.rgbx()
proc blendSaturation(backdrop, source: ColorRGBX): ColorRGBX =
proc blendSaturation(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let
backdrop = backdrop.rgba().color
source = source.rgba().color
blended = SetLum(SetSat(backdrop, Sat(source)), Lum(backdrop))
result = alphaFix(backdrop, source, blended).rgba.rgbx()
proc blendMask(backdrop, source: ColorRGBX): ColorRGBX =
proc blendMask(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let k = source.a.uint32
result.r = ((backdrop.r * k) div 255).uint8
result.g = ((backdrop.g * k) div 255).uint8
result.b = ((backdrop.b * k) div 255).uint8
result.a = ((backdrop.a * k) div 255).uint8
proc blendSubtractMask(backdrop, source: ColorRGBX): ColorRGBX =
proc blendSubtractMask(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let a = (backdrop.a.uint32 * (255 - source.a)) div 255
result.r = ((backdrop.r * a) div 255).uint8
result.g = ((backdrop.g * a) div 255).uint8
result.b = ((backdrop.b * a) div 255).uint8
result.a = a.uint8
proc blendExcludeMask(backdrop, source: ColorRGBX): ColorRGBX =
proc blendExcludeMask(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
let a = max(backdrop.a, source.a).uint32 - min(backdrop.a, source.a)
result.r = ((source.r * a) div 255).uint8
result.g = ((source.g * a) div 255).uint8
result.b = ((source.b * a) div 255).uint8
result.a = a.uint8
proc blendOverwrite(backdrop, source: ColorRGBX): ColorRGBX =
proc blendOverwrite(backdrop, source: ColorRGBX): ColorRGBX {.raises: [].} =
source
# proc blendWhite(backdrop, source: ColorRGBX): ColorRGBX =
# ## For testing
# rgbx(255, 255, 255, 255)
proc blender*(blendMode: BlendMode): Blender =
proc blender*(blendMode: BlendMode): Blender {.raises: [].} =
## Returns a blend function for a given blend mode.
case blendMode:
of bmNormal: blendNormal
@ -475,24 +475,24 @@ proc blender*(blendMode: BlendMode): Blender =
of bmSubtractMask: blendSubtractMask
of bmExcludeMask: blendExcludeMask
proc maskNormal(backdrop, source: uint8): uint8 =
proc maskNormal(backdrop, source: uint8): uint8 {.raises: [].} =
## Blending masks
blendAlpha(backdrop, source)
proc maskMask(backdrop, source: uint8): uint8 =
proc maskMask(backdrop, source: uint8): uint8 {.raises: [].} =
## Masking masks
((backdrop.uint32 * source) div 255).uint8
proc maskSubtract(backdrop, source: uint8): uint8 =
proc maskSubtract(backdrop, source: uint8): uint8 {.raises: [].} =
((backdrop.uint32 * (255 - source)) div 255).uint8
proc maskExclude(backdrop, source: uint8): uint8 =
proc maskExclude(backdrop, source: uint8): uint8 {.raises: [].} =
max(backdrop, source) - min(backdrop, source)
proc maskOverwrite(backdrop, source: uint8): uint8 =
proc maskOverwrite(backdrop, source: uint8): uint8 {.raises: [].} =
source
proc masker*(blendMode: BlendMode): Masker =
proc masker*(blendMode: BlendMode): Masker {.raises: [PixieError].} =
## Returns a blend masking function for a given blend masking mode.
case blendMode:
of bmNormal: maskNormal
@ -507,12 +507,12 @@ when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
type
BlenderSimd* = proc(blackdrop, source: M128i): M128i
BlenderSimd* = proc(blackdrop, source: M128i): M128i {.raises: [].}
## Function signature returned by blenderSimd.
MaskerSimd* = proc(blackdrop, source: M128i): M128i
MaskerSimd* = proc(blackdrop, source: M128i): M128i {.raises: [].}
## Function signature returned by maskerSimd.
proc blendNormalSimd(backdrop, source: M128i): M128i =
proc blendNormalSimd(backdrop, source: M128i): M128i {.raises: [].} =
let
alphaMask = mm_set1_epi32(cast[int32](0xff000000))
oddMask = mm_set1_epi16(cast[int16](0xff00))
@ -541,7 +541,7 @@ when defined(amd64) and not defined(pixieNoSimd):
mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8))
)
proc blendMaskSimd(backdrop, source: M128i): M128i =
proc blendMaskSimd(backdrop, source: M128i): M128i {.raises: [].} =
let
alphaMask = mm_set1_epi32(cast[int32](0xff000000))
oddMask = mm_set1_epi16(cast[int16](0xff00))
@ -562,10 +562,10 @@ when defined(amd64) and not defined(pixieNoSimd):
mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8))
proc blendOverwriteSimd(backdrop, source: M128i): M128i =
proc blendOverwriteSimd(backdrop, source: M128i): M128i {.raises: [].} =
source
proc blenderSimd*(blendMode: BlendMode): BlenderSimd =
proc blenderSimd*(blendMode: BlendMode): BlenderSimd {.raises: [PixieError].} =
## Returns a blend function for a given blend mode with SIMD support.
case blendMode:
of bmNormal: blendNormalSimd
@ -574,11 +574,11 @@ when defined(amd64) and not defined(pixieNoSimd):
else:
raise newException(PixieError, "No SIMD blender for " & $blendMode)
proc hasSimdBlender*(blendMode: BlendMode): bool =
proc hasSimdBlender*(blendMode: BlendMode): bool {.inline, raises: [].} =
## Is there a blend function for a given blend mode with SIMD support?
blendMode in {bmNormal, bmMask, bmOverwrite}
proc maskNormalSimd(backdrop, source: M128i): M128i =
proc maskNormalSimd(backdrop, source: M128i): M128i {.raises: [].} =
## Blending masks
let
oddMask = mm_set1_epi16(cast[int16](0xff00))
@ -615,7 +615,7 @@ when defined(amd64) and not defined(pixieNoSimd):
mm_or_si128(blendedEven, mm_slli_epi16(blendedOdd, 8))
proc maskMaskSimd(backdrop, source: M128i): M128i =
proc maskMaskSimd(backdrop, source: M128i): M128i {.raises: [].} =
let
oddMask = mm_set1_epi16(cast[int16](0xff00))
div255 = mm_set1_epi16(cast[int16](0x8081))
@ -636,7 +636,7 @@ when defined(amd64) and not defined(pixieNoSimd):
mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8))
proc maskerSimd*(blendMode: BlendMode): MaskerSimd =
proc maskerSimd*(blendMode: BlendMode): MaskerSimd {.raises: [PixieError].} =
## Returns a blend masking function with SIMD support.
case blendMode:
of bmNormal: maskNormalSimd
@ -645,7 +645,7 @@ when defined(amd64) and not defined(pixieNoSimd):
else:
raise newException(PixieError, "No SIMD masker for " & $blendMode)
proc hasSimdMasker*(blendMode: BlendMode): bool =
proc hasSimdMasker*(blendMode: BlendMode): bool {.inline, raises: [].} =
## Is there a blend masking function with SIMD support?
blendMode in {bmNormal, bmMask, bmOverwrite}

View file

@ -3,12 +3,12 @@ import bumpy, chroma, vmath
type
PixieError* = object of ValueError ## Raised if an operation fails.
proc lerp*(a, b: uint8, t: float32): uint8 {.inline.} =
proc lerp*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} =
## Linearly interpolate between a and b using t.
let t = round(t * 255).uint32
((a * (255 - t) + b * t) div 255).uint8
proc lerp*(a, b: ColorRGBX, t: float32): ColorRGBX {.inline.} =
proc lerp*(a, b: ColorRGBX, t: float32): ColorRGBX {.inline, raises: [].} =
## Linearly interpolate between a and b using t.
let x = round(t * 255).uint32
result.r = ((a.r.uint32 * (255 - x) + b.r.uint32 * x) div 255).uint8
@ -16,14 +16,14 @@ proc lerp*(a, b: ColorRGBX, t: float32): ColorRGBX {.inline.} =
result.b = ((a.b.uint32 * (255 - x) + b.b.uint32 * x) div 255).uint8
result.a = ((a.a.uint32 * (255 - x) + b.a.uint32 * x) div 255).uint8
proc lerp*(a, b: Color, v: float32): Color {.inline.} =
proc lerp*(a, b: Color, v: float32): Color {.inline, raises: [].} =
## Linearly interpolate between a and b using t.
result.r = lerp(a.r, b.r, v)
result.g = lerp(a.g, b.g, v)
result.b = lerp(a.b, b.b, v)
result.a = lerp(a.a, b.a, v)
proc snapToPixels*(rect: Rect): Rect =
proc snapToPixels*(rect: Rect): Rect {.raises: [].} =
let
xMin = rect.x
xMax = rect.x + rect.w

View file

@ -44,7 +44,7 @@ type
TextMetrics* = object
width*: float32
proc newContext*(image: Image): Context =
proc newContext*(image: Image): Context {.raises: [].} =
## Create a new Context that will draw to the parameter image.
result = Context()
result.image = image
@ -53,15 +53,17 @@ proc newContext*(image: Image): Context =
result.globalAlpha = 1
result.lineWidth = 1
result.miterLimit = 10
result.fillStyle = rgbx(0, 0, 0, 255)
result.strokeStyle = rgbx(0, 0, 0, 255)
result.fillStyle = newPaint(pkSolid)
result.fillStyle.color = color(0, 0, 0, 1)
result.strokeStyle = newPaint(pkSolid)
result.strokeStyle.color = color(0, 0, 0, 1)
result.fontSize = 12
proc newContext*(width, height: int): Context {.inline.} =
proc newContext*(width, height: int): Context {.inline, raises: [PixieError].} =
## Create a new Context that will draw to a new image of width and height.
newContext(newImage(width, height))
proc state(ctx: Context): ContextState =
proc state(ctx: Context): ContextState {.raises: [PixieError].} =
result.fillStyle = ctx.fillStyle
result.strokeStyle = ctx.strokeStyle
result.globalAlpha = ctx.globalAlpha
@ -76,7 +78,7 @@ proc state(ctx: Context): ContextState =
result.mat = ctx.mat
result.mask = if ctx.mask != nil: ctx.mask.copy() else: nil
proc save*(ctx: Context) {.inline.} =
proc save*(ctx: Context) {.inline, raises: [PixieError].} =
## Saves the entire state of the context by pushing the current state onto
## a stack.
ctx.stateStack.add(ctx.state())
@ -84,7 +86,7 @@ proc save*(ctx: Context) {.inline.} =
ctx.fillStyle = newPaint(ctx.fillStyle)
ctx.strokeStyle = newPaint(ctx.strokeStyle)
proc saveLayer*(ctx: Context) =
proc saveLayer*(ctx: Context) {.raises: [PixieError].} =
## Saves the entire state of the context by pushing the current state onto
## a stack and allocates a new image layer for subsequent drawing. Calling
## restore blends the current layer image onto the prior layer or root image.
@ -93,7 +95,7 @@ proc saveLayer*(ctx: Context) =
ctx.stateStack.add(state)
ctx.layer = newImage(ctx.image.width, ctx.image.height)
proc restore*(ctx: Context) =
proc restore*(ctx: Context) {.raises: [PixieError].} =
## Restores the most recently saved context state by popping the top entry
## in the drawing state stack. If there is no saved state, this method does
## nothing.
@ -128,7 +130,9 @@ proc restore*(ctx: Context) =
else: # Otherwise draw to the root image
ctx.image.draw(poppedLayer)
proc fill(ctx: Context, image: Image, path: Path, windingRule: WindingRule) =
proc fill(
ctx: Context, image: Image, path: Path, windingRule: WindingRule
) {.raises: [PixieError].} =
var image = image
if ctx.globalAlpha != 1:
@ -146,7 +150,7 @@ proc fill(ctx: Context, image: Image, path: Path, windingRule: WindingRule) =
ctx.layer.applyOpacity(ctx.globalAlpha)
ctx.restore()
proc stroke(ctx: Context, image: Image, path: Path) =
proc stroke(ctx: Context, image: Image, path: Path) {.raises: [PixieError].} =
var image = image
if ctx.globalAlpha != 1:
@ -168,17 +172,19 @@ proc stroke(ctx: Context, image: Image, path: Path) =
ctx.layer.applyOpacity(ctx.globalAlpha)
ctx.restore()
proc newFont(ctx: Context): Font =
proc newFont(ctx: Context): Font {.raises: [PixieError].} =
if ctx.font == "":
raise newException(PixieError, "No font has been set on this Context")
if ctx.font notin ctx.typefaces:
ctx.typefaces[ctx.font] = readTypeface(ctx.font)
result = newFont(ctx.typefaces[ctx.font])
result = newFont(ctx.typefaces.getOrDefault(ctx.font, nil))
result.size = ctx.fontSize
proc fillText(ctx: Context, image: Image, text: string, at: Vec2) =
proc fillText(
ctx: Context, image: Image, text: string, at: Vec2
) {.raises: [PixieError].} =
let font = newFont(ctx)
# Canvas positions text relative to the alphabetic baseline by default
@ -204,7 +210,9 @@ proc fillText(ctx: Context, image: Image, text: string, at: Vec2) =
ctx.layer.applyOpacity(ctx.globalAlpha)
ctx.restore()
proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) =
proc strokeText(
ctx: Context, image: Image, text: string, at: Vec2
) {.raises: [PixieError].} =
let font = newFont(ctx)
# Canvas positions text relative to the alphabetic baseline by default
@ -235,29 +243,29 @@ proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) =
ctx.layer.applyOpacity(ctx.globalAlpha)
ctx.restore()
proc beginPath*(ctx: Context) {.inline.} =
proc beginPath*(ctx: Context) {.inline, raises: [].} =
## Starts a new path by emptying the list of sub-paths.
ctx.path = newPath()
proc moveTo*(ctx: Context, v: Vec2) {.inline.} =
proc moveTo*(ctx: Context, v: Vec2) {.inline, raises: [].} =
## Begins a new sub-path at the point (x, y).
ctx.path.moveTo(v)
proc moveTo*(ctx: Context, x, y: float32) {.inline.} =
proc moveTo*(ctx: Context, x, y: float32) {.inline, raises: [].} =
## Begins a new sub-path at the point (x, y).
ctx.moveTo(vec2(x, y))
proc lineTo*(ctx: Context, v: Vec2) {.inline.} =
proc lineTo*(ctx: Context, v: Vec2) {.inline, raises: [].} =
## Adds a straight line to the current sub-path by connecting the sub-path's
## last point to the specified (x, y) coordinates.
ctx.path.lineTo(v)
proc lineTo*(ctx: Context, x, y: float32) {.inline.} =
proc lineTo*(ctx: Context, x, y: float32) {.inline, raises: [].} =
## Adds a straight line to the current sub-path by connecting the sub-path's
## last point to the specified (x, y) coordinates.
ctx.lineTo(vec2(x, y))
proc bezierCurveTo*(ctx: Context, cp1, cp2, to: Vec2) {.inline.} =
proc bezierCurveTo*(ctx: Context, cp1, cp2, to: Vec2) {.inline, raises: [].} =
## Adds a cubic Bézier curve to the current sub-path. It requires three
## points: the first two are control points and the third one is the end
## point. The starting point is the latest point in the current path,
@ -266,14 +274,16 @@ proc bezierCurveTo*(ctx: Context, cp1, cp2, to: Vec2) {.inline.} =
proc bezierCurveTo*(
ctx: Context, cp1x, cp1y, cp2x, cp2y, x, y: float32
) {.inline.} =
) {.inline, raises: [].} =
## Adds a cubic Bézier curve to the current sub-path. It requires three
## points: the first two are control points and the third one is the end
## point. The starting point is the latest point in the current path,
## which can be changed using moveTo() before creating the Bézier curve.
ctx.bezierCurveTo(vec2(cp1x, cp1y), vec2(cp2x, cp2y), vec2(x, y))
proc quadraticCurveTo*(ctx: Context, cpx, cpy, x, y: float32) {.inline.} =
proc quadraticCurveTo*(
ctx: Context, cpx, cpy, x, y: float32
) {.inline, raises: [].} =
## Adds a quadratic Bézier curve to the current sub-path. It requires two
## points: the first one is a control point and the second one is the end
## point. The starting point is the latest point in the current path,
@ -281,7 +291,9 @@ proc quadraticCurveTo*(ctx: Context, cpx, cpy, x, y: float32) {.inline.} =
## Bézier curve.
ctx.path.quadraticCurveTo(cpx, cpy, x, y)
proc quadraticCurveTo*(ctx: Context, ctrl, to: Vec2) {.inline.} =
proc quadraticCurveTo*(
ctx: Context, ctrl, to: Vec2
) {.inline, raises: [].} =
## Adds a quadratic Bézier curve to the current sub-path. It requires two
## points: the first one is a control point and the second one is the end
## point. The starting point is the latest point in the current path,
@ -289,45 +301,55 @@ proc quadraticCurveTo*(ctx: Context, ctrl, to: Vec2) {.inline.} =
## Bézier curve.
ctx.path.quadraticCurveTo(ctrl, to)
proc arc*(ctx: Context, x, y, r, a0, a1: float32, ccw: bool = false) =
proc arc*(
ctx: Context, x, y, r, a0, a1: float32, ccw: bool = false
) {.raises: [PixieError].} =
## Draws a circular arc.
ctx.path.arc(x, y, r, a0, a1, ccw)
proc arc*(ctx: Context, pos: Vec2, r: float32, a: Vec2, ccw: bool = false) =
proc arc*(
ctx: Context, pos: Vec2, r: float32, a: Vec2, ccw: bool = false
) {.raises: [PixieError].} =
## Adds a circular arc to the current sub-path.
ctx.path.arc(pos, r, a, ccw)
proc arcTo*(ctx: Context, x1, y1, x2, y2, radius: float32) =
proc arcTo*(
ctx: Context, x1, y1, x2, y2, radius: float32
) {.raises: [PixieError].} =
## Draws a circular arc using the given control points and radius.
ctx.path.arcTo(x1, y1, x2, y2, radius)
proc arcTo*(ctx: Context, a, b: Vec2, r: float32) =
proc arcTo*(
ctx: Context, a, b: Vec2, r: float32
) {.raises: [PixieError].} =
## Adds a circular arc using the given control points and radius.
ctx.path.arcTo(a, b, r)
proc closePath*(ctx: Context) {.inline.} =
proc closePath*(ctx: Context) {.inline, raises: [].} =
## Attempts to add a straight line from the current point to the start of
## the current sub-path. If the shape has already been closed or has only
## one point, this function does nothing.
ctx.path.closePath()
proc rect*(ctx: Context, rect: Rect) {.inline.} =
proc rect*(ctx: Context, rect: Rect) {.inline, raises: [].} =
## Adds a rectangle to the current path.
ctx.path.rect(rect)
proc rect*(ctx: Context, x, y, width, height: float32) {.inline.} =
proc rect*(ctx: Context, x, y, width, height: float32) {.inline, raises: [].} =
## Adds a rectangle to the current path.
ctx.path.rect(x, y, width, height)
proc ellipse*(ctx: Context, center: Vec2, rx, ry: float32) {.inline.} =
proc ellipse*(ctx: Context, center: Vec2, rx, ry: float32) {.inline, raises: [].} =
## Adds an ellipse to the current sub-path.
ctx.path.ellipse(center, rx, ry)
proc ellipse*(ctx: Context, x, y, rx, ry: float32) {.inline.} =
proc ellipse*(ctx: Context, x, y, rx, ry: float32) {.inline, raises: [].} =
## Adds an ellipse to the current sub-path.
ctx.path.ellipse(x, y, rx, ry)
proc fill*(ctx: Context, path: Path, windingRule = wrNonZero) =
proc fill*(
ctx: Context, path: Path, windingRule = wrNonZero
) {.raises: [PixieError].} =
## Fills the path with the current fillStyle.
if ctx.mask != nil and ctx.layer == nil:
ctx.saveLayer()
@ -338,13 +360,17 @@ proc fill*(ctx: Context, path: Path, windingRule = wrNonZero) =
else:
ctx.fill(ctx.image, path, windingRule)
proc fill*(ctx: Context, windingRule = wrNonZero) {.inline.} =
proc fill*(
ctx: Context, windingRule = wrNonZero
) {.inline, raises: [PixieError].} =
## Fills the current path with the current fillStyle.
ctx.fill(ctx.path, windingRule)
proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.}
proc clip*(ctx: Context, windingRule = wrNonZero) {.inline, raises: [PixieError].}
proc clip*(ctx: Context, path: Path, windingRule = wrNonZero) =
proc clip*(
ctx: Context, path: Path, windingRule = wrNonZero
) {.raises: [PixieError].} =
## Turns the path into the current clipping region. The previous clipping
## region, if any, is intersected with the current or given path to create
## the new clipping region.
@ -354,15 +380,17 @@ proc clip*(ctx: Context, path: Path, windingRule = wrNonZero) =
else:
ctx.mask.fillPath(path, windingRule = windingRule, blendMode = bmMask)
proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.} =
proc clip*(
ctx: Context, windingRule = wrNonZero
) {.inline, raises: [PixieError].} =
## Turns the current path into the current clipping region. The previous
## clipping region, if any, is intersected with the current or given path
## to create the new clipping region.
ctx.clip(ctx.path, windingRule)
proc stroke*(ctx: Context) {.inline.}
proc stroke*(ctx: Context) {.inline, raises: [PixieError].}
proc stroke*(ctx: Context, path: Path) =
proc stroke*(ctx: Context, path: Path) {.raises: [PixieError].} =
## Strokes (outlines) the current or given path with the current strokeStyle.
if ctx.mask != nil and ctx.layer == nil:
ctx.saveLayer()
@ -373,11 +401,11 @@ proc stroke*(ctx: Context, path: Path) =
else:
ctx.stroke(ctx.image, path)
proc stroke*(ctx: Context) {.inline.} =
proc stroke*(ctx: Context) {.inline, raises: [PixieError].} =
## Strokes (outlines) the current or given path with the current strokeStyle.
ctx.stroke(ctx.path)
proc clearRect*(ctx: Context, rect: Rect) =
proc clearRect*(ctx: Context, rect: Rect) {.raises: [PixieError].} =
## Erases the pixels in a rectangular area.
let paint = newPaint(pkSolid)
paint.blendMode = bmOverwrite
@ -385,37 +413,43 @@ proc clearRect*(ctx: Context, rect: Rect) =
let path = newPath()
path.rect(rect)
if ctx.layer != nil:
ctx.layer.fillPath( path, paint, ctx.mat)
ctx.layer.fillPath(path, paint, ctx.mat)
else:
ctx.image.fillPath(path, paint, ctx.mat)
proc clearRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
proc clearRect*(
ctx: Context, x, y, width, height: float32
) {.inline, raises: [PixieError].} =
## Erases the pixels in a rectangular area.
ctx.clearRect(rect(x, y, width, height))
proc fillRect*(ctx: Context, rect: Rect) =
proc fillRect*(ctx: Context, rect: Rect) {.raises: [PixieError].} =
## Draws a rectangle that is filled according to the current fillStyle.
let path = newPath()
path.rect(rect)
ctx.fill(path)
proc fillRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
proc fillRect*(
ctx: Context, x, y, width, height: float32
) {.inline, raises: [PixieError].} =
## Draws a rectangle that is filled according to the current fillStyle.
ctx.fillRect(rect(x, y, width, height))
proc strokeRect*(ctx: Context, rect: Rect) =
proc strokeRect*(ctx: Context, rect: Rect) {.raises: [PixieError].} =
## Draws a rectangle that is stroked (outlined) according to the current
## strokeStyle and other context settings.
let path = newPath()
path.rect(rect)
ctx.stroke(path)
proc strokeRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
proc strokeRect*(
ctx: Context, x, y, width, height: float32
) {.inline, raises: [PixieError].} =
## Draws a rectangle that is stroked (outlined) according to the current
## strokeStyle and other context settings.
ctx.strokeRect(rect(x, y, width, height))
proc fillText*(ctx: Context, text: string, at: Vec2) =
proc fillText*(ctx: Context, text: string, at: Vec2) {.raises: [PixieError].} =
## Draws a text string at the specified coordinates, filling the string's
## characters with the current fillStyle
if ctx.mask != nil and ctx.layer == nil:
@ -427,12 +461,14 @@ proc fillText*(ctx: Context, text: string, at: Vec2) =
else:
ctx.fillText(ctx.image, text, at)
proc fillText*(ctx: Context, text: string, x, y: float32) {.inline.} =
proc fillText*(
ctx: Context, text: string, x, y: float32
) {.inline, raises: [PixieError].} =
## Draws the outlines of the characters of a text string at the specified
## coordinates.
ctx.fillText(text, vec2(x, y))
proc strokeText*(ctx: Context, text: string, at: Vec2) =
proc strokeText*(ctx: Context, text: string, at: Vec2) {.raises: [PixieError].} =
## Draws the outlines of the characters of a text string at the specified
## coordinates.
if ctx.mask != nil and ctx.layer == nil:
@ -444,12 +480,14 @@ proc strokeText*(ctx: Context, text: string, at: Vec2) =
else:
ctx.strokeText(ctx.image, text, at)
proc strokeText*(ctx: Context, text: string, x, y: float32) {.inline.} =
proc strokeText*(
ctx: Context, text: string, x, y: float32
) {.inline, raises: [PixieError].} =
## Draws the outlines of the characters of a text string at the specified
## coordinates.
ctx.strokeText(text, vec2(x, y))
proc measureText*(ctx: Context, text: string): TextMetrics =
proc measureText*(ctx: Context, text: string): TextMetrics {.raises: [PixieError].} =
## Returns a TextMetrics object that contains information about the measured
## text (such as its width, for example).
let
@ -457,61 +495,63 @@ proc measureText*(ctx: Context, text: string): TextMetrics =
bounds = typeset(font, text).computeBounds()
result.width = bounds.x
proc getLineDash*(ctx: Context): seq[float32] {.inline.} =
proc getLineDash*(ctx: Context): seq[float32] {.inline, raises: [].} =
ctx.lineDash
proc setLineDash*(ctx: Context, lineDash: seq[float32]) {.inline.} =
proc setLineDash*(ctx: Context, lineDash: seq[float32]) {.inline, raises: [].} =
ctx.lineDash = lineDash
proc getTransform*(ctx: Context): Mat3 {.inline.} =
proc getTransform*(ctx: Context): Mat3 {.inline, raises: []} =
## Retrieves the current transform matrix being applied to the context.
ctx.mat
proc setTransform*(ctx: Context, transform: Mat3) {.inline.} =
proc setTransform*(ctx: Context, transform: Mat3) {.inline, raises: [].} =
## Overrides the transform matrix being applied to the context.
ctx.mat = transform
proc setTransform*(ctx: Context, a, b, c, d, e, f: float32) {.inline.} =
proc setTransform*(ctx: Context, a, b, c, d, e, f: float32) {.inline, raises: [].} =
## Overrides the transform matrix being applied to the context.
ctx.mat = mat3(a, b, 0, c, d, 0, e, f, 1)
proc transform*(ctx: Context, transform: Mat3) {.inline.} =
proc transform*(ctx: Context, transform: Mat3) {.inline, raises: [].} =
## Multiplies the current transform with the matrix described by the
## arguments of this method.
ctx.mat = ctx.mat * transform
proc transform*(ctx: Context, a, b, c, d, e, f: float32) {.inline.} =
proc transform*(ctx: Context, a, b, c, d, e, f: float32) {.inline, raises: [].} =
## Multiplies the current transform with the matrix described by the
## arguments of this method.
ctx.transform(mat3(a, b, 0, c, d, 0, e, f, 1))
proc translate*(ctx: Context, v: Vec2) {.inline.} =
proc translate*(ctx: Context, v: Vec2) {.inline, raises: [].} =
## Adds a translation transformation to the current matrix.
ctx.mat = ctx.mat * translate(v)
proc translate*(ctx: Context, x, y: float32) {.inline.} =
proc translate*(ctx: Context, x, y: float32) {.inline, raises: [].} =
## Adds a translation transformation to the current matrix.
ctx.mat = ctx.mat * translate(vec2(x, y))
proc scale*(ctx: Context, v: Vec2) {.inline.} =
proc scale*(ctx: Context, v: Vec2) {.inline, raises: [].} =
## Adds a scaling transformation to the context units horizontally and/or
## vertically.
ctx.mat = ctx.mat * scale(v)
proc scale*(ctx: Context, x, y: float32) {.inline.} =
proc scale*(ctx: Context, x, y: float32) {.inline, raises: [].} =
## Adds a scaling transformation to the context units horizontally and/or
## vertically.
ctx.mat = ctx.mat * scale(vec2(x, y))
proc rotate*(ctx: Context, angle: float32) {.inline.} =
proc rotate*(ctx: Context, angle: float32) {.inline, raises: [].} =
## Adds a rotation to the transformation matrix.
ctx.mat = ctx.mat * rotate(-angle)
proc resetTransform*(ctx: Context) {.inline.} =
proc resetTransform*(ctx: Context) {.inline, raises: [].} =
## Resets the current transform to the identity matrix.
ctx.mat = mat3()
proc drawImage*(ctx: Context, image: Image, dx, dy, dWidth, dHeight: float32) =
proc drawImage*(
ctx: Context, image: Image, dx, dy, dWidth, dHeight: float32
) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
let
imageMat = ctx.mat * translate(vec2(dx, dy)) * scale(vec2(
@ -530,15 +570,17 @@ proc drawImage*(ctx: Context, image: Image, dx, dy, dWidth, dHeight: float32) =
ctx.fillStyle = savedFillStyle
proc drawImage*(ctx: Context, image: Image, dx, dy: float32) =
proc drawImage*(
ctx: Context, image: Image, dx, dy: float32
) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32)
proc drawImage*(ctx: Context, image: Image, pos: Vec2) =
proc drawImage*(ctx: Context, image: Image, pos: Vec2) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
ctx.drawImage(image, pos.x, pos.y)
proc drawImage*(ctx: Context, image: Image, rect: Rect) =
proc drawImage*(ctx: Context, image: Image, rect: Rect) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
ctx.drawImage(image, rect.x, rect.y, rect.w, rect.h)
@ -547,12 +589,12 @@ proc drawImage*(
image: Image,
sx, sy, sWidth, sHeight,
dx, dy, dWidth, dHeight: float32
) =
) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
let image = image.subImage(sx.int, sy.int, sWidth.int, sHeight.int)
ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32)
proc drawImage*(ctx: Context, image: Image, src, dest: Rect) =
proc drawImage*(ctx: Context, image: Image, src, dest: Rect) {.raises: [PixieError].} =
## Draws a source image onto the destination image.
ctx.drawImage(
image,
@ -562,29 +604,31 @@ proc drawImage*(ctx: Context, image: Image, src, dest: Rect) =
proc isPointInPath*(
ctx: Context, path: Path, pos: Vec2, windingRule = wrNonZero
): bool =
): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path.
path.fillOverlaps(pos, ctx.mat, windingRule)
proc isPointInPath*(
ctx: Context, path: Path, x, y: float32, windingRule = wrNonZero
): bool {.inline.} =
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path.
ctx.isPointInPath(path, vec2(x, y), windingRule)
proc isPointInPath*(
ctx: Context, pos: Vec2, windingRule = wrNonZero
): bool {.inline.} =
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path.
ctx.isPointInPath(ctx.path, pos, windingRule)
proc isPointInPath*(
ctx: Context, x, y: float32, windingRule = wrNonZero
): bool {.inline.} =
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path.
ctx.isPointInPath(ctx.path, vec2(x, y), windingRule)
proc isPointInStroke*(ctx: Context, path: Path, pos: Vec2): bool =
proc isPointInStroke*(
ctx: Context, path: Path, pos: Vec2
): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained
## by the stroking of a path.
path.strokeOverlaps(
@ -597,17 +641,23 @@ proc isPointInStroke*(ctx: Context, path: Path, pos: Vec2): bool =
ctx.lineDash
)
proc isPointInStroke*(ctx: Context, path: Path, x, y: float32): bool {.inline.} =
proc isPointInStroke*(
ctx: Context, path: Path, x, y: float32
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained
## by the stroking of a path.
ctx.isPointInStroke(path, vec2(x, y))
proc isPointInStroke*(ctx: Context, pos: Vec2): bool {.inline.} =
proc isPointInStroke*(
ctx: Context, pos: Vec2
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained
## by the stroking of a path.
ctx.isPointInStroke(ctx.path, pos)
proc isPointInStroke*(ctx: Context, x, y: float32): bool {.inline.} =
proc isPointInStroke*(
ctx: Context, x, y: float32
): bool {.inline, raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained
## by the stroking of a path.
ctx.isPointInStroke(ctx.path, vec2(x, y))
@ -616,53 +666,69 @@ proc isPointInStroke*(ctx: Context, x, y: float32): bool {.inline.} =
# Additional procs that are not part of the JS API
#
proc roundedRect*(ctx: Context, x, y, w, h, nw, ne, se, sw: float32) {.inline.} =
proc roundedRect*(
ctx: Context, x, y, w, h, nw, ne, se, sw: float32
) {.inline, raises: [].} =
## Adds a rounded rectangle to the current path.
ctx.path.roundedRect(x, y, w, h, nw, ne, se, sw)
proc roundedRect*(ctx: Context, rect: Rect, nw, ne, se, sw: float32) {.inline.} =
proc roundedRect*(
ctx: Context, rect: Rect, nw, ne, se, sw: float32
) {.inline, raises: [].} =
## Adds a rounded rectangle to the current path.
ctx.path.roundedRect(rect, nw, ne, se, sw)
proc circle*(ctx: Context, cx, cy, r: float32) {.inline.} =
proc circle*(ctx: Context, cx, cy, r: float32) {.inline, raises: [].} =
## Adds a circle to the current path.
ctx.path.circle(cx, cy, r)
proc circle*(ctx: Context, circle: Circle) {.inline.} =
proc circle*(ctx: Context, circle: Circle) {.inline, raises: [].} =
## Adds a circle to the current path.
ctx.path.circle(circle)
proc polygon*(ctx: Context, x, y, size: float32, sides: int) {.inline.} =
proc polygon*(
ctx: Context, x, y, size: float32, sides: int
) {.inline, raises: [].} =
## Adds an n-sided regular polygon at (x, y) of size to the current path.
ctx.path.polygon(x, y, size, sides)
proc polygon*(ctx: Context, pos: Vec2, size: float32, sides: int) {.inline.} =
proc polygon*(
ctx: Context, pos: Vec2, size: float32, sides: int
) {.inline, raises: [].} =
## Adds an n-sided regular polygon at (x, y) of size to the current path.
ctx.path.polygon(pos, size, sides)
proc fillRoundedRect*(ctx: Context, rect: Rect, nw, ne, se, sw: float32) =
proc fillRoundedRect*(
ctx: Context, rect: Rect, nw, ne, se, sw: float32
) {.raises: [PixieError].} =
## Draws a rounded rectangle that is filled according to the current fillStyle.
let path = newPath()
path.roundedRect(rect, nw, ne, se, sw)
ctx.fill(path)
proc fillRoundedRect*(ctx: Context, rect: Rect, radius: float32) {.inline.} =
proc fillRoundedRect*(
ctx: Context, rect: Rect, radius: float32
) {.inline, raises: [PixieError].} =
## Draws a rounded rectangle that is filled according to the current fillStyle.
ctx.fillRoundedRect(rect, radius, radius, radius, radius)
proc strokeRoundedRect*(ctx: Context, rect: Rect, nw, ne, se, sw: float32) =
proc strokeRoundedRect*(
ctx: Context, rect: Rect, nw, ne, se, sw: float32
) {.raises: [PixieError].} =
## Draws a rounded rectangle that is stroked (outlined) according to the
## current strokeStyle and other context settings.
let path = newPath()
path.roundedRect(rect, nw, ne, se, sw)
ctx.stroke(path)
proc strokeRoundedRect*(ctx: Context, rect: Rect, radius: float32) {.inline.} =
proc strokeRoundedRect*(
ctx: Context, rect: Rect, radius: float32
) {.inline, raises: [PixieError].} =
## Draws a rounded rectangle that is stroked (outlined) according to the
## current strokeStyle and other context settings.
ctx.strokeRoundedRect(rect, radius, radius, radius, radius)
proc strokeSegment*(ctx: Context, segment: Segment) =
proc strokeSegment*(ctx: Context, segment: Segment) {.raises: [PixieError].} =
## Strokes a segment (draws a line from segment.at to segment.to) according
## to the current strokeStyle and other context settings.
let path = newPath()
@ -670,40 +736,52 @@ proc strokeSegment*(ctx: Context, segment: Segment) =
path.lineTo(segment.to)
ctx.stroke(path)
proc fillEllipse*(ctx: Context, center: Vec2, rx, ry: float32) =
proc fillEllipse*(
ctx: Context, center: Vec2, rx, ry: float32
) {.raises: [PixieError].} =
## Draws an ellipse that is filled according to the current fillStyle.
let path = newPath()
path.ellipse(center, rx, ry)
ctx.fill(path)
proc strokeEllipse*(ctx: Context, center: Vec2, rx, ry: float32) =
proc strokeEllipse*(
ctx: Context, center: Vec2, rx, ry: float32
) {.raises: [PixieError].} =
## Draws an ellipse that is stroked (outlined) according to the current
## strokeStyle and other context settings.
let path = newPath()
path.ellipse(center, rx, ry)
ctx.stroke(path)
proc fillCircle*(ctx: Context, circle: Circle) =
proc fillCircle*(
ctx: Context, circle: Circle
) {.raises: [PixieError].} =
## Draws a circle that is filled according to the current fillStyle
let path = newPath()
path.circle(circle)
ctx.fill(path)
proc strokeCircle*(ctx: Context, circle: Circle) =
proc strokeCircle*(
ctx: Context, circle: Circle
) {.raises: [PixieError].} =
## Draws a circle that is stroked (outlined) according to the current
## strokeStyle and other context settings.
let path = newPath()
path.circle(circle)
ctx.stroke(path)
proc fillPolygon*(ctx: Context, pos: Vec2, size: float32, sides: int) =
proc fillPolygon*(
ctx: Context, pos: Vec2, size: float32, sides: int
) {.raises: [PixieError].} =
## Draws an n-sided regular polygon at (x, y) of size that is filled according
## to the current fillStyle.
let path = newPath()
path.polygon(pos, size, sides)
ctx.fill(path)
proc strokePolygon*(ctx: Context, pos: Vec2, size: float32, sides: int) =
proc strokePolygon*(
ctx: Context, pos: Vec2, size: float32, sides: int
) {.raises: [PixieError].} =
## Draws an n-sided regular polygon at (x, y) of size that is stroked
## (outlined) according to the current strokeStyle and other context settings.
let path = newPath()

View file

@ -4,7 +4,7 @@ import chroma, flatty/binny, pixie/common, pixie/images
const bmpSignature* = "BM"
proc decodeBmp*(data: string): Image =
proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
## Decodes bitmap data into an Image.
# BMP Header
@ -48,11 +48,11 @@ proc decodeBmp*(data: string): Image =
offset += 3
result[x, result.height - y - 1] = rgba.rgbx()
proc decodeBmp*(data: seq[uint8]): Image {.inline.} =
proc decodeBmp*(data: seq[uint8]): Image {.inline, raises: [PixieError].} =
## Decodes bitmap data into an Image.
decodeBmp(cast[string](data))
proc encodeBmp*(image: Image): string =
proc encodeBmp*(image: Image): string {.raises: [].} =
## Encodes an image into the BMP file format.
# BMP Header

View file

@ -10,7 +10,7 @@ template failInvalid() =
when defined(release):
{.push checks: off.}
proc decodeGif*(data: string): Image =
proc decodeGif*(data: string): Image {.raises: [PixieError].} =
## Decodes GIF data into an Image.
if data.len <= 13: failInvalid()

View file

@ -6,7 +6,7 @@ when defined(pixieUseStb):
const
jpgStartOfImage* = [0xFF.uint8, 0xD8]
proc decodeJpg*(data: seq[uint8]): Image =
proc decodeJpg*(data: seq[uint8]): Image {.raises: [PixieError].} =
## Decodes the JPEG into an Image.
when not defined(pixieUseStb):
raise newException(PixieError, "Decoding JPG requires -d:pixieUseStb")
@ -19,10 +19,10 @@ proc decodeJpg*(data: seq[uint8]): Image =
result = newImage(width, height)
copyMem(result.data[0].addr, pixels[0].unsafeAddr, pixels.len)
proc decodeJpg*(data: string): Image {.inline.} =
proc decodeJpg*(data: string): Image {.inline, raises: [PixieError].} =
## Decodes the JPEG data into an Image.
decodeJpg(cast[seq[uint8]](data))
proc encodeJpg*(image: Image): string =
proc encodeJpg*(image: Image): string {.raises: [PixieError].} =
## Encodes Image into a JPEG data string.
raise newException(PixieError, "Encoding JPG not supported yet")

View file

@ -23,7 +23,7 @@ template failInvalid() =
when defined(release):
{.push checks: off.}
proc decodeHeader(data: string): PngHeader =
proc decodeHeader(data: string): PngHeader {.raises: [PixieError].} =
result.width = data.readUint32(0).swap().int
result.height = data.readUint32(4).swap().int
result.bitDepth = data.readUint8(8)
@ -79,7 +79,7 @@ proc decodeHeader(data: string): PngHeader =
if result.interlaceMethod != 0:
raise newException(PixieError, "Interlaced PNG not yet supported")
proc decodePalette(data: string): seq[ColorRGB] =
proc decodePalette(data: string): seq[ColorRGB] {.raises: [PixieError].} =
if data.len == 0 or data.len mod 3 != 0:
failInvalid()
@ -88,7 +88,9 @@ proc decodePalette(data: string): seq[ColorRGB] =
for i in 0 ..< data.len div 3:
result[i] = cast[ptr ColorRGB](data[i * 3].unsafeAddr)[]
proc unfilter(uncompressed: string, height, rowBytes, bpp: int): string =
proc unfilter(
uncompressed: string, height, rowBytes, bpp: int
): string {.raises: [].} =
result.setLen(uncompressed.len - height)
template uncompressedIdx(x, y: int): int =
@ -162,7 +164,7 @@ proc decodeImageData(
header: PngHeader,
palette: seq[ColorRGB],
transparency, data: string
): seq[ColorRGBA] =
): seq[ColorRGBA] {.raises: [PixieError].} =
result.setLen(header.width * header.height)
let
@ -314,7 +316,7 @@ proc decodeImageData(
else:
discard # Not possible, parseHeader validates
proc decodePng*(data: string): Image =
proc decodePng*(data: string): Image {.raises: [PixieError].} =
## Decodes the PNG data into an Image.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
@ -424,7 +426,9 @@ proc decodePng*(data: string): Image =
result = newImage(header.width, header.height)
copyMem(result.data[0].addr, pixels[0].addr, pixels.len * 4)
proc encodePng*(width, height, channels: int, data: pointer, len: int): string =
proc encodePng*(
width, height, channels: int, data: pointer, len: int
): string {.raises: [PixieError].} =
## Encodes the image data into the PNG file format.
## If data points to RGBA data, it is assumed to be straight alpha.
@ -503,7 +507,7 @@ proc encodePng*(width, height, channels: int, data: pointer, len: int): string =
result.add("IEND")
result.addUint32(crc32(result[result.len - 4 ..< result.len]).swap())
proc encodePng*(image: Image): string =
proc encodePng*(image: Image): string {.raises: [PixieError].} =
## Encodes the image data into the PNG file format.
if image.data.len == 0:
raise newException(
@ -514,7 +518,7 @@ proc encodePng*(image: Image): string =
copy.toStraightAlpha()
encodePng(image.width, image.height, 4, copy[0].addr, copy.len * 4)
proc encodePng*(mask: Mask): string =
proc encodePng*(mask: Mask): string {.raises: [PixieError].} =
## Encodes the mask data into the PNG file format.
if mask.data.len == 0:
raise newException(

View file

@ -33,15 +33,19 @@ type
template failInvalid() =
raise newException(PixieError, "Invalid SVG data")
proc attrOrDefault(node: XmlNode, name, default: string): string =
proc attrOrDefault(node: XmlNode, name, default: string): string {.raises: [].} =
result = node.attr(name)
if result.len == 0:
result = default
proc initCtx(): Ctx =
proc initCtx(): Ctx {.raises: [PixieError].} =
result.display = true
result.fill = parseHtmlColor("black").rgbx
result.stroke = parseHtmlColor("black").rgbx
try:
result.fill = parseHtmlColor("black").rgbx
result.stroke = parseHtmlColor("black").rgbx
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
result.strokeWidth = 1
result.transform = mat3()
result.strokeMiterLimit = defaultMiterLimit
@ -49,7 +53,7 @@ proc initCtx(): Ctx =
result.strokeOpacity = 1
result.linearGradients = newTable[string, LinearGradient]()
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
proc decodeCtxInternal(inherited: Ctx, node: XmlNode): Ctx =
result = inherited
proc splitArgs(s: string): seq[string] =
@ -313,14 +317,23 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx =
else:
failInvalidTransform(transform)
proc fill(img: Image, ctx: Ctx, path: Path) {.inline.} =
proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx {.raises: [PixieError].} =
try:
decodeCtxInternal(inherited, node)
except PixieError as e:
raise e
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
proc fill(img: Image, ctx: Ctx, path: Path) {.inline, raises: [PixieError].} =
if ctx.display and ctx.opacity > 0:
let paint = newPaint(ctx.fill)
if ctx.opacity != 1:
paint.opacity = paint.opacity * ctx.opacity
img.fillPath(path, paint, ctx.transform, ctx.fillRule)
proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
proc stroke(img: Image, ctx: Ctx, path: Path) {.inline, raises: [PixieError].} =
if ctx.display and ctx.opacity > 0:
let paint = newPaint(ctx.stroke)
if ctx.opacity != 1:
@ -336,7 +349,7 @@ proc stroke(img: Image, ctx: Ctx, path: Path) {.inline.} =
dashes = ctx.strokeDashArray
)
proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
proc drawInternal(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
if node.kind != xnElement:
# Skip <!-- comments -->
return
@ -353,7 +366,7 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
let ctx = decodeCtx(ctxStack[^1], node)
ctxStack.add(ctx)
for child in node:
img.draw(child, ctxStack)
img.drawInternal(child, ctxStack)
discard ctxStack.pop()
of "path":
@ -543,7 +556,20 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) =
else:
raise newException(PixieError, "Unsupported SVG tag: " & node.tag)
proc decodeSvg*(data: string, width = 0, height = 0): Image =
proc draw(
img: Image, node: XmlNode, ctxStack: var seq[Ctx]
) {.raises: [PixieError].} =
try:
drawInternal(img, node, ctxStack)
except PixieError as e:
raise e
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
proc decodeSvg*(
data: string, width = 0, height = 0
): Image {.raises: [PixieError].} =
## Render SVG file and return the image. Defaults to the SVG's view box size.
try:
let root = parseXml(data)

View file

@ -334,32 +334,34 @@ type
when defined(release):
{.push checks: off.}
proc eofCheck(buf: string, readTo: int) {.inline.} =
template eofCheck(buf: string, readTo: int) =
if readTo > buf.len:
raise newException(PixieError, "Unexpected error reading font data, EOF")
proc failUnsupported() =
template failUnsupported() =
raise newException(PixieError, "Unsupported font data")
proc readUint16Seq(buf: string, offset, len: int): seq[uint16] =
proc readUint16Seq(buf: string, offset, len: int): seq[uint16] {.raises: [].} =
result = newSeq[uint16](len)
for i in 0 ..< len:
result[i] = buf.readUint16(offset + i * 2).swap()
proc readFixed32(buf: string, offset: int): float32 =
proc readFixed32(buf: string, offset: int): float32 {.raises: [].} =
## Packed 32-bit value with major and minor version numbers.
ceil(buf.readInt32(offset).swap().float32 / 65536.0 * 100000.0) / 100000.0
proc readFixed16(buf: string, offset: int): float32 =
proc readFixed16(buf: string, offset: int): float32 {.raises: [].} =
## Reads 16-bit signed fixed number with the low 14 bits of fraction (2.14).
buf.readInt16(offset).swap().float32 / 16384.0
proc readLongDateTime(buf: string, offset: int): float64 =
proc readLongDateTime(buf: string, offset: int): float64 {.raises: [].} =
## Date and time represented in number of seconds since 12:00 midnight,
## January 1, 1904, UTC.
buf.readInt64(offset).swap().float64 - 2082844800
proc parseCmapTable(buf: string, offset: int): CmapTable =
proc parseCmapTable(
buf: string, offset: int
): CmapTable {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 4)
@ -454,7 +456,9 @@ proc parseCmapTable(buf: string, offset: int): CmapTable =
# TODO implement other cmap platformIDs
discard
proc parseHeadTable(buf: string, offset: int): HeadTable =
proc parseHeadTable(
buf: string, offset: int
): HeadTable {.raises: [PixieError].} =
buf.eofCheck(offset + 54)
result = HeadTable()
@ -483,7 +487,9 @@ proc parseHeadTable(buf: string, offset: int): HeadTable =
if result.glyphDataFormat != 0:
failUnsupported()
proc parseHheaTable(buf: string, offset: int): HheaTable =
proc parseHheaTable(
buf: string, offset: int
): HheaTable {.raises: [PixieError].} =
buf.eofCheck(offset + 36)
result = HheaTable()
@ -512,7 +518,9 @@ proc parseHheaTable(buf: string, offset: int): HheaTable =
failUnsupported()
result.numberOfHMetrics = buf.readUint16(offset + 34).swap()
proc parseMaxpTable(buf: string, offset: int): MaxpTable =
proc parseMaxpTable(
buf: string, offset: int
): MaxpTable {.raises: [PixieError].} =
buf.eofCheck(offset + 32)
result = MaxpTable()
@ -536,7 +544,7 @@ proc parseMaxpTable(buf: string, offset: int): MaxpTable =
proc parseHmtxTable(
buf: string, offset: int, hhea: HheaTable, maxp: MaxpTable
): HmtxTable =
): HmtxTable {.raises: [PixieError].} =
var i = offset
let
@ -557,7 +565,9 @@ proc parseHmtxTable(
result.leftSideBearings.add(buf.readInt16(i).swap())
i += 2
proc parseNameTable(buf: string, offset: int): NameTable =
proc parseNameTable(
buf: string, offset: int
): NameTable {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 6)
@ -583,7 +593,9 @@ proc parseNameTable(buf: string, offset: int): NameTable =
record.offset = buf.readUint16(i + 10).swap()
i += 12
proc parseOS2Table(buf: string, offset: int): OS2Table =
proc parseOS2Table(
buf: string, offset: int
): OS2Table {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 78)
@ -647,7 +659,7 @@ proc parseOS2Table(buf: string, offset: int): OS2Table =
proc parseLocaTable(
buf: string, offset: int, head: HeadTable, maxp: MaxpTable
): LocaTable =
): LocaTable {.raises: [PixieError].} =
var i = offset
result = LocaTable()
@ -664,13 +676,17 @@ proc parseLocaTable(
result.offsets.add(buf.readUint32(i).swap())
i += 4
proc parseGlyfTable(buf: string, offset: int, loca: LocaTable): GlyfTable =
proc parseGlyfTable(
buf: string, offset: int, loca: LocaTable
): GlyfTable {.raises: [].} =
result = GlyfTable()
result.offsets.setLen(loca.offsets.len)
for glyphId in 0 ..< loca.offsets.len:
result.offsets[glyphId] = offset.uint32 + loca.offsets[glyphId]
proc parseKernTable(buf: string, offset: int): KernTable =
proc parseKernTable(
buf: string, offset: int
): KernTable {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 2)
@ -721,11 +737,10 @@ proc parseKernTable(buf: string, offset: int): KernTable =
if pair.value != 0:
let key = (pair.left, pair.right)
var value = pair.value.float32
if key in result.kerningPairs:
if (table.coverage and 0b1000) != 0: # Override
discard
else: # Accumulate
value += result.kerningPairs[key]
if (table.coverage and 0b1000) != 0: # Override
discard
else: # Accumulate
value += result.kerningPairs.getOrDefault(key, 0)
result.kerningPairs[key] = value
elif version == 1:
@ -733,7 +748,9 @@ proc parseKernTable(buf: string, offset: int): KernTable =
else:
failUnsupported()
# proc parseLangSys(buf: string, offset: int): LangSys =
# proc parseLangSys(
# buf: string, offset: int
# ): LangSys {.raises: [PixieError].} =
# var i = offset
# buf.eofCheck(i + 6)
@ -821,14 +838,16 @@ proc parseKernTable(buf: string, offset: int): KernTable =
# result.featureRecords.add(featureRecord)
# i += 6
proc parseRangeRecord(buf: string, offset: int): RangeRecord =
proc parseRangeRecord(
buf: string, offset: int
): RangeRecord {.raises: [PixieError].} =
buf.eofCheck(offset + 6)
result.startGlyphID = buf.readUint16(offset + 0).swap()
result.endGlyphID = buf.readUint16(offset + 2).swap()
result.startCoverageIndex = buf.readUint16(offset + 4).swap()
proc parseCoverage(buf: string, offset: int): Coverage =
proc parseCoverage(buf: string, offset: int): Coverage {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 4)
@ -866,7 +885,7 @@ proc parseCoverage(buf: string, offset: int): Coverage =
else:
failUnsupported()
proc valueFormatSize(valueFormat: uint16): int =
proc valueFormatSize(valueFormat: uint16): int {.raises: [].} =
# countSetBits(valueFormat) * 2
var
n = valueFormat
@ -878,7 +897,7 @@ proc valueFormatSize(valueFormat: uint16): int =
proc parseValueRecord(
buf: string, offset: int, valueFormat: uint16
): ValueRecord =
): ValueRecord {.raises: [PixieError].} =
buf.eofCheck(offset + valueFormatSize(valueFormat))
var i = offset
@ -909,7 +928,7 @@ proc parseValueRecord(
proc parsePairValueRecord(
buf: string, offset: int, valueFormat1, valueFormat2: uint16
): PairValueRecord =
): PairValueRecord {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 2)
@ -923,7 +942,7 @@ proc parsePairValueRecord(
proc parsePairSet(
buf: string, offset: int, valueFormat1, valueFormat2: uint16
): PairSet =
): PairSet {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 2)
@ -942,7 +961,7 @@ proc parsePairSet(
proc parseClass2Record(
buf: string, offset: int, valueFormat1, valueFormat2: uint16
): Class2Record =
): Class2Record {.raises: [PixieError].} =
var i = offset
buf.eofCheck(
@ -955,7 +974,7 @@ proc parseClass2Record(
proc parseClass1Record(
buf: string, offset: int, valueFormat1, valueFormat2, class2Count: uint16
): Class1Record =
): Class1Record {.raises: [PixieError].} =
var i = offset
result.class2Records.setLen(class2Count.int)
@ -964,14 +983,16 @@ proc parseClass1Record(
parseClass2Record(buf, i, valueFormat1, valueFormat2)
i += valueFormatSize(valueFormat1) + valueFormatSize(valueFormat2)
proc parseClassRangeRecord(buf: string, offset: int): ClassRangeRecord =
proc parseClassRangeRecord(
buf: string, offset: int
): ClassRangeRecord {.raises: [PixieError].} =
buf.eofCheck(offset + 6)
result.startGlyphID = buf.readUint16(offset + 0).swap()
result.endGlyphID = buf.readUint16(offset + 2).swap()
result.class = buf.readUint16(offset + 4).swap()
proc parseClassDef(buf: string, offset: int): ClassDef =
proc parseClassDef(buf: string, offset: int): ClassDef {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 2)
@ -1003,7 +1024,7 @@ proc parseClassDef(buf: string, offset: int): ClassDef =
else:
failUnsupported()
proc parsePairPos(buf: string, offset: int): PairPos =
proc parsePairPos(buf: string, offset: int): PairPos {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 4)
@ -1136,7 +1157,9 @@ proc parsePairPos(buf: string, offset: int): PairPos =
else:
failUnsupported()
proc parseLookup(buf: string, offset: int, gpos: GposTable): Lookup =
proc parseLookup(
buf: string, offset: int, gpos: GposTable
): Lookup {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 6)
@ -1162,7 +1185,9 @@ proc parseLookup(buf: string, offset: int, gpos: GposTable): Lookup =
pairPos.classPairAdjustments.len > 0:
gpos.lookupList.pairPosTables.add(pairPos)
proc parseLookupList(buf: string, offset: int, gpos: GposTable): LookupList =
proc parseLookupList(
buf: string, offset: int, gpos: GposTable
): LookupList {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 2)
@ -1177,7 +1202,9 @@ proc parseLookupList(buf: string, offset: int, gpos: GposTable): LookupList =
for lookupOffset in result.lookupoffsets:
result.lookups.add(parseLookup(buf, offset + lookupOffset.int, gpos))
proc parseGposTable(buf: string, offset: int): GPOSTable =
proc parseGposTable(
buf: string, offset: int
): GPOSTable {.raises: [PixieError].} =
var i = offset
buf.eofCheck(i + 10)
@ -1208,7 +1235,9 @@ proc parseGposTable(buf: string, offset: int): GPOSTable =
result.lookupList =
parseLookupList(buf, offset + result.lookupListOffset.int, result)
proc parsePostTable(buf: string, offset: int): PostTable =
proc parsePostTable(
buf: string, offset: int
): PostTable {.raises: [PixieError].} =
buf.eofCheck(offset + 14)
result = PostTable()
@ -1218,15 +1247,14 @@ proc parsePostTable(buf: string, offset: int): PostTable =
result.underlineThickness = buf.readInt16(offset + 10).swap()
result.isFixedPitch = buf.readUint32(offset + 12).swap()
proc getGlyphId(opentype: OpenType, rune: Rune): uint16 {.inline.} =
if rune in opentype.cmap.runeToGlyphId:
result = opentype.cmap.runeToGlyphId[rune]
else:
discard # Index 0 is the "missing character" glyph
proc getGlyphId(opentype: OpenType, rune: Rune): uint16 {.inline, raises: [].} =
result = opentype.cmap.runeToGlyphId.getOrDefault(rune, 0)
proc parseGlyph(opentype: OpenType, glyphId: uint16): Path
proc parseGlyph(opentype: OpenType, glyphId: uint16): Path {.raises: [PixieError].}
proc parseGlyphPath(buf: string, offset, numberOfContours: int): Path =
proc parseGlyphPath(
buf: string, offset, numberOfContours: int
): Path {.raises: [PixieError].} =
if numberOfContours < 0:
raise newException(PixieError, "Glyph numberOfContours must be >= 0")
@ -1359,7 +1387,9 @@ proc parseGlyphPath(buf: string, offset, numberOfContours: int): Path =
result.closePath()
proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
proc parseCompositeGlyph(
opentype: OpenType, offset: int
): Path {.raises: [PixieError].} =
result = newPath()
var
@ -1454,7 +1484,9 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
moreComponents = (flags and 0b100000) != 0
proc parseGlyph(opentype: OpenType, glyphId: uint16): Path =
proc parseGlyph(
opentype: OpenType, glyphId: uint16
): Path {.raises: [PixieError].} =
if glyphId.int >= opentype.glyf.offsets.len:
raise newException(PixieError, "Invalid glyph ID " & $glyphId)
@ -1482,16 +1514,21 @@ proc parseGlyph(opentype: OpenType, glyphId: uint16): Path =
else:
parseGlyphPath(opentype.buf, i, numberOfContours)
proc parseGlyph(opentype: OpenType, rune: Rune): Path {.inline.} =
proc parseGlyph(
opentype: OpenType, rune: Rune
): Path {.inline, raises: [PixieError].} =
opentype.parseGlyph(opentype.getGlyphId(rune))
proc getGlyphPath*(opentype: OpenType, rune: Rune): Path =
proc getGlyphPath*(
opentype: OpenType, rune: Rune
): Path {.raises: [PixieError].} =
if rune notin opentype.glyphPaths:
opentype.glyphPaths[rune] = opentype.parseGlyph(rune)
opentype.glyphPaths[rune].transform(scale(vec2(1, -1)))
opentype.glyphPaths[rune]
let path = opentype.parseGlyph(rune)
path.transform(scale(vec2(1, -1)))
opentype.glyphPaths[rune] = path
opentype.glyphPaths.getOrDefault(rune, nil) # Never actually returns nil
proc getLeftSideBearing*(opentype: OpenType, rune: Rune): float32 =
proc getLeftSideBearing*(opentype: OpenType, rune: Rune): float32 {.raises: [].} =
let glyphId = opentype.getGlyphId(rune).int
if glyphId < opentype.hmtx.hMetrics.len:
result = opentype.hmtx.hMetrics[glyphId].leftSideBearing.float32
@ -1500,21 +1537,23 @@ proc getLeftSideBearing*(opentype: OpenType, rune: Rune): float32 =
if index > 0 and index < opentype.hmtx.leftSideBearings.len:
result = opentype.hmtx.leftSideBearings[index].float32
proc getAdvance*(opentype: OpenType, rune: Rune): float32 =
proc getAdvance*(opentype: OpenType, rune: Rune): float32 {.raises: [].} =
let glyphId = opentype.getGlyphId(rune).int
if glyphId < opentype.hmtx.hMetrics.len:
result = opentype.hmtx.hMetrics[glyphId].advanceWidth.float32
else:
result = opentype.hmtx.hMetrics[^1].advanceWidth.float32
proc getKerningAdjustment*(opentype: OpenType, left, right: Rune): float32 =
proc getKerningAdjustment*(
opentype: OpenType, left, right: Rune
): float32 {.raises: [].} =
if left notin opentype.cmap.runeToGlyphId or
right notin opentype.cmap.runeToGlyphId:
return
let
leftGlyphId = opentype.cmap.runeToGlyphId[left]
rightGlyphId = opentype.cmap.runeToGlyphId[right]
leftGlyphId = opentype.cmap.runeToGlyphId.getOrDefault(left, 0)
rightGlyphId = opentype.cmap.runeToGlyphId.getOrDefault(right, 0)
glyphPair = (leftGlyphId, rightGlyphId)
if opentype.gpos != nil:
@ -1525,27 +1564,23 @@ proc getKerningAdjustment*(opentype: OpenType, left, right: Rune): float32 =
case pairPos.posFormat:
of 1:
if glyphPair in pairPos.glyphPairAdjustments:
result = pairPos.glyphPairAdjustments[glyphPair].float32
result = pairPos.glyphPairAdjustments.getOrDefault(glyphPair, 0).float32
break
of 2:
var leftClass, rightClass: uint16
if leftGlyphId in pairPos.glyphIdToClass1:
leftClass = pairPos.glyphIdToClass1[leftGlyphId]
if rightGlyphId in pairPos.glyphIdToClass2:
rightClass = pairPos.glyphIdToClass2[rightGlyphId]
let classPair = (leftClass, rightClass)
let
leftClass = pairPos.glyphIdToClass1.getOrDefault(leftGlyphId, 0)
rightClass = pairPos.glyphIdToClass2.getOrDefault(rightGlyphId, 0)
classPair = (leftClass, rightClass)
if classPair in pairPos.classPairAdjustments:
result = pairPos.classPairAdjustments[classPair].float32
result = pairPos.classPairAdjustments.getOrDefault(classPair, 0).float32
break
else:
discard
elif opentype.kern != nil:
if glyphPair in opentype.kern.kerningPairs:
result = opentype.kern.kerningPairs[glyphPair]
result = opentype.kern.kerningPairs.getOrDefault(glyphPair, 0)
proc parseOpenType*(buf: string): OpenType =
proc parseOpenType*(buf: string): OpenType {.raises: [PixieError].} =
result = OpenType()
result.buf = buf
@ -1572,36 +1607,31 @@ proc parseOpenType*(buf: string): OpenType =
result.tableRecords[tableRecord.tag] = tableRecord
i += 16
const requiredTables = [
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "loca", "glyf",
"post"
]
for table in requiredTables:
if table notin result.tableRecords:
raise newException(PixieError, "Missing required font table " & table)
try:
result.cmap = parseCmapTable(buf, result.tableRecords["cmap"].offset.int)
result.head = parseHeadTable(buf, result.tableRecords["head"].offset.int)
result.hhea = parseHheaTable(buf, result.tableRecords["hhea"].offset.int)
result.maxp = parseMaxpTable(buf, result.tableRecords["maxp"].offset.int)
result.hmtx = parseHmtxTable(
buf, result.tableRecords["hmtx"].offset.int, result.hhea, result.maxp
)
result.name = parseNameTable(buf, result.tableRecords["name"].offset.int)
result.os2 = parseOS2Table(buf, result.tableRecords["OS/2"].offset.int)
result.loca = parseLocaTable(
buf, result.tableRecords["loca"].offset.int, result.head, result.maxp
)
result.glyf =
parseGlyfTable(buf, result.tableRecords["glyf"].offset.int, result.loca)
result.cmap = parseCmapTable(buf, result.tableRecords["cmap"].offset.int)
result.head = parseHeadTable(buf, result.tableRecords["head"].offset.int)
result.hhea = parseHheaTable(buf, result.tableRecords["hhea"].offset.int)
result.maxp = parseMaxpTable(buf, result.tableRecords["maxp"].offset.int)
result.hmtx = parseHmtxTable(
buf, result.tableRecords["hmtx"].offset.int, result.hhea, result.maxp
)
result.name = parseNameTable(buf, result.tableRecords["name"].offset.int)
result.os2 = parseOS2Table(buf, result.tableRecords["OS/2"].offset.int)
result.loca = parseLocaTable(
buf, result.tableRecords["loca"].offset.int, result.head, result.maxp
)
result.glyf =
parseGlyfTable(buf, result.tableRecords["glyf"].offset.int, result.loca)
if "kern" in result.tableRecords:
result.kern = parseKernTable(buf, result.tableRecords["kern"].offset.int)
if "kern" in result.tableRecords:
result.kern = parseKernTable(buf, result.tableRecords["kern"].offset.int)
if "GPOS" in result.tableRecords:
result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
if "GPOS" in result.tableRecords:
result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
result.post = parsePostTable(buf, result.tableRecords["post"].offset.int)
result.post = parsePostTable(buf, result.tableRecords["post"].offset.int)
except KeyError as e:
raise newException(PixieError, "Missing required font table: " & e.msg)
when defined(release):
{.pop.}

View file

@ -8,27 +8,22 @@ type SvgFont* = ref object
missingGlyphAdvance: float32
missingGlyphPath: Path
proc getGlyphPath*(svgFont: SvgFont, rune: Rune): Path =
if rune in svgFont.glyphPaths:
svgFont.glyphPaths[rune]
else:
svgFont.missingGlyphPath
proc getGlyphPath*(svgFont: SvgFont, rune: Rune): Path {.raises: [].} =
svgFont.glyphPaths.getOrDefault(rune, svgFont.missingGlyphPath)
proc getAdvance*(svgFont: SvgFont, rune: Rune): float32 =
if rune in svgFont.advances:
svgFont.advances[rune]
else:
svgFont.missingGlyphAdvance
proc getAdvance*(svgFont: SvgFont, rune: Rune): float32 {.raises: [].} =
svgFont.advances.getOrDefault(rune, svgFont.missingGlyphAdvance)
proc getKerningAdjustment*(svgFont: SvgFont, left, right: Rune): float32 =
proc getKerningAdjustment*(
svgFont: SvgFont, left, right: Rune
): float32 {.raises: [].} =
let pair = (left, right)
if pair in svgFont.kerningPairs:
result = svgFont.kerningPairs[pair]
result = svgFont.kerningPairs.getOrDefault(pair, 0)
proc failInvalid() =
template failInvalid() =
raise newException(PixieError, "Invalid SVG font data")
proc parseFloat(node: XmlNode, attr: string): float32 =
proc parseFloat(node: XmlNode, attr: string): float32 {.raises: [PixieError].} =
let value = node.attr(attr)
if value.len == 0:
raise newException(PixieError, "SVG font missing attr " & attr)
@ -37,12 +32,17 @@ proc parseFloat(node: XmlNode, attr: string): float32 =
except:
failInvalid()
proc parseSvgFont*(buf: string): SvgFont =
proc parseSvgFont*(buf: string): SvgFont {.raises: [PixieError].} =
result = SvgFont()
let
root = parseXml(buf)
defs = root.child("defs")
let root =
try:
parseXml(buf)
except:
let e = getCurrentException()
raise newException(PixieError, e.msg, e)
let defs = root.child("defs")
if defs == nil:
failInvalid()
@ -75,8 +75,9 @@ proc parseSvgFont*(buf: string): SvgFont =
if node.attr("horiz-adv-x").len > 0:
advance = node.parseFloat("horiz-adv-x")
result.advances[rune] = advance
result.glyphPaths[rune] = parsePath(node.attr("d"))
result.glyphPaths[rune].transform(scale(vec2(1, -1)))
let path = parsePath(node.attr("d"))
path.transform(scale(vec2(1, -1)))
result.glyphPaths[rune] = path
else:
discard # Multi-rune unicode?
of "hkern":

View file

@ -53,46 +53,48 @@ type
# tcSmallCaps
# tcSmallCapsForced
proc ascent*(typeface: Typeface): float32 {.inline.} =
proc ascent*(typeface: Typeface): float32 {.raises: [].} =
## The font ascender value in font units.
if typeface.opentype != nil:
typeface.opentype.hhea.ascender.float32
else:
typeface.svgFont.ascent
proc descent*(typeface: Typeface): float32 {.inline.} =
proc descent*(typeface: Typeface): float32 {.raises: [].} =
## The font descender value in font units.
if typeface.opentype != nil:
typeface.opentype.hhea.descender.float32
else:
typeface.svgFont.descent
proc lineGap*(typeface: Typeface): float32 {.inline.} =
proc lineGap*(typeface: Typeface): float32 {.raises: [].} =
## The font line gap value in font units.
if typeface.opentype != nil:
result = typeface.opentype.hhea.lineGap.float32
proc lineHeight*(typeface: Typeface): float32 {.inline.} =
proc lineHeight*(typeface: Typeface): float32 {.inline, raises: [].} =
## The default line height in font units.
typeface.ascent - typeface.descent + typeface.lineGap
proc underlinePosition(typeface: Typeface): float32 {.inline.} =
proc underlinePosition(typeface: Typeface): float32 {.raises: [].} =
if typeface.opentype != nil:
result = typeface.opentype.post.underlinePosition.float32
proc underlineThickness(typeface: Typeface): float32 {.inline.} =
proc underlineThickness(typeface: Typeface): float32 {.raises: [].} =
if typeface.opentype != nil:
result = typeface.opentype.post.underlineThickness.float32
proc strikeoutPosition(typeface: Typeface): float32 {.inline.} =
proc strikeoutPosition(typeface: Typeface): float32 {.raises: [].} =
if typeface.opentype != nil:
result = typeface.opentype.os2.yStrikeoutPosition.float32
proc strikeoutThickness(typeface: Typeface): float32 {.inline.} =
proc strikeoutThickness(typeface: Typeface): float32 {.raises: [].} =
if typeface.opentype != nil:
result = typeface.opentype.os2.yStrikeoutSize.float32
proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
proc getGlyphPath*(
typeface: Typeface, rune: Rune
): Path {.inline, raises: [PixieError].} =
## The glyph path for the rune.
result = newPath()
if rune.uint32 > SP.uint32: # Empty paths for control runes (not tofu)
@ -101,7 +103,7 @@ proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
else:
result.addPath(typeface.svgFont.getGlyphPath(rune))
proc getAdvance*(typeface: Typeface, rune: Rune): float32 {.inline.} =
proc getAdvance*(typeface: Typeface, rune: Rune): float32 {.inline, raises: [].} =
## The advance for the rune in pixels.
if typeface.opentype != nil:
typeface.opentype.getAdvance(rune)
@ -110,46 +112,47 @@ proc getAdvance*(typeface: Typeface, rune: Rune): float32 {.inline.} =
proc getKerningAdjustment*(
typeface: Typeface, left, right: Rune
): float32 {.inline.} =
): float32 {.inline, raises: [].} =
## The kerning adjustment for the rune pair, in pixels.
if typeface.opentype != nil:
typeface.opentype.getKerningAdjustment(left, right)
else:
typeface.svgfont.getKerningAdjustment(left, right)
proc scale*(font: Font): float32 {.inline.} =
proc scale*(font: Font): float32 {.inline, raises: [].} =
## The scale factor to transform font units into pixels.
if font.typeface.opentype != nil:
font.size / font.typeface.opentype.head.unitsPerEm.float32
else:
font.size / font.typeface.svgFont.unitsPerEm
proc defaultLineHeight*(font: Font): float32 {.inline.} =
proc defaultLineHeight*(font: Font): float32 {.inline, raises: [].} =
## The default line height in pixels for the current font size.
let fontUnits =
font.typeface.ascent - font.typeface.descent + font.typeface.lineGap
round(fontUnits * font.scale)
proc paint*(font: Font): var Paint =
proc paint*(font: Font): var Paint {.inline, raises: [].} =
font.paints[0]
proc `paint=`*(font: Font, paint: Paint) =
proc `paint=`*(font: Font, paint: Paint) {.inline, raises: [].} =
font.paints = @[paint]
proc newFont*(typeface: Typeface): Font =
proc newFont*(typeface: Typeface): Font {.raises: [].} =
result = Font()
result.typeface = typeface
result.size = 12
result.lineHeight = AutoLineHeight
result.paint = rgbx(0, 0, 0, 255)
result.paint = newPaint(pkSolid)
result.paint.color = color(0, 0, 0, 1)
proc newSpan*(text: string, font: Font): Span =
proc newSpan*(text: string, font: Font): Span {.raises: [].} =
## Creates a span, associating a font with the text.
result = Span()
result.text = text
result.font = font
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) {.raises: [].} =
case textCase:
of tcNormal:
discard
@ -166,7 +169,7 @@ proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
rune = rune.toUpper()
prevRune = rune
proc canWrap(rune: Rune): bool {.inline.} =
proc canWrap(rune: Rune): bool {.inline, raises: [].} =
rune == Rune(32) or rune.isWhiteSpace()
proc typeset*(
@ -175,7 +178,7 @@ proc typeset*(
hAlign = haLeft,
vAlign = vaTop,
wrap = true
): Arrangement =
): Arrangement {.raises: [].} =
## Lays out the character glyphs and returns the arrangement.
## Optional parameters:
## bounds: width determines wrapping and hAlign, height for vAlign
@ -396,7 +399,7 @@ proc typeset*(
hAlign = haLeft,
vAlign = vaTop,
wrap = true
): Arrangement {.inline.} =
): Arrangement {.inline, raises: [].} =
## Lays out the character glyphs and returns the arrangement.
## Optional parameters:
## bounds: width determines wrapping and hAlign, height for vAlign
@ -405,7 +408,7 @@ proc typeset*(
## wrap: enable/disable text wrapping
typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap)
proc computeBounds*(arrangement: Arrangement): Vec2 =
proc computeBounds*(arrangement: Arrangement): Vec2 {.raises: [].} =
## Computes the width and height of the arrangement in pixels.
if arrangement.runes.len > 0:
for i in 0 ..< arrangement.runes.len:
@ -415,22 +418,22 @@ proc computeBounds*(arrangement: Arrangement): Vec2 =
let finalRect = arrangement.selectionRects[^1]
result.y = finalRect.y + finalRect.h
proc computeBounds*(font: Font, text: string): Vec2 {.inline.} =
proc computeBounds*(font: Font, text: string): Vec2 {.inline, raises: [].} =
## Computes the width and height of the text in pixels.
font.typeset(text).computeBounds()
proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
proc computeBounds*(spans: seq[Span]): Vec2 {.inline, raises: [].} =
## Computes the width and height of the spans in pixels.
typeset(spans).computeBounds()
proc parseOtf*(buf: string): Typeface =
proc parseOtf*(buf: string): Typeface {.raises: [PixieError].} =
result = Typeface()
result.opentype = parseOpenType(buf)
proc parseTtf*(buf: string): Typeface =
proc parseTtf*(buf: string): Typeface {.raises: [PixieError].} =
parseOtf(buf)
proc parseSvgFont*(buf: string): Typeface =
proc parseSvgFont*(buf: string): Typeface {.raises: [PixieError].} =
result = Typeface()
result.svgFont = svgfont.parseSvgFont(buf)
@ -444,7 +447,7 @@ proc textUber(
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[],
stroke: static[bool] = false
) =
) {.raises: [PixieError].} =
var line: int
for spanIndex, (start, stop) in arrangement.spans:
let
@ -519,7 +522,7 @@ proc fillText*(
target: Image | Mask,
arrangement: Arrangement,
transform = mat3()
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Fills the text arrangement.
textUber(
target,
@ -535,7 +538,7 @@ proc fillText*(
bounds = vec2(0, 0),
hAlign = haLeft,
vAlign = vaTop
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Typesets and fills the text. Optional parameters:
## transform: translation or matrix to apply
## bounds: width determines wrapping and hAlign, height for vAlign
@ -552,7 +555,7 @@ proc strokeText*(
lineJoin = ljMiter,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Strokes the text arrangement.
textUber(
target,
@ -579,7 +582,7 @@ proc strokeText*(
lineJoin = ljMiter,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Typesets and strokes the text. Optional parameters:
## transform: translation or matrix to apply
## bounds: width determines wrapping and hAlign, height for vAlign
@ -598,20 +601,24 @@ proc strokeText*(
dashes
)
proc readTypeface*(filePath: string): Typeface =
proc readTypeface*(filePath: string): Typeface {.raises: [PixieError].} =
## Loads a typeface from a file.
result =
case splitFile(filePath).ext.toLowerAscii():
of ".ttf":
parseTtf(readFile(filePath))
of ".otf":
parseOtf(readFile(filePath))
of ".svg":
parseSvgFont(readFile(filePath))
else:
raise newException(PixieError, "Unsupported font format")
try:
result =
case splitFile(filePath).ext.toLowerAscii():
of ".ttf":
parseTtf(readFile(filePath))
of ".otf":
parseOtf(readFile(filePath))
of ".svg":
parseSvgFont(readFile(filePath))
else:
raise newException(PixieError, "Unsupported font format")
except IOError as e:
raise newException(PixieError, e.msg, e)
result.filePath = filePath
proc readFont*(filePath: string): Font =
proc readFont*(filePath: string): Font {.raises: [PixieError].} =
## Loads a font from a file.
newFont(readTypeface(filePath))

View file

@ -14,7 +14,7 @@ type
when defined(release):
{.push checks: off.}
proc newImage*(width, height: int): Image =
proc newImage*(width, height: int): Image {.raises: [PixieError].} =
## Creates a new image with the parameter dimensions.
if width <= 0 or height <= 0:
raise newException(PixieError, "Image width and height must be > 0")
@ -24,7 +24,7 @@ proc newImage*(width, height: int): Image =
result.height = height
result.data = newSeq[ColorRGBX](width * height)
proc newImage*(mask: Mask): Image =
proc newImage*(mask: Mask): Image {.raises: [PixieError].} =
result = newImage(mask.width, mask.height)
var i: int
when defined(amd64) and not defined(pixieNoSimd):
@ -39,59 +39,63 @@ proc newImage*(mask: Mask): Image =
let v = mask.data[i]
result.data[i] = rgbx(v, v, v, v)
proc wh*(image: Image): Vec2 {.inline.} =
proc wh*(image: Image): Vec2 {.inline, raises: [].} =
## Return with and height as a size vector.
vec2(image.width.float32, image.height.float32)
proc copy*(image: Image): Image =
proc copy*(image: Image): Image {.raises: [PixieError].} =
## Copies the image data into a new image.
result = newImage(image.width, image.height)
result.data = image.data
proc `$`*(image: Image): string =
proc `$`*(image: Image): string {.raises: [].} =
## Prints the image size.
"<Image " & $image.width & "x" & $image.height & ">"
proc inside*(image: Image, x, y: int): bool {.inline.} =
proc inside*(image: Image, x, y: int): bool {.inline, raises: [].} =
## Returns true if (x, y) is inside the image.
x >= 0 and x < image.width and y >= 0 and y < image.height
proc dataIndex*(image: Image, x, y: int): int {.inline.} =
proc dataIndex*(image: Image, x, y: int): int {.inline, raises: [].} =
image.width * y + x
proc getRgbaUnsafe*(image: Image, x, y: int): ColorRGBX {.inline.} =
proc getRgbaUnsafe*(image: Image, x, y: int): ColorRGBX {.inline, raises: [].} =
## Gets a color from (x, y) coordinates.
## * No bounds checking *
## Make sure that x, y are in bounds.
## Failure in the assumptions will case unsafe memory reads.
result = image.data[image.width * y + x]
proc `[]`*(image: Image, x, y: int): ColorRGBX {.inline.} =
proc `[]`*(image: Image, x, y: int): ColorRGBX {.inline, raises: [].} =
## Gets a pixel at (x, y) or returns transparent black if outside of bounds.
if image.inside(x, y):
return image.getRgbaUnsafe(x, y)
proc getColor*(image: Image, x, y: int): Color =
proc getColor*(image: Image, x, y: int): Color {.inline, raises: [].} =
## Gets a color at (x, y) or returns transparent black if outside of bounds.
image[x, y].color()
proc setRgbaUnsafe*(image: Image, x, y: int, color: SomeColor) {.inline.} =
proc setRgbaUnsafe*(
image: Image, x, y: int, color: SomeColor
) {.inline, raises: [].} =
## Sets a color from (x, y) coordinates.
## * No bounds checking *
## Make sure that x, y are in bounds.
## Failure in the assumptions will case unsafe memory writes.
image.data[image.dataIndex(x, y)] = color.asRgbx()
proc `[]=`*(image: Image, x, y: int, color: SomeColor) {.inline.} =
proc `[]=`*(image: Image, x, y: int, color: SomeColor) {.inline, raises: [].} =
## Sets a pixel at (x, y) or does nothing if outside of bounds.
if image.inside(x, y):
image.setRgbaUnsafe(x, y, color.asRgbx())
proc setColor*(image: Image, x, y: int, color: Color) =
proc setColor*(image: Image, x, y: int, color: Color) {.inline, raises: [].} =
## Sets a color at (x, y) or does nothing if outside of bounds.
image[x, y] = color.rgbx()
proc fillUnsafe*(data: var seq[ColorRGBX], color: SomeColor, start, len: int) =
proc fillUnsafe*(
data: var seq[ColorRGBX], color: SomeColor, start, len: int
) {.raises: [].} =
## Fills the image data with the parameter color starting at index start and
## continuing for len indices.
@ -122,11 +126,11 @@ proc fillUnsafe*(data: var seq[ColorRGBX], color: SomeColor, start, len: int) =
for j in i ..< start + len:
data[j] = rgbx
proc fill*(image: Image, color: SomeColor) {.inline.} =
proc fill*(image: Image, color: SomeColor) {.inline, raises: [].} =
## Fills the image with the parameter color.
fillUnsafe(image.data, color, 0, image.data.len)
proc isOneColor*(image: Image): bool =
proc isOneColor*(image: Image): bool {.raises: [].} =
## Checks if the entire image is the same color.
result = true
@ -149,7 +153,7 @@ proc isOneColor*(image: Image): bool =
if image.data[j] != color:
return false
proc isTransparent*(image: Image): bool =
proc isTransparent*(image: Image): bool {.raises: [].} =
## Checks if this image is fully transparent or not.
result = true
@ -174,7 +178,7 @@ proc isTransparent*(image: Image): bool =
if image.data[j].a != 0:
return false
proc flipHorizontal*(image: Image) =
proc flipHorizontal*(image: Image) {.raises: [].} =
## Flips the image around the Y axis.
let w = image.width div 2
for y in 0 ..< image.height:
@ -185,7 +189,7 @@ proc flipHorizontal*(image: Image) =
image.setRgbaUnsafe(image.width - x - 1, y, rgba1)
image.setRgbaUnsafe(x, y, rgba2)
proc flipVertical*(image: Image) =
proc flipVertical*(image: Image) {.raises: [].} =
## Flips the image around the X axis.
let h = image.height div 2
for y in 0 ..< h:
@ -196,7 +200,7 @@ proc flipVertical*(image: Image) =
image.setRgbaUnsafe(x, image.height - y - 1, rgba1)
image.setRgbaUnsafe(x, y, rgba2)
proc subImage*(image: Image, x, y, w, h: int): Image =
proc subImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError].} =
## Gets a sub image from this image.
if x < 0 or x + w > image.width:
@ -218,7 +222,7 @@ proc subImage*(image: Image, x, y, w, h: int): Image =
w * 4
)
proc diff*(master, image: Image): (float32, Image) =
proc diff*(master, image: Image): (float32, Image) {.raises: [PixieError].} =
## Compares the parameters and returns a score and image of the difference.
let
w = max(master.width, image.width)
@ -248,7 +252,7 @@ proc diff*(master, image: Image): (float32, Image) =
(100 * diffScore.float32 / diffTotal.float32, diffImage)
proc minifyBy2*(image: Image, power = 1): Image =
proc minifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
## Scales the image down by an integer scale.
if power < 0:
raise newException(PixieError, "Cannot minifyBy2 with negative power")
@ -323,7 +327,7 @@ proc minifyBy2*(image: Image, power = 1): Image =
# Set src as this result for if we do another power
src = result
proc magnifyBy2*(image: Image, power = 1): Image =
proc magnifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
## Scales image up by 2 ^ power.
if power < 0:
raise newException(PixieError, "Cannot magnifyBy2 with negative power")
@ -339,7 +343,7 @@ proc magnifyBy2*(image: Image, power = 1): Image =
for i in 0 ..< scale:
result.data[idx + i] = rgba
proc applyOpacity*(target: Image | Mask, opacity: float32) =
proc applyOpacity*(target: Image | Mask, opacity: float32) {.raises: [].} =
## Multiplies alpha of the image by opacity.
let opacity = round(255 * opacity).uint16
@ -405,7 +409,7 @@ proc applyOpacity*(target: Image | Mask, opacity: float32) =
for j in i ..< target.data.len:
target.data[j] = ((target.data[j] * opacity) div 255).uint8
proc invert*(target: Image | Mask) =
proc invert*(target: Image | Mask) {.raises: [].} =
## Inverts all of the colors and alpha.
var i: int
when defined(amd64) and not defined(pixieNoSimd):
@ -447,7 +451,7 @@ proc invert*(target: Image | Mask) =
proc blur*(
image: Image, radius: float32, outOfBounds: SomeColor = ColorRGBX()
) =
) {.raises: [PixieError].} =
## Applies Gaussian blur to the image given a radius.
let radius = round(radius).int
if radius == 0:
@ -512,7 +516,7 @@ proc blur*(
image.setRgbaUnsafe(x, y, rgbx(values))
proc newMask*(image: Image): Mask =
proc newMask*(image: Image): Mask {.raises: [PixieError].} =
## Returns a new mask using the alpha values of the parameter image.
result = newMask(image.width, image.height)
@ -544,7 +548,9 @@ proc newMask*(image: Image): Mask =
for j in i ..< image.data.len:
result.data[j] = image.data[j].a
proc getRgbaSmooth*(image: Image, x, y: float32, wrapped = false): ColorRGBX =
proc getRgbaSmooth*(
image: Image, x, y: float32, wrapped = false
): ColorRGBX {.raises: [].} =
## Gets a interpolated color with float point coordinates.
## Pixes outside the image are transparent.
let
@ -582,7 +588,7 @@ proc getRgbaSmooth*(image: Image, x, y: float32, wrapped = false): ColorRGBX =
proc drawCorrect(
a, b: Image | Mask, mat = mat3(), tiled = false, blendMode = bmNormal
) =
) {.raises: [PixieError].} =
## Draws one image onto another using matrix with color blending.
when type(a) is Image:
@ -634,7 +640,9 @@ proc drawCorrect(
let sample = b.getValueSmooth(xFloat, yFloat)
a.setValueUnsafe(x, y, masker(backdrop, sample))
proc drawUber(a, b: Image | Mask, mat = mat3(), blendMode = bmNormal) =
proc drawUber(
a, b: Image | Mask, mat = mat3(), blendMode = bmNormal
) {.raises: [PixieError].} =
let
corners = [
mat * vec2(0, 0),
@ -840,7 +848,7 @@ proc drawUber(a, b: Image | Mask, mat = mat3(), blendMode = bmNormal) =
proc draw*(
a, b: Image, transform = mat3(), blendMode = bmNormal
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Draws one image onto another using matrix with color blending.
when type(transform) is Vec2:
a.drawUber(b, translate(transform), blendMode)
@ -849,7 +857,7 @@ proc draw*(
proc draw*(
a, b: Mask, transform = mat3(), blendMode = bmMask
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Draws a mask onto a mask using a matrix with color blending.
when type(transform) is Vec2:
a.drawUber(b, translate(transform), blendMode)
@ -858,7 +866,7 @@ proc draw*(
proc draw*(
image: Image, mask: Mask, transform = mat3(), blendMode = bmMask
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Draws a mask onto an image using a matrix with color blending.
when type(transform) is Vec2:
image.drawUber(mask, translate(transform), blendMode)
@ -867,17 +875,19 @@ proc draw*(
proc draw*(
mask: Mask, image: Image, transform = mat3(), blendMode = bmMask
) {.inline.} =
) {.inline, raises: [PixieError].} =
## Draws a image onto a mask using a matrix with color blending.
when type(transform) is Vec2:
mask.drawUber(image, translate(transform), blendMode)
else:
mask.drawUber(image, transform, blendMode)
proc drawTiled*(dest, src: Image, mat: Mat3, blendMode = bmNormal) =
dest.drawCorrect(src, mat, true, blendMode)
proc drawTiled*(
dst, src: Image, mat: Mat3, blendMode = bmNormal
) {.raises: [PixieError]} =
dst.drawCorrect(src, mat, true, blendMode)
proc resize*(srcImage: Image, width, height: int): Image =
proc resize*(srcImage: Image, width, height: int): Image {.raises: [PixieError]} =
## Resize an image to a given height and width.
if width == srcImage.width and height == srcImage.height:
result = srcImage.copy()
@ -894,7 +904,7 @@ proc resize*(srcImage: Image, width, height: int): Image =
proc shadow*(
image: Image, offset: Vec2, spread, blur: float32, color: SomeColor
): Image =
): Image {.raises: [PixieError]} =
## Create a shadow of the image with the offset, spread and blur.
let
mask = image.newMask()
@ -906,7 +916,7 @@ proc shadow*(
result.fill(color)
result.draw(shifted, blendMode = bmMask)
proc superImage*(image: Image, x, y, w, h: int): Image =
proc superImage*(image: Image, x, y, w, h: int): Image {.raises: [PixieError]} =
## Either cuts a sub image or returns a super image with padded transparency.
if x >= 0 and x + w <= image.width and y >= 0 and y + h <= image.height:
result = image.subImage(x, y, w, h)

View file

@ -12,7 +12,7 @@ type
when defined(release):
{.push checks: off.}
proc newMask*(width, height: int): Mask =
proc newMask*(width, height: int): Mask {.raises: [PixieError].} =
## Creates a new mask with the parameter dimensions.
if width <= 0 or height <= 0:
raise newException(PixieError, "Mask width and height must be > 0")
@ -22,59 +22,59 @@ proc newMask*(width, height: int): Mask =
result.height = height
result.data = newSeq[uint8](width * height)
proc wh*(mask: Mask): Vec2 {.inline.} =
proc wh*(mask: Mask): Vec2 {.inline, raises: [].} =
## Return with and height as a size vector.
vec2(mask.width.float32, mask.height.float32)
proc copy*(mask: Mask): Mask =
proc copy*(mask: Mask): Mask {.raises: [PixieError].} =
## Copies the image data into a new image.
result = newMask(mask.width, mask.height)
result.data = mask.data
proc `$`*(mask: Mask): string =
proc `$`*(mask: Mask): string {.raises: [].} =
## Prints the mask size.
"<Mask " & $mask.width & "x" & $mask.height & ">"
proc inside*(mask: Mask, x, y: int): bool {.inline.} =
proc inside*(mask: Mask, x, y: int): bool {.inline, raises: [].} =
## Returns true if (x, y) is inside the mask.
x >= 0 and x < mask.width and y >= 0 and y < mask.height
proc dataIndex*(mask: Mask, x, y: int): int {.inline.} =
proc dataIndex*(mask: Mask, x, y: int): int {.inline, raises: [].} =
mask.width * y + x
proc getValueUnsafe*(mask: Mask, x, y: int): uint8 {.inline.} =
proc getValueUnsafe*(mask: Mask, x, y: int): uint8 {.inline, raises: [].} =
## Gets a value from (x, y) coordinates.
## * No bounds checking *
## Make sure that x, y are in bounds.
## Failure in the assumptions will case unsafe memory reads.
result = mask.data[mask.width * y + x]
proc `[]`*(mask: Mask, x, y: int): uint8 {.inline.} =
proc `[]`*(mask: Mask, x, y: int): uint8 {.inline, raises: [].} =
## Gets a value at (x, y) or returns transparent black if outside of bounds.
if mask.inside(x, y):
return mask.getValueUnsafe(x, y)
proc getValue*(mask: Mask, x, y: int): uint8 {.inline.} =
proc getValue*(mask: Mask, x, y: int): uint8 {.inline, raises: [].} =
## Gets a value at (x, y) or returns transparent black if outside of bounds.
mask[x, y]
proc setValueUnsafe*(mask: Mask, x, y: int, value: uint8) {.inline.} =
proc setValueUnsafe*(mask: Mask, x, y: int, value: uint8) {.inline, raises: [].} =
## Sets a value from (x, y) coordinates.
## * No bounds checking *
## Make sure that x, y are in bounds.
## Failure in the assumptions will case unsafe memory writes.
mask.data[mask.dataIndex(x, y)] = value
proc `[]=`*(mask: Mask, x, y: int, value: uint8) {.inline.} =
proc `[]=`*(mask: Mask, x, y: int, value: uint8) {.inline, raises: [].} =
## Sets a value at (x, y) or does nothing if outside of bounds.
if mask.inside(x, y):
mask.setValueUnsafe(x, y, value)
proc setValue*(mask: Mask, x, y: int, value: uint8) {.inline.} =
proc setValue*(mask: Mask, x, y: int, value: uint8) {.inline, raises: [].} =
## Sets a value at (x, y) or does nothing if outside of bounds.
mask[x, y] = value
proc minifyBy2*(mask: Mask, power = 1): Mask =
proc minifyBy2*(mask: Mask, power = 1): Mask {.raises: [PixieError].} =
## Scales the mask down by an integer scale.
if power < 0:
raise newException(PixieError, "Cannot minifyBy2 with negative power")
@ -157,16 +157,18 @@ proc minifyBy2*(mask: Mask, power = 1): Mask =
# Set src as this result for if we do another power
src = result
proc fillUnsafe*(data: var seq[uint8], value: uint8, start, len: int) =
proc fillUnsafe*(
data: var seq[uint8], value: uint8, start, len: int
) {.raises: [].} =
## Fills the mask data with the parameter value starting at index start and
## continuing for len indices.
nimSetMem(data[start].addr, value.cint, len)
proc fill*(mask: Mask, value: uint8) {.inline.} =
proc fill*(mask: Mask, value: uint8) {.inline, raises: [].} =
## Fills the mask with the parameter value.
fillUnsafe(mask.data, value, 0, mask.data.len)
proc getValueSmooth*(mask: Mask, x, y: float32): uint8 =
proc getValueSmooth*(mask: Mask, x, y: float32): uint8 {.raises: [].} =
## Gets a interpolated value with float point coordinates.
let
x0 = x.int
@ -194,7 +196,7 @@ proc getValueSmooth*(mask: Mask, x, y: float32): uint8 =
else:
topMix
proc spread*(mask: Mask, spread: float32) =
proc spread*(mask: Mask, spread: float32) {.raises: [PixieError].} =
## Grows the mask by spread.
let spread = round(spread).int
if spread == 0:
@ -227,7 +229,7 @@ proc spread*(mask: Mask, spread: float32) =
break
mask.setValueUnsafe(x, y, maxValue)
proc ceil*(mask: Mask) =
proc ceil*(mask: Mask) {.raises: [].} =
## A value of 0 stays 0. Anything else turns into 255.
var i: int
when defined(amd64) and not defined(pixieNoSimd):
@ -245,7 +247,7 @@ proc ceil*(mask: Mask) =
if mask.data[j] != 0:
mask.data[j] = 255
proc blur*(mask: Mask, radius: float32, outOfBounds: uint8 = 0) =
proc blur*(mask: Mask, radius: float32, outOfBounds: uint8 = 0) {.raises: [PixieError].} =
## Applies Gaussian blur to the image given a radius.
let radius = round(radius).int
if radius == 0:

View file

@ -15,7 +15,7 @@ type
blendMode*: BlendMode ## Blend mode.
opacity*: float32
# pkSolid
color*: Color ## Color to fill with.
color*: Color ## Color to fill with.
# pkImage, pkImageTiled:
image*: Image ## Image to fill with.
imageMat*: Mat3 ## Matrix of the filled image.
@ -25,16 +25,16 @@ type
ColorStop* = object
## Color stop on a gradient curve.
color*: Color ## Color of the stop.
color*: Color ## Color of the stop.
position*: float32 ## Gradient stop position 0..1.
SomePaint* = string | Paint | SomeColor
proc newPaint*(kind: PaintKind): Paint =
proc newPaint*(kind: PaintKind): Paint {.raises: [].} =
## Create a new Paint.
result = Paint(kind: kind, opacity: 1, imageMat: mat3())
proc newPaint*(paint: Paint): Paint =
proc newPaint*(paint: Paint): Paint {.raises: [].} =
## Create a new Paint with the same properties.
result = newPaint(paint.kind)
result.blendMode = paint.blendMode
@ -45,11 +45,16 @@ proc newPaint*(paint: Paint): Paint =
result.gradientHandlePositions = paint.gradientHandlePositions
result.gradientStops = paint.gradientStops
converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
converter parseSomePaint*(
paint: SomePaint
): Paint {.inline, raises: [PixieError].} =
## Given SomePaint, parse it in different ways.
when type(paint) is string:
result = newPaint(pkSolid)
result.color = parseHtmlColor(paint)
try:
result.color = parseHtmlColor(paint)
except:
raise newException(PixieError, "Unable to parse color " & paint)
elif type(paint) is SomeColor:
result = newPaint(pkSolid)
when type(paint) is Color:
@ -59,7 +64,7 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
elif type(paint) is Paint:
paint
proc toLineSpace(at, to, point: Vec2): float32 {.inline.} =
proc toLineSpace(at, to, point: Vec2): float32 {.inline, raises: [].} =
## Convert position on to where it would fall on a line between at and to.
let
d = to - at
@ -68,7 +73,7 @@ proc toLineSpace(at, to, point: Vec2): float32 {.inline.} =
proc gradientPut(
image: Image, paint: Paint, x, y: int, t: float32, stops: seq[ColorStop]
) =
) {.raises: [].} =
## Put an gradient color based on `t` - where are we related to a line.
var index = -1
for i, stop in stops:
@ -95,7 +100,7 @@ proc gradientPut(
color.a *= paint.opacity
image.setRgbaUnsafe(x, y, color.rgbx())
proc fillGradientLinear(image: Image, paint: Paint) =
proc fillGradientLinear(image: Image, paint: Paint) {.raises: [PixieError].} =
## Fills a linear gradient.
if paint.gradientHandlePositions.len != 2:
@ -117,7 +122,7 @@ proc fillGradientLinear(image: Image, paint: Paint) =
t = toLineSpace(at, to, xy)
image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientRadial(image: Image, paint: Paint) =
proc fillGradientRadial(image: Image, paint: Paint) {.raises: [PixieError].} =
## Fills a radial gradient.
if paint.gradientHandlePositions.len != 3:
@ -148,7 +153,7 @@ proc fillGradientRadial(image: Image, paint: Paint) =
t = (mat * xy).length()
image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientAngular(image: Image, paint: Paint) =
proc fillGradientAngular(image: Image, paint: Paint) {.raises: [PixieError].} =
## Fills an angular gradient.
if paint.gradientHandlePositions.len != 3:
@ -173,7 +178,7 @@ proc fillGradientAngular(image: Image, paint: Paint) =
t = (angle + gradientAngle + PI / 2).fixAngle() / 2 / PI + 0.5
image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradient*(image: Image, paint: Paint) =
proc fillGradient*(image: Image, paint: Paint) {.raises: [PixieError].} =
## Fills with the Paint gradient.
case paint.kind:

View file

@ -46,23 +46,23 @@ const
when defined(release):
{.push checks: off.}
proc newPath*(): Path =
proc newPath*(): Path {.raises: [].} =
## Create a new Path.
Path()
proc pixelScale(transform: Mat3): float32 =
proc pixelScale(transform: Mat3): float32 {.raises: [].} =
## What is the largest scale factor of this transform?
max(
vec2(transform[0, 0], transform[0, 1]).length,
vec2(transform[1, 0], transform[1, 1]).length
)
proc isRelative(kind: PathCommandKind): bool =
proc isRelative(kind: PathCommandKind): bool {.inline, raises: [].} =
kind in {
RMove, RLine, TQuad, RTQuad, RHLine, RVLine, RCubic, RSCubic, RQuad, RArc
}
proc parameterCount(kind: PathCommandKind): int =
proc parameterCount(kind: PathCommandKind): int {.raises: [].} =
## Returns number of parameters a path command has.
case kind:
of Close: 0
@ -72,7 +72,7 @@ proc parameterCount(kind: PathCommandKind): int =
of SCubic, RSCubic, Quad, RQuad: 4
of Arc, RArc: 7
proc `$`*(path: Path): string =
proc `$`*(path: Path): string {.raises: [].} =
## Turn path int into a string.
for i, command in path.commands:
case command.kind
@ -103,7 +103,7 @@ proc `$`*(path: Path): string =
if i != path.commands.len - 1 or j != command.numbers.len - 1:
result.add " "
proc parsePath*(path: string): Path =
proc parsePath*(path: string): Path {.raises: [PixieError].} =
## Converts a SVG style path string into seq of commands.
result = newPath()
@ -249,7 +249,7 @@ proc parsePath*(path: string): Path =
finishCommand(result)
proc transform*(path: Path, mat: Mat3) =
proc transform*(path: Path, mat: Mat3) {.raises: [].} =
## Apply a matrix transform to a path.
if mat == mat3():
return
@ -314,39 +314,39 @@ proc transform*(path: Path, mat: Mat3) =
command.numbers[5] = to.x
command.numbers[6] = to.y
proc addPath*(path: Path, other: Path) =
proc addPath*(path: Path, other: Path) {.raises: [].} =
## Adds a path to the current path.
path.commands.add(other.commands)
proc closePath*(path: Path) =
proc closePath*(path: Path) {.raises: [].} =
## Attempts to add a straight line from the current point to the start of
## the current sub-path. If the shape has already been closed or has only
## one point, this function does nothing.
path.commands.add(PathCommand(kind: Close))
path.at = path.start
proc moveTo*(path: Path, x, y: float32) =
proc moveTo*(path: Path, x, y: float32) {.raises: [].} =
## Begins a new sub-path at the point (x, y).
path.commands.add(PathCommand(kind: Move, numbers: @[x, y]))
path.start = vec2(x, y)
path.at = path.start
proc moveTo*(path: Path, v: Vec2) {.inline.} =
proc moveTo*(path: Path, v: Vec2) {.inline, raises: [].} =
## Begins a new sub-path at the point (x, y).
path.moveTo(v.x, v.y)
proc lineTo*(path: Path, x, y: float32) =
proc lineTo*(path: Path, x, y: float32) {.raises: [].} =
## Adds a straight line to the current sub-path by connecting the sub-path's
## last point to the specified (x, y) coordinates.
path.commands.add(PathCommand(kind: Line, numbers: @[x, y]))
path.at = vec2(x, y)
proc lineTo*(path: Path, v: Vec2) {.inline.} =
proc lineTo*(path: Path, v: Vec2) {.inline, raises: [].} =
## Adds a straight line to the current sub-path by connecting the sub-path's
## last point to the specified (x, y) coordinates.
path.lineTo(v.x, v.y)
proc bezierCurveTo*(path: Path, x1, y1, x2, y2, x3, y3: float32) =
proc bezierCurveTo*(path: Path, x1, y1, x2, y2, x3, y3: float32) {.raises: [].} =
## Adds a cubic Bézier curve to the current sub-path. It requires three
## points: the first two are control points and the third one is the end
## point. The starting point is the latest point in the current path,
@ -357,14 +357,14 @@ proc bezierCurveTo*(path: Path, x1, y1, x2, y2, x3, y3: float32) =
))
path.at = vec2(x3, y3)
proc bezierCurveTo*(path: Path, ctrl1, ctrl2, to: Vec2) {.inline.} =
proc bezierCurveTo*(path: Path, ctrl1, ctrl2, to: Vec2) {.inline, raises: [].} =
## Adds a cubic Bézier curve to the current sub-path. It requires three
## points: the first two are control points and the third one is the end
## point. The starting point is the latest point in the current path,
## which can be changed using moveTo() before creating the Bézier curve.
path.bezierCurveTo(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y)
proc quadraticCurveTo*(path: Path, x1, y1, x2, y2: float32) =
proc quadraticCurveTo*(path: Path, x1, y1, x2, y2: float32) {.raises: [].} =
## Adds a quadratic Bézier curve to the current sub-path. It requires two
## points: the first one is a control point and the second one is the end
## point. The starting point is the latest point in the current path,
@ -376,7 +376,7 @@ proc quadraticCurveTo*(path: Path, x1, y1, x2, y2: float32) =
))
path.at = vec2(x2, y2)
proc quadraticCurveTo*(path: Path, ctrl, to: Vec2) {.inline.} =
proc quadraticCurveTo*(path: Path, ctrl, to: Vec2) {.inline, raises: [].} =
## Adds a quadratic Bézier curve to the current sub-path. It requires two
## points: the first one is a control point and the second one is the end
## point. The starting point is the latest point in the current path,
@ -390,7 +390,7 @@ proc ellipticalArcTo*(
xAxisRotation: float32,
largeArcFlag, sweepFlag: bool,
x, y: float32
) =
) {.raises: [].} =
## Adds an elliptical arc to the current sub-path, using the given radius
## ratios, sweep flags, and end position.
path.commands.add(PathCommand(
@ -401,7 +401,9 @@ proc ellipticalArcTo*(
))
path.at = vec2(x, y)
proc arc*(path: Path, x, y, r, a0, a1: float32, ccw: bool) =
proc arc*(
path: Path, x, y, r, a0, a1: float32, ccw: bool
) {.raises: [PixieError].} =
## Adds a circular arc to the current sub-path.
if r == 0: # When radius is zero, do nothing.
return
@ -438,11 +440,13 @@ proc arc*(path: Path, x, y, r, a0, a1: float32, ccw: bool) =
path.at.y = y + r * sin(a1)
path.ellipticalArcTo(r, r, 0, angle >= PI, cw, path.at.x, path.at.y)
proc arc*(path: Path, pos: Vec2, r: float32, a: Vec2, ccw: bool = false) =
proc arc*(
path: Path, pos: Vec2, r: float32, a: Vec2, ccw: bool = false
) {.inline, raises: [PixieError].} =
## Adds a circular arc to the current sub-path.
path.arc(pos.x, pos.y, r, a.x, a.y, ccw)
proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) =
proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) {.raises: [PixieError].} =
## Adds a circular arc using the given control points and radius.
## Commonly used for making rounded corners.
if r < 0: # When radius is negative, error.
@ -483,11 +487,11 @@ proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) =
path.at.y = y1 + t21 * y21
path.ellipticalArcTo(r, r, 0, false, y01 * x20 > x01 * y20, path.at.x, path.at.y)
proc arcTo*(path: Path, a, b: Vec2, r: float32) =
proc arcTo*(path: Path, a, b: Vec2, r: float32) {.inline, raises: [PixieError].} =
## Adds a circular arc using the given control points and radius.
path.arcTo(a.x, a.y, b.x, b.y, r)
proc rect*(path: Path, x, y, w, h: float32, clockwise = true) =
proc rect*(path: Path, x, y, w, h: float32, clockwise = true) {.raises: [].} =
## Adds a rectangle.
## Clockwise param can be used to subtract a rect from a path when using
## even-odd winding rule.
@ -504,7 +508,7 @@ proc rect*(path: Path, x, y, w, h: float32, clockwise = true) =
path.lineTo(x + w, y)
path.closePath()
proc rect*(path: Path, rect: Rect, clockwise = true) {.inline.} =
proc rect*(path: Path, rect: Rect, clockwise = true) {.inline, raises: [].} =
## Adds a rectangle.
## Clockwise param can be used to subtract a rect from a path when using
## even-odd winding rule.
@ -516,7 +520,7 @@ const splineCircleK = 4.0 * (-1.0 + sqrt(2.0)) / 3
proc roundedRect*(
path: Path, x, y, w, h, nw, ne, se, sw: float32, clockwise = true
) =
) {.raises: [].} =
## Adds a rounded rectangle.
## Clockwise param can be used to subtract a rect from a path when using
## even-odd winding rule.
@ -583,13 +587,13 @@ proc roundedRect*(
proc roundedRect*(
path: Path, rect: Rect, nw, ne, se, sw: float32, clockwise = true
) {.inline.} =
) {.inline, raises: [].} =
## Adds a rounded rectangle.
## Clockwise param can be used to subtract a rect from a path when using
## even-odd winding rule.
path.roundedRect(rect.x, rect.y, rect.w, rect.h, nw, ne, se, sw, clockwise)
proc ellipse*(path: Path, cx, cy, rx, ry: float32) =
proc ellipse*(path: Path, cx, cy, rx, ry: float32) {.raises: [].} =
## Adds a ellipse.
let
magicX = splineCircleK * rx
@ -602,19 +606,19 @@ proc ellipse*(path: Path, cx, cy, rx, ry: float32) =
path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy)
path.closePath()
proc ellipse*(path: Path, center: Vec2, rx, ry: float32) {.inline.} =
proc ellipse*(path: Path, center: Vec2, rx, ry: float32) {.inline, raises: [].} =
## Adds a ellipse.
path.ellipse(center.x, center.y, rx, ry)
proc circle*(path: Path, cx, cy, r: float32) {.inline.} =
proc circle*(path: Path, cx, cy, r: float32) {.inline, raises: [].} =
## Adds a circle.
path.ellipse(cx, cy, r, r)
proc circle*(path: Path, circle: Circle) {.inline.} =
proc circle*(path: Path, circle: Circle) {.inline, raises: [].} =
## Adds a circle.
path.ellipse(circle.pos.x, circle.pos.y, circle.radius, circle.radius)
proc polygon*(path: Path, x, y, size: float32, sides: int) =
proc polygon*(path: Path, x, y, size: float32, sides: int) {.raises: [].} =
## Adds an n-sided regular polygon at (x, y) with the parameter size.
path.moveTo(x + size * cos(0.0), y + size * sin(0.0))
for side in 0 .. sides:
@ -623,13 +627,15 @@ proc polygon*(path: Path, x, y, size: float32, sides: int) =
y + size * sin(side.float32 * 2.0 * PI / sides.float32)
)
proc polygon*(path: Path, pos: Vec2, size: float32, sides: int) {.inline.} =
proc polygon*(
path: Path, pos: Vec2, size: float32, sides: int
) {.inline, raises: [].} =
## Adds a n-sided regular polygon at (x, y) with the parameter size.
path.polygon(pos.x, pos.y, size, sides)
proc commandsToShapes(
path: Path, closeSubpaths = false, pixelScale: float32 = 1.0
): seq[seq[Vec2]] =
): seq[seq[Vec2]] {.raises: [PixieError].} =
## Converts SVG-like commands to sequences of vectors.
var
start, at: Vec2
@ -991,7 +997,9 @@ proc commandsToShapes(
shape.addSegment(at, start)
result.add(shape)
proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] =
proc shapesToSegments(
shapes: seq[seq[Vec2]]
): seq[(Segment, int16)] {.raises: [].} =
## Converts the shapes into a set of filtered segments with winding value.
for shape in shapes:
for segment in shape.segments:
@ -1006,7 +1014,7 @@ proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] =
result.add((segment, winding))
proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool =
proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool {.raises: [].} =
## Returns true if the fill requires antialiasing.
template hasFractional(v: float32): bool =
@ -1021,13 +1029,13 @@ proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool =
# AA is required if all segments are not vertical or have fractional > 0
return true
proc transform(shapes: var seq[seq[Vec2]], transform: Mat3) =
proc transform(shapes: var seq[seq[Vec2]], transform: Mat3) {.raises: [].} =
if transform != mat3():
for shape in shapes.mitems:
for vec in shape.mitems:
vec = transform * vec
proc computeBounds(segments: seq[(Segment, int16)]): Rect =
proc computeBounds(segments: seq[(Segment, int16)]): Rect {.raises: [].} =
## Compute the bounds of the segments.
var
xMin = float32.high
@ -1049,7 +1057,9 @@ proc computeBounds(segments: seq[(Segment, int16)]): Rect =
result.w = xMax - xMin
result.h = yMax - yMin
proc computeBounds*(path: Path, transform = mat3()): Rect =
proc computeBounds*(
path: Path, transform = mat3()
): Rect {.raises: [PixieError].} =
## Compute the bounds of the path.
var shapes = path.commandsToShapes()
shapes.transform(transform)
@ -1057,7 +1067,7 @@ proc computeBounds*(path: Path, transform = mat3()): Rect =
proc partitionSegments(
segments: seq[(Segment, int16)], top, height: int
): Partitioning =
): Partitioning {.raises: [].} =
## Puts segments into the height partitions they intersect with.
let
maxPartitions = max(1, height div 10).uint32
@ -1081,7 +1091,9 @@ proc partitionSegments(
for i in atPartition .. toPartition:
result.partitions[i].add((segment, winding))
proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} =
proc getIndexForY(
partitioning: Partitioning, y: int
): uint32 {.inline, raises: [].} =
if partitioning.partitionHeight == 0 or partitioning.partitions.len == 1:
0.uint32
else:
@ -1090,7 +1102,9 @@ proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} =
partitioning.partitions.high.uint32
)
proc insertionSort(a: var seq[(float32, int16)], lo, hi: int) {.inline.} =
proc insertionSort(
a: var seq[(float32, int16)], lo, hi: int
) {.inline, raises: [].} =
for i in lo + 1 .. hi:
var
j = i - 1
@ -1100,7 +1114,7 @@ proc insertionSort(a: var seq[(float32, int16)], lo, hi: int) {.inline.} =
dec j
dec k
proc sort(a: var seq[(float32, int16)], inl, inr: int) =
proc sort(a: var seq[(float32, int16)], inl, inr: int) {.raises: [].} =
## Quicksort + insertion sort, in-place and faster than standard lib sort.
let n = inr - inl + 1
if n < 32:
@ -1122,7 +1136,9 @@ proc sort(a: var seq[(float32, int16)], inl, inr: int) =
sort(a, inl, r)
sort(a, l, inr)
proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} =
proc shouldFill(
windingRule: WindingRule, count: int
): bool {.inline, raises: [].} =
## Should we fill based on the current winding rule and count?
case windingRule:
of wrNonZero:
@ -1136,7 +1152,7 @@ iterator walk(
windingRule: WindingRule,
y: int,
size: Vec2
): (float32, float32, int32) =
): (float32, float32, int32) {.raises: [].} =
var
prevAt: float32
count: int32
@ -1168,7 +1184,7 @@ proc computeCoverages(
aa: bool,
partitioning: Partitioning,
windingRule: WindingRule
) {.inline.} =
) {.inline, raises: [].} =
let
quality = if aa: 5 else: 1 # Must divide 255 cleanly (1, 3, 5, 15, 17, 51, 85)
sampleCoverage = (255 div quality).uint8
@ -1238,7 +1254,9 @@ proc computeCoverages(
for j in i ..< fillStart + fillLen:
coverages[j - startX] += sampleCoverage
proc clearUnsafe(target: Image | Mask, startX, startY, toX, toY: int) =
proc clearUnsafe(
target: Image | Mask, startX, startY, toX, toY: int
) {.raises: [].} =
## Clears data from [start, to).
if startX == target.width or startY == target.height:
return
@ -1256,7 +1274,7 @@ proc fillCoverage(
startX, y: int,
coverages: seq[uint8],
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
var x = startX
when defined(amd64) and not defined(pixieNoSimd):
if blendMode.hasSimdBlender():
@ -1343,7 +1361,7 @@ proc fillCoverage(
startX, y: int,
coverages: seq[uint8],
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
var x = startX
when defined(amd64) and not defined(pixieNoSimd):
if blendMode.hasSimdMasker():
@ -1386,7 +1404,7 @@ proc fillHits(
numHits: int,
windingRule: WindingRule,
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
let blender = blendMode.blender()
var filledTo: int
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, image.wh):
@ -1434,7 +1452,7 @@ proc fillHits(
numHits: int,
windingRule: WindingRule,
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
let masker = blendMode.masker()
var filledTo: int
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, mask.wh):
@ -1478,7 +1496,7 @@ proc fillShapes(
color: SomeColor,
windingRule: WindingRule,
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
# Figure out the total bounds of all the shapes,
# rasterize only within the total bounds
let
@ -1536,7 +1554,7 @@ proc fillShapes(
shapes: seq[seq[Vec2]],
windingRule: WindingRule,
blendMode: BlendMode
) =
) {.raises: [PixieError].} =
# Figure out the total bounds of all the shapes,
# rasterize only within the total bounds
let
@ -1574,11 +1592,11 @@ proc fillShapes(
mask.clearUnsafe(0, 0, 0, startY)
mask.clearUnsafe(0, pathHeight, 0, mask.height)
proc miterLimitToAngle*(limit: float32): float32 =
proc miterLimitToAngle*(limit: float32): float32 {.inline, raises: [].} =
## Converts miter-limit-ratio to miter-limit-angle.
arcsin(1 / limit) * 2
proc angleToMiterLimit*(angle: float32): float32 =
proc angleToMiterLimit*(angle: float32): float32 {.inline, raises: [].} =
## Converts miter-limit-angle to miter-limit-ratio.
1 / sin(angle / 2)
@ -1589,7 +1607,7 @@ proc strokeShapes(
lineJoin: LineJoin,
miterLimit: float32,
dashes: seq[float32]
): seq[seq[Vec2]] =
): seq[seq[Vec2]] {.raises: [PixieError].} =
if strokeWidth <= 0:
return
@ -1725,7 +1743,7 @@ proc strokeShapes(
proc parseSomePath(
path: SomePath, closeSubpaths: bool, pixelScale: float32 = 1.0
): seq[seq[Vec2]] {.inline.} =
): seq[seq[Vec2]] {.inline, raises: [PixieError].} =
## Given SomePath, parse it in different ways.
when type(path) is string:
parsePath(path).commandsToShapes(closeSubpaths, pixelScale)
@ -1738,7 +1756,7 @@ proc fillPath*(
transform = mat3(),
windingRule = wrNonZero,
blendMode = bmNormal
) =
) {.raises: [PixieError].} =
## Fills a path.
var shapes = parseSomePath(path, true, transform.pixelScale())
shapes.transform(transform)
@ -1750,7 +1768,7 @@ proc fillPath*(
paint: Paint,
transform = mat3(),
windingRule = wrNonZero
) =
) {.raises: [PixieError].} =
## Fills a path.
if paint.opacity == 0:
return
@ -1804,7 +1822,7 @@ proc strokePath*(
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[],
blendMode = bmNormal
) =
) {.raises: [PixieError].} =
## Strokes a path.
var strokeShapes = strokeShapes(
parseSomePath(path, false, transform.pixelScale()),
@ -1827,7 +1845,7 @@ proc strokePath*(
lineJoin = ljMiter,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[]
) =
) {.raises: [PixieError].} =
## Strokes a path.
if paint.opacity == 0:
return
@ -1890,7 +1908,7 @@ proc overlaps(
shapes: seq[seq[Vec2]],
test: Vec2,
windingRule: WindingRule
): bool =
): bool {.raises: [].} =
var hits: seq[(float32, int16)]
let
@ -1920,7 +1938,7 @@ proc fillOverlaps*(
test: Vec2,
transform = mat3(), ## Applied to the path, not the test point.
windingRule = wrNonZero
): bool =
): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is contained in the current path.
var shapes = parseSomePath(path, true, transform.pixelScale())
shapes.transform(transform)
@ -1935,7 +1953,7 @@ proc strokeOverlaps*(
lineJoin = ljMiter,
miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[],
): bool =
): bool {.raises: [PixieError].} =
## Returns whether or not the specified point is inside the area contained
## by the stroking of a path.
var strokeShapes = strokeShapes(

View file

@ -154,7 +154,9 @@ block:
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.fillText(font, "First line")
image.fillText(font, "Second line", translate(vec2(0, font.defaultLineHeight)))
image.fillText(
font, "Second line", translate(vec2(0, font.defaultLineHeight))
)
doDiff(image, "basic7")