Skip to content

Commit

Permalink
[shaping] Document glyph metrics and fix vertical line thickness
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Dec 5, 2023
1 parent 0024ac6 commit 2599c46
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 23 deletions.
49 changes: 37 additions & 12 deletions shaping/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,31 @@ import (
// Glyph describes the attributes of a single glyph from a single
// font face in a shaped output.
type Glyph struct {
Width fixed.Int26_6
Height fixed.Int26_6
// Width is the width of the glyph content,
// expressed as a distance from the [XBearing],
// typically positive
Width fixed.Int26_6
// Height is the height of the glyph content,
// expressed as a distance from the [YBearing],
// typically negative
Height fixed.Int26_6
// XBearing is the distance between the dot (with offset applied) and
// the glyph content, typically positive
XBearing fixed.Int26_6
// YBearing is the distance between the dot (with offset applied) and
// the top of the glyph content, typically positive
YBearing fixed.Int26_6
// XAdvance is the distance between the current dot (without offset applied) and the next dot.
// It is typically positive for horizontal text, and always zero for vertical text.
XAdvance fixed.Int26_6
// XAdvance is the distance between the current dot (without offset applied) and the next dot.
// It is typically negative for vertical text, and always zero for horizontal text.
YAdvance fixed.Int26_6
XOffset fixed.Int26_6
YOffset fixed.Int26_6

// Offsets to be applied to the dot before actually drawing
// the glyph.
XOffset, YOffset fixed.Int26_6

// ClusterIndex is the lowest rune index of all runes shaped into
// this glyph cluster. All glyphs sharing the same cluster value
// are part of the same cluster and will have identical RuneCount
Expand All @@ -42,7 +59,7 @@ func (g Glyph) LeftSideBearing() fixed.Int26_6 {

// RightSideBearing returns the distance from the glyph's right edge to
// the edge of the glyph's advance. This value can be negative if the glyph's
// right edge is before the end of its advance.
// right edge is after the end of its advance.
func (g Glyph) RightSideBearing() fixed.Int26_6 {
return g.XAdvance - g.Width - g.XBearing
}
Expand All @@ -65,6 +82,13 @@ func (g Glyph) RightSideBearing() fixed.Int26_6 {
// - Descent GLYPH
// |
// - Gap
//
// For vertical text:
//
// Descent ------- Baseline --------------- Ascent --- Gap
// | | | |
// GLYPH GLYPH GLYPH
// GLYPH GLYPH GLYPH GLYPH GLYPH
type Bounds struct {
// Ascent is the maximum ascent away from the baseline. This value is typically
// positive in coordiate systems that grow up.
Expand All @@ -77,8 +101,9 @@ type Bounds struct {
Gap fixed.Int26_6
}

// LineHeight returns the height of a horizontal line of text described by b.
func (b Bounds) LineHeight() fixed.Int26_6 {
// LineThickness returns the thickness of a line of text described by b,
// that is its height for horizontal text, its width for vertical text.
func (b Bounds) LineThickness() fixed.Int26_6 {
return b.Ascent - b.Descent + b.Gap
}

Expand Down Expand Up @@ -166,14 +191,14 @@ func (o *Output) RecalculateAll() {
for i := range o.Glyphs {
g := &o.Glyphs[i]
advance += g.YAdvance
height := g.XBearing + g.XOffset
if height > tallest {
tallest = height
}
depth := height + g.Width
depth := g.XOffset + g.XBearing // start of the glyph
if depth < lowest {
lowest = depth
}
height := g.XOffset + g.Width // end of the glyph
if height > tallest {
tallest = height
}
}
} else { // horizontal
for i := range o.Glyphs {
Expand Down
22 changes: 11 additions & 11 deletions shaping/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ var (
GlyphID: deepGID,
XAdvance: fixed.I(int(10)),
YAdvance: fixed.I(int(10)),
XOffset: fixed.I(int(0)),
XOffset: -fixed.I(int(5)),
YOffset: fixed.I(int(0)),
Width: -fixed.I(int(10)),
Width: fixed.I(int(10)),
Height: -fixed.I(int(10)),
YBearing: fixed.I(int(0)),
XBearing: fixed.I(int(0)),
Expand All @@ -51,12 +51,12 @@ var (
GlyphID: offsetGID,
XAdvance: fixed.I(int(10)),
YAdvance: fixed.I(int(10)),
XOffset: fixed.I(int(2)),
XOffset: -fixed.I(int(2)),
YOffset: fixed.I(int(2)),
Width: -fixed.I(int(10)),
Width: fixed.I(int(10)),
Height: -fixed.I(int(10)),
YBearing: fixed.I(int(10)),
XBearing: fixed.I(int(10)),
XBearing: fixed.I(int(1)),
}
)

Expand Down Expand Up @@ -126,8 +126,8 @@ func TestRecalculate(t *testing.T) {
Glyphs: []shaping.Glyph{simpleGlyph},
Advance: simpleGlyph.YAdvance,
GlyphBounds: shaping.Bounds{
Ascent: simpleGlyph.XBearing,
Descent: fixed.I(0),
Ascent: simpleGlyph.Width,
Descent: 0,
},
LineBounds: expectedFontExtents,
},
Expand All @@ -140,8 +140,8 @@ func TestRecalculate(t *testing.T) {
Glyphs: []shaping.Glyph{simpleGlyph, deepGlyph},
Advance: simpleGlyph.YAdvance + deepGlyph.YAdvance,
GlyphBounds: shaping.Bounds{
Ascent: simpleGlyph.XBearing,
Descent: deepGlyph.XBearing + deepGlyph.Width,
Ascent: simpleGlyph.Width,
Descent: deepGlyph.XOffset + deepGlyph.XBearing,
},
LineBounds: expectedFontExtents,
},
Expand All @@ -154,8 +154,8 @@ func TestRecalculate(t *testing.T) {
Glyphs: []shaping.Glyph{offsetGlyph},
Advance: offsetGlyph.YAdvance,
GlyphBounds: shaping.Bounds{
Ascent: offsetGlyph.XBearing + offsetGlyph.XOffset,
Descent: fixed.I(0),
Ascent: offsetGlyph.Width + offsetGlyph.XOffset,
Descent: offsetGlyph.XOffset + offsetGlyph.XBearing,
},
LineBounds: expectedFontExtents,
},
Expand Down
124 changes: 124 additions & 0 deletions shaping/shaping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package shaping
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"path/filepath"
"runtime"
"testing"

hd "github.com/go-text/typesetting-utils/harfbuzz"
td "github.com/go-text/typesetting-utils/opentype"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
Expand Down Expand Up @@ -488,3 +494,121 @@ func TestFeatures(t *testing.T) {
out = shaper.Shape(input)
tu.Assert(t, len(out.Glyphs) == 1)
}

func ExampleShaper_Shape() {
textInput := []rune("abcdefghijklmnop")
withKerningFont := "harfbuzz_reference/in-house/fonts/e39391c77a6321c2ac7a2d644de0396470cd4bfe.ttf"
b, _ := hd.Files.ReadFile(withKerningFont)
face, _ := font.ParseTTF(bytes.NewReader(b))

shaper := HarfbuzzShaper{}
input := Input{
Text: textInput,
RunStart: 0,
RunEnd: len(textInput),
Direction: di.DirectionLTR,
Face: face,
Size: 16 * 72 * 10,
Script: language.Latin,
Language: language.NewLanguage("EN"),
}

drawHGlyphs(shaper.Shape(input), filepath.Join(os.TempDir(), "shape_horiz.png"))

input.Direction = di.DirectionTTB
drawVGlyphs(shaper.Shape(input), filepath.Join(os.TempDir(), "shape_vert.png"))

// Output:
}

var (
red = color.RGBA{R: 0xFF, A: 0xFF}
green = color.RGBA{G: 0xFF, A: 0xFF}
blue = color.RGBA{B: 0xFF, A: 0xFF}

Check failure on line 527 in shaping/shaping_test.go

View workflow job for this annotation

GitHub Actions / test

var blue is unused (U1000)

Check failure on line 527 in shaping/shaping_test.go

View workflow job for this annotation

GitHub Actions / test

var blue is unused (U1000)
)

func drawVLine(img *image.RGBA, start image.Point, height int, c color.RGBA) {
for y := start.Y; y <= start.Y+height; y++ {
img.SetRGBA(start.X, y, c)
}
}

func drawHLine(img *image.RGBA, start image.Point, width int, c color.RGBA) {
for x := start.X; x <= start.X+width; x++ {
img.SetRGBA(x, start.Y, c)
}
}

func drawRect(img *image.RGBA, min, max image.Point, c color.RGBA) {
for x := min.X; x <= max.X; x++ {
for y := min.Y; y <= max.Y; y++ {
img.SetRGBA(x, y, c)
}
}
}

// assume horizontal direction
func drawHGlyphs(out Output, file string) {
baseline := out.LineBounds.Ascent.Round()
height := out.LineBounds.LineThickness().Round()
width := out.Advance.Round()
img := image.NewRGBA(image.Rect(0, 0, width, height))
// white background
draw.Draw(img, img.Rect, image.NewUniform(color.White), image.Point{}, draw.Src)

drawHLine(img, image.Pt(0, baseline), width, color.RGBA{A: 0xFF})

dot := 0
for _, g := range out.Glyphs {
minX := dot + g.XOffset.Round() + g.XBearing.Round()
maxX := minX + g.Width.Round()
minY := baseline + g.YOffset.Round() - g.YBearing.Round()
maxY := minY - g.Height.Round()

drawRect(img, image.Pt(minX, minY), image.Pt(maxX, maxY), green)

// draw the dot ...
drawRect(img, image.Pt(dot-1, baseline-1), image.Pt(dot+1, baseline+1), color.RGBA{A: 0xFF})

// ... and advance
dot += g.XAdvance.Round()
drawVLine(img, image.Pt(dot, 0), height, red)
}

f, _ := os.Create(file)
_ = png.Encode(f, img)
}

// assume vertical direction
func drawVGlyphs(out Output, file string) {
baseline := -out.GlyphBounds.Descent.Round()
width := out.GlyphBounds.LineThickness().Round()
height := -out.Advance.Round()
img := image.NewRGBA(image.Rect(0, 0, width, height))
// white background
draw.Draw(img, img.Rect, image.NewUniform(color.White), image.Point{}, draw.Src)

drawVLine(img, image.Pt(baseline, 0), height, color.RGBA{A: 0xFF})

dot := 0
for _, g := range out.Glyphs {
dot += -g.YAdvance.Round()

minX := baseline + g.XOffset.Round() + g.XBearing.Round()
maxX := minX + g.Width.Round()

minY := dot + g.YOffset.Round() - g.YBearing.Round()
maxY := minY - g.Height.Round()

drawRect(img, image.Pt(minX, minY), image.Pt(maxX, maxY), green)

// draw the dot ...
drawRect(img, image.Pt(baseline-1, dot-1), image.Pt(baseline+1, dot+1), color.RGBA{A: 0xFF})

// ... and advance
drawHLine(img, image.Pt(0, dot), width, red)
}

f, _ := os.Create(file)
_ = png.Encode(f, img)
}

0 comments on commit 2599c46

Please sign in to comment.