Skip to content

Commit

Permalink
fix svg text handling
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Nov 18, 2024
1 parent 1442f0c commit da6385e
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 59 deletions.
2 changes: 1 addition & 1 deletion backend/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type TextDrawing struct {
// Matrix return the transformation scaling the text by [FontSize],
// translating if to (X, Y) and applying the [Angle] rotation
func (td TextDrawing) Matrix() matrix.Transform {
mat := matrix.New(td.FontSize*td.ScaleX, 0, 0, -td.FontSize, td.X, td.Y)
mat := matrix.New(td.ScaleX, 0, 0, -1, td.X, td.Y)
if td.Angle != 0 { // avoid useless multiplication if angle == 0
mat.RightMultBy(matrix.Rotation(td.Angle))
}
Expand Down
13 changes: 11 additions & 2 deletions svg/bounding_box.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@ type Rectangle struct {
X, Y, Width, Height Fl
}

var (
inf = Fl(math.Inf(+1))
emptyBbox = Rectangle{inf, inf, 0, 0}
)

// increase the rectangle to contain (x,y)
func (r *Rectangle) add(x, y Fl) {
if *r == emptyBbox {
r.X, r.Y = x, y
return
}
minX, minY := utils.MinF(r.X, x), utils.MinF(r.Y, y)
maxX, maxY := utils.MaxF(r.X+r.Width, x), utils.MaxF(r.Y+r.Height, y)
r.X, r.Y, r.Width, r.Height = minX, minY, maxX-minX, maxY-minY
Expand Down Expand Up @@ -96,8 +105,8 @@ func (svg) boundingBox(_ *attributes, _ drawingDims) (Rectangle, bool) {
return Rectangle{}, false
}

func (textSpan) boundingBox(_ *attributes, _ drawingDims) (Rectangle, bool) {
return Rectangle{}, false
func (t textSpan) boundingBox(_ *attributes, _ drawingDims) (Rectangle, bool) {
return t.textBoundingBox, t.textBoundingBox != emptyBbox
}

// bounding box for bezier curves
Expand Down
60 changes: 45 additions & 15 deletions svg/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package svg
import (
"fmt"
"math"
"strings"

"github.com/benoitkugler/webrender/backend"
pr "github.com/benoitkugler/webrender/css/properties"
"github.com/benoitkugler/webrender/logger"
"github.com/benoitkugler/webrender/matrix"
"github.com/benoitkugler/webrender/utils"
Expand Down Expand Up @@ -330,8 +330,8 @@ func (s svg) draw(dst backend.Canvas, attrs *attributes, img *SVGImage, dims dra
type image struct {
// width, height are common attributes

img backend.Image
preserveAspectRatio [2]string
img backend.Image
preserveRatio preserveAspectRatio
}

func newImage(node *cascadedNode, context *svgContext) (drawable, error) {
Expand All @@ -350,24 +350,54 @@ func newImage(node *cascadedNode, context *svgContext) (drawable, error) {
return nil, fmt.Errorf("failed to load image: %s", err)
}

aspectRatio, has := node.attrs["preserveAspectRatio"]
if !has {
aspectRatio = "xMidYMid"
}
l := strings.Fields(aspectRatio)
if len(l) > 2 {
return nil, fmt.Errorf("invalid preserveAspectRatio property: %s", aspectRatio)
}
var out image
copy(out.preserveAspectRatio[:], l)
out.img = img
out.preserveRatio = node.attrs.aspectRatio()

return out, nil
}

func (img image) draw(dst backend.Canvas, _ *attributes, _ *SVGImage, _ drawingDims) []vertex {
// TODO: support nested images
logger.WarningLogger.Println("nested image are not supported")
func (img image) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dims drawingDims) []vertex {
x, y := dims.point(attrs.x, attrs.y)
dst.State().Transform(matrix.Translation(x, y))
// base_url = node.get("{http://www.w3.org/XML/1998/namespace}base")
// url = node.get_href(base_url || svg.url)
// image = svg.context.get_image_from_uri(url, "image/*")
// if image == nil {
// return
// }

width, height := dims.point(attrs.width, attrs.height)
intrinsicWidth, intrinsicHeight, intrinsicRatio := img.img.GetIntrinsicSize(1, pr.Float(dims.fontSize))
if intrinsicWidth == nil && intrinsicHeight == nil {
if intrinsicRatio == nil || (width == 0 && height == 0) {
intrinsicWidth, intrinsicHeight = pr.Float(300), pr.Float(150)
} else if width == 0 {
intrinsicWidth, intrinsicHeight = intrinsicRatio.V()*pr.Float(height), pr.Float(height)
} else {
intrinsicWidth, intrinsicHeight = pr.Float(width), pr.Float(width)/intrinsicRatio.V()
}
} else if intrinsicWidth == nil {
intrinsicWidth = intrinsicRatio.V() * intrinsicHeight.V()
} else if intrinsicHeight == nil {
intrinsicHeight = intrinsicWidth.V() / intrinsicRatio.V()
}
intrinsic := Rectangle{0, 0, Fl(intrinsicWidth.V()), Fl(intrinsicHeight.V())}
if width == 0 {
width = intrinsic.Width
}
if height == 0 {
height = intrinsic.Height
}

scale_x, scale_y, translate_x, translate_y := img.preserveRatio.resolveTransforms(width, height, &intrinsic, nil)
dst.Rectangle(0, 0, width, height)
dst.State().Clip(false)
dst.OnNewStack(func() {
dst.State().Transform(matrix.Transform{A: scale_x, D: scale_y, E: translate_x, F: translate_y})
img.img.Draw(dst, svg.textContext, intrinsic.Width, intrinsic.Height, "auto")
})

return nil
}

Expand Down
19 changes: 14 additions & 5 deletions svg/elements_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ type textSpan struct {
textAnchor, displayAnchor anchor

baseline baseline

isText bool // only true for tag 'text'
textBoundingBox Rectangle
}

func newTextSpan(node *cascadedNode, tree *svgContext) (drawable, error) {
func newTextSpan(node *cascadedNode) (drawable, error) {
var out textSpan

out.text = string(node.text)
Expand Down Expand Up @@ -91,10 +94,14 @@ func newTextSpan(node *cascadedNode, tree *svgContext) (drawable, error) {

out.lengthAdjust = node.attrs["lengthAdjust"] == "spacingAndGlyphs"

return out, nil
out.isText = node.tag == "text"
out.textBoundingBox = emptyBbox

return &out, nil
}

func (t textSpan) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dims drawingDims) []vertex {
// returns the text bounding box
func (t *textSpan) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dims drawingDims) []vertex {
t.style.SetFontSize(pr.FToV(dims.fontSize))

splitted := text.SplitFirstLine(t.text, t.style, svg.textContext, pr.Inf, false, true)
Expand All @@ -120,7 +127,7 @@ func (t textSpan) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dim
letterSpacing := dims.length(t.letterSpacing)
textLength := dims.length(t.textLength)
scaleX := Fl(1.)
if textLength != 0 && t.text == "" {
if textLength != 0 && t.text != "" {
// calculate the number of spaces to be considered for the text
spacesCount := Fl(len(t.text) - 1)
if t.lengthAdjust {
Expand Down Expand Up @@ -240,7 +247,7 @@ func (t textSpan) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dim

layout.ApplyJustification()

doFill, doStroke := svg.setupPaint(dst, &svgNode{graphicContent: t, attributes: *attrs}, dims)
doFill, doStroke := svg.applyPainters(dst, &svgNode{graphicContent: t, attributes: *attrs}, dims)
dst.State().SetTextPaint(newPaintOp(doFill, doStroke, false))
texts = append(texts,
drawer.CreateFirstLine(layout, "none", pr.TaggedString{Tag: pr.None}, scaleX, xPosition, yPosition, angle))
Expand All @@ -250,5 +257,7 @@ func (t textSpan) draw(dst backend.Canvas, attrs *attributes, svg *SVGImage, dim
dst.DrawText(texts)
})

t.textBoundingBox = bbox

return nil
}
3 changes: 2 additions & 1 deletion svg/paint.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ func (dims drawingDims) resolveDashes(dashArray []Value, dashOffset Value) ([]Fl
return dashes, offset
}

func (svg *SVGImage) setupPaint(dst backend.Canvas, node *svgNode, dims drawingDims) (doFill, doStroke bool) {
// apply fill and stroke painters
func (svg *SVGImage) applyPainters(dst backend.Canvas, node *svgNode, dims drawingDims) (doFill, doStroke bool) {
strokeWidth := dims.length(node.strokeWidth)
doFill = node.fill.valid
doStroke = node.stroke.valid && strokeWidth > 0
Expand Down
12 changes: 6 additions & 6 deletions svg/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,17 @@ func parseValues(dataPoints string) (points []Value, err error) {

// parses opacity, stroke-opacity, fill-opacity attributes,
// returning 1 as a default value
func parseOpacity(op string) (Fl, error) {
op = strings.TrimSpace(op)
if op == "" {
func parseOpacity(value string) (Fl, error) {
value = strings.TrimSpace(value)
if value == "" {
return 1, nil
}
ratio := 1.
if strings.HasSuffix(op, "%") {
if strings.HasSuffix(value, "%") {
ratio = 100
op = strings.TrimSpace(op[:len(op)-2])
value = strings.TrimSpace(value[:len(value)-1])
}
out, err := strconv.ParseFloat(op, 32)
out, err := strconv.ParseFloat(value, 32)
return Fl(out / ratio), err
}

Expand Down
56 changes: 29 additions & 27 deletions svg/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ func (svg *SVGImage) drawNode(dst backend.Canvas, node *svgNode, dims drawingDim
}

// Handle text anchor
text, isText := node.graphicContent.(textSpan)
text, isText := node.graphicContent.(*textSpan)
var textAnchor anchor
if isText {
if isText && text.isText {
textAnchor = text.textAnchor
if len(node.children) != 0 && text.text == "" {
child, _ := node.children[0].graphicContent.(textSpan)
child, _ := node.children[0].graphicContent.(*textSpan)
textAnchor = child.textAnchor
}

Expand All @@ -148,26 +148,35 @@ func (svg *SVGImage) drawNode(dst backend.Canvas, node *svgNode, dims drawingDim
// 2) apply the path operation
// 3) conclude by calling Paint

doFill, doStroke := svg.setupPaint(dst, node, dims)
doFill, doStroke := svg.applyPainters(dst, node, dims)

var vertices []vertex
if visible && node.graphicContent != nil {
vertices = node.graphicContent.draw(dst, &node.attributes, svg, dims)
}

// draw markers
if len(vertices) != 0 {
svg.drawMarkers(dst, vertices, node, dims, paint)
// then recurse
if display {
for _, child := range node.children {
svg.drawNode(dst, child, dims, paint)

childText, isChildText := child.graphicContent.(*textSpan)
if visibleTextChild := (isText && isChildText && child.visible); visibleTextChild {
if bb := childText.textBoundingBox; bb != emptyBbox {
text.textBoundingBox.union(bb)
}
}
}
}

// Handle text anchor
if isText && (textAnchor == middle || textAnchor == end) {
if isText && text.isText && (textAnchor == middle || textAnchor == end) {
// pop stream
group := dst
dst = originalDst2

dst.OnNewStack(func() {
if bbox := node.textBoundingBox; bbox != (Rectangle{}) {
if bbox := text.textBoundingBox; bbox != emptyBbox {
x, y, width, height := bbox.X, bbox.Y, bbox.Width, bbox.Height
// Add extra space to include ink extents
group.SetBoundingBox(x-dims.fontSize, y-dims.fontSize, x+width+dims.fontSize, y+height+dims.fontSize)
Expand All @@ -188,15 +197,13 @@ func (svg *SVGImage) drawNode(dst backend.Canvas, node *svgNode, dims drawingDim

// do the actual painting :
// paint by filling and stroking the given node onto the graphic target
if _, isText := node.graphicContent.(textSpan); paint && !isText {
if paint && !isText {
dst.Paint(newPaintOp(doFill, doStroke, node.isFillEvenOdd))
}

// then recurse
if display {
for _, child := range node.children {
svg.drawNode(dst, child, dims, paint)
}
// draw markers
if len(vertices) != 0 {
svg.drawMarkers(dst, vertices, node, dims, paint)
}

// apply opacity group and restore original target
Expand Down Expand Up @@ -302,11 +309,8 @@ func (svg *SVGImage) drawMarkers(dst backend.Canvas, vertices []vertex, node *sv
// draw marker path
for _, child := range marker.children {
dst.OnNewStack(func() {
mat := matrix.Rotation(angle)
mat.LeftMultBy(matrix.Scaling(scaleX, scaleY))
mat.LeftMultBy(matrix.Translation(vertex.x, vertex.y))
mat.LeftMultBy(matrix.Translation(translateX, translateY))
dst.State().Transform(mat)
dst.State().Transform(matrix.Transform{A: scaleX, D: scaleY, E: vertex.x, F: vertex.y})
dst.State().Transform(matrix.Translation(-translateX, -translateY))

overflow := marker.overflow
if overflow == "hidden" || overflow == "scroll" {
Expand All @@ -321,10 +325,10 @@ func (svg *SVGImage) drawMarkers(dst backend.Canvas, vertices []vertex, node *sv
}
}

// compute scale and translation needed to preserve ratio
// translate is optional
// for marker tags, translate should be the resolved refX and refY values
// otherwise, it should be nil
// compute scale and translation needed to preserve ratio.
// [translate] is optional :
// for marker tags, [translate] should be the resolved refX and refY values;
// otherwise, it should be nil.
func (pr preserveAspectRatio) resolveTransforms(width, height Fl, viewbox *Rectangle, translate *point) (scaleX, scaleY, translateX, translateY Fl) {
if viewbox == nil {
return 1, 1, 0, 0
Expand Down Expand Up @@ -577,8 +581,6 @@ type attributes struct {

isFillEvenOdd bool
display, visible bool

textBoundingBox Rectangle
}

func (tree *svgContext) processNode(node *cascadedNode, defs definitions) (*svgNode, error) {
Expand Down Expand Up @@ -686,7 +688,7 @@ func (tree *svgContext) processGraphicNode(node *cascadedNode, children []*svgNo
case "svg":
out.graphicContent, err = newSvg(node, tree)
case "a", "text", "textPath", "tspan":
out.graphicContent, err = newTextSpan(node, tree)
out.graphicContent, err = newTextSpan(node)
isText = true
}

Expand Down
4 changes: 2 additions & 2 deletions svg/svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ func TestParseText1(t *testing.T) {
t.Fatal("unexpected children")
}

te, ok := img.root.children[0].graphicContent.(textSpan)
te, ok := img.root.children[0].graphicContent.(*textSpan)
if !ok {
t.Fatalf("unexpected content %T", img.root.children[0].graphicContent)
}
Expand Down Expand Up @@ -423,7 +423,7 @@ func TestParseText2(t *testing.T) {
t.Fatal("unexpected children")
}

te, ok := img.root.children[0].graphicContent.(textSpan)
te, ok := img.root.children[0].graphicContent.(*textSpan)
if !ok {
t.Fatalf("unexpected content %T", img.root.children[0].graphicContent)
}
Expand Down

0 comments on commit da6385e

Please sign in to comment.