blob: 8766d21713c680877482a03c0caad7f83c31970d [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"
"image/color"
"io/ioutil"
"math"
"path/filepath"
"strconv"
"testing"
"golang.org/x/image/math/f32"
)
// overwriteTestdataFiles is temporarily set to true when adding new
// testdataTestCases.
const overwriteTestdataFiles = false
// TestOverwriteTestdataFilesIsFalse tests that any change to
// overwriteTestdataFiles is only temporary. Programmers are assumed to run "go
// test" before sending out for code review or committing code.
func TestOverwriteTestdataFilesIsFalse(t *testing.T) {
if overwriteTestdataFiles {
t.Errorf("overwriteTestdataFiles is true; do not commit code changes")
}
}
func testEncode(t *testing.T, e *Encoder, wantFilename string) {
got, err := e.Bytes()
if err != nil {
t.Fatalf("encoding: %v", err)
}
if overwriteTestdataFiles {
if err := ioutil.WriteFile(filepath.FromSlash(wantFilename), got, 0666); err != nil {
t.Fatalf("WriteFile: %v", err)
}
return
}
want, err := ioutil.ReadFile(filepath.FromSlash(wantFilename))
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("\ngot %d bytes:\n% x\nwant %d bytes:\n% x", len(got), got, len(want), want)
}
}
func TestEncodeBlank(t *testing.T) {
var e Encoder
testEncode(t, &e, "testdata/blank.ivg")
}
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")
}
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) {
var e Encoder
for i, c := range faviconColors[:2] {
e.SetCReg(uint8(i), false, RGBAColor(c))
}
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
}
var args [7]float32
prevN, prevVerb := 0, byte(0)
for d, first := data.d, true; d[0] != '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
default:
if prevVerb == '\x00' {
panic("unrecognized verb")
}
n, verb, implicit = prevN, prevVerb, true
}
prevN, prevVerb = n, verb
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 {
t.Fatal(err)
}
args[i] = float32(f)
for ; d[j] == ' ' || d[j] == ','; j++ {
}
d = d[j:]
}
// 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]
}
}
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 '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])
default:
panic("unrecognized verb")
}
}
e.ClosePathEndPath()
}
testEncode(t, &e, "testdata/favicon.ivg")
}
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.SetRadialGradient(10, 10, -8, 8, 16, GradientSpreadReflect, rgb)
e.StartPath(0, -30, +2)
e.AbsHLineTo(+30)
e.AbsVLineTo(+14)
e.AbsHLineTo(-30)
e.ClosePathEndPath()
e.SetRadialGradient(10, 10, -8, 24, 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")
}
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")
}
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")
}