diff options
Diffstat (limited to 'shiny/iconvg/encode.go')
-rw-r--r-- | shiny/iconvg/encode.go | 605 |
1 files changed, 605 insertions, 0 deletions
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go new file mode 100644 index 0000000..3cf6360 --- /dev/null +++ b/shiny/iconvg/encode.go @@ -0,0 +1,605 @@ +// 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}, +} |