Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions font/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

package font

// SVGDocViewBox exposes [svgViewBox] for tests.
func SVGDocViewBox(doc []byte, upem uint16) SVGViewBox { return svgViewBox(doc, upem) }
13 changes: 10 additions & 3 deletions font/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ type GlyphSVG struct {
// and several glyphs may share the same Source
Source []byte

// ViewBox is the initial viewport of the SVG document :
Comment thread
hajimehoshi marked this conversation as resolved.
Outdated
// the rectangle in SVG user space mapped to the em square
// when rendering.
// It is resolved from the root <svg> element attributes,
// defaulting to (0, 0, upem, upem) as required by the specification.
ViewBox SVGViewBox
Comment thread
hajimehoshi marked this conversation as resolved.

// According to the specification, a fallback outline
// should be specified for each SVG glyphs
Outline GlyphOutline
Expand Down Expand Up @@ -179,7 +186,7 @@ func (bt bitmap) glyphData(gid gID, xPpem, yPpem uint16) (GlyphBitmap, error) {
return out, nil
}

func (s svg) glyphData(gid gID) (GlyphSVG, bool) {
func (s svg) glyphData(gid gID, upem uint16) (GlyphSVG, bool) {
data, ok := s.rawGlyphData(gid)
if !ok {
return GlyphSVG{}, false
Expand All @@ -193,7 +200,7 @@ func (s svg) glyphData(gid gID) (GlyphSVG, bool) {
}
}

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

// this file converts from font format for glyph outlines to
Expand Down Expand Up @@ -447,7 +454,7 @@ func (f *Face) GlyphDataOutline(gid GID) (GlyphOutline, bool) {

// GlyphDataSVG looks for glyph data in the 'SVG ' table.
func (f *Face) GlyphDataSVG(gid GID) (GlyphSVG, bool) {
outS, ok := f.svg.glyphData(gID(gid))
outS, ok := f.svg.glyphData(gID(gid), f.upem)
if !ok {
return GlyphSVG{}, false
}
Expand Down
196 changes: 196 additions & 0 deletions font/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
package font

import (
"bytes"
"fmt"
"strconv"
"strings"

"github.com/go-text/typesetting/font/opentype/tables"
)
Expand Down Expand Up @@ -52,3 +55,196 @@ func (s svg) rawGlyphData(gid gID) ([]byte, bool) {
}
return nil, false
}

// SVGViewBox is a rectangle in the "user" coordinate space
// of an SVG document.
type SVGViewBox struct {
MinX, MinY, Width, Height float32
}

// svgViewBox returns the initial viewport of the SVG document [doc],
// resolved from the root <svg> element attributes, defaulting to the
// em square given by [upem], as required by the specification.
func svgViewBox(doc []byte, upem uint16) SVGViewBox {
viewBox, width, height, ok := svgRootAttributes(doc)
if ok {
if vb, ok := parseSVGViewBox(viewBox); ok {
return vb
}
if w, okW := parseSVGLength(width); okW {
if h, okH := parseSVGLength(height); okH {
return SVGViewBox{0, 0, w, h}
}
}
}
em := float32(upem)
return SVGViewBox{0, 0, em, em}
}

// parseSVGViewBox parses the value of a viewBox attribute:
// four numbers separated by whitespace and/or commas.
func parseSVGViewBox(s string) (SVGViewBox, bool) {
fields := strings.FieldsFunc(s, func(r rune) bool {
return r == ',' || r == ' ' || r == '\t' || r == '\r' || r == '\n'
})
if len(fields) != 4 {
return SVGViewBox{}, false
}
var nums [4]float32
for i, field := range fields {
v, err := strconv.ParseFloat(field, 32)
if err != nil {
return SVGViewBox{}, false
}
nums[i] = float32(v)
}
Comment thread
hajimehoshi marked this conversation as resolved.
if nums[2] <= 0 || nums[3] <= 0 {
return SVGViewBox{}, false
}
return SVGViewBox{nums[0], nums[1], nums[2], nums[3]}, true
}

// parseSVGLength parses a positive width or height attribute value
// expressed in user units, such as "12" or "12px".
func parseSVGLength(s string) (float32, bool) {
s = strings.TrimSpace(s)
s = strings.TrimSuffix(s, "px")
v, err := strconv.ParseFloat(s, 32)
if err != nil || v <= 0 {
return 0, false
}
return float32(v), true
Comment thread
hajimehoshi marked this conversation as resolved.
Outdated
}

func isXMLSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' }

// svgRootAttributes scans the prolog of the XML document [doc] and returns
// the raw viewBox, width and height attribute values of the root <svg> element.
//
// A dedicated scanner is used instead of [encoding/xml], which would add
// a significant binary size overhead to this core package for the simple
// task of reading the root element attributes.
func svgRootAttributes(doc []byte) (viewBox, width, height string, ok bool) {
// skip an optional byte order mark
doc = bytes.TrimPrefix(doc, []byte{0xEF, 0xBB, 0xBF})
for len(doc) != 0 {
if isXMLSpace(doc[0]) {
doc = doc[1:]
continue
}
switch {
case bytes.HasPrefix(doc, []byte("<?")): // XML declaration or processing instruction
end := bytes.Index(doc, []byte("?>"))
if end == -1 {
return "", "", "", false
}
doc = doc[end+2:]
case bytes.HasPrefix(doc, []byte("<!--")): // comment
end := bytes.Index(doc, []byte("-->"))
if end == -1 {
return "", "", "", false
}
doc = doc[end+3:]
case bytes.HasPrefix(doc, []byte("<!")): // doctype declaration
end := svgDoctypeEnd(doc)
if end == -1 {
return "", "", "", false
}
doc = doc[end:]
case doc[0] == '<': // root start tag
return parseSVGStartTag(doc)
default:
return "", "", "", false
}
}
return "", "", "", false
}

// svgDoctypeEnd returns the index just after the closing '>' of the
// doctype declaration starting [doc], or -1.
func svgDoctypeEnd(doc []byte) int {
depth := 0 // nesting in the internal subset brackets
for i := 2; i < len(doc); i++ {
switch doc[i] {
case '[':
depth++
case ']':
depth--
case '>':
if depth <= 0 {
return i + 1
}
}
}
return -1
}
Comment thread
hajimehoshi marked this conversation as resolved.
Outdated

// parseSVGStartTag parses the start tag beginning [doc] and returns
// the raw viewBox, width and height attribute values, or false if the
// element is not an <svg> element or is malformed.
func parseSVGStartTag(doc []byte) (viewBox, width, height string, ok bool) {
doc = doc[1:] // '<'
i := 0
for i < len(doc) && !isXMLSpace(doc[i]) && doc[i] != '>' && doc[i] != '/' {
i++
}
name := doc[:i]
if j := bytes.LastIndexByte(name, ':'); j != -1 { // namespace prefix
name = name[j+1:]
}
// The element is matched by its local name only: the namespace binding
// is not verified, since conforming fonts always use the SVG namespace,
// and an svg element bound to another namespace is too unlikely to be
// worth rejecting.
if string(name) != "svg" {
return "", "", "", false
}
doc = doc[i:]
for {
for len(doc) != 0 && isXMLSpace(doc[0]) {
doc = doc[1:]
}
if len(doc) == 0 {
return "", "", "", false
}
if doc[0] == '>' || doc[0] == '/' { // end of the start tag
return viewBox, width, height, true
}
// attribute name
i = 0
for i < len(doc) && doc[i] != '=' && !isXMLSpace(doc[i]) && doc[i] != '>' && doc[i] != '/' {
i++
}
attr := doc[:i]
doc = doc[i:]
for len(doc) != 0 && isXMLSpace(doc[0]) {
doc = doc[1:]
}
if len(doc) == 0 || doc[0] != '=' {
return "", "", "", false
}
doc = doc[1:]
for len(doc) != 0 && isXMLSpace(doc[0]) {
doc = doc[1:]
}
if len(doc) == 0 || (doc[0] != '"' && doc[0] != '\'') {
return "", "", "", false
}
quote := doc[0]
doc = doc[1:]
end := bytes.IndexByte(doc, quote)
if end == -1 {
return "", "", "", false
}
value := string(doc[:end])
doc = doc[end+1:]
switch string(attr) {
case "viewBox":
viewBox = value
case "width":
width = value
case "height":
height = value
}
}
}
78 changes: 78 additions & 0 deletions font/svg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

package font_test

import (
"bytes"
"testing"

td "github.com/go-text/typesetting-utils/opentype"
"github.com/go-text/typesetting/font"
tu "github.com/go-text/typesetting/testutils"
)

func TestSVGViewBox(t *testing.T) {
const upem = 1000
for _, test := range []struct {
doc string
expected font.SVGViewBox
}{
// no viewport information: default to the em square
{`<svg xmlns="http://www.w3.org/2000/svg" id="glyph1"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{``, font.SVGViewBox{0, 0, upem, upem}},
{`not xml`, font.SVGViewBox{0, 0, upem, upem}},
{`<html></html>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg`, font.SVGViewBox{0, 0, upem, upem}},
// viewBox attribute
{`<svg viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
{`<svg viewBox="-10,-20 30,40"></svg>`, font.SVGViewBox{-10, -20, 30, 40}},
{`<svg viewBox=" 0 , 0 , 1e2 , 50.5 "></svg>`, font.SVGViewBox{0, 0, 100, 50.5}},
{`<svg viewBox='0 0 128 64'/>`, font.SVGViewBox{0, 0, 128, 64}},
{"<svg\tviewBox\t=\r\n\"0 0 128 128\"></svg>", font.SVGViewBox{0, 0, 128, 128}},
// invalid viewBox attributes
{`<svg viewBox="0 0 128"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 0 128"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 128 -1"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="a b c d"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
// width and height attributes
Comment thread
hajimehoshi marked this conversation as resolved.
{`<svg width="100" height="200"></svg>`, font.SVGViewBox{0, 0, 100, 200}},
{`<svg width="100px" height="50px"></svg>`, font.SVGViewBox{0, 0, 100, 50}},
{`<svg width="100"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg width="100%" height="100%"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
{`<svg width="12pt" height="12pt"></svg>`, font.SVGViewBox{0, 0, upem, upem}},
Comment thread
hajimehoshi marked this conversation as resolved.
Outdated
// viewBox has precedence over width and height
{`<svg width="100" height="200" viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
{`<svg viewBox="invalid" width="100" height="200"></svg>`, font.SVGViewBox{0, 0, 100, 200}},
// prolog, comments and doctype before the root element
{"\uFEFF" + `<svg viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
{`<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
{`<?xml version="1.0"?>
<!-- comment with <svg viewBox="0 0 1 1"> inside -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
{`<!DOCTYPE svg [ <!ENTITY foo "bar"> ]><svg viewBox="0 0 128 128"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
// namespace prefix on the root element
{`<svg:svg xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 128 128"></svg:svg>`, font.SVGViewBox{0, 0, 128, 128}},
// other attributes, possibly with tricky values
{`<svg xmlns="http://www.w3.org/2000/svg" title="a>b" viewBox="0 0 128 128" enable-background="new"></svg>`, font.SVGViewBox{0, 0, 128, 128}},
} {
got := font.SVGDocViewBox([]byte(test.doc), upem)
if got != test.expected {
t.Fatalf("document %q: expected %v, got %v", test.doc, test.expected, got)
}
}
}

func TestGlyphDataSVGViewBox(t *testing.T) {
// this font has no viewBox attribute in its SVG document:
// the viewport defaults to the em square
data, err := td.Files.ReadFile("toys/chromacheck-svg.ttf")
tu.AssertNoErr(t, err)

face, err := font.ParseTTF(bytes.NewReader(data))
tu.AssertNoErr(t, err)

glyph, ok := face.GlyphDataSVG(1)
tu.Assert(t, ok)
tu.Assert(t, glyph.ViewBox == font.SVGViewBox{0, 0, 1024, 1024})
}
Loading