blob: 57aa45139153f9cd9f141624297e312eda0eaaeb [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package iconvg
import (
"bytes"
"flag"
"image/color"
"math"
"os"
"path/filepath"
"runtime"
"strconv"
"testing"
"golang.org/x/image/math/f32"
)
// updateFlag controls whether to overwrite testdata files during tests.
// This can be useful when adding new testdataTestCases.
var updateFlag = flag.Bool("update", false, "Overwrite testdata files.")
func testEncode(t *testing.T, e *Encoder, wantFilename string) {
got, err := e.Bytes()
if err != nil {
t.Fatalf("encoding: %v", err)
}
if *updateFlag {
if err := os.WriteFile(filepath.FromSlash(wantFilename), got, 0666); err != nil {
t.Fatalf("WriteFile: %v", err)
}
return
}
want, err := os.ReadFile(filepath.FromSlash(wantFilename))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if !bytes.Equal(got, want) {
// The IconVG encoder is expected to be completely deterministic across all
// platforms and Go compilers, so check that we get exactly the right bytes.
//
// If we get slightly different bytes on some supported platform (for example,
// a new GOOS/GOARCH port, or a different but spec-compliant Go compiler) due
// to non-determinism in floating-point math, the encoder needs to be fixed.
//
// See golang.org/issue/43219#issuecomment-748531069.
t.Errorf("\ngot %d bytes (on GOOS=%s GOARCH=%s, using compiler %q):\n% x\nwant %d bytes:\n% x",
len(got), runtime.GOOS, runtime.GOARCH, runtime.Compiler, got, len(want), want)
gotDisasm, err1 := disassemble(got)
wantDisasm, err2 := disassemble(want)
if err1 == nil && err2 == nil {
diffLines(t, string(gotDisasm), string(wantDisasm))
}
}
}
func TestEncodeActionInfo(t *testing.T) {
for _, res := range []string{"lores", "hires"} {
var e Encoder
e.Reset(Metadata{
ViewBox: Rectangle{
Min: f32.Vec2{-24, -24},
Max: f32.Vec2{+24, +24},
},
Palette: DefaultPalette,
})
e.HighResolutionCoordinates = res == "hires"
e.StartPath(0, 0, -20)
e.AbsCubeTo(-11.05, -20, -20, -11.05, -20, 0)
e.RelSmoothCubeTo(8.95, 20, 20, 20)
e.RelSmoothCubeTo(20, -8.95, 20, -20)
e.AbsSmoothCubeTo(11.05, -20, 0, -20)
e.ClosePathRelMoveTo(2, 30)
e.RelHLineTo(-4)
e.AbsVLineTo(-2)
e.RelHLineTo(4)
e.RelVLineTo(12)
e.ClosePathRelMoveTo(0, -16)
e.RelHLineTo(-4)
e.RelVLineTo(-4)
e.RelHLineTo(4)
e.RelVLineTo(4)
e.ClosePathEndPath()
testEncode(t, &e, "testdata/action-info."+res+".ivg")
}
}
func TestEncodeArcs(t *testing.T) {
var e Encoder
e.SetCReg(1, false, RGBAColor(color.RGBA{0xff, 0x00, 0x00, 0xff}))
e.SetCReg(2, false, RGBAColor(color.RGBA{0xff, 0xff, 0x00, 0xff}))
e.SetCReg(3, false, RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0xff}))
e.SetCReg(4, false, RGBAColor(color.RGBA{0x00, 0x00, 0x80, 0xff}))
e.StartPath(1, -10, 0)
e.RelHLineTo(-15)
e.RelArcTo(15, 15, 0, true, false, 15, -15)
e.ClosePathEndPath()
e.StartPath(2, -14, -4)
e.RelVLineTo(-15)
e.RelArcTo(15, 15, 0, false, false, -15, 15)
e.ClosePathEndPath()
const thirtyDegrees = 30.0 / 360
e.StartPath(3, -15, 30)
e.RelLineTo(5.0, -2.5)
e.RelArcTo(2.5, 2.5, -thirtyDegrees, false, true, 5.0, -2.5)
e.RelLineTo(5.0, -2.5)
e.RelArcTo(2.5, 5.0, -thirtyDegrees, false, true, 5.0, -2.5)
e.RelLineTo(5.0, -2.5)
e.RelArcTo(2.5, 7.5, -thirtyDegrees, false, true, 5.0, -2.5)
e.RelLineTo(5.0, -2.5)
e.RelArcTo(2.5, 10.0, -thirtyDegrees, false, true, 5.0, -2.5)
e.RelLineTo(5.0, -2.5)
e.AbsVLineTo(30)
e.ClosePathEndPath()
for largeArc := 0; largeArc <= 1; largeArc++ {
for sweep := 0; sweep <= 1; sweep++ {
e.StartPath(4, 10+8*float32(sweep), -28+8*float32(largeArc))
e.RelArcTo(6, 3, 0, largeArc != 0, sweep != 0, 6, 3)
e.ClosePathEndPath()
}
}
testEncode(t, &e, "testdata/arcs.ivg")
}
func TestEncodeBlank(t *testing.T) {
var e Encoder
testEncode(t, &e, "testdata/blank.ivg")
}
var cowbellGradients = []struct {
radial bool
// Linear gradient coefficients.
x1, y1 float32
x2, y2 float32
tx, ty float32
// Radial gradient coefficients.
cx, cy, r float32
transform f32.Aff3
stops []GradientStop
}{{
// The 0th element is unused.
}, {
radial: true,
cx: -102.14,
cy: 20.272,
r: 18.012,
transform: f32.Aff3{
.33050, -.50775, 65.204,
.17296, .97021, 16.495,
},
stops: []GradientStop{
{Offset: 0, Color: color.RGBA{0xed, 0xd4, 0x00, 0xff}},
{Offset: 1, Color: color.RGBA{0xfc, 0xe9, 0x4f, 0xff}},
},
}, {
radial: true,
cx: -97.856,
cy: 26.719,
r: 18.61,
transform: f32.Aff3{
.35718, -.11527, 51.072,
.044280, .92977, 7.6124,
},
stops: []GradientStop{
{Offset: 0, Color: color.RGBA{0xed, 0xd4, 0x00, 0xff}},
{Offset: 1, Color: color.RGBA{0xfc, 0xe9, 0x4f, 0xff}},
},
}, {
x1: -16.183,
y1: 35.723,
x2: -18.75,
y2: 29.808,
tx: 48.438,
ty: -.22321,
stops: []GradientStop{
{Offset: 0, Color: color.RGBA{0x39, 0x21, 0x00, 0xff}},
{Offset: 1, Color: color.RGBA{0x0f, 0x08, 0x00, 0xff}},
},
}}
var cowbellSVGData = []struct {
rgba color.RGBA
gradient int
d string
transform *f32.Aff3
}{{
gradient: 2,
d: "m5.6684 17.968l.265-4.407 13.453 19.78.301 8.304-14.019-23.677z",
}, {
gradient: 1,
d: "m19.299 33.482l-13.619-19.688 3.8435-2.684.0922-2.1237 4.7023-2.26 2.99 1.1274 4.56-1.4252 20.719 16.272-23.288 10.782z",
}, {
rgba: color.RGBA{0xfd * 127 / 255, 0xee * 127 / 255, 0x74 * 127 / 255, 127},
d: "m19.285 32.845l-13.593-19.079 3.995-2.833.1689-2.0377 1.9171-.8635 18.829 18.965-11.317 5.848z",
}, {
rgba: color.RGBA{0xc4, 0xa0, 0x00, 0xff},
d: "m19.211 40.055c-.11-.67-.203-2.301-.205-3.624l-.003-2.406-2.492-3.769c-3.334-5.044-11.448-17.211-9.6752-14.744.3211.447 1.6961 2.119 2.1874 2.656.4914.536 1.3538 1.706 1.9158 2.6 2.276 3.615 8.232 12.056 8.402 12.056.1 0 10.4-5.325 11.294-5.678.894-.354 11.25-4.542 11.45-4.342.506.506 1.27 7.466.761 8.08-.392.473-5.06 3.672-10.256 6.121-5.195 2.45-11.984 4.269-12.594 4.269-.421 0-.639-.338-.785-1.219z",
}, {
gradient: 3,
d: "m19.825 33.646c.422-.68 10.105-5.353 10.991-5.753s9.881-4.123 10.468-4.009c.512.099.844 6.017.545 6.703-.23.527-8.437 4.981-9.516 5.523-1.225.616-11.642 4.705-12.145 4.369-.553-.368-.707-6.245-.343-6.833z",
}, {
rgba: color.RGBA{0x00, 0x00, 0x00, 0xff},
d: "m21.982 5.8789-4.865 1.457-2.553-1.1914-5.3355 2.5743l-.015625.29688-.097656 1.8672-4.1855 2.7383.36719 4.5996.054687.0957s3.2427 5.8034 6.584 11.654c1.6707 2.9255 3.3645 5.861 4.6934 8.0938.66442 1.1164 1.2366 2.0575 1.6719 2.7363.21761.33942.40065.6121.54883.81641.07409.10215.13968.18665.20312.25976.06345.07312.07886.13374.27148.22461.27031.12752.38076.06954.54102.04883.16025-.02072.34015-.05724.55078-.10938.42126-.10427.95998-.26728 1.584-.4707 1.248-.40685 2.8317-.97791 4.3926-1.5586 3.1217-1.1614 6.1504-2.3633 6.1504-2.3633l.02539-.0098.02539-.01367s2.5368-1.3591 5.1211-2.8027c1.2922-.72182 2.5947-1.4635 3.6055-2.0723.50539-.30438.93732-.57459 1.2637-.79688.16318-.11114.29954-.21136.41211-.30273.11258-.09138.19778-.13521.30273-.32617.16048-.292.13843-.48235.1543-.78906s.01387-.68208.002-1.1094c-.02384-.8546-.09113-1.9133-.17188-2.9473-.161-2.067-.373-4.04-.373-4.04l-.021-.211-20.907-16.348zm-.209 1.1055 20.163 15.766c.01984.1875.19779 1.8625.34961 3.8066.08004 1.025.14889 2.0726.17188 2.8965.01149.41192.01156.76817-.002 1.0293-.01351.26113-.09532.47241-.0332.35938.05869-.10679.01987-.0289-.05664.0332s-.19445.14831-.34375.25c-.29859.20338-.72024.46851-1.2168.76758-.99311.59813-2.291 1.3376-3.5781 2.0566-2.5646 1.4327-5.0671 2.7731-5.0859 2.7832-.03276.01301-3.0063 1.1937-6.0977 2.3438-1.5542.5782-3.1304 1.1443-4.3535 1.543-.61154.19936-1.1356.35758-1.5137.45117-.18066.04472-.32333.07255-.41992.08594-.02937-.03686-.05396-.06744-.0957-.125-.128-.176-.305-.441-.517-.771-.424-.661-.993-1.594-1.655-2.705-1.323-2.223-3.016-5.158-4.685-8.08-3.3124-5.8-6.4774-11.465-6.5276-11.555l-.3008-3.787 4.1134-2.692.109-2.0777 4.373-2.1133 2.469 1.1523 4.734-1.4179z",
}}
func inv(x *f32.Aff3) f32.Aff3 {
invDet := 1 / (x[0]*x[4] - x[1]*x[3])
return f32.Aff3{
+x[4] * invDet,
-x[1] * invDet,
(x[1]*x[5] - x[2]*x[4]) * invDet,
-x[3] * invDet,
+x[0] * invDet,
(x[2]*x[3] - x[0]*x[5]) * invDet,
}
}
func TestEncodeCowbell(t *testing.T) {
var e Encoder
e.Reset(Metadata{
ViewBox: Rectangle{
Min: f32.Vec2{0, 0},
Max: f32.Vec2{+48, +48},
},
Palette: DefaultPalette,
})
for _, data := range cowbellSVGData {
if data.rgba != (color.RGBA{}) {
e.SetCReg(0, false, RGBAColor(data.rgba))
} else if data.gradient != 0 {
g := cowbellGradients[data.gradient]
if g.radial {
iform := inv(&g.transform)
iform[2] -= g.cx
iform[5] -= g.cy
for i := range iform {
iform[i] /= g.r
}
e.SetGradient(10, 10, true, iform, GradientSpreadPad, g.stops)
} else {
x1 := g.x1 + g.tx
y1 := g.y1 + g.ty
x2 := g.x2 + g.tx
y2 := g.y2 + g.ty
e.SetLinearGradient(10, 10, x1, y1, x2, y2, GradientSpreadPad, g.stops)
}
}
if err := encodePathData(&e, data.d, 0, false); err != nil {
t.Fatal(err)
}
}
testEncode(t, &e, "testdata/cowbell.ivg")
}
func TestEncodeElliptical(t *testing.T) {
var e Encoder
const (
cx, cy = -20, -10
rx, ry = 0, 24
sx, sy = 30, 15
)
e.SetEllipticalGradient(10, 10, cx, cy, rx, ry, sx, sy, GradientSpreadReflect, []GradientStop{
{Offset: 0, Color: color.RGBA{0xc0, 0x00, 0x00, 0xff}},
{Offset: 1, Color: color.RGBA{0x00, 0x00, 0xc0, 0xff}},
})
e.StartPath(0, -32, -32)
e.AbsHLineTo(+32)
e.AbsVLineTo(+32)
e.AbsHLineTo(-32)
e.ClosePathEndPath()
e.SetCReg(0, false, RGBAColor(color.RGBA{0xff, 0xff, 0xff, 0xff}))
diamond := func(x, y float32) {
e.StartPath(0, x-1, y)
e.AbsLineTo(x, y-1)
e.AbsLineTo(x+1, y)
e.AbsLineTo(x, y+1)
e.ClosePathEndPath()
}
diamond(cx, cy)
diamond(cx+rx, cy+ry)
diamond(cx+sx, cy+sy)
testEncode(t, &e, "testdata/elliptical.ivg")
}
var faviconColors = []color.RGBA{
{0x76, 0xe1, 0xfe, 0xff},
{0x38, 0x4e, 0x54, 0xff},
{0xff, 0xff, 0xff, 0xff},
{0x17, 0x13, 0x11, 0xff},
{0x00, 0x00, 0x00, 0x54},
{0xff, 0xfc, 0xfb, 0xff},
{0xc3, 0x8c, 0x74, 0xff},
{0x23, 0x20, 0x1f, 0xff},
}
var faviconSVGData = []struct {
faviconColorsIndex int
d string
}{{
faviconColorsIndex: 1,
d: "m16.092 1.002c-1.1057.01-2.2107.048844-3.3164.089844-2.3441.086758-4.511.88464-6.2832 2.1758a3.8208 3.5794 29.452 0 0 -.8947 -.6856 3.8208 3.5794 29.452 0 0 -5.0879 1.2383 3.8208 3.5794 29.452 0 0 1.5664 4.9961 3.8208 3.5794 29.452 0 0 .3593 .1758c-.2784.9536-.4355 1.9598-.4355 3.0078v20h28v-20c0-1.042-.152-2.0368-.418-2.9766a3.5794 3.8208 60.548 0 0 .43359 -.20703 3.5794 3.8208 60.548 0 0 1.5684 -4.9961 3.5794 3.8208 60.548 0 0 -5.0879 -1.2383 3.5794 3.8208 60.548 0 0 -.92969 .72461c-1.727-1.257-3.843-2.0521-6.1562-2.2148-1.1058-.078-2.2126-.098844-3.3184-.089844z",
}, {
faviconColorsIndex: 0,
d: "m16 3c-4.835 0-7.9248 1.0791-9.7617 2.8906-.4777-.4599-1.2937-1.0166-1.6309-1.207-.9775-.5520-2.1879-.2576-2.7051.6582-.5171.9158-.1455 2.1063.8321 2.6582.2658.1501 1.2241.5845 1.7519.7441-.3281.9946-.4863 2.0829-.4863 3.2559v20h24c-.049-7.356 0-18 0-20 0-1.209-.166-2.3308-.516-3.3496.539-.2011 1.243-.5260 1.463-.6504.978-.5519 1.351-1.7424.834-2.6582s-1.729-1.2102-2.707-.6582c-.303.1711-.978.6356-1.463 1.0625-1.854-1.724-4.906-2.7461-9.611-2.7461z",
}, {
faviconColorsIndex: 1,
d: "m3.0918 5.9219c-.060217.00947-.10772.020635-.14648.033203-.019384.00628-.035462.013581-.052734.021484-.00864.00395-.019118.00825-.03125.015625-.00607.00369-.011621.00781-.021484.015625-.00493.00391-.017342.015389-.017578.015625-.0002366.0002356-.025256.031048-.025391.03125a.19867 .19867 0 0 0 .26367 .28320c.0005595-.0002168.00207-.00128.00391-.00195a.19867 .19867 0 0 0 .00391 -.00195c.015939-.00517.045148-.013113.085937-.019531.081581-.012836.20657-.020179.36719.00391.1020.0152.2237.0503.3535.0976-.3277.0694-.5656.1862-.7227.3145-.1143.0933-.1881.1903-.2343.2695-.023099.0396-.039499.074216-.050781.10547-.00564.015626-.00989.029721-.013672.046875-.00189.00858-.00458.017085-.00586.03125-.0006392.00708-.0005029.014724 0 .027344.0002516.00631.00192.023197.00195.023437.0000373.0002412.0097.036937.00977.037109a.19867 .19867 0 0 0 .38477 -.039063 .19867 .19867 0 0 0 0 -.00195c.00312-.00751.00865-.015947.017578-.03125.0230-.0395.0660-.0977.1425-.1601.1530-.1250.4406-.2702.9863-.2871a.19930 .19930 0 0 0 .082031 -.019531c.12649.089206.25979.19587.39844.32422a.19867 .19867 0 1 0 .2696 -.2911c-.6099-.5646-1.1566-.7793-1.5605-.8398-.2020-.0303-.3679-.0229-.4883-.0039z",
}, {
faviconColorsIndex: 1,
d: "m28.543 5.8203c-.12043-.018949-.28631-.026379-.48828.00391-.40394.060562-.94869.27524-1.5586.83984a.19867 .19867 0 1 0 .26953 .29102c.21354-.19768.40814-.33222.59180-.44141.51624.023399.79659.16181.94531.28320.07652.062461.11952.12063.14258.16016.0094.016037.01458.025855.01758.033203a.19867 .19867 0 0 0 .38476 .039063c.000062-.0001719.0097-.036868.0098-.037109.000037-.0002412.0017-.017125.002-.023437.000505-.012624.000639-.020258 0-.027344-.0013-.01417-.004-.022671-.0059-.03125-.0038-.017158-.008-.031248-.01367-.046875-.01128-.031254-.02768-.067825-.05078-.10742-.04624-.079195-.12003-.17424-.23437-.26758-.11891-.097066-.28260-.18832-.49609-.25781.01785-.00328.03961-.011119.05664-.013672.16062-.024082.28561-.016738.36719-.00391.03883.00611.06556.012409.08203.017578.000833.0002613.0031.0017.0039.00195a.19867 .19867 0 0 0 .271 -.2793c-.000135-.0002016-.02515-.031014-.02539-.03125-.000236-.0002356-.01265-.011717-.01758-.015625-.0099-.00782-.01737-.01194-.02344-.015625-.01213-.00737-.02066-.011673-.0293-.015625-.01727-.0079-.03336-.013247-.05273-.019531-.03877-.012568-.08822-.025682-.14844-.035156z",
}, {
faviconColorsIndex: 2,
d: "m15.171 9.992a4.8316 4.8316 0 0 1 -4.832 4.832 4.8316 4.8316 0 0 1 -4.8311 -4.832 4.8316 4.8316 0 0 1 4.8311 -4.8316 4.8316 4.8316 0 0 1 4.832 4.8316z",
}, {
faviconColorsIndex: 2,
d: "m25.829 9.992a4.6538 4.6538 0 0 1 -4.653 4.654 4.6538 4.6538 0 0 1 -4.654 -4.654 4.6538 4.6538 0 0 1 4.654 -4.6537 4.6538 4.6538 0 0 1 4.653 4.6537z",
}, {
faviconColorsIndex: 3,
d: "m14.377 9.992a1.9631 1.9631 0 0 1 -1.963 1.963 1.9631 1.9631 0 0 1 -1.963 -1.963 1.9631 1.9631 0 0 1 1.963 -1.963 1.9631 1.9631 0 0 1 1.963 1.963z",
}, {
faviconColorsIndex: 3,
d: "m25.073 9.992a1.9631 1.9631 0 0 1 -1.963 1.963 1.9631 1.9631 0 0 1 -1.963 -1.963 1.9631 1.9631 0 0 1 1.963 -1.963 1.9631 1.9631 0 0 1 1.963 1.963z",
}, {
faviconColorsIndex: 4,
d: "m14.842 15.555h2.2156c.40215 0 .72590.3237.72590.7259v2.6545c0 .4021-.32375.7259-.72590.7259h-2.2156c-.40215 0-.72590-.3238-.72590-.7259v-2.6545c0-.4022.32375-.7259.72590-.7259z",
}, {
faviconColorsIndex: 5,
d: "m14.842 14.863h2.2156c.40215 0 .72590.3238.72590.7259v2.6546c0 .4021-.32375.7259-.72590.7259h-2.2156c-.40215 0-.72590-.3238-.72590-.7259v-2.6546c0-.4021.32375-.7259.72590-.7259z",
}, {
faviconColorsIndex: 4,
d: "m20 16.167c0 .838-.87123 1.2682-2.1448 1.1659-.02366 0-.04795-.6004-.25415-.5832-.50367.042-1.0959-.02-1.686-.02-.61294 0-1.2063.1826-1.6855.017-.11023-.038-.17830.5838-.26153.5816-1.2437-.033-2.0788-.3383-2.0788-1.1618 0-1.2118 1.8156-2.1941 4.0554-2.1941 2.2397 0 4.0554.9823 4.0554 2.1941z",
}, {
faviconColorsIndex: 6,
d: "m19.977 15.338c0 .5685-.43366.8554-1.1381 1.0001-.29193.06-.63037.096-1.0037.1166-.56405.032-1.2078.031-1.8912.031-.67283 0-1.3072 0-1.8649-.029-.30627-.017-.58943-.043-.84316-.084-.81383-.1318-1.325-.417-1.325-1.0344 0-1.1601 1.8056-2.1006 4.033-2.1006s4.033.9405 4.033 2.1006z",
}, {
faviconColorsIndex: 7,
d: "m18.025 13.488a2.0802 1.3437 0 0 1 -2.0802 1.3437 2.0802 1.3437 0 0 1 -2.0802 -1.3437 2.0802 1.3437 0 0 1 2.0802 -1.3437 2.0802 1.3437 0 0 1 2.0802 1.3437z",
}}
func TestEncodeFavicon(t *testing.T) {
// Set up a base color for theming the favicon, gopher blue by default.
pal := DefaultPalette
pal[0] = faviconColors[0] // color.RGBA{0x76, 0xe1, 0xfe, 0xff}
var e Encoder
e.Reset(Metadata{
ViewBox: DefaultViewBox,
Palette: pal,
})
// The favicon graphic also uses a dark version of that base color. blend
// is 75% dark (CReg[63]) and 25% the base color (pal[0]).
dark := color.RGBA{0x23, 0x1d, 0x1b, 0xff}
blend := BlendColor(0x40, 0xff, 0x80)
// First, set CReg[63] to dark, then set CReg[63] to the blend of that dark
// color with pal[0].
e.SetCReg(1, false, RGBAColor(dark))
e.SetCReg(1, false, blend)
// Check that, for the suggested palette, blend resolves to the
// (non-themable) SVG file's faviconColors[1].
got := blend.Resolve(&pal, &[64]color.RGBA{
63: dark,
})
want := faviconColors[1]
if got != want {
t.Fatalf("Blend:\ngot %#02x\nwant %#02x", got, want)
}
// Set aside the remaining, non-themable colors.
remainingColors := faviconColors[2:]
seenFCI2 := false
for _, data := range faviconSVGData {
adj := uint8(data.faviconColorsIndex)
if adj >= 2 {
if !seenFCI2 {
seenFCI2 = true
for i, c := range remainingColors {
e.SetCReg(uint8(i), false, RGBAColor(c))
}
}
adj -= 2
}
if err := encodePathData(&e, data.d, adj, true); err != nil {
t.Fatal(err)
}
}
testEncode(t, &e, "testdata/favicon.ivg")
}
func encodePathData(e *Encoder, d string, adj uint8, normalizeTo64X64 bool) error {
var args [7]float32
prevN, prevVerb := 0, byte(0)
for first := true; d != "z"; first = false {
n, verb, implicit := 0, d[0], false
switch d[0] {
case 'H', 'h', 'V', 'v':
n = 1
case 'L', 'M', 'l', 'm':
n = 2
case 'S', 's':
n = 4
case 'C', 'c':
n = 6
case 'A', 'a':
n = 7
case 'z':
n = 0
default:
if prevVerb == '\x00' {
panic("unrecognized verb")
}
n, verb, implicit = prevN, prevVerb, true
}
prevN, prevVerb = n, verb
if prevVerb == 'M' {
prevVerb = 'L'
} else if prevVerb == 'm' {
prevVerb = 'l'
}
if !implicit {
d = d[1:]
}
for i := 0; i < n; i++ {
nDots := 0
if d[0] == '.' {
nDots = 1
}
j := 1
for ; ; j++ {
switch d[j] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
continue
case '.':
nDots++
if nDots == 1 {
continue
}
}
break
}
f, err := strconv.ParseFloat(d[:j], 64)
if err != nil {
return err
}
args[i] = float32(f)
for ; d[j] == ' ' || d[j] == ','; j++ {
}
d = d[j:]
}
if normalizeTo64X64 {
// The original SVG is 32x32 units, with the top left being (0, 0).
// Normalize to 64x64 units, with the center being (0, 0).
if verb == 'A' {
args[0] = 2 * args[0]
args[1] = 2 * args[1]
args[2] /= 360
args[5] = 2*args[5] - 32
args[6] = 2*args[6] - 32
} else if verb == 'a' {
args[0] = 2 * args[0]
args[1] = 2 * args[1]
args[2] /= 360
args[5] = 2 * args[5]
args[6] = 2 * args[6]
} else if first || ('A' <= verb && verb <= 'Z') {
for i := range args {
args[i] = 2*args[i] - 32
}
} else {
for i := range args {
args[i] = 2 * args[i]
}
}
} else if verb == 'A' || verb == 'a' {
args[2] /= 360
}
if first {
first = false
e.StartPath(adj, args[0], args[1])
continue
}
switch verb {
case 'H':
e.AbsHLineTo(args[0])
case 'h':
e.RelHLineTo(args[0])
case 'V':
e.AbsVLineTo(args[0])
case 'v':
e.RelVLineTo(args[0])
case 'L':
e.AbsLineTo(args[0], args[1])
case 'l':
e.RelLineTo(args[0], args[1])
case 'm':
e.ClosePathRelMoveTo(args[0], args[1])
case 'S':
e.AbsSmoothCubeTo(args[0], args[1], args[2], args[3])
case 's':
e.RelSmoothCubeTo(args[0], args[1], args[2], args[3])
case 'C':
e.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
case 'c':
e.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
case 'A':
e.AbsArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
case 'a':
e.RelArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
case 'z':
// No-op.
default:
panic("unrecognized verb")
}
}
e.ClosePathEndPath()
return nil
}
func TestEncodeGradient(t *testing.T) {
rgb := []GradientStop{
{Offset: 0.00, Color: color.RGBA{0xff, 0x00, 0x00, 0xff}},
{Offset: 0.25, Color: color.RGBA{0x00, 0xff, 0x00, 0xff}},
{Offset: 0.50, Color: color.RGBA{0x00, 0x00, 0xff, 0xff}},
{Offset: 1.00, Color: color.RGBA{0x00, 0x00, 0x00, 0xff}},
}
cmy := []GradientStop{
{Offset: 0.00, Color: color.RGBA{0x00, 0xff, 0xff, 0xff}},
{Offset: 0.25, Color: color.RGBA{0xff, 0xff, 0xff, 0xff}},
{Offset: 0.50, Color: color.RGBA{0xff, 0x00, 0xff, 0xff}},
{Offset: 0.75, Color: color.RGBA{0x00, 0x00, 0x00, 0x00}},
{Offset: 1.00, Color: color.RGBA{0xff, 0xff, 0x00, 0xff}},
}
var e Encoder
e.SetLinearGradient(10, 10, -12, -30, +12, -18, GradientSpreadNone, rgb)
e.StartPath(0, -30, -30)
e.AbsHLineTo(+30)
e.AbsVLineTo(-18)
e.AbsHLineTo(-30)
e.ClosePathEndPath()
e.SetLinearGradient(10, 10, -12, -14, +12, -2, GradientSpreadPad, cmy)
e.StartPath(0, -30, -14)
e.AbsHLineTo(+30)
e.AbsVLineTo(-2)
e.AbsHLineTo(-30)
e.ClosePathEndPath()
e.SetCircularGradient(10, 10, -8, 8, 0, 16, GradientSpreadReflect, rgb)
e.StartPath(0, -30, +2)
e.AbsHLineTo(+30)
e.AbsVLineTo(+14)
e.AbsHLineTo(-30)
e.ClosePathEndPath()
e.SetCircularGradient(10, 10, -8, 24, 0, 16, GradientSpreadRepeat, cmy)
e.StartPath(0, -30, +18)
e.AbsHLineTo(+30)
e.AbsVLineTo(+30)
e.AbsHLineTo(-30)
e.ClosePathEndPath()
testEncode(t, &e, "testdata/gradient.ivg")
}
func TestEncodeLODPolygon(t *testing.T) {
var e Encoder
poly := func(n int) {
const r = 28
angle := 2 * math.Pi / float64(n)
e.StartPath(0, r, 0)
for i := 1; i < n; i++ {
e.AbsLineTo(
float32(r*math.Cos(angle*float64(i))),
float32(r*math.Sin(angle*float64(i))),
)
}
e.ClosePathEndPath()
}
e.StartPath(0, -28, -20)
e.AbsVLineTo(-28)
e.AbsHLineTo(-20)
e.ClosePathEndPath()
e.SetLOD(0, 80)
poly(3)
e.SetLOD(80, positiveInfinity)
poly(5)
e.SetLOD(0, positiveInfinity)
e.StartPath(0, +28, +20)
e.AbsVLineTo(+28)
e.AbsHLineTo(+20)
e.ClosePathEndPath()
testEncode(t, &e, "testdata/lod-polygon.ivg")
}
var video005PrimitiveSVGData = []struct {
r, g, b uint32
x0, y0 int
x1, y1 int
x2, y2 int
}{
{0x17, 0x06, 0x05, 162, 207, 271, 186, 195, -16},
{0xe9, 0xf5, 0xf8, -16, 179, 140, -11, 16, -8},
{0x00, 0x04, 0x27, 97, 96, 221, 21, 214, 111},
{0x89, 0xd9, 0xff, 262, -6, 271, 104, 164, -16},
{0x94, 0xbd, 0xc5, 204, 104, 164, 207, 59, 104},
{0xd4, 0x81, 0x3d, -16, 36, 123, 195, -16, 194},
{0x00, 0x00, 0x00, 164, 19, 95, 77, 138, 13},
{0x39, 0x11, 0x19, 50, 143, 115, 185, -4, 165},
{0x00, 0x3d, 0x81, 86, 109, 53, 76, 90, 24},
{0xfc, 0xc6, 0x9c, 31, 161, 80, 105, -16, 28},
{0x9e, 0xdd, 0xff, 201, -7, 31, -16, 2, 60},
{0x01, 0x20, 0x39, 132, 85, 240, -5, 173, 130},
{0xfd, 0xbc, 0x8f, 193, 127, 231, 94, 250, 124},
{0x43, 0x06, 0x00, 251, 207, 237, 83, 271, 97},
{0x80, 0xbf, 0xee, 117, 134, 88, 177, 90, 28},
{0x00, 0x00, 0x00, 127, 38, 172, 68, 223, 55},
{0x19, 0x0e, 0x16, 201, 204, 161, 101, 271, 192},
{0xf6, 0xaa, 0x71, 201, 164, 226, 141, 261, 152},
{0xe0, 0x36, 0x00, -16, -2, 29, -16, -6, 58},
{0xff, 0xe4, 0xba, 146, 45, 118, 75, 148, 76},
{0x00, 0x00, 0x12, 118, 44, 107, 109, 100, 51},
{0xbd, 0xd5, 0xe4, 271, 41, 253, -16, 211, 89},
{0x52, 0x00, 0x00, 87, 127, 83, 150, 55, 111},
{0x00, 0xb3, 0xa1, 124, 185, 135, 207, 194, 176},
{0x22, 0x00, 0x00, 59, 151, 33, 124, 52, 169},
{0xbe, 0xcb, 0xcb, 149, 42, 183, -16, 178, 47},
{0xff, 0xd4, 0xb1, 211, 119, 184, 100, 182, 124},
{0xff, 0xe1, 0x39, 73, 207, 140, 180, -13, 187},
{0xa7, 0xb0, 0xad, 122, 181, 200, 182, 93, 82},
{0x00, 0x00, 0x00, 271, 168, 170, 185, 221, 207},
}
func TestEncodeVideo005Primitive(t *testing.T) {
// The division by 4 is because the SVG width is 256 units and the IconVG
// width is 64 (from -32 to +32).
//
// The subtraction by 0.5 is because the SVG file contains the line:
// <g transform="translate(0.5 0.5)">
scaleX := func(i int) float32 { return float32(i)/4 - (32 - 0.5/4) }
scaleY := func(i int) float32 { return float32(i)/4 - (24 - 0.5/4) }
var e Encoder
e.Reset(Metadata{
ViewBox: Rectangle{
Min: f32.Vec2{-32, -24},
Max: f32.Vec2{+32, +24},
},
Palette: DefaultPalette,
})
e.SetCReg(0, false, RGBAColor(color.RGBA{0x7c, 0x7e, 0x7c, 0xff}))
e.StartPath(0, -32, -24)
e.AbsHLineTo(+32)
e.AbsVLineTo(+24)
e.AbsHLineTo(-32)
e.ClosePathEndPath()
for _, v := range video005PrimitiveSVGData {
e.SetCReg(0, false, RGBAColor(color.RGBA{
uint8(v.r * 128 / 255),
uint8(v.g * 128 / 255),
uint8(v.b * 128 / 255),
128,
}))
e.StartPath(0, scaleX(v.x0), scaleY(v.y0))
e.AbsLineTo(scaleX(v.x1), scaleY(v.y1))
e.AbsLineTo(scaleX(v.x2), scaleY(v.y2))
e.ClosePathEndPath()
}
testEncode(t, &e, "testdata/video-005.primitive.ivg")
}