From 9097d7e2e5d45a27b1dc0ea9623a4986af4c0f2f Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 4 Jul 2026 02:03:19 +0900 Subject: [PATCH 1/4] [font] resolve the initial viewport of SVG glyphs The SVG documents of some fonts, such as Noto Color Emoji, do not specify a viewBox attribute on their root element. The OpenType specification defines the initial viewport of an SVG glyph document as the em square (head.unitsPerEm), so renderers relying on the document alone ended up with an empty viewport and rendered nothing. Expose the resolved viewport as a new ViewBox field on GlyphSVG, computed from the root element attributes: the viewBox attribute if present, then the width and height attributes, then the em square. The root element is scanned with a small dedicated parser rather than encoding/xml, to avoid its binary size overhead in this core package. Updates go-text/typesetting#191 Co-Authored-By: Claude Fable 5 --- font/export_test.go | 6 ++ font/renderer.go | 13 ++- font/svg.go | 196 ++++++++++++++++++++++++++++++++++++++++++++ font/svg_test.go | 78 ++++++++++++++++++ 4 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 font/export_test.go create mode 100644 font/svg_test.go diff --git a/font/export_test.go b/font/export_test.go new file mode 100644 index 00000000..8e551fb8 --- /dev/null +++ b/font/export_test.go @@ -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) } diff --git a/font/renderer.go b/font/renderer.go index e54755ad..b53516a8 100644 --- a/font/renderer.go +++ b/font/renderer.go @@ -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 element attributes, + // defaulting to (0, 0, upem, upem) as required by the specification. + ViewBox SVGViewBox + // According to the specification, a fallback outline // should be specified for each SVG glyphs Outline GlyphOutline @@ -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 @@ -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 @@ -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 } diff --git a/font/svg.go b/font/svg.go index 7b8d5d62..7c31cc36 100644 --- a/font/svg.go +++ b/font/svg.go @@ -3,7 +3,10 @@ package font import ( + "bytes" "fmt" + "strconv" + "strings" "github.com/go-text/typesetting/font/opentype/tables" ) @@ -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 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) + } + 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 +} + +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 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("")) + if end == -1 { + return "", "", "", false + } + doc = doc[end+2:] + case bytes.HasPrefix(doc, []byte("")) + if end == -1 { + return "", "", "", false + } + doc = doc[end+3:] + case bytes.HasPrefix(doc, []byte("' 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 +} + +// 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 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 + } + } +} diff --git a/font/svg_test.go b/font/svg_test.go new file mode 100644 index 00000000..f9413082 --- /dev/null +++ b/font/svg_test.go @@ -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 + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {`not xml`, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, 128, 128}}, + {``, font.SVGViewBox{-10, -20, 30, 40}}, + {``, font.SVGViewBox{0, 0, 100, 50.5}}, + {``, font.SVGViewBox{0, 0, 128, 64}}, + {"", font.SVGViewBox{0, 0, 128, 128}}, + // invalid viewBox attributes + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + // width and height attributes + {``, font.SVGViewBox{0, 0, 100, 200}}, + {``, font.SVGViewBox{0, 0, 100, 50}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + // viewBox has precedence over width and height + {``, font.SVGViewBox{0, 0, 128, 128}}, + {``, font.SVGViewBox{0, 0, 100, 200}}, + // prolog, comments and doctype before the root element + {"\uFEFF" + ``, font.SVGViewBox{0, 0, 128, 128}}, + {``, font.SVGViewBox{0, 0, 128, 128}}, + {` + + + `, font.SVGViewBox{0, 0, 128, 128}}, + {` ]>`, font.SVGViewBox{0, 0, 128, 128}}, + // namespace prefix on the root element + {``, font.SVGViewBox{0, 0, 128, 128}}, + // other attributes, possibly with tricky values + {``, 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}) +} From c0a32f6ac9a9638cc822205c8ea108e4493d6ba1 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 4 Jul 2026 02:44:02 +0900 Subject: [PATCH 2/4] [font] address review feedback on the SVG viewport parsing Reject the non-finite number forms accepted by strconv.ParseFloat ("NaN", "Inf", ...) in viewBox, width and height attribute values, falling back to the em-square default instead of propagating them. Ignore brackets and '>' inside quoted literals and comments when skipping a doctype internal subset, so that contents such as "> do not end the scan early. Co-Authored-By: Claude Fable 5 --- font/renderer.go | 2 +- font/svg.go | 47 +++++++++++++++++++++++++++++++++++++++-------- font/svg_test.go | 7 +++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/font/renderer.go b/font/renderer.go index b53516a8..2fa4f98b 100644 --- a/font/renderer.go +++ b/font/renderer.go @@ -65,7 +65,7 @@ type GlyphSVG struct { // and several glyphs may share the same Source Source []byte - // ViewBox is the initial viewport of the SVG document : + // 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 element attributes, diff --git a/font/svg.go b/font/svg.go index 7c31cc36..2b945e1f 100644 --- a/font/svg.go +++ b/font/svg.go @@ -5,6 +5,7 @@ package font import ( "bytes" "fmt" + "math" "strconv" "strings" @@ -81,6 +82,16 @@ func svgViewBox(doc []byte, upem uint16) SVGViewBox { 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) { @@ -92,11 +103,11 @@ func parseSVGViewBox(s string) (SVGViewBox, bool) { } var nums [4]float32 for i, field := range fields { - v, err := strconv.ParseFloat(field, 32) - if err != nil { + v, ok := parseSVGNumber(field) + if !ok { return SVGViewBox{}, false } - nums[i] = float32(v) + nums[i] = v } if nums[2] <= 0 || nums[3] <= 0 { return SVGViewBox{}, false @@ -109,11 +120,11 @@ func parseSVGViewBox(s string) (SVGViewBox, bool) { 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 { + v, ok := parseSVGNumber(s) + if !ok || v <= 0 { return 0, false } - return float32(v), true + return v, true } func isXMLSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' } @@ -162,10 +173,30 @@ func svgRootAttributes(doc []byte) (viewBox, width, height string, ok bool) { // svgDoctypeEnd returns the index just after the closing '>' of the // doctype declaration starting [doc], or -1. +// Brackets, quotes and '>' inside quoted literals and comments are +// ignored, so that an internal subset does not end the scan early. func svgDoctypeEnd(doc []byte) int { - depth := 0 // nesting in the internal subset brackets + depth := 0 // nesting in the internal subset brackets + var quote byte // current quoted literal delimiter, or 0 for i := 2; i < len(doc); i++ { - switch doc[i] { + c := doc[i] + if quote != 0 { + if c == quote { + quote = 0 + } + continue + } + switch c { + case '"', '\'': + quote = c + case '<': + if bytes.HasPrefix(doc[i:], []byte("")) + if end == -1 { + return -1 + } + i += end + 2 + } case '[': depth++ case ']': diff --git a/font/svg_test.go b/font/svg_test.go index f9413082..2137f4c5 100644 --- a/font/svg_test.go +++ b/font/svg_test.go @@ -34,12 +34,17 @@ func TestSVGViewBox(t *testing.T) { {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, // width and height attributes {``, font.SVGViewBox{0, 0, 100, 200}}, {``, font.SVGViewBox{0, 0, 100, 50}}, {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, // viewBox has precedence over width and height {``, font.SVGViewBox{0, 0, 128, 128}}, {``, font.SVGViewBox{0, 0, 100, 200}}, @@ -51,6 +56,8 @@ func TestSVGViewBox(t *testing.T) { `, font.SVGViewBox{0, 0, 128, 128}}, {` ]>`, font.SVGViewBox{0, 0, 128, 128}}, + {`"> ]>`, font.SVGViewBox{0, 0, 128, 128}}, + {` here --> ]>`, font.SVGViewBox{0, 0, 128, 128}}, // namespace prefix on the root element {``, font.SVGViewBox{0, 0, 128, 128}}, // other attributes, possibly with tricky values From ab9e1a2283db661a448a8417e8a353ccbd42daf7 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 5 Jul 2026 00:49:47 +0900 Subject: [PATCH 3/4] [font] parse SVG glyph documents with encoding/xml Replace the dedicated XML prolog scanner with encoding/xml to read the root element attributes. This removes hand-written handling of the byte order mark, XML declaration, comments and doctype internal subsets, delegating it to the standard library. Using a proper XML parser also makes the element namespace available, so the root is now required to be an svg element in the SVG namespace (a missing namespace is tolerated, a foreign one is rejected). Co-Authored-By: Claude Opus 4.8 --- font/export_test.go | 4 +- font/svg.go | 160 ++++++-------------------------------------- font/svg_test.go | 29 +++----- 3 files changed, 34 insertions(+), 159 deletions(-) diff --git a/font/export_test.go b/font/export_test.go index 8e551fb8..ac48223a 100644 --- a/font/export_test.go +++ b/font/export_test.go @@ -2,5 +2,5 @@ package font -// SVGDocViewBox exposes [svgViewBox] for tests. -func SVGDocViewBox(doc []byte, upem uint16) SVGViewBox { return svgViewBox(doc, upem) } +// SVGViewBoxForTest exposes [svgViewBox] for tests. +func SVGViewBoxForTest(doc []byte, upem uint16) SVGViewBox { return svgViewBox(doc, upem) } diff --git a/font/svg.go b/font/svg.go index 2b945e1f..0b5168e2 100644 --- a/font/svg.go +++ b/font/svg.go @@ -4,6 +4,7 @@ package font import ( "bytes" + "encoding/xml" "fmt" "math" "strconv" @@ -127,155 +128,38 @@ func parseSVGLength(s string) (float32, bool) { return v, true } -func isXMLSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' } +// svgNamespace is the namespace of SVG elements. +const svgNamespace = "http://www.w3.org/2000/svg" -// svgRootAttributes scans the prolog of the XML document [doc] and returns -// the raw viewBox, width and height attribute values of the root 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. +// 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) { - // 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("")) - if end == -1 { - return "", "", "", false - } - doc = doc[end+2:] - case bytes.HasPrefix(doc, []byte("")) - if end == -1 { - return "", "", "", false - } - doc = doc[end+3:] - case bytes.HasPrefix(doc, []byte("' of the -// doctype declaration starting [doc], or -1. -// Brackets, quotes and '>' inside quoted literals and comments are -// ignored, so that an internal subset does not end the scan early. -func svgDoctypeEnd(doc []byte) int { - depth := 0 // nesting in the internal subset brackets - var quote byte // current quoted literal delimiter, or 0 - for i := 2; i < len(doc); i++ { - c := doc[i] - if quote != 0 { - if c == quote { - quote = 0 - } - continue - } - switch c { - case '"', '\'': - quote = c - case '<': - if bytes.HasPrefix(doc[i:], []byte("")) - if end == -1 { - return -1 - } - i += end + 2 - } - case '[': - depth++ - case ']': - depth-- - case '>': - if depth <= 0 { - return i + 1 - } + if start, isStart := tok.(xml.StartElement); isStart { + root = start + break } } - return -1 -} - -// 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 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" { + // 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 } - 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) { + for _, attr := range root.Attr { + switch attr.Name.Local { case "viewBox": - viewBox = value + viewBox = attr.Value case "width": - width = value + width = attr.Value case "height": - height = value + height = attr.Value } } + return viewBox, width, height, true } diff --git a/font/svg_test.go b/font/svg_test.go index 2137f4c5..e341f4a9 100644 --- a/font/svg_test.go +++ b/font/svg_test.go @@ -21,14 +21,17 @@ func TestSVGViewBox(t *testing.T) { {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, {`not xml`, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, 128, 128}}, + // viewBox attribute (a missing namespace is tolerated) {``, font.SVGViewBox{0, 0, 128, 128}}, {``, font.SVGViewBox{-10, -20, 30, 40}}, {``, font.SVGViewBox{0, 0, 100, 50.5}}, {``, font.SVGViewBox{0, 0, 128, 64}}, - {"", font.SVGViewBox{0, 0, 128, 128}}, // invalid viewBox attributes {``, font.SVGViewBox{0, 0, upem, upem}}, {``, font.SVGViewBox{0, 0, upem, upem}}, @@ -48,22 +51,10 @@ func TestSVGViewBox(t *testing.T) { // viewBox has precedence over width and height {``, font.SVGViewBox{0, 0, 128, 128}}, {``, font.SVGViewBox{0, 0, 100, 200}}, - // prolog, comments and doctype before the root element - {"\uFEFF" + ``, font.SVGViewBox{0, 0, 128, 128}}, - {``, font.SVGViewBox{0, 0, 128, 128}}, - {` - - - `, font.SVGViewBox{0, 0, 128, 128}}, - {` ]>`, font.SVGViewBox{0, 0, 128, 128}}, - {`"> ]>`, font.SVGViewBox{0, 0, 128, 128}}, - {` here --> ]>`, font.SVGViewBox{0, 0, 128, 128}}, - // namespace prefix on the root element - {``, font.SVGViewBox{0, 0, 128, 128}}, - // other attributes, possibly with tricky values - {``, font.SVGViewBox{0, 0, 128, 128}}, + // content before the root element is skipped + {``, font.SVGViewBox{0, 0, 128, 128}}, } { - got := font.SVGDocViewBox([]byte(test.doc), upem) + got := font.SVGViewBoxForTest([]byte(test.doc), upem) if got != test.expected { t.Fatalf("document %q: expected %v, got %v", test.doc, test.expected, got) } From 7d4636339842e77db12cd86bb2ffc704f1d00106 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 5 Jul 2026 02:44:20 +0900 Subject: [PATCH 4/4] [font] make the SVG viewport test internal to the package Move the test to package font, matching the rest of the package, and call svgViewBox directly instead of through an export bridge. This removes the export_test.go file. Co-Authored-By: Claude Opus 4.8 --- font/export_test.go | 6 ---- font/svg_test.go | 76 +++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 47 deletions(-) delete mode 100644 font/export_test.go diff --git a/font/export_test.go b/font/export_test.go deleted file mode 100644 index ac48223a..00000000 --- a/font/export_test.go +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR BSD-3-Clause - -package font - -// SVGViewBoxForTest exposes [svgViewBox] for tests. -func SVGViewBoxForTest(doc []byte, upem uint16) SVGViewBox { return svgViewBox(doc, upem) } diff --git a/font/svg_test.go b/font/svg_test.go index e341f4a9..dcbcefc8 100644 --- a/font/svg_test.go +++ b/font/svg_test.go @@ -1,13 +1,10 @@ // SPDX-License-Identifier: Unlicense OR BSD-3-Clause -package font_test +package font import ( - "bytes" "testing" - td "github.com/go-text/typesetting-utils/opentype" - "github.com/go-text/typesetting/font" tu "github.com/go-text/typesetting/testutils" ) @@ -15,46 +12,46 @@ func TestSVGViewBox(t *testing.T) { const upem = 1000 for _, test := range []struct { doc string - expected font.SVGViewBox + expected SVGViewBox }{ // no viewport information: default to the em square - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {`not xml`, font.SVGViewBox{0, 0, upem, upem}}, - {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {`not xml`, SVGViewBox{0, 0, upem, upem}}, + {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, 128, 128}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, 128, 128}}, // viewBox attribute (a missing namespace is tolerated) - {``, font.SVGViewBox{0, 0, 128, 128}}, - {``, font.SVGViewBox{-10, -20, 30, 40}}, - {``, font.SVGViewBox{0, 0, 100, 50.5}}, - {``, font.SVGViewBox{0, 0, 128, 64}}, + {``, SVGViewBox{0, 0, 128, 128}}, + {``, SVGViewBox{-10, -20, 30, 40}}, + {``, SVGViewBox{0, 0, 100, 50.5}}, + {``, SVGViewBox{0, 0, 128, 64}}, // invalid viewBox attributes - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, // width and height attributes - {``, font.SVGViewBox{0, 0, 100, 200}}, - {``, font.SVGViewBox{0, 0, 100, 50}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, - {``, font.SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, 100, 200}}, + {``, SVGViewBox{0, 0, 100, 50}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, + {``, SVGViewBox{0, 0, upem, upem}}, // viewBox has precedence over width and height - {``, font.SVGViewBox{0, 0, 128, 128}}, - {``, font.SVGViewBox{0, 0, 100, 200}}, + {``, SVGViewBox{0, 0, 128, 128}}, + {``, SVGViewBox{0, 0, 100, 200}}, // content before the root element is skipped - {``, font.SVGViewBox{0, 0, 128, 128}}, + {``, SVGViewBox{0, 0, 128, 128}}, } { - got := font.SVGViewBoxForTest([]byte(test.doc), upem) + got := svgViewBox([]byte(test.doc), upem) if got != test.expected { t.Fatalf("document %q: expected %v, got %v", test.doc, test.expected, got) } @@ -64,13 +61,10 @@ func TestSVGViewBox(t *testing.T) { 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) + 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 == font.SVGViewBox{0, 0, 1024, 1024}) + tu.Assert(t, glyph.ViewBox == SVGViewBox{0, 0, 1024, 1024}) }