Merge pull request #251 from guzba/master

font as ref object
This commit is contained in:
treeform 2021-08-06 09:06:49 -07:00 committed by GitHub
commit 3832fb3227
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 91 deletions

View file

@ -10,20 +10,6 @@ type
FileFormat* = enum FileFormat* = enum
ffPng, ffBmp, ffJpg, ffGif ffPng, ffBmp, ffJpg, ffGif
proc readFont*(filePath: string): Font =
## Loads a font 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")
result.typeface.filePath = filePath
converter autoStraightAlpha*(c: ColorRGBX): ColorRGBA {.inline.} = converter autoStraightAlpha*(c: ColorRGBX): ColorRGBA {.inline.} =
## Convert a paremultiplied alpha RGBA to a straight alpha RGBA. ## Convert a paremultiplied alpha RGBA to a straight alpha RGBA.
c.rgba() c.rgba()

View file

@ -1,5 +1,5 @@
import bumpy, chroma, pixie/blends, pixie/common, pixie/fonts, pixie/images, import bumpy, chroma, tables, pixie/blends, pixie/common, pixie/fonts,
pixie/masks, pixie/paints, pixie/paths, vmath pixie/images, pixie/masks, pixie/paints, pixie/paths, vmath
## This file provides a Nim version of the Canvas 2D API commonly used on the ## This file provides a Nim version of the Canvas 2D API commonly used on the
## web. The goal is to make picking up Pixie easy for developers familiar with ## web. The goal is to make picking up Pixie easy for developers familiar with
@ -15,7 +15,8 @@ type
miterLimit*: float32 miterLimit*: float32
lineCap*: LineCap lineCap*: LineCap
lineJoin*: LineJoin lineJoin*: LineJoin
font*: Font font*: string ## File path to a .ttf or .otf file.
fontSize*: float32
textAlign*: HAlignMode textAlign*: HAlignMode
path: Path path: Path
lineDash: seq[float32] lineDash: seq[float32]
@ -23,6 +24,7 @@ type
mask: Mask mask: Mask
layer: Image layer: Image
stateStack: seq[ContextState] stateStack: seq[ContextState]
typefaces: Table[string, Typeface]
ContextState = object ContextState = object
fillStyle, strokeStyle: Paint fillStyle, strokeStyle: Paint
@ -31,7 +33,8 @@ type
miterLimit: float32 miterLimit: float32
lineCap: LineCap lineCap: LineCap
lineJoin: LineJoin lineJoin: LineJoin
font: Font font: string
fontSize*: float32
textAlign: HAlignMode textAlign: HAlignMode
lineDash: seq[float32] lineDash: seq[float32]
mat: Mat3 mat: Mat3
@ -51,6 +54,7 @@ proc newContext*(image: Image): Context =
result.miterLimit = 10 result.miterLimit = 10
result.fillStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255)) result.fillStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255))
result.strokeStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255)) result.strokeStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255))
result.fontSize = 12
proc newContext*(width, height: int): Context {.inline.} = proc newContext*(width, height: int): Context {.inline.} =
## Create a new Context that will draw to a new image of width and height. ## Create a new Context that will draw to a new image of width and height.
@ -65,6 +69,7 @@ proc state(ctx: Context): ContextState =
result.lineCap = ctx.lineCap result.lineCap = ctx.lineCap
result.lineJoin = ctx.lineJoin result.lineJoin = ctx.lineJoin
result.font = ctx.font result.font = ctx.font
result.fontSize = ctx.fontSize
result.textAlign = ctx.textAlign result.textAlign = ctx.textAlign
result.lineDash = ctx.lineDash result.lineDash = ctx.lineDash
result.mat = ctx.mat result.mat = ctx.mat
@ -104,6 +109,7 @@ proc restore*(ctx: Context) =
ctx.lineCap = state.lineCap ctx.lineCap = state.lineCap
ctx.lineJoin = state.lineJoin ctx.lineJoin = state.lineJoin
ctx.font = state.font ctx.font = state.font
ctx.fontSize = state.fontSize
ctx.textAlign = state.textAlign ctx.textAlign = state.textAlign
ctx.lineDash = state.lineDash ctx.lineDash = state.lineDash
ctx.mat = state.mat ctx.mat = state.mat
@ -158,15 +164,24 @@ proc stroke(ctx: Context, image: Image, path: Path) =
ctx.layer.applyOpacity(ctx.globalAlpha) ctx.layer.applyOpacity(ctx.globalAlpha)
ctx.restore() ctx.restore()
proc fillText(ctx: Context, image: Image, text: string, at: Vec2) = proc createFont(ctx: Context): Font =
if ctx.font.typeface == nil: if ctx.font == "":
raise newException(PixieError, "No font has been set on this Context") 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.size = ctx.fontSize
proc fillText(ctx: Context, image: Image, text: string, at: Vec2) =
let font = ctx.createFont()
# Canvas positions text relative to the alphabetic baseline by default # Canvas positions text relative to the alphabetic baseline by default
var at = at var at = at
at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) at.y -= round(font.typeface.ascent * font.scale)
ctx.font.paint = ctx.fillStyle font.paint = ctx.fillStyle
var image = image var image = image
@ -175,7 +190,7 @@ proc fillText(ctx: Context, image: Image, text: string, at: Vec2) =
image = ctx.layer image = ctx.layer
image.fillText( image.fillText(
ctx.font, font,
text, text,
ctx.mat * translate(at), ctx.mat * translate(at),
hAlign = ctx.textAlign hAlign = ctx.textAlign
@ -186,14 +201,13 @@ proc fillText(ctx: Context, image: Image, text: string, at: Vec2) =
ctx.restore() ctx.restore()
proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) = proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) =
if ctx.font.typeface == nil: let font = ctx.createFont()
raise newException(PixieError, "No font has been set on this Context")
# Canvas positions text relative to the alphabetic baseline by default # Canvas positions text relative to the alphabetic baseline by default
var at = at var at = at
at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) at.y -= round(font.typeface.ascent * font.scale)
ctx.font.paint = ctx.strokeStyle font.paint = ctx.strokeStyle
var image = image var image = image
@ -202,7 +216,7 @@ proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) =
image = ctx.layer image = ctx.layer
image.strokeText( image.strokeText(
ctx.font, font,
text, text,
ctx.mat * translate(at), ctx.mat * translate(at),
ctx.lineWidth, ctx.lineWidth,
@ -419,10 +433,9 @@ proc strokeText*(ctx: Context, text: string, x, y: float32) {.inline.} =
proc measureText*(ctx: Context, text: string): TextMetrics = proc measureText*(ctx: Context, text: string): TextMetrics =
## Returns a TextMetrics object that contains information about the measured ## Returns a TextMetrics object that contains information about the measured
## text (such as its width, for example). ## text (such as its width, for example).
if ctx.font.typeface == nil: let
raise newException(PixieError, "No font has been set on this Context") font = ctx.createFont()
bounds = typeset(font, text).computeBounds()
let bounds = typeset(ctx.font, text).computeBounds()
result.width = bounds.x result.width = bounds.x
proc getLineDash*(ctx: Context): seq[float32] {.inline.} = proc getLineDash*(ctx: Context): seq[float32] {.inline.} =

View file

@ -1,5 +1,5 @@
import bumpy, chroma, pixie/fontformats/opentype, pixie/fontformats/svgfont, import bumpy, chroma, common, os, pixie/fontformats/opentype, pixie/fontformats/svgfont,
pixie/images, pixie/masks, pixie/paints, pixie/paths, unicode, vmath pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, unicode, vmath
const const
AutoLineHeight* = -1.float32 ## Use default line height for the font size AutoLineHeight* = -1.float32 ## Use default line height for the font size
@ -12,7 +12,7 @@ type
svgFont: SvgFont svgFont: SvgFont
filePath*: string filePath*: string
Font* = object Font* = ref object
typeface*: Typeface typeface*: Typeface
size*: float32 ## Font size in pixels. size*: float32 ## Font size in pixels.
lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height. lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
@ -128,15 +128,19 @@ proc defaultLineHeight*(font: Font): float32 {.inline.} =
font.typeface.ascent - font.typeface.descent + font.typeface.lineGap font.typeface.ascent - font.typeface.descent + font.typeface.lineGap
round(fontUnits * font.scale) round(fontUnits * font.scale)
proc paint*(font: var Font): var Paint = proc paint*(font: Font): var Paint =
font.paints[0] font.paints[0]
proc paint*(font: Font): lent Paint = proc `paint=`*(font: Font, paint: Paint) =
font.paints[0]
proc `paint=`*(font: var Font, paint: Paint) =
font.paints = @[paint] font.paints = @[paint]
proc newFont*(typeface: Typeface): Font =
result = Font()
result.typeface = typeface
result.size = 12
result.lineHeight = AutoLineHeight
result.paint = rgbx(0, 0, 0, 255)
proc newSpan*(text: string, font: Font): Span = proc newSpan*(text: string, font: Font): Span =
## Creates a span, associating a font with the text. ## Creates a span, associating a font with the text.
result = Span() result = Span()
@ -306,8 +310,8 @@ proc typeset*(
let let
font = result.fonts[spanIndex] font = result.fonts[spanIndex]
lineHeight = lineHeight =
if font.lineheight >= 0: if font.lineHeight >= 0:
font.lineheight font.lineHeight
else: else:
font.defaultLineHeight font.defaultLineHeight
var fontUnitInitialY = font.typeface.ascent + font.typeface.lineGap / 2 var fontUnitInitialY = font.typeface.ascent + font.typeface.lineGap / 2
@ -328,8 +332,8 @@ proc typeset*(
let let
font = result.fonts[spanIndex] font = result.fonts[spanIndex]
fontLineHeight = fontLineHeight =
if font.lineheight >= 0: if font.lineHeight >= 0:
font.lineheight font.lineHeight
else: else:
font.defaultLineHeight font.defaultLineHeight
lineHeights[line] = max(lineHeights[line], fontLineHeight) lineHeights[line] = max(lineHeights[line], fontLineHeight)
@ -417,22 +421,16 @@ proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
## Computes the width and height of the spans in pixels. ## Computes the width and height of the spans in pixels.
typeset(spans).computeBounds() typeset(spans).computeBounds()
proc parseOtf*(buf: string): Font = proc parseOtf*(buf: string): Typeface =
result.typeface = Typeface() result = Typeface()
result.typeface.opentype = parseOpenType(buf) result.opentype = parseOpenType(buf)
result.size = 12
result.lineHeight = AutoLineHeight
result.paint = rgbx(0, 0, 0, 255)
proc parseTtf*(buf: string): Font = proc parseTtf*(buf: string): Typeface =
parseOtf(buf) parseOtf(buf)
proc parseSvgFont*(buf: string): Font = proc parseSvgFont*(buf: string): Typeface =
result.typeface = Typeface() result = Typeface()
result.typeface.svgFont = svgfont.parseSvgFont(buf) result.svgFont = svgfont.parseSvgFont(buf)
result.size = 12
result.lineHeight = AutoLineHeight
result.paint = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255))
proc textUber( proc textUber(
target: Image | Mask, target: Image | Mask,
@ -597,3 +595,21 @@ proc strokeText*(
miterLimit, miterLimit,
dashes dashes
) )
proc readTypeface*(filePath: string): Typeface =
## 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")
result.filePath = filePath
proc readFont*(filePath: string): Font =
## Loads a font from a file.
newFont(readTypeface(filePath))

View file

@ -280,8 +280,11 @@ block:
block: block:
let ctx = newContext(newImage(300, 150)) let ctx = newContext(newImage(300, 150))
ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf") ctx.font = "tests/fonts/Roboto-Regular_1.ttf"
ctx.font.size = 50 ctx.fontSize = 50
ctx.save()
ctx.fontSize = 30
ctx.restore()
ctx.fillText("Hello world", 50, 90) ctx.fillText("Hello world", 50, 90)
@ -290,8 +293,8 @@ block:
block: block:
let ctx = newContext(newImage(300, 150)) let ctx = newContext(newImage(300, 150))
ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf") ctx.font = "tests/fonts/Roboto-Regular_1.ttf"
ctx.font.size = 50 ctx.fontSize = 50
ctx.strokeText("Hello world", 50, 90) ctx.strokeText("Hello world", 50, 90)
@ -467,8 +470,8 @@ block:
let image = newImage(300, 150) let image = newImage(300, 150)
let ctx = newContext(image) let ctx = newContext(image)
ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf") ctx.font = "tests/fonts/Roboto-Regular_1.ttf"
ctx.font.size = 50 ctx.fontSize = 50
ctx.fillStyle = "blue" ctx.fillStyle = "blue"
ctx.saveLayer() ctx.saveLayer()
@ -486,7 +489,7 @@ block:
block: block:
let ctx = newContext(100, 100) let ctx = newContext(100, 100)
ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf") ctx.font = "tests/fonts/Roboto-Regular_1.ttf"
let metrics = ctx.measureText("Hello world") let metrics = ctx.measureText("Hello world")
doAssert metrics.width == 61 doAssert metrics.width == 61

View file

@ -738,41 +738,41 @@ block:
block: block:
let let
roboto = readFont("tests/fonts/Roboto-Regular_1.ttf") roboto = readTypeface("tests/fonts/Roboto-Regular_1.ttf")
aclonica = readFont("tests/fonts/Aclonica-Regular_1.ttf") aclonica = readTypeface("tests/fonts/Aclonica-Regular_1.ttf")
ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf") ubuntu = readTypeface("tests/fonts/Ubuntu-Regular_1.ttf")
ibm = readFont("tests/fonts/IBMPlexSans-Regular_2.ttf") ibm = readTypeface("tests/fonts/IBMPlexSans-Regular_2.ttf")
noto = readFont("tests/fonts/NotoSans-Regular_4.ttf") noto = readTypeface("tests/fonts/NotoSans-Regular_4.ttf")
var font1 = roboto var font1 = newFont(roboto)
font1.size = 64 font1.size = 64
var font2 = aclonica var font2 = newFont(aclonica)
font2.size = 80 font2.size = 80
var font3 = ibm var font3 = newFont(ibm)
font3.size = 40 font3.size = 40
var font4 = ubuntu var font4 = newFont(ubuntu)
font4.size = 56 font4.size = 56
var font5 = noto var font5 = newFont(noto)
font5.size = 72 font5.size = 72
var font6 = roboto var font6 = newFont(roboto)
font6.size = 48 font6.size = 48
var font7 = noto var font7 = newFont(noto)
font7.size = 64 font7.size = 64
var font8 = ubuntu var font8 = newFont(ubuntu)
font8.size = 54 font8.size = 54
font8.paint.color = rgba(255, 0, 0, 255) font8.paint.color = rgba(255, 0, 0, 255)
var font9 = roboto var font9 = newFont(roboto)
font9.size = 48 font9.size = 48
var font10 = aclonica var font10 = newFont(aclonica)
font10.size = 48 font10.size = 48
font10.lineHeight = 120 font10.lineHeight = 120
@ -799,20 +799,20 @@ block:
doDiff(image, "spans4") doDiff(image, "spans4")
block: block:
let ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf") let ubuntu = readTypeface("tests/fonts/Ubuntu-Regular_1.ttf")
var font1 = ubuntu var font1 = newFont(ubuntu)
font1.size = 15 font1.size = 15
font1.paint = "#CACACA" font1.paint = "#CACACA"
var font2 = ubuntu var font2 = newFont(ubuntu)
font2.size = 84 font2.size = 84
var font3 = ubuntu var font3 = newFont(ubuntu)
font3.size = 18 font3.size = 18
font3.paint = "#007FF4" font3.paint = "#007FF4"
var font4 = ubuntu var font4 = newFont(ubuntu)
font4.size = 20 font4.size = 20
font4.paint = "#4F4F4F" font4.paint = "#4F4F4F"
@ -925,29 +925,29 @@ block:
doDiff(image, "strikethrough3") doDiff(image, "strikethrough3")
block: block:
let ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf") let ubuntu = readTypeface("tests/fonts/Ubuntu-Regular_1.ttf")
var font1 = ubuntu var font1 = newFont(ubuntu)
font1.size = 15 font1.size = 15
font1.paint = "#CACACA" font1.paint = "#CACACA"
var font2 = ubuntu var font2 = newFont(ubuntu)
font2.size = 84 font2.size = 84
var font3 = ubuntu var font3 = newFont(ubuntu)
font3.size = 18 font3.size = 18
font3.paint = "#007FF4" font3.paint = "#007FF4"
var font4 = ubuntu var font4 = newFont(ubuntu)
font4.size = 20 font4.size = 20
font4.paint = "#4F4F4F" font4.paint = "#4F4F4F"
var font5 = ubuntu var font5 = newFont(ubuntu)
font5.size = 20 font5.size = 20
font5.paint = "#4F4F4F" font5.paint = "#4F4F4F"
font5.underline = true font5.underline = true
var font6 = ubuntu var font6 = newFont(ubuntu)
font6.size = 20 font6.size = 20
font6.paint = "#4F4F4F" font6.paint = "#4F4F4F"
font6.strikethrough = true font6.strikethrough = true