Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fontscan/fontmap_cache_android.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fontscan

import "fmt"

func platformCacheDir() (string, error) {
// There is no stable way to infer the proper place to store the cache
// with access to the Java runtime for the application. Rather than
Expand Down
11 changes: 11 additions & 0 deletions harfbuzz/harfbuzz_shape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ func (mft testInput) shape(t *testing.T, verify bool) (string, error) {
return "", err
}

// check that YAdvance is only used for vertical text
// and XAdvance for horizontal text
isHorizontal := buffer.Props.Direction.isHorizontal()
for _, pos := range buffer.Pos {
if isHorizontal {
tu.Assert(t, pos.YAdvance == 0)
} else {
tu.Assert(t, pos.XAdvance == 0)
}
}

return buffer.serialize(font, mft.format), nil
}

Expand Down
113 changes: 85 additions & 28 deletions shaping/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package shaping

import (
"math"

"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"golang.org/x/image/math/fixed"
Expand Down Expand Up @@ -155,30 +157,30 @@ type Output struct {

// ToFontUnit converts a metrics (typically found in [Glyph] fields)
// to unscaled font units.
func (o *Output) ToFontUnit(v fixed.Int26_6) float32 {
return float32(v) / float32(o.Size) * float32(o.Face.Upem())
func (out *Output) ToFontUnit(v fixed.Int26_6) float32 {
return float32(v) / float32(out.Size) * float32(out.Face.Upem())
}

// FromFontUnit converts an unscaled font value to the current [Size]
func (o *Output) FromFontUnit(v float32) fixed.Int26_6 {
return fixed.Int26_6(v * float32(o.Size) / float32(o.Face.Upem()))
func (out *Output) FromFontUnit(v float32) fixed.Int26_6 {
return fixed.Int26_6(v * float32(out.Size) / float32(out.Face.Upem()))
}

// RecomputeAdvance updates only the Advance field based on the current
// contents of the Glyphs field. It is faster than RecalculateAll(),
// and can be used to speed up line wrapping logic.
func (o *Output) RecomputeAdvance() {
func (out *Output) RecomputeAdvance() {
advance := fixed.Int26_6(0)
if o.Direction.IsVertical() {
for _, g := range o.Glyphs {
if out.Direction.IsVertical() {
for _, g := range out.Glyphs {
advance += g.YAdvance
}
} else { // horizontal
for _, g := range o.Glyphs {
for _, g := range out.Glyphs {
advance += g.XAdvance
}
}
o.Advance = advance
out.Advance = advance
}

// advanceSpaceAware adjust the value in [Advance]
Expand All @@ -189,45 +191,45 @@ func (o *Output) RecomputeAdvance() {
// because the trailing space in this run will always be internal to the paragraph.
//
// TODO: should we take into account multiple spaces ?
func (o *Output) advanceSpaceAware(paragraphDir di.Direction) fixed.Int26_6 {
L := len(o.Glyphs)
if L == 0 || paragraphDir != o.Direction {
return o.Advance
func (out *Output) advanceSpaceAware(paragraphDir di.Direction) fixed.Int26_6 {
L := len(out.Glyphs)
if L == 0 || paragraphDir != out.Direction {
return out.Advance
}

// adjust the last to account for spaces
var lastG Glyph
if o.Direction.Progression() == di.FromTopLeft {
lastG = o.Glyphs[L-1]
if out.Direction.Progression() == di.FromTopLeft {
lastG = out.Glyphs[L-1]
} else {
lastG = o.Glyphs[0]
lastG = out.Glyphs[0]
}
if o.Direction.IsVertical() {
if out.Direction.IsVertical() {
if lastG.Height == 0 {
return o.Advance - lastG.YAdvance
return out.Advance - lastG.YAdvance
}
} else { // horizontal
if lastG.Width == 0 {
return o.Advance - lastG.XAdvance
return out.Advance - lastG.XAdvance
}
}
return o.Advance - lastG.endLetterSpacing
return out.Advance - lastG.endLetterSpacing
}

// RecalculateAll updates the all other fields of the Output
// to match the current contents of the Glyphs field.
// This method will fail with UnimplementedDirectionError if the Output
// direction is unimplemented.
func (o *Output) RecalculateAll() {
func (out *Output) RecalculateAll() {
var (
advance fixed.Int26_6
ascent fixed.Int26_6
descent fixed.Int26_6
)

if o.Direction.IsVertical() {
for i := range o.Glyphs {
g := &o.Glyphs[i]
if out.Direction.IsVertical() {
for i := range out.Glyphs {
g := &out.Glyphs[i]
advance += g.YAdvance
depth := g.XOffset + g.XBearing // start of the glyph
if depth < descent {
Expand All @@ -239,8 +241,8 @@ func (o *Output) RecalculateAll() {
}
}
} else { // horizontal
for i := range o.Glyphs {
g := &o.Glyphs[i]
for i := range out.Glyphs {
g := &out.Glyphs[i]
advance += g.XAdvance
height := g.YBearing + g.YOffset
if height > ascent {
Expand All @@ -252,8 +254,8 @@ func (o *Output) RecalculateAll() {
}
}
}
o.Advance = advance
o.GlyphBounds = Bounds{
out.Advance = advance
out.GlyphBounds = Bounds{
Ascent: ascent,
Descent: descent,
}
Expand Down Expand Up @@ -303,6 +305,46 @@ func (out *Output) moveCrossAxis(d fixed.Int26_6) {
out.GlyphBounds.Descent += d
}

func (out *Output) applyTabs(text []rune, columnWidth, runStart fixed.Int26_6) {
isVertical := out.Direction.IsVertical()
columnWidthF := float64(columnWidth) / 64
var advance fixed.Int26_6
for i, g := range out.Glyphs {
gAdvance := g.XAdvance
if isVertical {
gAdvance = g.YAdvance
}
isTab := g.RuneCount == 1 && g.GlyphCount == 1 && text[g.ClusterIndex] == '\t'
if !isTab {
advance += gAdvance
continue
}

var updatedTabAdvance fixed.Int26_6
if columnWidth == 0 {
// simply trim the advance, nothing else to do
} else {
// update the advance of the glyph so that the next glyph is "tab-aligned" :
// we want the "end" of the tab to be a multiple of columnWidth, that is :
// (runStart + advance + updatedTabAdvance) % columnWith == 0
glyphStartF := float64(runStart+advance) / 64
remainder := math.Mod(glyphStartF, columnWidthF)
updatedTabAdvance = fixed.Int26_6((columnWidthF - remainder) * 64)
}

if isVertical {
out.Glyphs[i].YAdvance = updatedTabAdvance
} else {
out.Glyphs[i].XAdvance = updatedTabAdvance
}

advance += updatedTabAdvance
}

// no need to call RecomputeAdvance
out.Advance = advance
}

// AdjustBaselines aligns runs with different baselines.
//
// For vertical text, it centralizes 'sideways' runs, so
Expand Down Expand Up @@ -349,3 +391,18 @@ func (l Line) AdjustBaselines() {
l[i].moveCrossAxis(-middle)
}
}

// AlignTabs updates the advance of glyphs mapped to '\t' runes,
// so that tabs are aligned on columns defined by [columnWidth].
//
// [lineOffset] may be non zero if the line starts after the first column.
//
// As a special case, if [columnWidth] is zero,
// tabs are trimmed (their advance is set to 0).
func (l Line) AlignTabs(text []rune, columnWidth, lineOffset fixed.Int26_6) {
runsAdvance := lineOffset // the position of the start of the current run
for i := range l {
l[i].applyTabs(text, columnWidth, runsAdvance)
runsAdvance += l[i].Advance
}
}
43 changes: 43 additions & 0 deletions shaping/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,46 @@ func TestAdvanceSpaceAware(t *testing.T) {
})
}
}

func TestLine_applyTabs(t *testing.T) {
text := []rune("A first run\twith tab. A second run\t\ta\t.")
// simplify with 1:1 rune glyph mapping
glyphs := make([]Glyph, len(text))
for i := range text {
glyphs[i] = Glyph{ClusterIndex: i, RuneCount: 1, GlyphCount: 1, XAdvance: fixed.I(1)}
}

run1 := Output{Glyphs: glyphs[0:22]}
run2 := Output{Glyphs: glyphs[22:]}
run1.RecalculateAll()
run2.RecalculateAll()
line := Line{run1, run2}

// simple
line.AlignTabs(text, fixed.I(5), 0)
tu.Assert(t, run1.Glyphs[11].XAdvance == fixed.I(4))
tu.Assert(t, run2.Glyphs[34-22].XAdvance == fixed.I(3))
tu.Assert(t, run2.Glyphs[35-22].XAdvance == fixed.I(5))
tu.Assert(t, run2.Glyphs[37-22].XAdvance == fixed.I(4))

// with offset
line.AlignTabs(text, fixed.I(5), fixed.I(2))
tu.Assert(t, run1.Glyphs[11].XAdvance == fixed.I(2))
tu.Assert(t, run2.Glyphs[34-22].XAdvance == fixed.I(3))
tu.Assert(t, run2.Glyphs[35-22].XAdvance == fixed.I(5))
tu.Assert(t, run2.Glyphs[37-22].XAdvance == fixed.I(4))

// with offset
line.AlignTabs(text, fixed.I(5), fixed.I(7))
tu.Assert(t, run1.Glyphs[11].XAdvance == fixed.I(2))
tu.Assert(t, run2.Glyphs[34-22].XAdvance == fixed.I(3))
tu.Assert(t, run2.Glyphs[35-22].XAdvance == fixed.I(5))
tu.Assert(t, run2.Glyphs[37-22].XAdvance == fixed.I(4))

// clear all tabs
line.AlignTabs(text, fixed.I(0), 0)
tu.Assert(t, run1.Glyphs[11].XAdvance == fixed.I(0))
tu.Assert(t, run2.Glyphs[34-22].XAdvance == fixed.I(0))
tu.Assert(t, run2.Glyphs[35-22].XAdvance == fixed.I(0))
tu.Assert(t, run2.Glyphs[37-22].XAdvance == fixed.I(0))
}
16 changes: 13 additions & 3 deletions shaping/wrapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,12 @@ type WrappedLine struct {
// of the next line. It will equal len(text) if all the text
// fit in one line.
NextLine int

// TrimmedTrailingWhitespace is the space taken by trailing whitespace
// before if was trimmed (usually positive).
// It is zero if [DisableTrailingWhitespaceTrim] is set to true,
// or if there is no whitespace at the end of the line.
TrimmedTrailingWhitespace fixed.Int26_6
}

// swapVisualOrder inverts the visual index of runs in [subline], by swapping pairs of visual indices across the midpoint
Expand Down Expand Up @@ -913,6 +919,7 @@ func computeBidiOrdering(dir di.Direction, finalLine Line) {
}

func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, bool) {
var trimmed fixed.Int26_6
if len(finalLine) > 0 {
computeBidiOrdering(l.config.Direction, finalLine)
if !l.config.DisableTrailingWhitespaceTrim {
Expand All @@ -927,11 +934,12 @@ func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, b
break
}
}
finalVisualRun := &finalLine[goalIdx]

// This next block locates the first/last visual glyph on the line and
// zeroes its advance if it is whitespace.
finalVisualRun := &finalLine[goalIdx]
var finalVisualGlyph *Glyph
if L := len(finalVisualRun.Glyphs); L > 0 {
var finalVisualGlyph *Glyph
if l.config.Direction.Progression() == di.FromTopLeft {
finalVisualGlyph = &finalVisualRun.Glyphs[L-1]
} else {
Expand All @@ -947,7 +955,9 @@ func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, b
finalVisualGlyph.XAdvance = 0
}
}
beforeTrim := finalVisualRun.Advance
finalVisualRun.RecomputeAdvance()
trimmed = beforeTrim - finalVisualRun.Advance
}
}

Expand Down Expand Up @@ -984,7 +994,7 @@ func (l *LineWrapper) postProcessLine(finalLine Line, done bool) (WrappedLine, b
l.more = false
}

return WrappedLine{finalLine, truncated, l.lineStartRune}, done
return WrappedLine{finalLine, truncated, l.lineStartRune, trimmed}, done
}

// WrapNextLine wraps the shaped glyphs of a paragraph to a particular max width.
Expand Down
26 changes: 26 additions & 0 deletions shaping/wrapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3538,3 +3538,29 @@ func TestRequiredBreaks(t *testing.T) {
}
}
}

func TestTrimmedTrailingWhitespace(t *testing.T) {
face := loadOpentypeFont(t, "../font/testdata/UbuntuMono-R.ttf")
text := []rune("The quick")
run := (&HarfbuzzShaper{}).Shape(Input{
Text: text,
Face: face,
Size: 72,
RunEnd: len(text),
})
tu.Assert(t, run.Glyphs[0].XAdvance == fixed.I(1) && run.Glyphs[3].XAdvance == fixed.I(1))

var wrapper LineWrapper

wrapper.Prepare(WrapConfig{BreakPolicy: WhenNecessary}, text, NewSliceIterator([]Output{run.copy()}))
line, _ := wrapper.WrapNextLine(4) // cut right after the space
tu.Assert(t, line.NextLine == 4)
tu.Assert(t, line.Line[0].Advance == fixed.I(3)) // the space is collapsed
tu.Assert(t, line.TrimmedTrailingWhitespace == fixed.I(1)) // and we know how much was collapsed

wrapper.Prepare(WrapConfig{BreakPolicy: WhenNecessary, DisableTrailingWhitespaceTrim: true}, text, NewSliceIterator([]Output{run.copy()}))
line, _ = wrapper.WrapNextLine(4) // cut right after the space
tu.Assert(t, line.NextLine == 4)
tu.Assert(t, line.Line[0].Advance == fixed.I(4)) // the space is not collapsed
tu.Assert(t, line.TrimmedTrailingWhitespace == fixed.I(0))
}
Loading