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
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:
// 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
111 changes: 111 additions & 0 deletions font/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
package font

import (
"bytes"
"encoding/xml"
"fmt"
"math"
"strconv"
"strings"

"github.com/go-text/typesetting/font/opentype/tables"
)
Expand Down Expand Up @@ -52,3 +57,109 @@ 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}
}

// parseSVGNumber parses a finite number, rejecting the "NaN" and
// "Inf" forms accepted by [strconv.ParseFloat].
func parseSVGNumber(s string) (float32, bool) {
v, err := strconv.ParseFloat(s, 32)
if err != nil || math.IsNaN(v) || math.IsInf(v, 0) {
return 0, false
}
return float32(v), true
}

// 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, ok := parseSVGNumber(field)
if !ok {
return SVGViewBox{}, false
}
nums[i] = v
}
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, ok := parseSVGNumber(s)
if !ok || v <= 0 {
return 0, false
}
return v, true
}

// svgNamespace is the namespace of SVG elements.
const svgNamespace = "http://www.w3.org/2000/svg"

// svgRootAttributes returns the raw viewBox, width and height attribute
// values of the root element of the XML document [doc], or false if the
// document has no root element or if it is not an svg element.
func svgRootAttributes(doc []byte) (viewBox, width, height string, ok bool) {
dec := xml.NewDecoder(bytes.NewReader(doc))
var root xml.StartElement
for {
tok, err := dec.Token()
if err != nil { // includes io.EOF, i.e. no root element
return "", "", "", false
}
if start, isStart := tok.(xml.StartElement); isStart {
root = start
break
}
}
// A missing namespace is tolerated, but a foreign one is rejected.
if root.Name.Local != "svg" || (root.Name.Space != "" && root.Name.Space != svgNamespace) {
return "", "", "", false
}
for _, attr := range root.Attr {
switch attr.Name.Local {
case "viewBox":
viewBox = attr.Value
case "width":
width = attr.Value
case "height":
height = attr.Value
}
}
return viewBox, width, height, true
}
70 changes: 70 additions & 0 deletions font/svg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

package font

import (
"testing"

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

func TestSVGViewBox(t *testing.T) {
const upem = 1000
for _, test := range []struct {
doc string
expected SVGViewBox
}{
// no viewport information: default to the em square
{`<svg xmlns="http://www.w3.org/2000/svg" id="glyph1"></svg>`, SVGViewBox{0, 0, upem, upem}},
{``, SVGViewBox{0, 0, upem, upem}},
{`not xml`, SVGViewBox{0, 0, upem, upem}},
{`<svg`, SVGViewBox{0, 0, upem, upem}}, // malformed
// the root element must be an svg element, in the SVG namespace or none
{`<html viewBox="0 0 128 128"></html>`, SVGViewBox{0, 0, upem, upem}},
{`<svg xmlns="http://example.com/wrong" viewBox="0 0 128 128"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<w:svg xmlns:w="http://example.com/wrong" viewBox="0 0 128 128"></w:svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg:svg xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 128 128"></svg:svg>`, SVGViewBox{0, 0, 128, 128}},
// viewBox attribute (a missing namespace is tolerated)
{`<svg viewBox="0 0 128 128"></svg>`, SVGViewBox{0, 0, 128, 128}},
{`<svg viewBox="-10,-20 30,40"></svg>`, SVGViewBox{-10, -20, 30, 40}},
{`<svg viewBox=" 0 , 0 , 1e2 , 50.5 "></svg>`, SVGViewBox{0, 0, 100, 50.5}},
{`<svg viewBox='0 0 128 64'/>`, SVGViewBox{0, 0, 128, 64}},
// invalid viewBox attributes
{`<svg viewBox="0 0 128"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 0 128"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 128 -1"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="a b c d"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="NaN 0 128 128"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 Inf 128"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg viewBox="0 0 128 +infinity"></svg>`, SVGViewBox{0, 0, upem, upem}},
// width and height attributes
{`<svg width="100" height="200"></svg>`, SVGViewBox{0, 0, 100, 200}},
{`<svg width="100px" height="50px"></svg>`, SVGViewBox{0, 0, 100, 50}},
{`<svg width="100"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg width="100%" height="100%"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg width="12pt" height="12pt"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg width="NaN" height="NaN"></svg>`, SVGViewBox{0, 0, upem, upem}},
{`<svg width="Inf" height="Inf"></svg>`, SVGViewBox{0, 0, upem, upem}},
// viewBox has precedence over width and height
{`<svg width="100" height="200" viewBox="0 0 128 128"></svg>`, SVGViewBox{0, 0, 128, 128}},
{`<svg viewBox="invalid" width="100" height="200"></svg>`, SVGViewBox{0, 0, 100, 200}},
// content before the root element is skipped
{`<?xml version="1.0"?><!-- comment --><!DOCTYPE svg><svg viewBox="0 0 128 128"></svg>`, SVGViewBox{0, 0, 128, 128}},
} {
got := svgViewBox([]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
font := loadFont(t, "toys/chromacheck-svg.ttf")
face := Face{Font: font}

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