summaryrefslogtreecommitdiffstats
path: root/shiny/iconvg/upgrade.go
diff options
context:
space:
mode:
Diffstat (limited to 'shiny/iconvg/upgrade.go')
-rw-r--r--shiny/iconvg/upgrade.go1100
1 files changed, 1100 insertions, 0 deletions
diff --git a/shiny/iconvg/upgrade.go b/shiny/iconvg/upgrade.go
new file mode 100644
index 0000000..5cfdef0
--- /dev/null
+++ b/shiny/iconvg/upgrade.go
@@ -0,0 +1,1100 @@
+// Copyright 2021 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"
+ "errors"
+ "image/color"
+ "math"
+)
+
+// UpgradeToFileFormatVersion1Options are the options to the
+// UpgradeToFileFormatVersion1 function.
+type UpgradeToFileFormatVersion1Options struct {
+ // ArcsExpandWithHighResolutionCoordinates is like the
+ // Encoder.HighResolutionCoordinates field. It controls whether to favor
+ // file size (false) or precision (true) when replacing File Format Version
+ // 0's arcs with cubic Bézier curves.
+ ArcsExpandWithHighResolutionCoordinates bool
+}
+
+// UpgradeToFileFormatVersion1 upgrades IconVG data from the 2016 experimental
+// "File Format Version 0" to the 2021 "File Format Version 1".
+//
+// This package (golang.org/x/exp/shiny/iconvg) holds a decoder for FFV0,
+// including this function to convert from FFV0 to FFV1. Different packages
+// (github.com/google/iconvg/src/go/*) decode FFV1.
+//
+// Amongst some new features and other clean-ups, FFV1 sets up the capability
+// for animated vector graphics, therefore removing some FFV0 features (such as
+// arc segments) that can be hard to animate smoothly. The IconvG FFV1 format
+// and its design decisions are discussed at
+// https://github.com/google/iconvg/issues/4#issuecomment-874105547
+func UpgradeToFileFormatVersion1(v0 []byte, opts *UpgradeToFileFormatVersion1Options) (v1 []byte, retErr error) {
+ u := &upgrader{}
+ if opts != nil {
+ u.opts = *opts
+ }
+ for i := range u.creg {
+ u.creg[i] = upgradeColor{
+ typ: ColorTypePaletteIndex,
+ paletteIndex: uint8(i),
+ }
+ }
+
+ if !bytes.HasPrefix(v0, magicBytes) {
+ return nil, errInvalidMagicIdentifier
+ }
+ v1 = append(v1, "\x8AIVG"...)
+ v0 = v0[4:]
+
+ v1, v0, retErr = u.upgradeMetadata(v1, v0)
+ if retErr != nil {
+ return nil, retErr
+ }
+
+ v1, _, retErr = u.upgradeBytecode(v1, v0)
+ if retErr != nil {
+ return nil, retErr
+ }
+
+ return v1, nil
+}
+
+const (
+ upgradeVerbMoveTo = 0
+ upgradeVerbLineTo = 1
+ upgradeVerbQuadTo = 2
+ upgradeVerbCubeTo = 3
+)
+
+type upgrader struct {
+ opts UpgradeToFileFormatVersion1Options
+
+ // These fields hold the current path's geometry.
+ verbs []uint8
+ args [][2]float32
+
+ // These fields track most of the FFV0 virtual machine register state. The
+ // FFV1 register model is different enough that we don't just translate
+ // each FFV0 register-related opcode individually.
+ creg [64]upgradeColor
+ nreg [64]float32
+ csel uint32
+ nsel uint32
+ fill uint32
+
+ // These fields track the most recent color written to FFV1 register
+ // REGS[SEL+7] (and SEL is kept at 56). As a file size optimization, we
+ // don't have to emit the first half of "Set REGS[SEL+7] = etc; Use
+ // REGS[SEL+7]" if the register already holds the "etc" value.
+ regsSel7 color.RGBA
+ hasRegsSel7 bool
+
+ // calculatingJumpLOD is whether the upgrader.upgradeBytecode method is
+ // being called recursively. FFV0 sets a Level-Of-Detail filter that
+ // applies implicitly until the next SetLOD opcode (if any). FFV1 instead
+ // explicitly gives the number of opcodes to skip if outside the LOD range.
+ calculatingJumpLOD bool
+}
+
+func (u *upgrader) upgradeMetadata(v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
+ nMetadataChunks, n := v0.decodeNatural()
+ if n == 0 {
+ return nil, nil, errInvalidNumberOfMetadataChunks
+ }
+ v1.encodeNaturalFFV1(nMetadataChunks)
+ v0 = v0[n:]
+
+ for ; nMetadataChunks > 0; nMetadataChunks-- {
+ length, n := v0.decodeNatural()
+ if n == 0 {
+ return nil, nil, errInvalidMetadataChunkLength
+ }
+ v0 = v0[n:]
+ if uint64(length) > uint64(len(v0)) {
+ return nil, nil, errInvalidMetadataChunkLength
+ }
+ upgrade, err := u.upgradeMetadataChunk(v0[:length])
+ if err != nil {
+ return nil, nil, err
+ }
+ v1.encodeNaturalFFV1(uint32(len(upgrade)))
+ v1 = append(v1, upgrade...)
+ v0 = v0[length:]
+ }
+ return v1, v0, nil
+}
+
+func (u *upgrader) upgradeMetadataChunk(v0 buffer) (v1 buffer, retErr error) {
+ mid, n := v0.decodeNatural()
+ if n == 0 {
+ return nil, errInvalidMetadataIdentifier
+ }
+ switch mid {
+ case midViewBox:
+ mid = ffv1MIDViewBox
+ case midSuggestedPalette:
+ mid = ffv1MIDSuggestedPalette
+ default:
+ return nil, errInvalidMetadataIdentifier
+ }
+ v1.encodeNaturalFFV1(mid)
+ v0 = v0[n:]
+
+ switch mid {
+ case ffv1MIDViewBox:
+ for i := 0; i < 4; i++ {
+ x, n := v0.decodeNatural()
+ if n == 0 {
+ return nil, errInvalidViewBox
+ }
+ v1.encodeNaturalFFV1(x)
+ v0 = v0[n:]
+ }
+ if len(v0) != 0 {
+ return nil, errInvalidViewBox
+ }
+
+ case ffv1MIDSuggestedPalette:
+ if len(v0) == 0 {
+ return nil, errInvalidSuggestedPalette
+ }
+ numColors := 1 + int(v0[0]&0x3f)
+ colorLength := 1 + int(v0[0]>>6)
+ v1 = append(v1, uint8(numColors-1))
+ v0 = v0[1:]
+ for i := 0; i < numColors; i++ {
+ c, n := Color{}, 0
+ switch colorLength {
+ case 1:
+ c, n = v0.decodeColor1()
+ case 2:
+ c, n = v0.decodeColor2()
+ case 3:
+ c, n = v0.decodeColor3Direct()
+ case 4:
+ c, n = v0.decodeColor4()
+ }
+ if n == 0 {
+ return nil, errInvalidSuggestedPalette
+ } else if (c.typ == ColorTypeRGBA) && validAlphaPremulColor(c.data) {
+ v1 = append(v1, c.data.R, c.data.G, c.data.B, c.data.A)
+ } else {
+ v1 = append(v1, 0x00, 0x00, 0x00, 0xff)
+ }
+ v0 = v0[n:]
+ }
+ if len(v0) != 0 {
+ return nil, errInvalidSuggestedPalette
+ }
+ }
+ return v1, nil
+}
+
+func (u *upgrader) upgradeBytecode(v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
+ uf := upgradeFunc(upgradeStyling)
+ for len(v0) > 0 {
+ uf, v1, v0, retErr = uf(u, v1, v0)
+ if retErr != nil {
+ if retErr == errCalculatingJumpLOD {
+ return v1, v0, nil
+ }
+ return nil, nil, retErr
+ }
+ }
+ return v1, v0, nil
+}
+
+var errCalculatingJumpLOD = errors.New("iconvg: calculating JumpLOD")
+
+type upgradeFunc func(*upgrader, buffer, buffer) (upgradeFunc, buffer, buffer, error)
+
+func upgradeStyling(u *upgrader, v1 buffer, v0 buffer) (uf upgradeFunc, newV1 buffer, newV0 buffer, retErr error) {
+ for len(v0) > 0 {
+ switch opcode := v0[0]; {
+ case opcode < 0x80: // "Set CSEL/NSEL"
+ if opcode < 0x40 {
+ u.csel = uint32(opcode & 63)
+ } else {
+ u.nsel = uint32(opcode & 63)
+ }
+ v0 = v0[1:]
+
+ case opcode < 0xa8: // "Set CREG[etc] to an etc color"
+ adj := uint32(opcode & 7)
+ if adj == 7 {
+ adj = 0
+ }
+ index := (u.csel - adj) & 63
+
+ v0 = v0[1:]
+ c, n := Color{}, 0
+ switch (opcode - 0x80) >> 3 {
+ case 0:
+ c, n = v0.decodeColor1()
+ case 1:
+ c, n = v0.decodeColor2()
+ case 2:
+ c, n = v0.decodeColor3Direct()
+ case 3:
+ c, n = v0.decodeColor4()
+ case 4:
+ c, n = v0.decodeColor3Indirect()
+ }
+ if n == 0 {
+ return nil, nil, nil, errInvalidColor
+ }
+ u.creg[index], retErr = u.resolve(c, false)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ v0 = v0[n:]
+
+ if (opcode & 7) == 7 {
+ u.csel = (u.csel + 1) & 63
+ }
+
+ case opcode < 0xc0: // "Set NREG[etc] to a real number"
+ adj := uint32(opcode & 7)
+ if adj == 7 {
+ adj = 0
+ }
+ index := (u.nsel - adj) & 63
+
+ v0 = v0[1:]
+ f, n := float32(0), 0
+ switch (opcode - 0x80) >> 3 {
+ case 5:
+ f, n = v0.decodeReal()
+ case 6:
+ f, n = v0.decodeCoordinate()
+ case 7:
+ f, n = v0.decodeZeroToOne()
+ }
+ if n == 0 {
+ return nil, nil, nil, errInvalidNumber
+ }
+ u.nreg[index] = f
+ v0 = v0[n:]
+
+ if (opcode & 7) == 7 {
+ u.nsel = (u.nsel + 1) & 63
+ }
+
+ case opcode < 0xc7: // Start path.
+ adj := uint32(opcode & 7)
+ u.fill = (u.csel - adj) & 63
+ v1 = append(v1, 0x35) // FFV1 MoveTo.
+ v0 = v0[1:]
+ return upgradeDrawing, v1, v0, nil
+
+ case opcode == 0xc7: // "Set LOD"
+ if u.calculatingJumpLOD {
+ u.calculatingJumpLOD = false
+ return nil, v1, v0, errCalculatingJumpLOD
+ }
+
+ v0 = v0[1:]
+ lod := [2]float32{}
+ for i := range lod {
+ f, n := v0.decodeReal()
+ if n == 0 {
+ return nil, nil, nil, errInvalidNumber
+ }
+ lod[i] = f
+ v0 = v0[n:]
+ }
+ if (lod[0] == 0) && math.IsInf(float64(lod[1]), +1) {
+ break
+ }
+
+ u.calculatingJumpLOD = true
+ ifTrue := []byte(nil)
+ if ifTrue, v0, retErr = u.upgradeBytecode(nil, v0); retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ nInstructions := countFFV1Instructions(ifTrue)
+ if nInstructions >= (1 << 30) {
+ return nil, nil, nil, errUnsupportedUpgrade
+ }
+ v1 = append(v1, 0x3a) // FFV1 JumpLOD.
+ v1.encodeNaturalFFV1(uint32(nInstructions))
+ v1.encodeCoordinateFFV1(lod[0])
+ v1.encodeCoordinateFFV1(lod[1])
+ v1 = append(v1, ifTrue...)
+
+ default:
+ return nil, nil, nil, errUnsupportedStylingOpcode
+ }
+ }
+ return upgradeStyling, v1, v0, nil
+}
+
+func upgradeDrawing(u *upgrader, v1 buffer, v0 buffer) (uf upgradeFunc, newV1 buffer, newV0 buffer, retErr error) {
+ u.verbs = u.verbs[:0]
+ u.args = u.args[:0]
+
+ coords := [3][2]float32{}
+ pen := [2]float32{}
+ prevSmoothType := smoothTypeNone
+ prevSmoothPoint := [2]float32{}
+
+ // Handle the implicit M after a "Start path" styling op.
+ v0, retErr = decodeCoordinates(pen[:2], nil, v0)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ u.verbs = append(u.verbs, upgradeVerbMoveTo)
+ u.args = append(u.args, pen)
+ startingPoint := pen
+
+ for len(v0) > 0 {
+ switch opcode := v0[0]; {
+ case opcode < 0xc0: // LineTo, QuadTo, CubeTo.
+ nCoordPairs, nReps, relative, smoothType := 0, 1+int(opcode&0x0f), false, smoothTypeNone
+ switch opcode >> 4 {
+ case 0x00, 0x01: // "L (absolute lineTo)"
+ nCoordPairs = 1
+ nReps = 1 + int(opcode&0x1f)
+ case 0x02, 0x03: // "l (relative lineTo)"
+ nCoordPairs = 1
+ nReps = 1 + int(opcode&0x1f)
+ relative = true
+ case 0x04: // "T (absolute smooth quadTo)"
+ nCoordPairs = 1
+ smoothType = smoothTypeQuad
+ case 0x05: // "t (relative smooth quadTo)"
+ nCoordPairs = 1
+ relative = true
+ smoothType = smoothTypeQuad
+ case 0x06: // "Q (absolute quadTo)"
+ nCoordPairs = 2
+ case 0x07: // "q (relative quadTo)"
+ nCoordPairs = 2
+ relative = true
+ case 0x08: // "S (absolute smooth cubeTo)"
+ nCoordPairs = 2
+ smoothType = smoothTypeCube
+ case 0x09: // "s (relative smooth cubeTo)"
+ nCoordPairs = 2
+ relative = true
+ smoothType = smoothTypeCube
+ case 0x0a: // "C (absolute cubeTo)"
+ nCoordPairs = 3
+ case 0x0b: // "c (relative cubeTo)"
+ nCoordPairs = 3
+ relative = true
+ }
+ v0 = v0[1:]
+
+ for i := 0; i < nReps; i++ {
+ smoothIndex := 0
+ if smoothType != smoothTypeNone {
+ smoothIndex = 1
+ if smoothType != prevSmoothType {
+ coords[0][0] = pen[0]
+ coords[0][1] = pen[1]
+ } else {
+ coords[0][0] = (2 * pen[0]) - prevSmoothPoint[0]
+ coords[0][1] = (2 * pen[1]) - prevSmoothPoint[1]
+ }
+ }
+ allCoords := coords[:smoothIndex+nCoordPairs]
+ explicitCoords := allCoords[smoothIndex:]
+
+ v0, retErr = decodeCoordinatePairs(explicitCoords, nil, v0)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ if relative {
+ for c := range explicitCoords {
+ explicitCoords[c][0] += pen[0]
+ explicitCoords[c][1] += pen[1]
+ }
+ }
+
+ u.verbs = append(u.verbs, uint8(len(allCoords)))
+ u.args = append(u.args, allCoords...)
+
+ pen = allCoords[len(allCoords)-1]
+ if len(allCoords) == 2 {
+ prevSmoothPoint = allCoords[0]
+ prevSmoothType = smoothTypeQuad
+ } else if len(allCoords) == 3 {
+ prevSmoothPoint = allCoords[1]
+ prevSmoothType = smoothTypeCube
+ } else {
+ prevSmoothType = smoothTypeNone
+ }
+ }
+
+ case opcode < 0xe0: // ArcTo.
+ v1, v0, retErr = u.upgradeArcs(&pen, v1, v0)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ prevSmoothType = smoothTypeNone
+
+ default: // Other drawing opcodes.
+ v0 = v0[1:]
+ switch opcode {
+ case 0xe1: // "z (closePath); end path"
+ goto endPath
+
+ case 0xe2, 0xe3: // "z (closePath); M (absolute/relative moveTo)"
+ v0, retErr = decodeCoordinatePairs(coords[:1], nil, v0)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ if opcode == 0xe2 {
+ pen[0] = coords[0][0]
+ pen[1] = coords[0][1]
+ } else {
+ pen[0] += coords[0][0]
+ pen[1] += coords[0][1]
+ }
+ u.verbs = append(u.verbs, upgradeVerbMoveTo)
+ u.args = append(u.args, pen)
+
+ default:
+ tmp := [1]float32{}
+ v0, retErr = decodeCoordinates(tmp[:1], nil, v0)
+ if retErr != nil {
+ return nil, nil, nil, retErr
+ }
+ switch opcode {
+ case 0xe6: // "H (absolute horizontal lineTo)"
+ pen[0] = tmp[0]
+ case 0xe7: // "h (relative horizontal lineTo)"
+ pen[0] += tmp[0]
+ case 0xe8: // "V (absolute vertical lineTo)"
+ pen[1] = tmp[0]
+ case 0xe9: // "v (relative vertical lineTo)"
+ pen[1] += tmp[0]
+ default:
+ return nil, nil, nil, errUnsupportedDrawingOpcode
+ }
+ u.verbs = append(u.verbs, upgradeVerbLineTo)
+ u.args = append(u.args, pen)
+ }
+ prevSmoothType = smoothTypeNone
+ }
+ }
+
+endPath:
+ v1, retErr = u.finishDrawing(v1, startingPoint)
+ return upgradeStyling, v1, v0, retErr
+}
+
+func (u *upgrader) finishDrawing(v1 buffer, startingPoint [2]float32) (newV1 buffer, retErr error) {
+ v1.encodeCoordinatePairFFV1(u.args[0])
+
+ for i, j := 1, 1; i < len(u.verbs); {
+ curr := u.args[j-1]
+ runLength := u.computeRunLength(u.verbs[i:])
+ verb := u.verbs[i]
+
+ if verb == upgradeVerbMoveTo {
+ v1 = append(v1, 0x35) // FFV1 MoveTo.
+ v1.encodeCoordinatePairFFV1(u.args[j])
+ i += 1
+ j += 1
+ continue
+ }
+
+ switch verb {
+ case upgradeVerbLineTo:
+ if ((runLength == 3) && ((j + 3) == len(u.args)) && u.looksLikeParallelogram3(&curr, u.args[j:], &startingPoint)) ||
+ ((runLength == 4) && u.looksLikeParallelogram4(&curr, u.args[j:j+4])) {
+ v1 = append(v1, 0x34) // FFV1 Parallelogram.
+ v1.encodeCoordinatePairFFV1(u.args[j+0])
+ v1.encodeCoordinatePairFFV1(u.args[j+1])
+ i += 4
+ j += 4 * 1
+ continue
+ }
+ case upgradeVerbCubeTo:
+ if (runLength == 4) && u.looksLikeEllipse(&curr, u.args[j:j+(4*3)]) {
+ v1 = append(v1, 0x33) // FFV1 Ellipse (4 quarters).
+ v1.encodeCoordinatePairFFV1(u.args[j+2])
+ v1.encodeCoordinatePairFFV1(u.args[j+5])
+ i += 4
+ j += 4 * 3
+ continue
+ }
+ }
+
+ opcodeBase := 0x10 * (verb - 1) // FFV1 LineTo / QuadTo / CubeTo.
+ if runLength < 16 {
+ v1 = append(v1, opcodeBase|uint8(runLength))
+ } else {
+ v1 = append(v1, opcodeBase)
+ v1.encodeNaturalFFV1(uint32(runLength) - 16)
+ }
+ args := u.args[j : j+(runLength*int(verb))]
+ for _, arg := range args {
+ v1.encodeCoordinatePairFFV1(arg)
+ }
+ i += runLength
+ j += len(args)
+ }
+
+ return u.emitFill(v1)
+}
+
+func (u *upgrader) emitFill(v1 buffer) (newV1 buffer, retErr error) {
+ switch c := u.creg[u.fill]; c.typ {
+ case ColorTypeRGBA:
+ if validAlphaPremulColor(c.rgba) {
+ if !u.hasRegsSel7 || (u.regsSel7 != c.rgba) {
+ u.hasRegsSel7, u.regsSel7 = true, c.rgba
+ v1 = append(v1, 0x57, // FFV1 Set REGS[SEL+7].hi32.
+ c.rgba.R, c.rgba.G, c.rgba.B, c.rgba.A)
+ }
+ v1 = append(v1, 0x87) // FFV1 Fill (flat color) with REGS[SEL+7].
+
+ } else if (c.rgba.A == 0) && (c.rgba.B&0x80 != 0) {
+ nStops := int(c.rgba.R & 63)
+ cBase := int(c.rgba.G & 63)
+ nBase := int(c.rgba.B & 63)
+ if nStops < 2 {
+ return nil, errInvalidColor
+ } else if nStops > 17 {
+ return nil, errUnsupportedUpgrade
+ }
+
+ v1 = append(v1, 0x70|uint8(nStops-2)) // FFV1 SEL -= N; Set REGS[SEL+1 .. SEL+1+N].
+ for i := 0; i < nStops; i++ {
+ if stopOffset := u.nreg[(nBase+i)&63]; stopOffset <= 0 {
+ v1 = append(v1, 0x00, 0x00, 0x00, 0x00)
+ } else if stopOffset < 1 {
+ u := uint32(stopOffset * 0x10000)
+ v1 = append(v1, uint8(u>>0), uint8(u>>8), uint8(u>>16), uint8(u>>24))
+ } else {
+ v1 = append(v1, 0x00, 0x00, 0x01, 0x00)
+ }
+
+ if stopColor := u.creg[(cBase+i)&63]; stopColor.typ != ColorTypeRGBA {
+ return nil, errUnsupportedUpgrade
+ } else {
+ v1 = append(v1,
+ stopColor.rgba.R,
+ stopColor.rgba.G,
+ stopColor.rgba.B,
+ stopColor.rgba.A,
+ )
+ }
+ }
+
+ nMatrixElements := 0
+ if c.rgba.B&0x40 == 0 {
+ v1 = append(v1, 0x91, // FFV1 Fill (linear gradient) with REGS[SEL+1 .. SEL+1+N].
+ (c.rgba.G&0xc0)|uint8(nStops-2))
+ nMatrixElements = 3
+ } else {
+ v1 = append(v1, 0xa1, // FFV1 Fill (radial gradient) with REGS[SEL+1 .. SEL+1+N].
+ (c.rgba.G&0xc0)|uint8(nStops-2))
+ nMatrixElements = 6
+ }
+ for i := 0; i < nMatrixElements; i++ {
+ u := math.Float32bits(u.nreg[(nBase+i-6)&63])
+ v1 = append(v1, uint8(u>>0), uint8(u>>8), uint8(u>>16), uint8(u>>24))
+ }
+
+ v1 = append(v1, 0x36, // FFV1 SEL += N.
+ uint8(nStops))
+ } else {
+ return nil, errInvalidColor
+ }
+
+ case ColorTypePaletteIndex:
+ if c.paletteIndex < 7 {
+ v1 = append(v1, 0x88+c.paletteIndex) // FFV1 Fill (flat color) with REGS[SEL+8+N].
+ } else {
+ v1 = append(v1, 0x56, // FFV1 Set REGS[SEL+6].hi32.
+ 0x80|c.paletteIndex, 0, 0, 0,
+ 0x86) // FFV1 Fill (flat color) with REGS[SEL+6].
+ }
+
+ case ColorTypeBlend:
+ if c.color0.typ == ColorTypeRGBA {
+ v1 = append(v1, 0x53, // FFV1 Set REGS[SEL+3].hi32.
+ c.color0.rgba.R, c.color0.rgba.G, c.color0.rgba.B, c.color0.rgba.A)
+ }
+ if c.color1.typ == ColorTypeRGBA {
+ v1 = append(v1, 0x54, // FFV1 Set REGS[SEL+4].hi32.
+ c.color1.rgba.R, c.color1.rgba.G, c.color1.rgba.B, c.color1.rgba.A)
+ }
+ v1 = append(v1, 0x55, // FFV1 Set REGS[SEL+5].hi32.
+ c.blend)
+ if c.color0.typ == ColorTypeRGBA {
+ v1 = append(v1, 0xfe)
+ } else {
+ v1 = append(v1, 0x80|c.color0.paletteIndex)
+ }
+ if c.color1.typ == ColorTypeRGBA {
+ v1 = append(v1, 0xff)
+ } else {
+ v1 = append(v1, 0x80|c.color1.paletteIndex)
+ }
+ v1 = append(v1, 0, 0x85) // FFV1 Fill (flat color) with REGS[SEL+5].
+ }
+
+ return v1, nil
+}
+
+func (u *upgrader) computeRunLength(verbs []uint8) int {
+ firstVerb := verbs[0]
+ if firstVerb == 0 {
+ return 1
+ }
+ n := 1
+ for ; (n < len(verbs)) && (verbs[n] == firstVerb); n++ {
+ }
+ return n
+}
+
+// looksLikeParallelogram3 is like looksLikeParallelogram4 but the final point
+// (implied by the ClosePath op) is separate from the middle 3 args.
+func (u *upgrader) looksLikeParallelogram3(curr *[2]float32, args [][2]float32, final *[2]float32) bool {
+ if len(args) != 3 {
+ panic("unreachable")
+ }
+ return (*curr == *final) &&
+ (curr[0] == (args[0][0] - args[1][0] + args[2][0])) &&
+ (curr[1] == (args[0][1] - args[1][1] + args[2][1]))
+}
+
+// looksLikeParallelogram4 returns whether the 5 coordinate pairs (A, B, C, D,
+// E) form a parallelogram:
+//
+// E=A B
+//
+// o---------o
+// \ \
+// \ \
+// \ \
+// o---------o
+// D C
+//
+// Specifically, it checks that (A == E) and ((A - B) == (D - C)). That last
+// equation can be rearranged as (A == (B - C + D)).
+//
+// The motivation is that, if looksLikeParallelogram4 is true, then the 5 input
+// coordinate pairs can then be compressed to 3: A, B and C. Or, if the current
+// point A is implied by context then 4 input pairs can be compressed to 2.
+func (u *upgrader) looksLikeParallelogram4(curr *[2]float32, args [][2]float32) bool {
+ if len(args) != 4 {
+ panic("unreachable")
+ }
+ return (*curr == args[3]) &&
+ (curr[0] == (args[0][0] - args[1][0] + args[2][0])) &&
+ (curr[1] == (args[0][1] - args[1][1] + args[2][1]))
+}
+
+// looksLikeEllipse returns whether the 13 coordinate pairs (A, A+, B-, B, B+,
+// C- C, C+, D-, D, D+, A-, E) form a cubic Bézier approximation to an ellipse.
+// Let A± denote the two tangent vectors (A+ - A) and (A - A-) and likewise for
+// B±, C± and D±.
+//
+// A+ B-
+//
+// E=A o o B
+// A- o---------o B+
+//
+// o \ \ o
+// \ X \
+// o \ \ o
+// D+ o---------o C-
+// D o o C
+// D- C+
+//
+// See https://nigeltao.github.io/blog/2021/three-points-define-ellipse.html
+// for a better version of that ASCII art.
+//
+// Specifically, it checks that (A, B, C, D, E), also known as (*curr, args[2],
+// args[5], args[8] and args[11]), forms a parallelogram. If so, let X be the
+// parallelogram center and define two axis vectors: r = B-X and s = C-X.
+//
+// These axes define the parallelogram's or ellipse's shape but they are not
+// necessarily orthogonal and hence not necessarily the ellipse's major
+// (longest) and minor (shortest) axes. If s is a 90 degree rotation of r then
+// the parallelogram is a square and the ellipse is a circle.
+//
+// This function further checks that the A±, B± C± and D± tangents are
+// approximately equal to +λ×r, +λ×s, -λ×r and -λ×s, where λ = ((math.Sqrt2 -
+// 1) × 4 / 3) comes from the cubic Bézier approximation to a quarter-circle.
+//
+// The motivation is that, if looksLikeEllipse is true, then the 13 input
+// coordinate pairs can then be compressed to 3: A, B and C. Or, if the current
+// point A is implied by context then 12 input pairs can be compressed to 2.
+func (u *upgrader) looksLikeEllipse(curr *[2]float32, args [][2]float32) bool {
+ if len(args) != 12 {
+ panic("unreachable")
+ }
+ if (*curr != args[11]) ||
+ (curr[0] != (args[2][0] - args[5][0] + args[8][0])) ||
+ (curr[1] != (args[2][1] - args[5][1] + args[8][1])) {
+ return false
+ }
+ center := [2]float32{
+ (args[2][0] + args[8][0]) / 2,
+ (args[2][1] + args[8][1]) / 2,
+ }
+
+ // 0.5522847498307933984022516322796 ≈ ((math.Sqrt2 - 1) × 4 / 3), the
+ // tangent lengths (as a fraction of the radius) for a commonly used cubic
+ // Bézier approximation to a circle. Multiplying that by 0.98 and 1.02
+ // checks that we're within 2% of that fraction.
+ //
+ // This also covers the slightly different 0.551784777779014 constant,
+ // recommended by https://pomax.github.io/bezierinfo/#circles_cubic
+ const λMin = 0.98 * 0.5522847498307933984022516322796
+ const λMax = 1.02 * 0.5522847498307933984022516322796
+
+ // Check the first axis.
+ r := [2]float32{
+ args[2][0] - center[0],
+ args[2][1] - center[1],
+ }
+ rMin := [2]float32{r[0] * λMin, r[1] * λMin}
+ rMax := [2]float32{r[0] * λMax, r[1] * λMax}
+ if rMin[0] > rMax[0] {
+ rMin[0], rMax[0] = rMax[0], rMin[0]
+ }
+ if rMin[1] > rMax[1] {
+ rMin[1], rMax[1] = rMax[1], rMin[1]
+ }
+ if !within(args[0][0]-curr[0], args[0][1]-curr[1], rMin, rMax) ||
+ !within(args[4][0]-args[5][0], args[4][1]-args[5][1], rMin, rMax) ||
+ !within(args[5][0]-args[6][0], args[5][1]-args[6][1], rMin, rMax) ||
+ !within(args[11][0]-args[10][0], args[11][1]-args[10][1], rMin, rMax) {
+ return false
+ }
+
+ // Check the second axis.
+ s := [2]float32{
+ args[5][0] - center[0],
+ args[5][1] - center[1],
+ }
+ sMin := [2]float32{s[0] * λMin, s[1] * λMin}
+ sMax := [2]float32{s[0] * λMax, s[1] * λMax}
+ if sMin[0] > sMax[0] {
+ sMin[0], sMax[0] = sMax[0], sMin[0]
+ }
+ if sMin[1] > sMax[1] {
+ sMin[1], sMax[1] = sMax[1], sMin[1]
+ }
+ if !within(args[2][0]-args[1][0], args[2][1]-args[1][1], sMin, sMax) ||
+ !within(args[3][0]-args[2][0], args[3][1]-args[2][1], sMin, sMax) ||
+ !within(args[7][0]-args[8][0], args[7][1]-args[8][1], sMin, sMax) ||
+ !within(args[8][0]-args[9][0], args[8][1]-args[9][1], sMin, sMax) {
+ return false
+ }
+
+ return true
+}
+
+func within(v0 float32, v1 float32, min [2]float32, max [2]float32) bool {
+ return (min[0] <= v0) && (v0 <= max[0]) && (min[1] <= v1) && (v1 <= max[1])
+}
+
+func (u *upgrader) upgradeArcs(pen *[2]float32, v1 buffer, v0 buffer) (newV1 buffer, newV0 buffer, retErr error) {
+ coords := [6]float32{}
+ largeArc, sweep := false, false
+ opcode := v0[0]
+ v0 = v0[1:]
+ nReps := 1 + int(opcode&0x0f)
+ for i := 0; i < nReps; i++ {
+ v0, retErr = decodeCoordinates(coords[:2], nil, v0)
+ if retErr != nil {
+ return nil, nil, retErr
+ }
+ coords[2], v0, retErr = decodeAngle(nil, v0)
+ if retErr != nil {
+ return nil, nil, retErr
+ }
+ largeArc, sweep, v0, retErr = decodeArcToFlags(nil, v0)
+ if retErr != nil {
+ return nil, nil, retErr
+ }
+ v0, retErr = decodeCoordinates(coords[4:6], nil, v0)
+ if retErr != nil {
+ return nil, nil, retErr
+ }
+ if (opcode >> 4) == 0x0d {
+ coords[4] += pen[0]
+ coords[5] += pen[1]
+ }
+ u.upgradeArc(pen, coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
+ pen[0] = coords[4]
+ pen[1] = coords[5]
+ }
+ return v1, v0, nil
+}
+
+func (u *upgrader) upgradeArc(pen *[2]float32, rx, ry, xAxisRotation float32, largeArc, sweep bool, finalX, finalY float32) {
+ // We follow the "Conversion from endpoint to center parameterization"
+ // algorithm as per
+ // https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+
+ // There seems to be a bug in the spec's "implementation notes".
+ //
+ // Actual implementations, such as
+ // - https://git.gnome.org/browse/librsvg/tree/rsvg-path.c
+ // - http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java
+ // - https://java.net/projects/svgsalamander/sources/svn/content/trunk/svg-core/src/main/java/com/kitfox/svg/pathcmd/Arc.java
+ // - https://github.com/millermedeiros/SVGParser/blob/master/com/millermedeiros/geom/SVGArc.as
+ // do something slightly different (marked with a †).
+
+ // (†) The Abs isn't part of the spec. Neither is checking that Rx and Ry
+ // are non-zero (and non-NaN).
+ Rx := math.Abs(float64(rx))
+ Ry := math.Abs(float64(ry))
+ if !(Rx > 0 && Ry > 0) {
+ u.verbs = append(u.verbs, upgradeVerbLineTo)
+ u.args = append(u.args, [2]float32{finalX, finalY})
+ return
+ }
+
+ x1 := float64(pen[0])
+ y1 := float64(pen[1])
+ x2 := float64(finalX)
+ y2 := float64(finalY)
+
+ phi := 2 * math.Pi * float64(xAxisRotation)
+
+ // Step 1: Compute (x1′, y1′)
+ halfDx := (x1 - x2) / 2
+ halfDy := (y1 - y2) / 2
+ cosPhi := math.Cos(phi)
+ sinPhi := math.Sin(phi)
+ x1Prime := +cosPhi*halfDx + sinPhi*halfDy
+ y1Prime := -sinPhi*halfDx + cosPhi*halfDy
+
+ // Step 2: Compute (cx′, cy′)
+ rxSq := Rx * Rx
+ rySq := Ry * Ry
+ x1PrimeSq := x1Prime * x1Prime
+ y1PrimeSq := y1Prime * y1Prime
+
+ // (†) Check that the radii are large enough.
+ radiiCheck := x1PrimeSq/rxSq + y1PrimeSq/rySq
+ if radiiCheck > 1 {
+ c := math.Sqrt(radiiCheck)
+ Rx *= c
+ Ry *= c
+ rxSq = Rx * Rx
+ rySq = Ry * Ry
+ }
+
+ denom := rxSq*y1PrimeSq + rySq*x1PrimeSq
+ step2 := 0.0
+ if a := rxSq*rySq/denom - 1; a > 0 {
+ step2 = math.Sqrt(a)
+ }
+ if largeArc == sweep {
+ step2 = -step2
+ }
+ cxPrime := +step2 * Rx * y1Prime / Ry
+ cyPrime := -step2 * Ry * x1Prime / Rx
+
+ // Step 3: Compute (cx, cy) from (cx′, cy′)
+ cx := +cosPhi*cxPrime - sinPhi*cyPrime + (x1+x2)/2
+ cy := +sinPhi*cxPrime + cosPhi*cyPrime + (y1+y2)/2
+
+ // Step 4: Compute θ1 and Δθ
+ ax := (+x1Prime - cxPrime) / Rx
+ ay := (+y1Prime - cyPrime) / Ry
+ bx := (-x1Prime - cxPrime) / Rx
+ by := (-y1Prime - cyPrime) / Ry
+ theta1 := angle(1, 0, ax, ay)
+ deltaTheta := angle(ax, ay, bx, by)
+ if sweep {
+ if deltaTheta < 0 {
+ deltaTheta += 2 * math.Pi
+ }
+ } else {
+ if deltaTheta > 0 {
+ deltaTheta -= 2 * math.Pi
+ }
+ }
+
+ // This ends the
+ // https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+ // algorithm. What follows below is specific to this implementation.
+
+ // We approximate an arc by one or more cubic Bézier curves.
+ n := int(math.Ceil(math.Abs(deltaTheta) / (math.Pi/2 + 0.001)))
+ for i := 0; i < n; i++ {
+ u.arcSegmentTo(cx, cy,
+ theta1+deltaTheta*float64(i+0)/float64(n),
+ theta1+deltaTheta*float64(i+1)/float64(n),
+ Rx, Ry, cosPhi, sinPhi,
+ )
+ }
+}
+
+// arcSegmentTo approximates an arc by a cubic Bézier curve. The mathematical
+// formulae for the control points are the same as that used by librsvg.
+func (u *upgrader) arcSegmentTo(cx, cy, theta1, theta2, rx, ry, cosPhi, sinPhi float64) {
+ halfDeltaTheta := (theta2 - theta1) * 0.5
+ q := math.Sin(halfDeltaTheta * 0.5)
+ t := (8 * q * q) / (3 * math.Sin(halfDeltaTheta))
+ cos1 := math.Cos(theta1)
+ sin1 := math.Sin(theta1)
+ cos2 := math.Cos(theta2)
+ sin2 := math.Sin(theta2)
+ x1 := rx * (+cos1 - t*sin1)
+ y1 := ry * (+sin1 + t*cos1)
+ x2 := rx * (+cos2 + t*sin2)
+ y2 := ry * (+sin2 - t*cos2)
+ x3 := rx * (+cos2)
+ y3 := ry * (+sin2)
+ highResolutionCoordinates := u.opts.ArcsExpandWithHighResolutionCoordinates
+ u.verbs = append(u.verbs, upgradeVerbCubeTo)
+ u.args = append(u.args,
+ [2]float32{
+ quantize(float32(cx+cosPhi*x1-sinPhi*y1), highResolutionCoordinates),
+ quantize(float32(cy+sinPhi*x1+cosPhi*y1), highResolutionCoordinates),
+ },
+ [2]float32{
+ quantize(float32(cx+cosPhi*x2-sinPhi*y2), highResolutionCoordinates),
+ quantize(float32(cy+sinPhi*x2+cosPhi*y2), highResolutionCoordinates),
+ },
+ [2]float32{
+ quantize(float32(cx+cosPhi*x3-sinPhi*y3), highResolutionCoordinates),
+ quantize(float32(cy+sinPhi*x3+cosPhi*y3), highResolutionCoordinates),
+ },
+ )
+}
+
+func countFFV1Instructions(src buffer) (ret uint64) {
+ for len(src) > 0 {
+ ret++
+ opcode := src[0]
+ src = src[1:]
+
+ switch {
+ case opcode < 0x40:
+ switch {
+ case opcode < 0x30:
+ nReps := uint32(opcode & 15)
+ if nReps == 0 {
+ n := 0
+ nReps, n = src.decodeNaturalFFV1()
+ src = src[n:]
+ nReps += 16
+ }
+ nCoords := 2 * (1 + int(opcode>>4))
+ for ; nReps > 0; nReps-- {
+ for i := 0; i < nCoords; i++ {
+ _, n := src.decodeNaturalFFV1()
+ src = src[n:]
+ }
+ }
+ case opcode < 0x35:
+ for i := 0; i < 4; i++ {
+ _, n := src.decodeNaturalFFV1()
+ src = src[n:]
+ }
+ case opcode == 0x35:
+ for i := 0; i < 2; i++ {
+ _, n := src.decodeNaturalFFV1()
+ src = src[n:]
+ }
+ case opcode == 0x36:
+ src = src[1:]
+ case opcode == 0x37:
+ // No-op.
+ default:
+ // upgradeBytecode (with calculatingJumpLOD set) will not emit
+ // jump or call instructions.
+ panic("unexpected FFV1 instruction")
+ }
+
+ case opcode < 0x80:
+ switch (opcode >> 4) & 3 {
+ case 0, 1:
+ src = src[4:]
+ case 2:
+ src = src[8:]
+ default:
+ src = src[8*(2+int(opcode&15)):]
+ }
+
+ case opcode < 0xc0:
+ switch (opcode >> 4) & 3 {
+ case 0:
+ // No-op.
+ case 1:
+ src = src[13:]
+ case 2:
+ src = src[25:]
+ default:
+ // upgradeBytecode (with calculatingJumpLOD set) will not emit
+ // reserved instructions.
+ panic("unexpected FFV1 instruction")
+ }
+
+ default:
+ // upgradeBytecode (with calculatingJumpLOD set) will not emit
+ // reserved instructions.
+ panic("unexpected FFV1 instruction")
+ }
+ }
+ return ret
+}
+
+type upgradeColor struct {
+ typ ColorType
+ paletteIndex uint8
+ blend uint8
+ rgba color.RGBA
+ color0 *upgradeColor
+ color1 *upgradeColor
+}
+
+func (u *upgrader) resolve(c Color, denyBlend bool) (upgradeColor, error) {
+ switch c.typ {
+ case ColorTypeRGBA:
+ return upgradeColor{
+ typ: ColorTypeRGBA,
+ rgba: c.data,
+ }, nil
+ case ColorTypePaletteIndex:
+ return upgradeColor{
+ typ: ColorTypePaletteIndex,
+ paletteIndex: c.paletteIndex(),
+ }, nil
+ case ColorTypeCReg:
+ upgrade := u.creg[c.cReg()]
+ if denyBlend && (upgrade.typ == ColorTypeBlend) {
+ return upgradeColor{}, errUnsupportedUpgrade
+ }
+ return upgrade, nil
+ }
+
+ if denyBlend {
+ return upgradeColor{}, errUnsupportedUpgrade
+ }
+ t, c0, c1 := c.blend()
+ color0, err := u.resolve(decodeColor1(c0), true)
+ if err != nil {
+ return upgradeColor{}, err
+ }
+ color1, err := u.resolve(decodeColor1(c1), true)
+ if err != nil {
+ return upgradeColor{}, err
+ }
+ return upgradeColor{
+ typ: ColorTypeBlend,
+ blend: t,
+ color0: &color0,
+ color1: &color1,
+ }, nil
+}