123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- // 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 (
- "errors"
- "image/color"
- "math"
- "golang.org/x/image/math/f32"
- )
- var (
- errCSELUsedAsBothGradientAndStop = errors.New("iconvg: CSEL used as both gradient and stop")
- errDrawingOpsUsedInStylingMode = errors.New("iconvg: drawing ops used in styling mode")
- errInvalidSelectorAdjustment = errors.New("iconvg: invalid selector adjustment")
- errInvalidIncrementingAdjustment = errors.New("iconvg: invalid incrementing adjustment")
- errStylingOpsUsedInDrawingMode = errors.New("iconvg: styling ops used in drawing mode")
- errTooManyGradientStops = errors.New("iconvg: too many gradient stops")
- )
- type mode uint8
- const (
- modeInitial mode = iota
- modeStyling
- modeDrawing
- )
- // Encoder is an IconVG encoder.
- //
- // The zero value is usable. Calling Reset, which is optional, sets the
- // Metadata for the subsequent encoded form. If Reset is not called before
- // other Encoder methods, the default metadata is implied.
- //
- // It aims to emit byte-identical Bytes output for the same input, independent
- // of the platform (and specifically its floating-point hardware).
- type Encoder struct {
- // HighResolutionCoordinates is whether the encoder should encode
- // coordinate numbers for subsequent paths at the best possible resolution
- // afforded by the underlying graphic format.
- //
- // By default (false), the encoder quantizes coordinates to 1/64th of a
- // unit if possible (the default graphic size is 64 by 64 units, so
- // 1/4096th of the default width or height). Each such coordinate can
- // therefore be encoded in either 1 or 2 bytes. If true, some coordinates
- // will be encoded in 4 bytes, giving greater accuracy but larger file
- // sizes. On the Material Design icon set, the 950 or so icons take up
- // around 40% more bytes (172K vs 123K) at high resolution.
- //
- // See the package documentation for more details on the coordinate number
- // encoding format.
- HighResolutionCoordinates bool
- // highResolutionCoordinates is a local copy, copied during StartPath, to
- // avoid having to specify the semantics of modifying the exported field
- // while drawing.
- highResolutionCoordinates bool
- buf buffer
- altBuf buffer
- metadata Metadata
- err error
- lod0 float32
- lod1 float32
- cSel uint8
- nSel uint8
- mode mode
- drawOp byte
- drawArgs []float32
- scratch [12]byte
- }
- // Bytes returns the encoded form.
- func (e *Encoder) Bytes() ([]byte, error) {
- if e.err != nil {
- return nil, e.err
- }
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- }
- return []byte(e.buf), nil
- }
- // Reset resets the Encoder for the given Metadata.
- //
- // This includes setting e.HighResolutionCoordinates to false.
- func (e *Encoder) Reset(m Metadata) {
- *e = Encoder{
- buf: append(e.buf[:0], magic...),
- metadata: m,
- mode: modeStyling,
- lod1: positiveInfinity,
- }
- nMetadataChunks := 0
- mcViewBox := m.ViewBox != DefaultViewBox
- if mcViewBox {
- nMetadataChunks++
- }
- mcSuggestedPalette := m.Palette != DefaultPalette
- if mcSuggestedPalette {
- nMetadataChunks++
- }
- e.buf.encodeNatural(uint32(nMetadataChunks))
- if mcViewBox {
- e.altBuf = e.altBuf[:0]
- e.altBuf.encodeNatural(midViewBox)
- e.altBuf.encodeCoordinate(m.ViewBox.Min[0])
- e.altBuf.encodeCoordinate(m.ViewBox.Min[1])
- e.altBuf.encodeCoordinate(m.ViewBox.Max[0])
- e.altBuf.encodeCoordinate(m.ViewBox.Max[1])
- e.buf.encodeNatural(uint32(len(e.altBuf)))
- e.buf = append(e.buf, e.altBuf...)
- }
- if mcSuggestedPalette {
- n := 63
- for ; n >= 0 && m.Palette[n] == (color.RGBA{0x00, 0x00, 0x00, 0xff}); n-- {
- }
- // Find the shortest encoding that can represent all of m.Palette's n+1
- // explicit colors.
- enc1, enc2, enc3 := true, true, true
- for _, c := range m.Palette[:n+1] {
- if enc1 && (!is1(c.R) || !is1(c.G) || !is1(c.B) || !is1(c.A)) {
- enc1 = false
- }
- if enc2 && (!is2(c.R) || !is2(c.G) || !is2(c.B) || !is2(c.A)) {
- enc2 = false
- }
- if enc3 && (c.A != 0xff) {
- enc3 = false
- }
- }
- e.altBuf = e.altBuf[:0]
- e.altBuf.encodeNatural(midSuggestedPalette)
- if enc1 {
- e.altBuf = append(e.altBuf, byte(n)|0x00)
- for _, c := range m.Palette[:n+1] {
- x, _ := encodeColor1(RGBAColor(c))
- e.altBuf = append(e.altBuf, x)
- }
- } else if enc2 {
- e.altBuf = append(e.altBuf, byte(n)|0x40)
- for _, c := range m.Palette[:n+1] {
- x, _ := encodeColor2(RGBAColor(c))
- e.altBuf = append(e.altBuf, x[0], x[1])
- }
- } else if enc3 {
- e.altBuf = append(e.altBuf, byte(n)|0x80)
- for _, c := range m.Palette[:n+1] {
- e.altBuf = append(e.altBuf, c.R, c.G, c.B)
- }
- } else {
- e.altBuf = append(e.altBuf, byte(n)|0xc0)
- for _, c := range m.Palette[:n+1] {
- e.altBuf = append(e.altBuf, c.R, c.G, c.B, c.A)
- }
- }
- e.buf.encodeNatural(uint32(len(e.altBuf)))
- e.buf = append(e.buf, e.altBuf...)
- }
- }
- func (e *Encoder) appendDefaultMetadata() {
- e.buf = append(e.buf[:0], magic...)
- e.buf = append(e.buf, 0x00) // There are zero metadata chunks.
- e.mode = modeStyling
- }
- func (e *Encoder) CSel() uint8 {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- }
- return e.cSel
- }
- func (e *Encoder) NSel() uint8 {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- }
- return e.nSel
- }
- func (e *Encoder) LOD() (lod0, lod1 float32) {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- }
- return e.lod0, e.lod1
- }
- func (e *Encoder) checkModeStyling() {
- if e.mode == modeStyling {
- return
- }
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- return
- }
- e.err = errStylingOpsUsedInDrawingMode
- }
- func (e *Encoder) SetCSel(cSel uint8) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- e.cSel = cSel & 0x3f
- e.buf = append(e.buf, e.cSel)
- }
- func (e *Encoder) SetNSel(nSel uint8) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- e.nSel = nSel & 0x3f
- e.buf = append(e.buf, e.nSel|0x40)
- }
- func (e *Encoder) SetCReg(adj uint8, incr bool, c Color) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- if adj > 6 {
- e.err = errInvalidSelectorAdjustment
- return
- }
- if incr {
- if adj != 0 {
- e.err = errInvalidIncrementingAdjustment
- }
- adj = 7
- }
- if x, ok := encodeColor1(c); ok {
- e.buf = append(e.buf, adj|0x80, x)
- return
- }
- if x, ok := encodeColor2(c); ok {
- e.buf = append(e.buf, adj|0x88, x[0], x[1])
- return
- }
- if x, ok := encodeColor3Direct(c); ok {
- e.buf = append(e.buf, adj|0x90, x[0], x[1], x[2])
- return
- }
- if x, ok := encodeColor4(c); ok {
- e.buf = append(e.buf, adj|0x98, x[0], x[1], x[2], x[3])
- return
- }
- if x, ok := encodeColor3Indirect(c); ok {
- e.buf = append(e.buf, adj|0xa0, x[0], x[1], x[2])
- return
- }
- panic("unreachable")
- }
- func (e *Encoder) SetNReg(adj uint8, incr bool, f float32) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- if adj > 6 {
- e.err = errInvalidSelectorAdjustment
- return
- }
- if incr {
- if adj != 0 {
- e.err = errInvalidIncrementingAdjustment
- }
- adj = 7
- }
- // Try three different encodings and pick the shortest.
- b := buffer(e.scratch[0:0])
- opcode, iBest, nBest := uint8(0xa8), 0, b.encodeReal(f)
- b = buffer(e.scratch[4:4])
- if n := b.encodeCoordinate(f); n < nBest {
- opcode, iBest, nBest = 0xb0, 4, n
- }
- b = buffer(e.scratch[8:8])
- if n := b.encodeZeroToOne(f); n < nBest {
- opcode, iBest, nBest = 0xb8, 8, n
- }
- e.buf = append(e.buf, adj|opcode)
- e.buf = append(e.buf, e.scratch[iBest:iBest+nBest]...)
- }
- func (e *Encoder) SetLOD(lod0, lod1 float32) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- e.lod0 = lod0
- e.lod1 = lod1
- e.buf = append(e.buf, 0xc7)
- e.buf.encodeReal(lod0)
- e.buf.encodeReal(lod1)
- }
- // SetGradient sets CREG[CSEL] to encode the gradient whose colors defined by
- // spread and stops. Its geometry is either linear or radial, depending on the
- // radial argument, and the given affine transformation matrix maps from
- // graphic coordinate space defined by the metadata's viewBox (e.g. from (-32,
- // -32) to (+32, +32)) to gradient coordinate space. Gradient coordinate space
- // is where a linear gradient ranges from x=0 to x=1, and a radial gradient has
- // center (0, 0) and radius 1.
- //
- // The colors of the n stops are encoded at CREG[cBase+0], CREG[cBase+1], ...,
- // CREG[cBase+n-1]. Similarly, the offsets of the n stops are encoded at
- // NREG[nBase+0], NREG[nBase+1], ..., NREG[nBase+n-1]. Additional parameters
- // are stored at NREG[nBase-4], NREG[nBase-3], NREG[nBase-2] and NREG[nBase-1].
- //
- // The CSEL and NSEL selector registers maintain the same values after the
- // method returns as they had when the method was called.
- //
- // See the package documentation for more details on the gradient encoding
- // format and the derivation of common transformation matrices.
- func (e *Encoder) SetGradient(cBase, nBase uint8, radial bool, transform f32.Aff3, spread GradientSpread, stops []GradientStop) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- if len(stops) > 64-len(transform) {
- e.err = errTooManyGradientStops
- return
- }
- if x, y := e.cSel, e.cSel+64; (cBase <= x && x < cBase+uint8(len(stops))) ||
- (cBase <= y && y < cBase+uint8(len(stops))) {
- e.err = errCSELUsedAsBothGradientAndStop
- return
- }
- oldCSel := e.cSel
- oldNSel := e.nSel
- cBase &= 0x3f
- nBase &= 0x3f
- bFlags := uint8(0x80)
- if radial {
- bFlags = 0xc0
- }
- e.SetCReg(0, false, RGBAColor(color.RGBA{
- R: uint8(len(stops)),
- G: cBase | uint8(spread<<6),
- B: nBase | bFlags,
- A: 0x00,
- }))
- e.SetCSel(cBase)
- e.SetNSel(nBase)
- for i, v := range transform {
- e.SetNReg(uint8(len(transform)-i), false, v)
- }
- for _, s := range stops {
- r, g, b, a := s.Color.RGBA()
- e.SetCReg(0, true, RGBAColor(color.RGBA{
- R: uint8(r >> 8),
- G: uint8(g >> 8),
- B: uint8(b >> 8),
- A: uint8(a >> 8),
- }))
- e.SetNReg(0, true, s.Offset)
- }
- e.SetCSel(oldCSel)
- e.SetNSel(oldNSel)
- }
- // SetLinearGradient is like SetGradient with radial=false except that the
- // transformation matrix is implicitly defined by two boundary points (x1, y1)
- // and (x2, y2).
- func (e *Encoder) SetLinearGradient(cBase, nBase uint8, x1, y1, x2, y2 float32, spread GradientSpread, stops []GradientStop) {
- // See the package documentation's appendix for a derivation of the
- // transformation matrix.
- dx, dy := x2-x1, y2-y1
- d := dx*dx + dy*dy
- ma := dx / d
- mb := dy / d
- e.SetGradient(cBase, nBase, false, f32.Aff3{
- ma, mb, -ma*x1 - mb*y1,
- 0, 0, 0,
- }, spread, stops)
- }
- // SetCircularGradient is like SetGradient with radial=true except that the
- // transformation matrix is implicitly defined by a center (cx, cy) and a
- // radius vector (rx, ry) such that (cx+rx, cy+ry) is on the circle.
- func (e *Encoder) SetCircularGradient(cBase, nBase uint8, cx, cy, rx, ry float32, spread GradientSpread, stops []GradientStop) {
- // See the package documentation's appendix for a derivation of the
- // transformation matrix.
- invR := float32(1 / math.Sqrt(float64(rx*rx+ry*ry)))
- e.SetGradient(cBase, nBase, true, f32.Aff3{
- invR, 0, -cx * invR,
- 0, invR, -cy * invR,
- }, spread, stops)
- }
- // SetEllipticalGradient is like SetGradient with radial=true except that the
- // transformation matrix is implicitly defined by a center (cx, cy) and two
- // axis vectors (rx, ry) and (sx, sy) such that (cx+rx, cy+ry) and (cx+sx,
- // cy+sy) are on the ellipse.
- func (e *Encoder) SetEllipticalGradient(cBase, nBase uint8, cx, cy, rx, ry, sx, sy float32, spread GradientSpread, stops []GradientStop) {
- // Explicitly disable FMA in the floating-point calculations below
- // to get consistent results on all platforms, and in turn produce
- // a byte-identical encoding.
- // See https://golang.org/ref/spec#Floating_point_operators and issue 43219.
- // See the package documentation's appendix for a derivation of the
- // transformation matrix.
- invRSSR := 1 / (float32(rx*sy) - float32(sx*ry))
- ma := +sy * invRSSR
- mb := -sx * invRSSR
- mc := -float32(ma*cx) - float32(mb*cy)
- md := -ry * invRSSR
- me := +rx * invRSSR
- mf := -float32(md*cx) - float32(me*cy)
- e.SetGradient(cBase, nBase, true, f32.Aff3{
- ma, mb, mc,
- md, me, mf,
- }, spread, stops)
- }
- func (e *Encoder) StartPath(adj uint8, x, y float32) {
- e.checkModeStyling()
- if e.err != nil {
- return
- }
- if adj > 6 {
- e.err = errInvalidSelectorAdjustment
- return
- }
- e.highResolutionCoordinates = e.HighResolutionCoordinates
- e.buf = append(e.buf, uint8(0xc0+adj))
- e.buf.encodeCoordinate(quantize(x, e.highResolutionCoordinates))
- e.buf.encodeCoordinate(quantize(y, e.highResolutionCoordinates))
- e.mode = modeDrawing
- }
- func (e *Encoder) AbsHLineTo(x float32) { e.draw('H', x, 0, 0, 0, 0, 0) }
- func (e *Encoder) RelHLineTo(x float32) { e.draw('h', x, 0, 0, 0, 0, 0) }
- func (e *Encoder) AbsVLineTo(y float32) { e.draw('V', y, 0, 0, 0, 0, 0) }
- func (e *Encoder) RelVLineTo(y float32) { e.draw('v', y, 0, 0, 0, 0, 0) }
- func (e *Encoder) AbsLineTo(x, y float32) { e.draw('L', x, y, 0, 0, 0, 0) }
- func (e *Encoder) RelLineTo(x, y float32) { e.draw('l', x, y, 0, 0, 0, 0) }
- func (e *Encoder) AbsSmoothQuadTo(x, y float32) { e.draw('T', x, y, 0, 0, 0, 0) }
- func (e *Encoder) RelSmoothQuadTo(x, y float32) { e.draw('t', x, y, 0, 0, 0, 0) }
- func (e *Encoder) AbsQuadTo(x1, y1, x, y float32) { e.draw('Q', x1, y1, x, y, 0, 0) }
- func (e *Encoder) RelQuadTo(x1, y1, x, y float32) { e.draw('q', x1, y1, x, y, 0, 0) }
- func (e *Encoder) AbsSmoothCubeTo(x2, y2, x, y float32) { e.draw('S', x2, y2, x, y, 0, 0) }
- func (e *Encoder) RelSmoothCubeTo(x2, y2, x, y float32) { e.draw('s', x2, y2, x, y, 0, 0) }
- func (e *Encoder) AbsCubeTo(x1, y1, x2, y2, x, y float32) { e.draw('C', x1, y1, x2, y2, x, y) }
- func (e *Encoder) RelCubeTo(x1, y1, x2, y2, x, y float32) { e.draw('c', x1, y1, x2, y2, x, y) }
- func (e *Encoder) ClosePathEndPath() { e.draw('Z', 0, 0, 0, 0, 0, 0) }
- func (e *Encoder) ClosePathAbsMoveTo(x, y float32) { e.draw('Y', x, y, 0, 0, 0, 0) }
- func (e *Encoder) ClosePathRelMoveTo(x, y float32) { e.draw('y', x, y, 0, 0, 0, 0) }
- func (e *Encoder) AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
- e.arcTo('A', rx, ry, xAxisRotation, largeArc, sweep, x, y)
- }
- func (e *Encoder) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
- e.arcTo('a', rx, ry, xAxisRotation, largeArc, sweep, x, y)
- }
- func (e *Encoder) arcTo(drawOp byte, rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
- flags := uint32(0)
- if largeArc {
- flags |= 0x01
- }
- if sweep {
- flags |= 0x02
- }
- e.draw(drawOp, rx, ry, xAxisRotation, float32(flags), x, y)
- }
- func (e *Encoder) draw(drawOp byte, arg0, arg1, arg2, arg3, arg4, arg5 float32) {
- if e.err != nil {
- return
- }
- if e.mode != modeDrawing {
- e.err = errDrawingOpsUsedInStylingMode
- return
- }
- if e.drawOp != drawOp {
- e.flushDrawOps()
- }
- e.drawOp = drawOp
- switch drawOps[drawOp].nArgs {
- case 0:
- // No-op.
- case 1:
- e.drawArgs = append(e.drawArgs, arg0)
- case 2:
- e.drawArgs = append(e.drawArgs, arg0, arg1)
- case 4:
- e.drawArgs = append(e.drawArgs, arg0, arg1, arg2, arg3)
- case 6:
- e.drawArgs = append(e.drawArgs, arg0, arg1, arg2, arg3, arg4, arg5)
- default:
- panic("unreachable")
- }
- switch drawOp {
- case 'Z':
- e.mode = modeStyling
- fallthrough
- case 'Y', 'y':
- e.flushDrawOps()
- }
- }
- func (e *Encoder) flushDrawOps() {
- if e.drawOp == 0x00 {
- return
- }
- if op := drawOps[e.drawOp]; op.nArgs == 0 {
- e.buf = append(e.buf, op.opcodeBase)
- } else {
- n := len(e.drawArgs) / int(op.nArgs)
- for i := 0; n > 0; {
- m := n
- if m > int(op.maxRepCount) {
- m = int(op.maxRepCount)
- }
- e.buf = append(e.buf, op.opcodeBase+uint8(m)-1)
- switch e.drawOp {
- default:
- for j := m * int(op.nArgs); j > 0; j-- {
- e.buf.encodeCoordinate(quantize(e.drawArgs[i], e.highResolutionCoordinates))
- i++
- }
- case 'A', 'a':
- for j := m; j > 0; j-- {
- e.buf.encodeCoordinate(quantize(e.drawArgs[i+0], e.highResolutionCoordinates))
- e.buf.encodeCoordinate(quantize(e.drawArgs[i+1], e.highResolutionCoordinates))
- e.buf.encodeAngle(e.drawArgs[i+2])
- e.buf.encodeNatural(uint32(e.drawArgs[i+3]))
- e.buf.encodeCoordinate(quantize(e.drawArgs[i+4], e.highResolutionCoordinates))
- e.buf.encodeCoordinate(quantize(e.drawArgs[i+5], e.highResolutionCoordinates))
- i += 6
- }
- }
- n -= m
- }
- }
- e.drawOp = 0x00
- e.drawArgs = e.drawArgs[:0]
- }
- func quantize(coord float32, highResolutionCoordinates bool) float32 {
- if !highResolutionCoordinates && (-128 <= coord && coord < 128) {
- x := math.Floor(float64(coord*64 + 0.5))
- return float32(x) / 64
- }
- return coord
- }
- var drawOps = [256]struct {
- opcodeBase byte
- maxRepCount uint8
- nArgs uint8
- }{
- 'L': {0x00, 32, 2},
- 'l': {0x20, 32, 2},
- 'T': {0x40, 16, 2},
- 't': {0x50, 16, 2},
- 'Q': {0x60, 16, 4},
- 'q': {0x70, 16, 4},
- 'S': {0x80, 16, 4},
- 's': {0x90, 16, 4},
- 'C': {0xa0, 16, 6},
- 'c': {0xb0, 16, 6},
- 'A': {0xc0, 16, 6},
- 'a': {0xd0, 16, 6},
- // Z means close path and then end path.
- 'Z': {0xe1, 1, 0},
- // Y/y means close path and then open a new path (with a MoveTo/moveTo).
- 'Y': {0xe2, 1, 2},
- 'y': {0xe3, 1, 2},
- 'H': {0xe6, 1, 1},
- 'h': {0xe7, 1, 1},
- 'V': {0xe8, 1, 1},
- 'v': {0xe9, 1, 1},
- }
|