diff options
Diffstat (limited to 'shiny/iconvg/upgrade.go')
-rw-r--r-- | shiny/iconvg/upgrade.go | 1100 |
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 +} |