diff --git a/src/pixie/fontformats/opentype.nim b/src/pixie/fontformats/opentype.nim index 477c181..e34cce6 100644 --- a/src/pixie/fontformats/opentype.nim +++ b/src/pixie/fontformats/opentype.nim @@ -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 diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim index 764b0b0..a322ea9 100644 --- a/src/pixie/fonts.nim +++ b/src/pixie/fonts.nim @@ -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, diff --git a/tests/fonts/diffs/spans6.png b/tests/fonts/diffs/spans6.png new file mode 100644 index 0000000..f8a226a Binary files /dev/null and b/tests/fonts/diffs/spans6.png differ diff --git a/tests/fonts/diffs/strikethrough1.png b/tests/fonts/diffs/strikethrough1.png new file mode 100644 index 0000000..d833e60 Binary files /dev/null and b/tests/fonts/diffs/strikethrough1.png differ diff --git a/tests/fonts/diffs/strikethrough2.png b/tests/fonts/diffs/strikethrough2.png new file mode 100644 index 0000000..4321b53 Binary files /dev/null and b/tests/fonts/diffs/strikethrough2.png differ diff --git a/tests/fonts/diffs/strikethrough3.png b/tests/fonts/diffs/strikethrough3.png new file mode 100644 index 0000000..2fbe162 Binary files /dev/null and b/tests/fonts/diffs/strikethrough3.png differ diff --git a/tests/fonts/diffs/underline1.png b/tests/fonts/diffs/underline1.png new file mode 100644 index 0000000..65b24e3 Binary files /dev/null and b/tests/fonts/diffs/underline1.png differ diff --git a/tests/fonts/diffs/underline2.png b/tests/fonts/diffs/underline2.png new file mode 100644 index 0000000..80e75aa Binary files /dev/null and b/tests/fonts/diffs/underline2.png differ diff --git a/tests/fonts/diffs/underline3.png b/tests/fonts/diffs/underline3.png new file mode 100644 index 0000000..c169d93 Binary files /dev/null and b/tests/fonts/diffs/underline3.png differ diff --git a/tests/fonts/image_stroke.png b/tests/fonts/image_stroke.png index 33b744b..3caaca2 100644 Binary files a/tests/fonts/image_stroke.png and b/tests/fonts/image_stroke.png differ diff --git a/tests/fonts/mask_stroke.png b/tests/fonts/mask_stroke.png index d95d4c9..01b15c2 100644 Binary files a/tests/fonts/mask_stroke.png and b/tests/fonts/mask_stroke.png differ diff --git a/tests/fonts/masters/spans6.png b/tests/fonts/masters/spans6.png new file mode 100644 index 0000000..877b3d5 Binary files /dev/null and b/tests/fonts/masters/spans6.png differ diff --git a/tests/fonts/masters/strikethrough1.png b/tests/fonts/masters/strikethrough1.png new file mode 100644 index 0000000..fa5da6b Binary files /dev/null and b/tests/fonts/masters/strikethrough1.png differ diff --git a/tests/fonts/masters/strikethrough2.png b/tests/fonts/masters/strikethrough2.png new file mode 100644 index 0000000..03d8ad0 Binary files /dev/null and b/tests/fonts/masters/strikethrough2.png differ diff --git a/tests/fonts/masters/strikethrough3.png b/tests/fonts/masters/strikethrough3.png new file mode 100644 index 0000000..a02808e Binary files /dev/null and b/tests/fonts/masters/strikethrough3.png differ diff --git a/tests/fonts/masters/underline1.png b/tests/fonts/masters/underline1.png new file mode 100644 index 0000000..9b05012 Binary files /dev/null and b/tests/fonts/masters/underline1.png differ diff --git a/tests/fonts/masters/underline2.png b/tests/fonts/masters/underline2.png new file mode 100644 index 0000000..fcbde16 Binary files /dev/null and b/tests/fonts/masters/underline2.png differ diff --git a/tests/fonts/masters/underline3.png b/tests/fonts/masters/underline3.png new file mode 100644 index 0000000..eb3e8b7 Binary files /dev/null and b/tests/fonts/masters/underline3.png differ diff --git a/tests/fonts/rendered/spans6.png b/tests/fonts/rendered/spans6.png new file mode 100644 index 0000000..3164950 Binary files /dev/null and b/tests/fonts/rendered/spans6.png differ diff --git a/tests/fonts/rendered/strikethrough1.png b/tests/fonts/rendered/strikethrough1.png new file mode 100644 index 0000000..30b8cff Binary files /dev/null and b/tests/fonts/rendered/strikethrough1.png differ diff --git a/tests/fonts/rendered/strikethrough2.png b/tests/fonts/rendered/strikethrough2.png new file mode 100644 index 0000000..88a8e0c Binary files /dev/null and b/tests/fonts/rendered/strikethrough2.png differ diff --git a/tests/fonts/rendered/strikethrough3.png b/tests/fonts/rendered/strikethrough3.png new file mode 100644 index 0000000..ff6d87e Binary files /dev/null and b/tests/fonts/rendered/strikethrough3.png differ diff --git a/tests/fonts/rendered/underline1.png b/tests/fonts/rendered/underline1.png new file mode 100644 index 0000000..bed08df Binary files /dev/null and b/tests/fonts/rendered/underline1.png differ diff --git a/tests/fonts/rendered/underline2.png b/tests/fonts/rendered/underline2.png new file mode 100644 index 0000000..11493d2 Binary files /dev/null and b/tests/fonts/rendered/underline2.png differ diff --git a/tests/fonts/rendered/underline3.png b/tests/fonts/rendered/underline3.png new file mode 100644 index 0000000..f7dec25 Binary files /dev/null and b/tests/fonts/rendered/underline3.png differ diff --git a/tests/images/context/bezierCurveTo_1.png b/tests/images/context/bezierCurveTo_1.png index 83bb3bf..a007d41 100644 Binary files a/tests/images/context/bezierCurveTo_1.png and b/tests/images/context/bezierCurveTo_1.png differ diff --git a/tests/images/context/bezierCurveTo_2.png b/tests/images/context/bezierCurveTo_2.png index e7a04f5..bfb877e 100644 Binary files a/tests/images/context/bezierCurveTo_2.png and b/tests/images/context/bezierCurveTo_2.png differ diff --git a/tests/images/context/closePath_1.png b/tests/images/context/closePath_1.png index c91d8e6..bd5fc25 100644 Binary files a/tests/images/context/closePath_1.png and b/tests/images/context/closePath_1.png differ diff --git a/tests/images/context/ellipse_1.png b/tests/images/context/ellipse_1.png index 0f8f5fe..23d777a 100644 Binary files a/tests/images/context/ellipse_1.png and b/tests/images/context/ellipse_1.png differ diff --git a/tests/images/context/quadracticCurveTo_1.png b/tests/images/context/quadracticCurveTo_1.png index 42ee8dc..a48003e 100644 Binary files a/tests/images/context/quadracticCurveTo_1.png and b/tests/images/context/quadracticCurveTo_1.png differ diff --git a/tests/images/context/strokeText_1.png b/tests/images/context/strokeText_1.png index fb22006..f408ac1 100644 Binary files a/tests/images/context/strokeText_1.png and b/tests/images/context/strokeText_1.png differ diff --git a/tests/images/masks/strokeEllipse.png b/tests/images/masks/strokeEllipse.png index af1c50c..4d3f3ee 100644 Binary files a/tests/images/masks/strokeEllipse.png and b/tests/images/masks/strokeEllipse.png differ diff --git a/tests/images/masks/strokePolygon.png b/tests/images/masks/strokePolygon.png index 4d88470..00e432a 100644 Binary files a/tests/images/masks/strokePolygon.png and b/tests/images/masks/strokePolygon.png differ diff --git a/tests/images/masks/strokeRoundedRect.png b/tests/images/masks/strokeRoundedRect.png index 7f7a31c..d567654 100644 Binary files a/tests/images/masks/strokeRoundedRect.png and b/tests/images/masks/strokeRoundedRect.png differ diff --git a/tests/images/paths/pathStroke3.png b/tests/images/paths/pathStroke3.png index 428b9fa..01c0f55 100644 Binary files a/tests/images/paths/pathStroke3.png and b/tests/images/paths/pathStroke3.png differ diff --git a/tests/images/paths/pixelScale.png b/tests/images/paths/pixelScale.png index 0abcddc..6b8606f 100644 Binary files a/tests/images/paths/pixelScale.png and b/tests/images/paths/pixelScale.png differ diff --git a/tests/images/strokeEllipse.png b/tests/images/strokeEllipse.png index 7c95036..50562a0 100644 Binary files a/tests/images/strokeEllipse.png and b/tests/images/strokeEllipse.png differ diff --git a/tests/images/strokePolygon.png b/tests/images/strokePolygon.png index e124ee3..d833617 100644 Binary files a/tests/images/strokePolygon.png and b/tests/images/strokePolygon.png differ diff --git a/tests/images/strokeRoundedRect.png b/tests/images/strokeRoundedRect.png index 0ade755..fdfa991 100644 Binary files a/tests/images/strokeRoundedRect.png and b/tests/images/strokeRoundedRect.png differ diff --git a/tests/test_fonts.nim b/tests/test_fonts.nim index cf69394..e50214f 100644 --- a/tests/test_fonts.nim +++ b/tests/test_fonts.nim @@ -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")