underline, strikethrough
|
@ -304,11 +304,11 @@ type
|
|||
lookupList: LookupList
|
||||
|
||||
PostTable = ref object
|
||||
version: float32
|
||||
italicAngle: float32
|
||||
underlinePosition: int16
|
||||
underlineThickness: int16
|
||||
isFixedPitch: uint32
|
||||
version*: float32
|
||||
italicAngle*: float32
|
||||
underlinePosition*: int16
|
||||
underlineThickness*: int16
|
||||
isFixedPitch*: uint32
|
||||
|
||||
OpenType* = ref object
|
||||
buf*: string
|
||||
|
|
|
@ -18,6 +18,8 @@ type
|
|||
lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
|
||||
paint*: Paint
|
||||
textCase*: TextCase
|
||||
underline*: bool ## Apply an underline.
|
||||
strikethrough*: bool ## Apply a strikethrough.
|
||||
noKerningAdjustments*: bool ## Optionally disable kerning pair adjustments
|
||||
|
||||
Span* = ref object
|
||||
|
@ -25,6 +27,7 @@ type
|
|||
font*: Font
|
||||
|
||||
Arrangement* = ref object
|
||||
lines*: seq[(int, int)] ## The (start, stop) of the lines of text.
|
||||
spans*: seq[(int, int)] ## The (start, stop) of the spans in the text.
|
||||
fonts*: seq[Font] ## The font for each span.
|
||||
runes*: seq[Rune] ## The runes of the text.
|
||||
|
@ -72,6 +75,22 @@ proc lineHeight*(typeface: Typeface): float32 {.inline.} =
|
|||
## The default line height in font units.
|
||||
typeface.ascent - typeface.descent + typeface.lineGap
|
||||
|
||||
proc underlinePosition(typeface: Typeface): float32 {.inline.} =
|
||||
if typeface.opentype != nil:
|
||||
result = typeface.opentype.post.underlinePosition.float32
|
||||
|
||||
proc underlineThickness(typeface: Typeface): float32 {.inline.} =
|
||||
if typeface.opentype != nil:
|
||||
result = typeface.opentype.post.underlineThickness.float32
|
||||
|
||||
proc strikeoutPosition(typeface: Typeface): float32 {.inline.} =
|
||||
if typeface.opentype != nil:
|
||||
result = typeface.opentype.os2.yStrikeoutPosition.float32
|
||||
|
||||
proc strikeoutThickness(typeface: Typeface): float32 {.inline.} =
|
||||
if typeface.opentype != nil:
|
||||
result = typeface.opentype.os2.yStrikeoutSize.float32
|
||||
|
||||
proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
|
||||
## The glyph path for the rune.
|
||||
if rune.uint32 > SP.uint32: # Empty paths for control runes (not tofu)
|
||||
|
@ -177,7 +196,7 @@ proc typeset*(
|
|||
result.positions.setLen(result.runes.len)
|
||||
result.selectionRects.setLen(result.runes.len)
|
||||
|
||||
var lines = @[(0, 0)] # (start, stop)
|
||||
result.lines = @[(0, 0)] # (start, stop)
|
||||
|
||||
block: # Arrange the glyphs horizontally first (handling line breaks)
|
||||
proc advance(font: Font, runes: seq[Rune], i: int): float32 {.inline.} =
|
||||
|
@ -200,10 +219,10 @@ proc typeset*(
|
|||
at.x = 0
|
||||
at.y += 1
|
||||
prevCanWrap = 0
|
||||
lines[^1][1] = runeIndex
|
||||
result.lines[^1][1] = runeIndex
|
||||
# Start a new line if we are not at the end
|
||||
if runeIndex + 1 < result.runes.len:
|
||||
lines.add((runeIndex + 1, 0))
|
||||
result.lines.add((runeIndex + 1, 0))
|
||||
else:
|
||||
let advance = advance(font, result.runes, runeIndex)
|
||||
if wrap and rune != SP and bounds.x > 0 and at.x + advance > bounds.x:
|
||||
|
@ -221,8 +240,8 @@ proc typeset*(
|
|||
at.x += advance(font, result.runes, i)
|
||||
dec lineStart
|
||||
|
||||
lines[^1][1] = lineStart - 1
|
||||
lines.add((lineStart, 0))
|
||||
result.lines[^1][1] = lineStart - 1
|
||||
result.lines.add((lineStart, 0))
|
||||
|
||||
if rune.canWrap():
|
||||
prevCanWrap = runeIndex
|
||||
|
@ -231,12 +250,12 @@ proc typeset*(
|
|||
result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
|
||||
at.x += advance
|
||||
|
||||
lines[^1][1] = result.runes.len - 1
|
||||
result.lines[^1][1] = result.runes.len - 1
|
||||
|
||||
if hAlign != haLeft:
|
||||
# Since horizontal alignment adjustments are different for each line,
|
||||
# find the start and stop of each line of text.
|
||||
for (start, stop) in lines:
|
||||
for (start, stop) in result.lines:
|
||||
var furthestX: float32
|
||||
for i in countdown(stop, start):
|
||||
if result.runes[i] != SP and result.runes[i] != LF:
|
||||
|
@ -289,11 +308,11 @@ proc typeset*(
|
|||
) / 2
|
||||
maxInitialY = max(maxInitialY, round(fontUnitInitialY * font.scale))
|
||||
for runeIndex in start .. stop:
|
||||
if runeIndex == lines[0][1]:
|
||||
if runeIndex == result.lines[0][1]:
|
||||
break outer
|
||||
maxInitialY
|
||||
|
||||
var lineHeights = newSeq[float32](lines.len)
|
||||
var lineHeights = newSeq[float32](result.lines.len)
|
||||
block: # Compute each line's line height
|
||||
var line: int
|
||||
for spanIndex, (start, stop) in result.spans:
|
||||
|
@ -306,11 +325,12 @@ proc typeset*(
|
|||
font.defaultLineHeight
|
||||
lineHeights[line] = max(lineHeights[line], fontLineHeight)
|
||||
for runeIndex in start .. stop:
|
||||
if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
|
||||
if line + 1 < result.lines.len and
|
||||
runeIndex == result.lines[line + 1][0]:
|
||||
inc line
|
||||
lineHeights[line] = max(lineHeights[line], fontLineHeight)
|
||||
# Handle when span and line endings coincide
|
||||
if line + 1 < lines.len and stop == lines[line][1]:
|
||||
if line + 1 < result.lines.len and stop == result.lines[line][1]:
|
||||
inc line
|
||||
|
||||
block: # Vertically position the glyphs
|
||||
|
@ -326,7 +346,8 @@ proc typeset*(
|
|||
else:
|
||||
font.defaultLineHeight
|
||||
for runeIndex in start .. stop:
|
||||
if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
|
||||
if line + 1 < result.lines.len and
|
||||
runeIndex == result.lines[line + 1][0]:
|
||||
inc line
|
||||
baseline += lineHeights[line]
|
||||
result.positions[runeIndex].y = baseline
|
||||
|
@ -410,14 +431,46 @@ proc fillText*(
|
|||
transform: Vec2 | Mat3 = vec2(0, 0)
|
||||
) =
|
||||
## Fills the text arrangement.
|
||||
var line: int
|
||||
for spanIndex, (start, stop) in arrangement.spans:
|
||||
let font = arrangement.fonts[spanIndex]
|
||||
let
|
||||
font = arrangement.fonts[spanIndex]
|
||||
underlineThickness = font.typeface.underlineThickness * font.scale
|
||||
underlinePosition = font.typeface.underlinePosition * font.scale
|
||||
strikeoutThickness = font.typeface.strikeoutThickness * font.scale
|
||||
strikeoutPosition = font.typeface.strikeoutPosition * font.scale
|
||||
for runeIndex in start .. stop:
|
||||
let position = arrangement.positions[runeIndex]
|
||||
|
||||
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
|
||||
path.transform(
|
||||
translate(arrangement.positions[runeIndex]) *
|
||||
translate(position) *
|
||||
scale(vec2(font.scale))
|
||||
)
|
||||
|
||||
var applyDecoration = true
|
||||
if runeIndex == arrangement.lines[line][1]:
|
||||
inc line
|
||||
if arrangement.runes[runeIndex] == SP:
|
||||
# Do not apply decoration to the space at end of lines
|
||||
applyDecoration = false
|
||||
|
||||
if applyDecoration:
|
||||
if font.underline:
|
||||
path.rect(
|
||||
arrangement.selectionRects[runeIndex].x,
|
||||
position.y - underlinePosition + underlineThickness / 2,
|
||||
arrangement.selectionRects[runeIndex].w,
|
||||
underlineThickness
|
||||
)
|
||||
elif font.strikethrough:
|
||||
path.rect(
|
||||
arrangement.selectionRects[runeIndex].x,
|
||||
position.y - strikeoutPosition,
|
||||
arrangement.selectionRects[runeIndex].w,
|
||||
strikeoutThickness
|
||||
)
|
||||
|
||||
when type(target) is Image:
|
||||
target.fillPath(path, font.paint, transform)
|
||||
else: # target is Mask
|
||||
|
@ -450,14 +503,46 @@ proc strokeText*(
|
|||
dashes: seq[float32] = @[]
|
||||
) =
|
||||
## Strokes the text arrangement.
|
||||
var line: int
|
||||
for spanIndex, (start, stop) in arrangement.spans:
|
||||
let font = arrangement.fonts[spanIndex]
|
||||
let
|
||||
font = arrangement.fonts[spanIndex]
|
||||
underlineThickness = font.typeface.underlineThickness * font.scale
|
||||
underlinePosition = font.typeface.underlinePosition * font.scale
|
||||
strikeoutThickness = font.typeface.strikeoutThickness * font.scale
|
||||
strikeoutPosition = font.typeface.strikeoutPosition * font.scale
|
||||
for runeIndex in start .. stop:
|
||||
let position = arrangement.positions[runeIndex]
|
||||
|
||||
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
|
||||
path.transform(
|
||||
translate(arrangement.positions[runeIndex]) *
|
||||
translate(position) *
|
||||
scale(vec2(font.scale))
|
||||
)
|
||||
|
||||
var applyDecoration = true
|
||||
if runeIndex == arrangement.lines[line][1]:
|
||||
inc line
|
||||
if arrangement.runes[runeIndex] == SP:
|
||||
# Do not apply decoration to the space at end of lines
|
||||
applyDecoration = false
|
||||
|
||||
if applyDecoration:
|
||||
if font.underline:
|
||||
path.rect(
|
||||
arrangement.selectionRects[runeIndex].x,
|
||||
position.y - underlinePosition + underlineThickness / 2,
|
||||
arrangement.selectionRects[runeIndex].w,
|
||||
underlineThickness
|
||||
)
|
||||
elif font.strikethrough:
|
||||
path.rect(
|
||||
arrangement.selectionRects[runeIndex].x,
|
||||
position.y - strikeoutPosition,
|
||||
arrangement.selectionRects[runeIndex].w,
|
||||
strikeoutThickness
|
||||
)
|
||||
|
||||
when type(target) is Image:
|
||||
target.strokePath(
|
||||
path,
|
||||
|
|
BIN
tests/fonts/diffs/spans6.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
tests/fonts/diffs/strikethrough1.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
tests/fonts/diffs/strikethrough2.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
tests/fonts/diffs/strikethrough3.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
tests/fonts/diffs/underline1.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
tests/fonts/diffs/underline2.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
tests/fonts/diffs/underline3.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
BIN
tests/fonts/masters/spans6.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
tests/fonts/masters/strikethrough1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
tests/fonts/masters/strikethrough2.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
tests/fonts/masters/strikethrough3.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
tests/fonts/masters/underline1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
tests/fonts/masters/underline2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
tests/fonts/masters/underline3.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
tests/fonts/rendered/spans6.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
tests/fonts/rendered/strikethrough1.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
tests/fonts/rendered/strikethrough2.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
tests/fonts/rendered/strikethrough3.png
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
tests/fonts/rendered/underline1.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
tests/fonts/rendered/underline2.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
tests/fonts/rendered/underline3.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 961 B After Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 896 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
@ -781,18 +781,18 @@ block:
|
|||
|
||||
var font1 = ubuntu
|
||||
font1.size = 15
|
||||
font1.paint.color = parseHtmlColor("#CACACA").rgba()
|
||||
font1.paint = "#CACACA"
|
||||
|
||||
var font2 = ubuntu
|
||||
font2.size = 84
|
||||
|
||||
var font3 = ubuntu
|
||||
font3.size = 18
|
||||
font3.paint.color = parseHtmlColor("#007FF4").rgba()
|
||||
font3.paint = "#007FF4"
|
||||
|
||||
var font4 = ubuntu
|
||||
font4.size = 20
|
||||
font4.paint.color = parseHtmlColor("#4F4F4F").rgba()
|
||||
font4.paint = "#4F4F4F"
|
||||
|
||||
let spans = @[
|
||||
newSpan("verb [with object] ", font1),
|
||||
|
@ -809,3 +809,142 @@ block:
|
|||
image.fillText(arrangement, vec2(20, 20))
|
||||
|
||||
doDiff(image, "spans5")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.underline = true
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.fillText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "underline1")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.underline = true
|
||||
font.paint = rgba(0, 0, 0, 127)
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.fillText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "underline2")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.underline = true
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.strokeText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "underline3")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.strikethrough = true
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.fillText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "strikethrough1")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.strikethrough = true
|
||||
font.paint = rgba(0, 0, 0, 127)
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.fillText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "strikethrough2")
|
||||
|
||||
block:
|
||||
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
|
||||
font.size = 24
|
||||
font.strikethrough = true
|
||||
|
||||
let image = newImage(200, 100)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
image.strokeText(
|
||||
font,
|
||||
"Wrapping text to new line",
|
||||
bounds = vec2(200, 0)
|
||||
)
|
||||
|
||||
doDiff(image, "strikethrough3")
|
||||
|
||||
block:
|
||||
let ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
|
||||
|
||||
var font1 = ubuntu
|
||||
font1.size = 15
|
||||
font1.paint = "#CACACA"
|
||||
|
||||
var font2 = ubuntu
|
||||
font2.size = 84
|
||||
|
||||
var font3 = ubuntu
|
||||
font3.size = 18
|
||||
font3.paint = "#007FF4"
|
||||
|
||||
var font4 = ubuntu
|
||||
font4.size = 20
|
||||
font4.paint = "#4F4F4F"
|
||||
|
||||
var font5 = ubuntu
|
||||
font5.size = 20
|
||||
font5.paint = "#4F4F4F"
|
||||
font5.underline = true
|
||||
|
||||
var font6 = ubuntu
|
||||
font6.size = 20
|
||||
font6.paint = "#4F4F4F"
|
||||
font6.strikethrough = true
|
||||
|
||||
let spans = @[
|
||||
newSpan("verb [with object] ", font1),
|
||||
newSpan("strallow\n", font2),
|
||||
newSpan("\nstral·low\n", font3),
|
||||
newSpan("\n1. free (something) from ", font4),
|
||||
newSpan("restrictive restrictions", font5),
|
||||
newSpan(" ", font4),
|
||||
newSpan("\"the regulations are intended to strallow changes in public policy\" ", font6)
|
||||
]
|
||||
|
||||
let image = newImage(400, 400)
|
||||
image.fill(rgba(255, 255, 255, 255))
|
||||
|
||||
let arrangement = typeset(spans, bounds = vec2(360, 360))
|
||||
|
||||
image.fillText(arrangement, vec2(20, 20))
|
||||
|
||||
doDiff(image, "spans6")
|
||||
|
|