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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/dhconnelly/rtreego v1.2.0
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f
github.com/stretchr/testify v1.11.1
github.com/yuin/gopher-lua v1.1.2
golang.org/x/image v0.43.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiY
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f h1:uLR2GaV0kWYZ3Ns3l3sjtiN+mOWAQadvrL8HXcyKjl0=
github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f/go.mod h1:LZwgIPG9X6nH6j5Ef+xMFspl6Hru4b5EJxzMfeqHYJY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
28 changes: 23 additions & 5 deletions pkg/s100/symbols/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
// and rasterizes them in pure Go. It is the single source of truth for
// SVG→raster, used by the sprite-atlas builder.
//
// Two oksvg defects are worked around here: it ignores a non-zero viewBox
// origin (we normalize to "0 0 W H" and wrap the content in a translate), and
// it applies stroke-width in device px without scaling by the draw transform
// (we pre-multiply stroke-width by the px/mm scale).
// Three oksvg/rasterx defects are worked around here: oksvg ignores a non-zero
// viewBox origin (we normalize to "0 0 W H" and wrap the content in a
// translate); it applies stroke-width in device px without scaling by the draw
// transform (we pre-multiply stroke-width by the px/mm scale); and it ignores
// the fill-rule attribute while rasterx's ScannerGV cannot do even-odd fills at
// all (we force even-odd winding and rasterize through scanFT, which honours
// it) so the catalogue's even-odd danger symbols don't fill their holes solid.
package symbols

import (
Expand All @@ -23,6 +26,7 @@ import (

"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
"github.com/srwiley/scanFT"
)

var cssRuleRE = regexp.MustCompile(`\.([A-Za-z0-9_]+)\s*\{([^}]*)\}`)
Expand Down Expand Up @@ -58,14 +62,28 @@ func Render(svg []byte, css map[string]string, pxPerMM float64) (*Rendered, erro
if err != nil {
return nil, err
}
// oksvg ignores the SVG fill-rule attribute and always fills with the
// nonzero winding rule. Every S-101 symbol is authored with
// fill-rule="evenodd": the danger hatch of ISODGR01/DANGER0x and similar
// glyphs is a single compound path whose inner subpath is a hole, and
// nonzero winding fills that hole solid (an ISODGR01 with no star in it).
// Force even-odd winding on every path so the holes render. For simple,
// non-self-intersecting paths even-odd and nonzero are identical, so this
// is safe for the rest of the set.
for i := range icon.SVGPaths {
icon.SVGPaths[i].UseNonZeroWinding = false
}
w := int(math.Ceil(vb[2] * pxPerMM))
h := int(math.Ceil(vb[3] * pxPerMM))
if w < 1 || h < 1 {
return nil, fmt.Errorf("degenerate viewBox %gx%g", vb[2], vb[3])
}
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
icon.SetTarget(0, 0, float64(w), float64(h))
scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())
// scanFT (freetype-backed) honours the even-odd winding set above;
// rasterx's own ScannerGV silently ignores it (it is nonzero-only), which
// fills the danger-symbol holes solid.
scanner := scanFT.NewScannerFT(w, h, scanFT.NewRGBAPainter(rgba))
icon.Draw(rasterx.NewDasher(w, h, scanner), 1.0)

// rasterx writes alpha-premultiplied RGBA; draw.Src into NRGBA converts to
Expand Down
38 changes: 38 additions & 0 deletions pkg/s100/symbols/symbols_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package symbols

import "testing"

// TestRenderEvenOddHole guards the fill-rule="evenodd" handling. Every S-101
// symbol is authored with even-odd winding; the ISODGR01/DANGER0x danger glyphs
// carve their inner shape out of the outer body with a single compound path
// whose inner subpath is a hole. oksvg ignores fill-rule and fills nonzero,
// which would fill that hole solid (the "ISODGR01 with no star in it" bug).
func TestRenderEvenOddHole(t *testing.T) {
// Outer 8x8 square with a concentric 4x4 inner subpath, both wound the same
// way. Under nonzero winding the inner square fills solid; under even-odd it
// is a hole. fill-rule="evenodd" sits on the root <svg> exactly as the
// catalogue authors it.
svg := []byte(`<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 10 10" fill-rule="evenodd">
<path class="fX" d="M -4,-4 L 4,-4 L 4,4 L -4,4 L -4,-4 Z M -2,-2 L 2,-2 L 2,2 L -2,2 L -2,-2 Z"/>
</svg>`)
css := map[string]string{"fX": "fill:#C045D1"}

const pxPerMM = 10
r, err := Render(svg, css, pxPerMM)
if err != nil {
t.Fatalf("Render: %v", err)
}

// Pivot is the SVG origin (0,0); the hole is centred there.
cx, cy := int(r.PivotX), int(r.PivotY)
if _, _, _, a := r.Image.At(cx, cy).RGBA(); a != 0 {
t.Errorf("centre pixel (%d,%d) alpha=%d, want 0 (even-odd hole filled solid — fill-rule ignored)", cx, cy, a>>8)
}

// A pixel in the ring between the two squares must be painted.
rx, ry := int(r.PivotX+3*pxPerMM), cy // svg (3,0)
if _, _, _, a := r.Image.At(rx, ry).RGBA(); a == 0 {
t.Errorf("ring pixel (%d,%d) is transparent, want painted", rx, ry)
}
}