| // 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" |
| "errors" |
| "image/color" |
| ) |
| |
| var ( |
| errInconsistentMetadataChunkLength = errors.New("iconvg: inconsistent metadata chunk length") |
| errInvalidColor = errors.New("iconvg: invalid color") |
| errInvalidMagicIdentifier = errors.New("iconvg: invalid magic identifier") |
| errInvalidMetadataChunkLength = errors.New("iconvg: invalid metadata chunk length") |
| errInvalidMetadataIdentifier = errors.New("iconvg: invalid metadata identifier") |
| errInvalidNumber = errors.New("iconvg: invalid number") |
| errInvalidNumberOfMetadataChunks = errors.New("iconvg: invalid number of metadata chunks") |
| errInvalidSuggestedPalette = errors.New("iconvg: invalid suggested palette") |
| errInvalidViewBox = errors.New("iconvg: invalid view box") |
| errUnsupportedDrawingOpcode = errors.New("iconvg: unsupported drawing opcode") |
| errUnsupportedMetadataIdentifier = errors.New("iconvg: unsupported metadata identifier") |
| errUnsupportedStylingOpcode = errors.New("iconvg: unsupported styling opcode") |
| ) |
| |
| var midDescriptions = [...]string{ |
| midViewBox: "viewBox", |
| midSuggestedPalette: "suggested palette", |
| } |
| |
| // Destination handles the actions decoded from an IconVG graphic's opcodes. |
| // |
| // When passed to Decode, the first method called (if any) will be Reset. No |
| // methods will be called at all if an error is encountered in the encoded form |
| // before the metadata is fully decoded. |
| type Destination interface { |
| Reset(m Metadata) |
| |
| SetCSel(cSel uint8) |
| SetNSel(nSel uint8) |
| SetCReg(adj uint8, incr bool, c Color) |
| SetNReg(adj uint8, incr bool, f float32) |
| SetLOD(lod0, lod1 float32) |
| |
| StartPath(adj uint8, x, y float32) |
| ClosePathEndPath() |
| ClosePathAbsMoveTo(x, y float32) |
| ClosePathRelMoveTo(x, y float32) |
| |
| AbsHLineTo(x float32) |
| RelHLineTo(x float32) |
| AbsVLineTo(y float32) |
| RelVLineTo(y float32) |
| AbsLineTo(x, y float32) |
| RelLineTo(x, y float32) |
| AbsSmoothQuadTo(x, y float32) |
| RelSmoothQuadTo(x, y float32) |
| AbsQuadTo(x1, y1, x, y float32) |
| RelQuadTo(x1, y1, x, y float32) |
| AbsSmoothCubeTo(x2, y2, x, y float32) |
| RelSmoothCubeTo(x2, y2, x, y float32) |
| AbsCubeTo(x1, y1, x2, y2, x, y float32) |
| RelCubeTo(x1, y1, x2, y2, x, y float32) |
| AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) |
| RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) |
| } |
| |
| type printer func(b []byte, format string, args ...interface{}) |
| |
| // DecodeOptions are the optional parameters to the Decode function. |
| type DecodeOptions struct { |
| // Palette is an optional 64 color palette. If one isn't provided, the |
| // IconVG graphic's suggested palette will be used. |
| Palette *Palette |
| } |
| |
| // DecodeMetadata decodes only the metadata in an IconVG graphic. |
| func DecodeMetadata(src []byte) (m Metadata, err error) { |
| m.ViewBox = DefaultViewBox |
| m.Palette = DefaultPalette |
| if err = decode(nil, nil, &m, true, src, nil); err != nil { |
| return Metadata{}, err |
| } |
| return m, nil |
| } |
| |
| // Decode decodes an IconVG graphic. |
| func Decode(dst Destination, src []byte, opts *DecodeOptions) error { |
| m := Metadata{ |
| ViewBox: DefaultViewBox, |
| Palette: DefaultPalette, |
| } |
| if opts != nil && opts.Palette != nil { |
| m.Palette = *opts.Palette |
| } |
| return decode(dst, nil, &m, false, src, opts) |
| } |
| |
| func decode(dst Destination, p printer, m *Metadata, metadataOnly bool, src buffer, opts *DecodeOptions) (err error) { |
| if !bytes.HasPrefix(src, magicBytes) { |
| return errInvalidMagicIdentifier |
| } |
| if p != nil { |
| p(src[:len(magic)], "IconVG Magic identifier\n") |
| } |
| src = src[len(magic):] |
| |
| nMetadataChunks, n := src.decodeNatural() |
| if n == 0 { |
| return errInvalidNumberOfMetadataChunks |
| } |
| if p != nil { |
| p(src[:n], "Number of metadata chunks: %d\n", nMetadataChunks) |
| } |
| src = src[n:] |
| |
| for ; nMetadataChunks > 0; nMetadataChunks-- { |
| src, err = decodeMetadataChunk(p, m, src, opts) |
| if err != nil { |
| return err |
| } |
| } |
| if metadataOnly { |
| return nil |
| } |
| if dst != nil { |
| dst.Reset(*m) |
| } |
| |
| mf := modeFunc(decodeStyling) |
| for len(src) > 0 { |
| mf, src, err = mf(dst, p, src) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func decodeMetadataChunk(p printer, m *Metadata, src buffer, opts *DecodeOptions) (src1 buffer, err error) { |
| length, n := src.decodeNatural() |
| if n == 0 { |
| return nil, errInvalidMetadataChunkLength |
| } |
| if p != nil { |
| p(src[:n], "Metadata chunk length: %d\n", length) |
| } |
| src = src[n:] |
| lenSrcWant := int64(len(src)) - int64(length) |
| |
| mid, n := src.decodeNatural() |
| if n == 0 { |
| return nil, errInvalidMetadataIdentifier |
| } |
| if mid >= uint32(len(midDescriptions)) { |
| return nil, errUnsupportedMetadataIdentifier |
| } |
| if p != nil { |
| p(src[:n], "Metadata Identifier: %d (%s)\n", mid, midDescriptions[mid]) |
| } |
| src = src[n:] |
| |
| switch mid { |
| case midViewBox: |
| if m.ViewBox.Min[0], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil { |
| return nil, errInvalidViewBox |
| } |
| if m.ViewBox.Min[1], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil { |
| return nil, errInvalidViewBox |
| } |
| if m.ViewBox.Max[0], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil { |
| return nil, errInvalidViewBox |
| } |
| if m.ViewBox.Max[1], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil { |
| return nil, errInvalidViewBox |
| } |
| if m.ViewBox.Min[0] > m.ViewBox.Max[0] || m.ViewBox.Min[1] > m.ViewBox.Max[1] || |
| isNaNOrInfinity(m.ViewBox.Min[0]) || isNaNOrInfinity(m.ViewBox.Min[1]) || |
| isNaNOrInfinity(m.ViewBox.Max[0]) || isNaNOrInfinity(m.ViewBox.Max[1]) { |
| return nil, errInvalidViewBox |
| } |
| |
| case midSuggestedPalette: |
| if len(src) == 0 { |
| return nil, errInvalidSuggestedPalette |
| } |
| length, format := 1+int(src[0]&0x3f), src[0]>>6 |
| decode := buffer.decodeColor4 |
| switch format { |
| case 0: |
| decode = buffer.decodeColor1 |
| case 1: |
| decode = buffer.decodeColor2 |
| case 2: |
| decode = buffer.decodeColor3Direct |
| } |
| if p != nil { |
| p(src[:1], " %d palette colors, %d bytes per color\n", length, 1+format) |
| } |
| src = src[1:] |
| |
| for i := 0; i < length; i++ { |
| c, n := decode(src) |
| if n == 0 { |
| return nil, errInvalidSuggestedPalette |
| } |
| rgba := c.rgba() |
| if c.typ != ColorTypeRGBA || !validAlphaPremulColor(rgba) { |
| rgba = color.RGBA{0x00, 0x00, 0x00, 0xff} |
| } |
| if p != nil { |
| p(src[:n], " RGBA %02x%02x%02x%02x\n", rgba.R, rgba.G, rgba.B, rgba.A) |
| } |
| src = src[n:] |
| if opts == nil || opts.Palette == nil { |
| m.Palette[i] = rgba |
| } |
| } |
| |
| default: |
| return nil, errUnsupportedMetadataIdentifier |
| } |
| |
| if int64(len(src)) != lenSrcWant { |
| return nil, errInconsistentMetadataChunkLength |
| } |
| return src, nil |
| } |
| |
| // modeFunc is the decoding mode: whether we are decoding styling or drawing |
| // opcodes. |
| // |
| // It is a function type. The decoding loop calls this function to decode and |
| // execute the next opcode from the src buffer, returning the subsequent mode |
| // and the remaining source bytes. |
| type modeFunc func(dst Destination, p printer, src buffer) (modeFunc, buffer, error) |
| |
| func decodeStyling(dst Destination, p printer, src buffer) (modeFunc, buffer, error) { |
| switch opcode := src[0]; { |
| case opcode < 0x80: |
| if opcode < 0x40 { |
| opcode &= 0x3f |
| if p != nil { |
| p(src[:1], "Set CSEL = %d\n", opcode) |
| } |
| src = src[1:] |
| if dst != nil { |
| dst.SetCSel(opcode) |
| } |
| } else { |
| opcode &= 0x3f |
| if p != nil { |
| p(src[:1], "Set NSEL = %d\n", opcode) |
| } |
| src = src[1:] |
| if dst != nil { |
| dst.SetNSel(opcode) |
| } |
| } |
| return decodeStyling, src, nil |
| case opcode < 0xa8: |
| return decodeSetCReg(dst, p, src, opcode) |
| case opcode < 0xc0: |
| return decodeSetNReg(dst, p, src, opcode) |
| case opcode < 0xc7: |
| return decodeStartPath(dst, p, src, opcode) |
| case opcode == 0xc7: |
| return decodeSetLOD(dst, p, src) |
| } |
| return nil, nil, errUnsupportedStylingOpcode |
| } |
| |
| func decodeSetCReg(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) { |
| nBytes, directness, adj := 0, "", opcode&0x07 |
| var decode func(buffer) (Color, int) |
| incr := adj == 7 |
| if incr { |
| adj = 0 |
| } |
| |
| switch (opcode - 0x80) >> 3 { |
| case 0: |
| nBytes, directness, decode = 1, "", buffer.decodeColor1 |
| case 1: |
| nBytes, directness, decode = 2, "", buffer.decodeColor2 |
| case 2: |
| nBytes, directness, decode = 3, " (direct)", buffer.decodeColor3Direct |
| case 3: |
| nBytes, directness, decode = 4, "", buffer.decodeColor4 |
| case 4: |
| nBytes, directness, decode = 3, " (indirect)", buffer.decodeColor3Indirect |
| } |
| if p != nil { |
| if incr { |
| p(src[:1], "Set CREG[CSEL-0] to a %d byte%s color; CSEL++\n", nBytes, directness) |
| } else { |
| p(src[:1], "Set CREG[CSEL-%d] to a %d byte%s color\n", adj, nBytes, directness) |
| } |
| } |
| src = src[1:] |
| |
| c, n := decode(src) |
| if n == 0 { |
| return nil, nil, errInvalidColor |
| } |
| |
| if p != nil { |
| printColor(src[:n], p, c, "") |
| } |
| src = src[n:] |
| |
| if dst != nil { |
| dst.SetCReg(adj, incr, c) |
| } |
| |
| return decodeStyling, src, nil |
| } |
| |
| func printColor(src []byte, p printer, c Color, prefix string) { |
| switch c.typ { |
| case ColorTypeRGBA: |
| if rgba := c.rgba(); validAlphaPremulColor(rgba) { |
| p(src, " %sRGBA %02x%02x%02x%02x\n", prefix, rgba.R, rgba.G, rgba.B, rgba.A) |
| } else if rgba.A == 0 && rgba.B&0x80 != 0 { |
| p(src, " %sgradient (NSTOPS=%d, CBASE=%d, NBASE=%d, %s, %s)\n", |
| prefix, |
| rgba.R&0x3f, |
| rgba.G&0x3f, |
| rgba.B&0x3f, |
| gradientShapeNames[(rgba.B>>6)&0x01], |
| gradientSpreadNames[rgba.G>>6], |
| ) |
| } else { |
| p(src, " %snonsensical color\n", prefix) |
| } |
| case ColorTypePaletteIndex: |
| p(src, " %scustomPalette[%d]\n", prefix, c.paletteIndex()) |
| case ColorTypeCReg: |
| p(src, " %sCREG[%d]\n", prefix, c.cReg()) |
| case ColorTypeBlend: |
| t, c0, c1 := c.blend() |
| p(src[:1], " blend %d:%d c0:c1\n", 0xff-t, t) |
| printColor(src[1:2], p, decodeColor1(c0), " c0: ") |
| printColor(src[2:3], p, decodeColor1(c1), " c1: ") |
| } |
| } |
| |
| func decodeSetNReg(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) { |
| decode, typ, adj := buffer.decodeZeroToOne, "zero-to-one", opcode&0x07 |
| incr := adj == 7 |
| if incr { |
| adj = 0 |
| } |
| |
| switch (opcode - 0xa8) >> 3 { |
| case 0: |
| decode, typ = buffer.decodeReal, "real" |
| case 1: |
| decode, typ = buffer.decodeCoordinate, "coordinate" |
| } |
| if p != nil { |
| if incr { |
| p(src[:1], "Set NREG[NSEL-0] to a %s number; NSEL++\n", typ) |
| } else { |
| p(src[:1], "Set NREG[NSEL-%d] to a %s number\n", adj, typ) |
| } |
| } |
| src = src[1:] |
| |
| f, n := decode(src) |
| if n == 0 { |
| return nil, nil, errInvalidNumber |
| } |
| if p != nil { |
| p(src[:n], " %g\n", f) |
| } |
| src = src[n:] |
| |
| if dst != nil { |
| dst.SetNReg(adj, incr, f) |
| } |
| |
| return decodeStyling, src, nil |
| } |
| |
| func decodeStartPath(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) { |
| adj := opcode & 0x07 |
| if p != nil { |
| p(src[:1], "Start path, filled with CREG[CSEL-%d]; M (absolute moveTo)\n", adj) |
| } |
| src = src[1:] |
| |
| x, src, err := decodeNumber(p, src, buffer.decodeCoordinate) |
| if err != nil { |
| return nil, nil, err |
| } |
| y, src, err := decodeNumber(p, src, buffer.decodeCoordinate) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| if dst != nil { |
| dst.StartPath(adj, x, y) |
| } |
| |
| return decodeDrawing, src, nil |
| } |
| |
| func decodeSetLOD(dst Destination, p printer, src buffer) (modeFunc, buffer, error) { |
| if p != nil { |
| p(src[:1], "Set LOD\n") |
| } |
| src = src[1:] |
| |
| lod0, src, err := decodeNumber(p, src, buffer.decodeReal) |
| if err != nil { |
| return nil, nil, err |
| } |
| lod1, src, err := decodeNumber(p, src, buffer.decodeReal) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| if dst != nil { |
| dst.SetLOD(lod0, lod1) |
| } |
| return decodeStyling, src, nil |
| } |
| |
| func decodeDrawing(dst Destination, p printer, src buffer) (mf modeFunc, src1 buffer, err error) { |
| var coords [6]float32 |
| |
| switch opcode := src[0]; { |
| case opcode < 0xe0: |
| op, nCoords, nReps := "", 0, 1+int(opcode&0x0f) |
| switch opcode >> 4 { |
| case 0x00, 0x01: |
| op = "L (absolute lineTo)" |
| nCoords = 2 |
| nReps = 1 + int(opcode&0x1f) |
| case 0x02, 0x03: |
| op = "l (relative lineTo)" |
| nCoords = 2 |
| nReps = 1 + int(opcode&0x1f) |
| case 0x04: |
| op = "T (absolute smooth quadTo)" |
| nCoords = 2 |
| case 0x05: |
| op = "t (relative smooth quadTo)" |
| nCoords = 2 |
| case 0x06: |
| op = "Q (absolute quadTo)" |
| nCoords = 4 |
| case 0x07: |
| op = "q (relative quadTo)" |
| nCoords = 4 |
| case 0x08: |
| op = "S (absolute smooth cubeTo)" |
| nCoords = 4 |
| case 0x09: |
| op = "s (relative smooth cubeTo)" |
| nCoords = 4 |
| case 0x0a: |
| op = "C (absolute cubeTo)" |
| nCoords = 6 |
| case 0x0b: |
| op = "c (relative cubeTo)" |
| nCoords = 6 |
| case 0x0c: |
| op = "A (absolute arcTo)" |
| nCoords = 0 |
| case 0x0d: |
| op = "a (relative arcTo)" |
| nCoords = 0 |
| } |
| |
| if p != nil { |
| p(src[:1], "%s, %d reps\n", op, nReps) |
| } |
| src = src[1:] |
| |
| for i := 0; i < nReps; i++ { |
| if p != nil && i != 0 { |
| p(nil, "%s, implicit\n", op) |
| } |
| var largeArc, sweep bool |
| if op[0] != 'A' && op[0] != 'a' { |
| src, err = decodeCoordinates(coords[:nCoords], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| } else { |
| // We have an absolute or relative arcTo. |
| src, err = decodeCoordinates(coords[:2], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| coords[2], src, err = decodeAngle(p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| largeArc, sweep, src, err = decodeArcToFlags(p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| src, err = decodeCoordinates(coords[4:6], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| } |
| |
| if dst == nil { |
| continue |
| } |
| switch op[0] { |
| case 'L': |
| dst.AbsLineTo(coords[0], coords[1]) |
| case 'l': |
| dst.RelLineTo(coords[0], coords[1]) |
| case 'T': |
| dst.AbsSmoothQuadTo(coords[0], coords[1]) |
| case 't': |
| dst.RelSmoothQuadTo(coords[0], coords[1]) |
| case 'Q': |
| dst.AbsQuadTo(coords[0], coords[1], coords[2], coords[3]) |
| case 'q': |
| dst.RelQuadTo(coords[0], coords[1], coords[2], coords[3]) |
| case 'S': |
| dst.AbsSmoothCubeTo(coords[0], coords[1], coords[2], coords[3]) |
| case 's': |
| dst.RelSmoothCubeTo(coords[0], coords[1], coords[2], coords[3]) |
| case 'C': |
| dst.AbsCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]) |
| case 'c': |
| dst.RelCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]) |
| case 'A': |
| dst.AbsArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5]) |
| case 'a': |
| dst.RelArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5]) |
| } |
| } |
| |
| case opcode == 0xe1: |
| if p != nil { |
| p(src[:1], "z (closePath); end path\n") |
| } |
| src = src[1:] |
| if dst != nil { |
| dst.ClosePathEndPath() |
| } |
| return decodeStyling, src, nil |
| |
| case opcode == 0xe2: |
| if p != nil { |
| p(src[:1], "z (closePath); M (absolute moveTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:2], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.ClosePathAbsMoveTo(coords[0], coords[1]) |
| } |
| |
| case opcode == 0xe3: |
| if p != nil { |
| p(src[:1], "z (closePath); m (relative moveTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:2], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.ClosePathRelMoveTo(coords[0], coords[1]) |
| } |
| |
| case opcode == 0xe6: |
| if p != nil { |
| p(src[:1], "H (absolute horizontal lineTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:1], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.AbsHLineTo(coords[0]) |
| } |
| |
| case opcode == 0xe7: |
| if p != nil { |
| p(src[:1], "h (relative horizontal lineTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:1], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.RelHLineTo(coords[0]) |
| } |
| |
| case opcode == 0xe8: |
| if p != nil { |
| p(src[:1], "V (absolute vertical lineTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:1], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.AbsVLineTo(coords[0]) |
| } |
| |
| case opcode == 0xe9: |
| if p != nil { |
| p(src[:1], "v (relative vertical lineTo)\n") |
| } |
| src = src[1:] |
| src, err = decodeCoordinates(coords[:1], p, src) |
| if err != nil { |
| return nil, nil, err |
| } |
| if dst != nil { |
| dst.RelVLineTo(coords[0]) |
| } |
| |
| default: |
| return nil, nil, errUnsupportedDrawingOpcode |
| } |
| return decodeDrawing, src, nil |
| } |
| |
| type decodeNumberFunc func(buffer) (float32, int) |
| |
| func decodeNumber(p printer, src buffer, dnf decodeNumberFunc) (float32, buffer, error) { |
| x, n := dnf(src) |
| if n == 0 { |
| return 0, nil, errInvalidNumber |
| } |
| if p != nil { |
| p(src[:n], " %+g\n", x) |
| } |
| return x, src[n:], nil |
| } |
| |
| func decodeCoordinates(coords []float32, p printer, src buffer) (src1 buffer, err error) { |
| for i := range coords { |
| coords[i], src, err = decodeNumber(p, src, buffer.decodeCoordinate) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return src, nil |
| } |
| |
| func decodeAngle(p printer, src buffer) (float32, buffer, error) { |
| x, n := src.decodeZeroToOne() |
| if n == 0 { |
| return 0, nil, errInvalidNumber |
| } |
| if p != nil { |
| p(src[:n], " %v × 360 degrees (%v degrees)\n", x, x*360) |
| } |
| return x, src[n:], nil |
| } |
| |
| func decodeArcToFlags(p printer, src buffer) (bool, bool, buffer, error) { |
| x, n := src.decodeNatural() |
| if n == 0 { |
| return false, false, nil, errInvalidNumber |
| } |
| if p != nil { |
| p(src[:n], " %#x (largeArc=%d, sweep=%d)\n", x, (x>>0)&0x01, (x>>1)&0x01) |
| } |
| return (x>>0)&0x01 != 0, (x>>1)&0x01 != 0, src[n:], nil |
| } |