diff --git a/fontscan/fontmap_cache_android.go b/fontscan/fontmap_cache_android.go index b396ded3..43c71e2d 100644 --- a/fontscan/fontmap_cache_android.go +++ b/fontscan/fontmap_cache_android.go @@ -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 diff --git a/harfbuzz/harfbuzz_shape_test.go b/harfbuzz/harfbuzz_shape_test.go index eb19b2f5..27c53aa1 100644 --- a/harfbuzz/harfbuzz_shape_test.go +++ b/harfbuzz/harfbuzz_shape_test.go @@ -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 } diff --git a/shaping/output.go b/shaping/output.go index 236d9de5..2174fff1 100644 --- a/shaping/output.go +++ b/shaping/output.go @@ -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" @@ -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] @@ -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 { @@ -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 { @@ -252,8 +254,8 @@ func (o *Output) RecalculateAll() { } } } - o.Advance = advance - o.GlyphBounds = Bounds{ + out.Advance = advance + out.GlyphBounds = Bounds{ Ascent: ascent, Descent: descent, } @@ -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 @@ -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 + } +} diff --git a/shaping/output_test.go b/shaping/output_test.go index 752237f1..9f0bd652 100644 --- a/shaping/output_test.go +++ b/shaping/output_test.go @@ -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)) +} diff --git a/shaping/wrapping.go b/shaping/wrapping.go index 195536d9..fa092aa7 100644 --- a/shaping/wrapping.go +++ b/shaping/wrapping.go @@ -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 @@ -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 { @@ -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 { @@ -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 } } @@ -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. diff --git a/shaping/wrapping_test.go b/shaping/wrapping_test.go index 6e9eaf79..1cfc980a 100644 --- a/shaping/wrapping_test.go +++ b/shaping/wrapping_test.go @@ -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)) +}