Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for vertical text #124

Merged
merged 23 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
22adb71
[unicodedata] add vertical orientation
benoitkugler Nov 28, 2023
e439bd9
Merge branch 'main' into vertical-scripts
benoitkugler Dec 5, 2023
78a27fe
expose vertical script data for more efficient query
benoitkugler Dec 5, 2023
d06c8e8
Merge branch 'vertical-bounds' into vertical-scripts
benoitkugler Dec 6, 2023
2f88fba
[shaping] initial support for vertical glyph rotation
benoitkugler Dec 6, 2023
93d0246
[shaping] rename file for consistency with wrapping.go
benoitkugler Dec 7, 2023
12a97d9
[shaping] fix vertical bounds, properly taking into account negative …
benoitkugler Dec 7, 2023
cdf3a0c
add sideways helper for outlines
benoitkugler Dec 7, 2023
0361ff7
[shaping] add sideways mode to the shaper
benoitkugler Dec 8, 2023
a8f0dee
[shaping] add sample for RTL vertical text
benoitkugler Dec 8, 2023
cb04a76
[shaping] add vertical orientation to the input segmenter
benoitkugler Dec 8, 2023
8b5e415
Merge branch 'main' into vertical-scripts
benoitkugler Dec 8, 2023
9796152
[shaping] organize test rasterizer and add vertical script examples
benoitkugler Dec 11, 2023
ebfecdf
[shaping] more idiomatic samples in test
benoitkugler Dec 11, 2023
fea6bbb
[harfbuzz] properly scale glyph X and Y offsets
benoitkugler Dec 12, 2023
570b07b
Merge branch 'fix-offset-scaling' into vertical-scripts
benoitkugler Dec 12, 2023
b81c4ad
properly apply YOffset
benoitkugler Dec 12, 2023
0e869e6
fix rotation; add converter helpers
benoitkugler Dec 12, 2023
291c26f
fix dot position
benoitkugler Dec 13, 2023
582c627
[shaping] adjust baseline for vertical sideways text
benoitkugler Dec 14, 2023
d4aaea9
fix doc typo
benoitkugler Dec 14, 2023
03cc2f6
[shaping] fix image dims computation in visual tests
benoitkugler Dec 14, 2023
efc3936
Cleanup names and documentation
benoitkugler Dec 20, 2023
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
85 changes: 74 additions & 11 deletions di/direction.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package di

import (
"github.com/go-text/typesetting/harfbuzz"
)

// Direction indicates the layout direction of a piece of text.
type Direction uint8

Expand All @@ -14,30 +18,52 @@ const (
DirectionBTT
)

const (
progression Direction = 1 << iota
// axisVertical is the bit for the axis, 0 for horizontal, 1 for vertical
axisVertical

// If this flag is set, the orientation is chosen
// using the [verticalSideways] flag.
// Otherwise, the segmenter will resolve the orientation based
// on unicode properties
verticalOrientationSet
// verticalSideways is set for 'sideways', unset for 'upright'
// It implies BVerticalOrientationSet is set
verticalSideways
)

// IsVertical returns whether d is laid out on a vertical
// axis. If the return value is false, d is on the horizontal
// axis.
func (d Direction) IsVertical() bool {
return d == DirectionBTT || d == DirectionTTB
}
func (d Direction) IsVertical() bool { return d&axisVertical != 0 }

