blob: b5bb381bb5ec2bf89d3ea2558d55fb51dbe6d03f [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package iconvg
import (
"bytes"
"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
}