Skip to content
Merged
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
27 changes: 7 additions & 20 deletions font/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
package font

import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"

ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
Expand Down Expand Up @@ -62,7 +59,8 @@ type GlyphSVG struct {
// The SVG image content, decompressed if needed.
// The actual glyph description is an SVG element
// with id="glyph<GID>" (as in id="glyph12"),
// and several glyphs may share the same Source
// and several glyphs may share the same Source.
// Source is read-only; copy it before modifying it.
Source []byte

// ViewBox is the initial viewport of the SVG document:
Expand Down Expand Up @@ -187,26 +185,15 @@ func (bt bitmap) glyphData(gid gID, xPpem, yPpem uint16) (GlyphBitmap, error) {
}

func (s svg) glyphData(gid gID, upem uint16) (GlyphSVG, bool) {
data, ok := s.rawGlyphData(gid)
doc, ok := s.rawGlyphData(gid)
if !ok {
return GlyphSVG{}, false
}

// un-compress if needed
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return GlyphSVG{}, false
}
defer r.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err != nil {
return GlyphSVG{}, false
}
data = buf.Bytes()
data, viewBox, ok := doc.resolve(upem)
if !ok {
return GlyphSVG{}, false
}

return GlyphSVG{Source: data, ViewBox: svgViewBox(data, upem)}, true
return GlyphSVG{Source: data, ViewBox: viewBox}, true
}

// this file converts from font format for glyph outlines to
Expand Down
36 changes: 33 additions & 3 deletions font/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ package font

import (
"bytes"
"compress/gzip"
"encoding/xml"
"fmt"
"io"
"math"
"strconv"
"strings"
"sync"

"github.com/go-text/typesetting/font/opentype/tables"
)
Expand Down Expand Up @@ -39,20 +42,47 @@ type svgDocument struct {
svg []byte
first gID // The first glyph ID in the range described by this index entry.
last gID // The last glyph ID in the range described by this index entry. Must be >= startGlyphID.

// resolved on first use and shared by every glyph of the document
once sync.Once
resolved []byte // un-compressed
viewBox SVGViewBox
valid bool
}

// resolve un-compresses the document if needed and parses its viewBox
func (doc *svgDocument) resolve(upem uint16) ([]byte, SVGViewBox, bool) {
doc.once.Do(func() {
data := doc.svg
if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return
}
defer r.Close()
data, err = io.ReadAll(r)
if err != nil {
return
}
}
doc.resolved, doc.viewBox = data, svgViewBox(data, upem)
doc.valid = true
})
return doc.resolved, doc.viewBox, doc.valid
}

// rawGlyphData returns the SVG document for [gid], or false.
func (s svg) rawGlyphData(gid gID) ([]byte, bool) {
func (s svg) rawGlyphData(gid gID) (*svgDocument, bool) {
// binary search
for i, j := 0, len(s); i < j; {
h := i + (j-i)/2
entry := s[h]
entry := &s[h]
if gid < entry.first {
j = h
} else if entry.last < gid {
i = h + 1
} else {
return entry.svg, true
return entry, true
}
}
return nil, false
Expand Down
58 changes: 58 additions & 0 deletions font/svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
package font

import (
"bytes"
"compress/gzip"
"sync"
"testing"

"github.com/go-text/typesetting/font/opentype/tables"
Expand Down Expand Up @@ -77,3 +80,58 @@ func TestSVGOffsetOverflow(t *testing.T) {
}})
tu.Assert(t, err != nil)
}

func BenchmarkGlyphDataSVG(b *testing.B) {
font := loadFont(b, "toys/chromacheck-svg.ttf")
face := NewFace(font)

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = face.GlyphDataSVG(1)
}
}

func TestSVGResolveConcurrent(t *testing.T) {
const source = `<svg viewBox="1 2 30 40"/>`
var compressed bytes.Buffer
w := gzip.NewWriter(&compressed)
if _, err := w.Write([]byte(source)); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
badChecksum := append([]byte(nil), compressed.Bytes()...)
badChecksum[len(badChecksum)-8] ^= 1
for _, test := range []struct {
name string
data []byte
valid bool
}{
{"plain", []byte(source), true},
{"gzip", compressed.Bytes(), true},
{"truncated header", []byte{0x1f, 0x8b}, false},
{"truncated body", compressed.Bytes()[:10], false},
{"invalid checksum", badChecksum, false},
} {
t.Run(test.name, func(t *testing.T) {
s := svg{{first: 0, last: 7, svg: test.data}}
var wg sync.WaitGroup
for gid := gID(0); gid <= 7; gid++ {
wg.Add(1)
go func(gid gID) {
defer wg.Done()
for repeat := 0; repeat < 2; repeat++ {
glyph, ok := s.glyphData(gid, 1000)
if ok != test.valid {
t.Errorf("glyph %d: success %v, want %v", gid, ok, test.valid)
} else if ok && (string(glyph.Source) != source || glyph.ViewBox != (SVGViewBox{1, 2, 30, 40})) {
t.Errorf("glyph %d: unexpected source or viewBox: %+v", gid, glyph)
}
}
}(gid)
}
wg.Wait()
})
}
}
Loading