// Axis returns the layout axis for d.
func (d Direction) Axis() Axis {
switch d {
case DirectionBTT, DirectionTTB:
if d.IsVertical() {
return Vertical
default:
return Horizontal
}
return Horizontal
}

// SwitchAxis switches from horizontal to vertical (and vice versa), preserving
// the progression.
func (d Direction) SwitchAxis() Direction { return d ^ axisVertical }

// Progression returns the text layout progression for d.
func (d Direction) Progression() Progression {
switch d {
case DirectionTTB, DirectionLTR:
if d&progression == 0 {
return FromTopLeft
default:
return TowardTopLeft
}
return TowardTopLeft
}

// SetProgression sets the progression, preserving the others bits.
func (d *Direction) SetProgression(p Progression) {
if p == FromTopLeft {
*d &= ^progression
} else {
*d |= progression
}
}

Expand Down Expand Up @@ -65,3 +91,40 @@ const (
// of TowardTopLeft progression.
TowardTopLeft Progression = true
)

// HasVerticalOrientation returns true if the direction has set up
// an orientation for vertical text (typically using [SetSideways] or [SetUpright])
func (d Direction) HasVerticalOrientation() bool { return d&verticalOrientationSet != 0 }

// IsSideways returns true if the direction is vertical with a 'sideways'
// orientation.
//
// When shaping vertical text, 'sideways' means that the glyphs are rotated
// by 90°, clock-wise. This flag should be used by renderers to properly
// rotate the glyphs when drawing.
func (d Direction) IsSideways() bool { return d.IsVertical() && d&verticalSideways != 0 }

// SetSideways makes d vertical with 'sideways' or 'upright' orientation, preserving only the
// progression.
func (d *Direction) SetSideways(sideways bool) {
*d |= axisVertical | verticalOrientationSet
if sideways {
*d |= verticalSideways
} else {
*d &= ^verticalSideways
}
}

// Harfbuzz returns the equivalent direction used by harfbuzz.
func (d Direction) Harfbuzz() harfbuzz.Direction {
switch d & (progression | axisVertical) {
case DirectionRTL:
return harfbuzz.RightToLeft
case DirectionBTT:
return harfbuzz.BottomToTop
case DirectionTTB:
return harfbuzz.TopToBottom
default:
return harfbuzz.LeftToRight
}
}
65 changes: 65 additions & 0 deletions di/direction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package di

import (
"testing"

"github.com/go-text/typesetting/harfbuzz"
tu "github.com/go-text/typesetting/opentype/testutils"
)

func TestDirection(t *testing.T) {
tu.Assert(t, DirectionLTR.Axis() == Horizontal)
tu.Assert(t, DirectionRTL.Axis() == Horizontal)
tu.Assert(t, DirectionTTB.Axis() == Vertical)
tu.Assert(t, DirectionBTT.Axis() == Vertical)
tu.Assert(t, !DirectionLTR.IsVertical())
tu.Assert(t, !DirectionRTL.IsVertical())
tu.Assert(t, DirectionTTB.IsVertical())
tu.Assert(t, DirectionBTT.IsVertical())

tu.Assert(t, DirectionLTR.Progression() == FromTopLeft)
tu.Assert(t, DirectionRTL.Progression() == TowardTopLeft)
tu.Assert(t, DirectionTTB.Progression() == FromTopLeft)
tu.Assert(t, DirectionBTT.Progression() == TowardTopLeft)

tu.Assert(t, !DirectionTTB.IsSideways())
tu.Assert(t, !DirectionBTT.IsSideways())

tu.Assert(t, DirectionLTR.SwitchAxis() == DirectionTTB)
tu.Assert(t, DirectionRTL.SwitchAxis() == DirectionBTT)
tu.Assert(t, DirectionTTB.SwitchAxis() == DirectionLTR)
tu.Assert(t, DirectionBTT.SwitchAxis() == DirectionRTL)

tu.Assert(t, DirectionLTR.Harfbuzz() == harfbuzz.LeftToRight)
tu.Assert(t, DirectionRTL.Harfbuzz() == harfbuzz.RightToLeft)
tu.Assert(t, DirectionTTB.Harfbuzz() == harfbuzz.TopToBottom)
tu.Assert(t, DirectionBTT.Harfbuzz() == harfbuzz.BottomToTop)

tu.Assert(t, !DirectionLTR.HasVerticalOrientation())
tu.Assert(t, !DirectionRTL.HasVerticalOrientation())
tu.Assert(t, !DirectionTTB.HasVerticalOrientation())
tu.Assert(t, !DirectionBTT.HasVerticalOrientation())

for _, test := range []struct {
sideways bool
progression Progression
hb harfbuzz.Direction
}{
{true, FromTopLeft, harfbuzz.TopToBottom},
{true, TowardTopLeft, harfbuzz.BottomToTop},
{false, FromTopLeft, harfbuzz.TopToBottom},
{false, TowardTopLeft, harfbuzz.BottomToTop},
} {
d := axisVertical
d.SetProgression(test.progression)

tu.Assert(t, !d.HasVerticalOrientation())
d.SetSideways(test.sideways)

tu.Assert(t, d.HasVerticalOrientation())
tu.Assert(t, d.IsSideways() == test.sideways)
tu.Assert(t, d.Axis() == Vertical)
tu.Assert(t, d.Progression() == test.progression)
tu.Assert(t, d.Harfbuzz() == test.hb)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/go-text/typesetting
go 1.17

require (
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04
golang.org/x/image v0.3.0
golang.org/x/text v0.9.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8=
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down
6 changes: 4 additions & 2 deletions harfbuzz/fonts.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,25 @@ func (f *Font) getGlyphHOriginWithFallback(glyph GID) (Position, Position) {
if !ok {
x, y, ok = f.face.GlyphVOrigin(glyph)
if ok {
x, y := f.emScalefX(float32(x)), f.emScalefY(float32(y))
dx, dy := f.guessVOriginMinusHOrigin(glyph)
return x - dx, y - dy
}
}
return x, y
return f.emScalefX(float32(x)), f.emScalefY(float32(y))
}

func (f *Font) getGlyphVOriginWithFallback(glyph GID) (Position, Position) {
x, y, ok := f.face.GlyphVOrigin(glyph)
if !ok {
x, y, ok = f.face.GlyphHOrigin(glyph)
if ok {
x, y := f.emScalefX(float32(x)), f.emScalefY(float32(y))
dx, dy := f.guessVOriginMinusHOrigin(glyph)
return x + dx, y + dy
}
}
return x, y
return f.emScalefX(float32(x)), f.emScalefY(float32(y))
}

func (f *Font) guessVOriginMinusHOrigin(glyph GID) (x, y Position) {
Expand Down
8 changes: 8 additions & 0 deletions harfbuzz/harfbuzz_shape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import (

func TestShapeExpected(t *testing.T) {
tests := collectTests(t)

// add tests based on the C++ binary
tests = append(tests,
// check we properly scale the offset values
newTestData(t, "", "perf_reference/fonts/Roboto-Regular.ttf;--direction=ttb --font-size=2000;U+0061,U+0062;[gid70=0@-544,-1700+0,-2343|gid71=1@-562,-1912+0,-2343]"),
newTestData(t, "", "perf_reference/fonts/Roboto-Regular.ttf;--direction=ttb --font-size=3000;U+0061,U+0062;[gid70=0@-816,-2550+0,-3515|gid71=1@-842,-2868+0,-3515]"),
)

fmt.Printf("Running %d tests...\n", len(tests))

for _, testD := range tests {
Expand Down
15 changes: 15 additions & 0 deletions opentype/api/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ type GlyphOutline struct {
Segments []Segment
}

// Sideways updates the coordinates of the outline by applying
// a 90° clockwise rotation, and adding [yOffset] afterwards.
//
// When used for vertical text, pass
// -Glyph.YOffset, converted in font units, as [yOffset]
// (a positive value to lift the glyph up).
func (o GlyphOutline) Sideways(yOffset float32) {
for i := range o.Segments {
target := o.Segments[i].Args[:]
target[0].X, target[0].Y = target[0].Y, -target[0].X+yOffset
target[1].X, target[1].Y = target[1].Y, -target[1].X+yOffset
target[2].X, target[2].Y = target[2].Y, -target[2].X+yOffset
}
}

type SegmentOp uint8

const (
Expand Down
Loading
Loading