Skip to content

Commit d57d4b4

Browse files
authored
Opentype rewrite of textsdf package (#14)
* begin work on rewriting with opentype * finish opentype port
1 parent 3b7cf03 commit d57d4b4

4 files changed

Lines changed: 135 additions & 105 deletions

File tree

forge/textsdf/font.go

Lines changed: 131 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import (
55
"fmt"
66
"unicode"
77

8-
"github.com/golang/freetype/truetype"
98
"github.com/soypat/geometry/ms2"
109
"github.com/soypat/gsdf"
1110
"github.com/soypat/gsdf/glbuild"
1211
"golang.org/x/image/font"
12+
"golang.org/x/image/font/sfnt"
1313
"golang.org/x/image/math/fixed"
1414
)
1515

@@ -26,8 +26,8 @@ type FontConfig struct {
2626

2727
// Font implements font parsing and glyph (character) generation.
2828
type Font struct {
29-
ttf truetype.Font
30-
gb truetype.GlyphBuf
29+
buf sfnt.Buffer
30+
sfn *sfnt.Font
3131
// basicGlyphs optimized array access for common ASCII glyphs.
3232
basicGlyphs [lastBasic - firstBasic + 1]glyph
3333
// Other kinds of glyphs.
@@ -50,12 +50,12 @@ func (f *Font) Configure(cfg FontConfig) error {
5050

5151
// LoadTTFBytes loads a TTF file blob into f. After calling Load the Font is ready to generate text SDFs.
5252
func (f *Font) LoadTTFBytes(ttf []byte) error {
53-
font, err := truetype.Parse(ttf)
53+
font, err := sfnt.Parse(ttf)
5454
if err != nil {
5555
return err
5656
}
5757
f.reset()
58-
f.ttf = *font
58+
f.sfn = font
5959
return nil
6060
}
6161

@@ -88,38 +88,48 @@ type glyph struct {
8888
// Glyph locations are set starting at x=0 and appended in positive x direction.
8989
func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
9090
var shapes []glbuild.Shader2D
91-
scale := f.scale()
92-
var idxPrev truetype.Index
93-
var xOfs int64
94-
scalout := f.scaleout()
91+
ppem := f.scale()
92+
93+
var idxPrev sfnt.GlyphIndex
94+
var xOfs fixed.Int26_6
95+
scaleout := f.scaleout()
9596
for ic, c := range s {
9697
if !unicode.IsGraphic(c) {
9798
return nil, fmt.Errorf("char %q not graphic", c)
9899
}
99100

100-
idx := truetype.Index(c)
101-
hm := f.ttf.HMetric(scale, idx)
101+
idx, err := f.sfn.GlyphIndex(&f.buf, c)
102+
if err != nil {
103+
return nil, fmt.Errorf("char %q glyph index: %w", c, err)
104+
}
105+
106+
advance, err := f.sfn.GlyphAdvance(&f.buf, idx, ppem, font.HintingNone)
107+
if err != nil {
108+
return nil, fmt.Errorf("char %q advance: %w", c, err)
109+
}
110+
102111
if unicode.IsSpace(c) {
103112
if c == '\t' {
104-
hm.AdvanceWidth *= 4
113+
advance *= 4
105114
}
106-
xOfs += int64(hm.AdvanceWidth)
115+
xOfs += advance
107116
continue
108117
}
118+
109119
charshape, err := f.Glyph(c)
110120
if err != nil {
111121
return nil, fmt.Errorf("char %q: %w", c, err)
112122
}
113123

114-
kern := f.ttf.Kern(scale, idxPrev, idx)
115-
xOfs += int64(kern)
116-
idxPrev = idx
117-
if ic == 0 {
118-
xOfs += int64(hm.LeftSideBearing)
124+
if ic > 0 {
125+
kern, _ := f.sfn.Kern(&f.buf, idxPrev, idx, ppem, font.HintingNone)
126+
xOfs += kern
119127
}
120-
charshape = f.bld.Translate2D(charshape, float32(xOfs)*scalout, 0)
128+
idxPrev = idx
129+
130+
charshape = f.bld.Translate2D(charshape, float32(xOfs)*scaleout, 0)
121131
shapes = append(shapes, charshape)
122-
xOfs += int64(hm.AdvanceWidth)
132+
xOfs += advance
123133
}
124134
if len(shapes) == 1 {
125135
return shapes[0], nil
@@ -132,12 +142,17 @@ func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
132142

133143
// Kern returns the horizontal adjustment for the given glyph pair. A positive kern means to move the glyphs further apart.
134144
func (f *Font) Kern(c0, c1 rune) float32 {
135-
return float32(f.ttf.Kern(f.scale(), truetype.Index(c0), truetype.Index(c1)))
145+
idx0, _ := f.sfn.GlyphIndex(&f.buf, c0)
146+
idx1, _ := f.sfn.GlyphIndex(&f.buf, c1)
147+
kern, _ := f.sfn.Kern(&f.buf, idx0, idx1, f.scale(), font.HintingNone)
148+
return float32(kern) * f.scaleout()
136149
}
137150

138-
// Kern returns the horizontal adjustment for the given glyph pair. A positive kern means to move the glyphs further apart.
151+
// AdvanceWidth returns the horizontal advance width for the given glyph.
139152
func (f *Font) AdvanceWidth(c rune) float32 {
140-
return float32(f.ttf.HMetric(f.scale(), truetype.Index(c)).AdvanceWidth)
153+
idx, _ := f.sfn.GlyphIndex(&f.buf, c)
154+
advance, _ := f.sfn.GlyphAdvance(&f.buf, idx, f.scale(), font.HintingNone)
155+
return float32(advance) * f.scaleout()
141156
}
142157

143158
// Glyph returns a SDF for a character defined by the argument rune.
@@ -177,11 +192,12 @@ func (f *Font) glyph(c rune) (g *glyph, err error) {
177192
}
178193

179194
func (f *Font) scale() fixed.Int26_6 {
180-
return fixed.Int26_6(f.ttf.FUnitsPerEm())
195+
units := f.sfn.UnitsPerEm()
196+
return fixed.Int26_6(units)
181197
}
182198

183199
func (f *Font) rawbounds() ms2.Box {
184-
bb := f.ttf.Bounds(f.scale())
200+
bb, _ := f.sfn.Bounds(&f.buf, f.scale(), font.HintingNone)
185201
return ms2.Box{
186202
Min: ms2.Vec{X: float32(bb.Min.X), Y: float32(bb.Min.Y)},
187203
Max: ms2.Vec{X: float32(bb.Max.X), Y: float32(bb.Max.Y)},
@@ -196,32 +212,38 @@ func (f *Font) scaleout() float32 {
196212
}
197213

198214
func (f *Font) makeGlyph(char rune) (glyph, error) {
199-
g := &f.gb
200215
bld := f.bld
201216

202-
idx := f.ttf.Index(char)
203-
scale := f.scale()
204-
// hm := f.ttf.HMetric(scale, idx)
205-
err := g.Load(&f.ttf, scale, idx, font.HintingNone)
217+
idx, err := f.sfn.GlyphIndex(&f.buf, char)
206218
if err != nil {
207219
return glyph{}, err
208220
}
209-
scaleout := f.scaleout()
210221

222+
ppem := f.scale()
223+
segments, err := f.sfn.LoadGlyph(&f.buf, idx, ppem, nil)
224+
if err != nil {
225+
return glyph{}, err
226+
}
227+
228+
scaleout := f.scaleout()
211229
tol := f.reltol
212-
// Build Glyph.
213-
shape, fill, err := glyphCurve(bld, g.Points, 0, g.Ends[0], tol, scaleout)
230+
231+
// Split segments into contours (each MoveTo starts a new contour).
232+
contours := splitContours(segments)
233+
if len(contours) == 0 {
234+
return glyph{}, errors.New("glyph has no contours")
235+
}
236+
237+
// Build first contour.
238+
shape, fill, err := segmentsToPolygon(bld, contours[0], tol, scaleout)
214239
if err != nil {
215240
return glyph{}, err
216-
} else if !fill {
217-
_ = fill // This is not an error...
218-
// return glyph{}, errors.New("first glyph shape is negative space")
219241
}
220-
start := g.Ends[0]
221-
g.Ends = g.Ends[1:]
222-
for _, end := range g.Ends {
223-
sdf, fill, err := glyphCurve(bld, g.Points, start, end, tol, scaleout)
224-
start = end
242+
_ = fill // First contour fill direction is not necessarily an error.
243+
244+
// Process remaining contours.
245+
for _, contour := range contours[1:] {
246+
sdf, fill, err := segmentsToPolygon(bld, contour, tol, scaleout)
225247
if err != nil {
226248
return glyph{}, err
227249
}
@@ -234,77 +256,82 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
234256
return glyph{sdf: shape}, nil
235257
}
236258

237-
func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int, tol, scale float32) (glbuild.Shader2D, bool, error) {
259+
// splitContours splits segments into separate contours. Each contour starts with a MoveTo.
260+
func splitContours(segments sfnt.Segments) []sfnt.Segments {
261+
var contours []sfnt.Segments
262+
var current sfnt.Segments
263+
for _, seg := range segments {
264+
if seg.Op == sfnt.SegmentOpMoveTo && len(current) > 0 {
265+
contours = append(contours, current)
266+
current = nil
267+
}
268+
current = append(current, seg)
269+
}
270+
if len(current) > 0 {
271+
contours = append(contours, current)
272+
}
273+
return contours
274+
}
275+
276+
// segmentsToPolygon converts sfnt segments to a polygon.
277+
// Returns the polygon SDF, whether it's a fill (positive winding), and any error.
278+
func segmentsToPolygon(bld *gsdf.Builder, segments sfnt.Segments, tol, scale float32) (glbuild.Shader2D, bool, error) {
238279
var (
239-
sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: tol}
240-
windingSum float32
280+
poly []ms2.Vec
281+
windingSum float32
282+
prev ms2.Vec
283+
quadsampler = ms2.Spline3Sampler{
284+
Spline: ms2.SplineBezierQuadratic(),
285+
Tolerance: tol,
286+
}
287+
cubicSampler = ms2.Spline3Sampler{
288+
Spline: ms2.SplineBezierCubic(),
289+
Tolerance: tol,
290+
}
241291
)
242-
points = points[start:end]
243-
n := len(points)
244-
i := 0
245-
var poly []ms2.Vec
246-
vPrev := p2v(points[n-1], scale)
247-
for i < n {
248-
p0, p1, p2 := points[i], points[(i+1)%n], points[(i+2)%n]
249-
onBits := onbits3(points, 0, n, i)
250-
v0, v1, v2 := p2v(p0, scale), p2v(p1, scale), p2v(p2, scale)
251-
implicit0 := ms2.Scale(0.5, ms2.Add(v0, v1))
252-
implicit1 := ms2.Scale(0.5, ms2.Add(v1, v2))
253-
switch onBits {
254-
case 0b010, 0b110:
255-
// implicit off start case?
256-
fallthrough
257-
case 0b011, 0b111:
258-
// on-on Straight line.
259-
poly = append(poly, v0)
260-
i += 1
261-
windingSum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
262-
vPrev = v0
263-
continue
264292

265-
case 0b000:
266-
// implicit-off-implicit.
267-
sampler.SetSplinePoints(implicit0, v1, implicit1, ms2.Vec{})
268-
v0 = implicit0
269-
i += 1
270-
271-
case 0b001:
272-
// on-off-implicit.
273-
sampler.SetSplinePoints(v0, v1, implicit1, ms2.Vec{})
274-
i += 1
275-
276-
case 0b100:
277-
// implicit-off-on.
278-
sampler.SetSplinePoints(implicit0, v1, v2, ms2.Vec{})
279-
v0 = implicit0
280-
i += 2
281-
282-
case 0b101:
283-
// On-off-on.
284-
sampler.SetSplinePoints(v0, v1, v2, ms2.Vec{})
285-
i += 2
293+
for _, seg := range segments {
294+
switch seg.Op {
295+
case sfnt.SegmentOpMoveTo:
296+
// Start of contour - note: sfnt Y axis increases downward, so we negate Y.
297+
prev = fixedToVec(seg.Args[0], scale)
298+
299+
case sfnt.SegmentOpLineTo:
300+
p := fixedToVec(seg.Args[0], scale)
301+
poly = append(poly, prev)
302+
windingSum += (prev.X - p.X) * (prev.Y + p.Y)
303+
prev = p
304+
305+
case sfnt.SegmentOpQuadTo:
306+
// Quadratic bezier: prev -> Args[0] (control) -> Args[1] (end)
307+
ctrl := fixedToVec(seg.Args[0], scale)
308+
end := fixedToVec(seg.Args[1], scale)
309+
quadsampler.SetSplinePoints(prev, ctrl, end, ms2.Vec{})
310+
poly = append(poly, prev)
311+
poly = quadsampler.SampleBisect(poly, 4)
312+
windingSum += (prev.X - end.X) * (prev.Y + end.Y)
313+
prev = end
314+
315+
case sfnt.SegmentOpCubeTo:
316+
// Cubic bezier: prev -> Args[0] (ctrl1) -> Args[1] (ctrl2) -> Args[2] (end)
317+
ctrl1 := fixedToVec(seg.Args[0], scale)
318+
ctrl2 := fixedToVec(seg.Args[1], scale)
319+
end := fixedToVec(seg.Args[2], scale)
320+
cubicSampler.SetSplinePoints(prev, ctrl1, ctrl2, end)
321+
poly = append(poly, prev)
322+
poly = cubicSampler.SampleBisect(poly, 4)
323+
windingSum += (prev.X - end.X) * (prev.Y + end.Y)
324+
prev = end
286325
}
287-
poly = append(poly, v0) // Append start point.
288-
poly = sampler.SampleBisect(poly, 4)
289-
windingSum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
290-
vPrev = v0
291326
}
292-
return bld.NewPolygon(poly), windingSum > 0, bld.Err()
327+
return bld.NewPolygon(poly), windingSum < 0, bld.Err()
293328
}
294329

295-
func p2v(p truetype.Point, scale float32) ms2.Vec {
330+
// fixedToVec converts a fixed.Point26_6 to ms2.Vec with scaling.
331+
// Note: sfnt has Y increasing downward, so we negate Y to flip to standard math coordinates.
332+
func fixedToVec(p fixed.Point26_6, scale float32) ms2.Vec {
296333
return ms2.Vec{
297334
X: float32(p.X) * scale,
298-
Y: float32(p.Y) * scale,
335+
Y: -float32(p.Y) * scale, // Negate Y to flip coordinate system.
299336
}
300337
}
301-
302-
var quadBezier = ms2.SplineBezierQuadratic()
303-
304-
func onbits3(points []truetype.Point, start, end, i int) uint32 {
305-
n := end - start
306-
p0, p1, p2 := points[i], points[start+(i+1)%n], points[start+(i+2)%n]
307-
return p0.Flags&1 |
308-
(p1.Flags&1)<<1 |
309-
(p2.Flags&1)<<2
310-
}

forge/textsdf/glyph_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func TestABC(t *testing.T) {
1313
const okchar = "BCDEFGHIJK"
14-
const badchar = "iB~"
14+
const badchar = "ABbDdgoOpPqQR"
1515
var f Font
1616
err := f.LoadTTFBytes(ISO3098TTF())
1717
if err != nil {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ require (
1717
require (
1818
github.com/go-gl/glfw v0.0.0-20250301202403-da16c1255728 // indirect
1919
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
20+
golang.org/x/text v0.20.0 // indirect
2021
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ
2626
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
2727
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
2828
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
29+
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
30+
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=

0 commit comments

Comments
 (0)