// 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 }