diff options
author | Benau <Benau@users.noreply.github.com> | 2021-08-25 04:32:50 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-24 22:32:50 +0200 |
commit | 53cafa9f3d0c8be33821fc7338b1da97e91d9cc6 (patch) | |
tree | 964a225219099a1a1c282e27913767da588191b4 /vendor/github.com/kettek/apng | |
parent | d4195deb3a6305c49c50ff30e8af978c7f1bdd92 (diff) | |
download | matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.tar.gz matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.tar.bz2 matterbridge-msglm-53cafa9f3d0c8be33821fc7338b1da97e91d9cc6.zip |
Convert .tgs with go libraries (and cgo) (telegram) (#1569)
This commit adds support for go/cgo tgs conversion when building with the -tags `cgo`
The default binaries are still "pure" go and uses the old way of converting.
* Move lottie_convert.py conversion code to its own file
* Add optional libtgsconverter
* Update vendor
* Apply suggestions from code review
* Update bridge/helper/libtgsconverter.go
Co-authored-by: Wim <wim@42.be>
Diffstat (limited to 'vendor/github.com/kettek/apng')
-rw-r--r-- | vendor/github.com/kettek/apng/LICENSE | 30 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/README.md | 114 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/apng.go | 13 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/frame.go | 47 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/paeth.go | 71 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/reader.go | 1141 | ||||
-rw-r--r-- | vendor/github.com/kettek/apng/writer.go | 713 |
7 files changed, 2129 insertions, 0 deletions
diff --git a/vendor/github.com/kettek/apng/LICENSE b/vendor/github.com/kettek/apng/LICENSE new file mode 100644 index 00000000..399ef573 --- /dev/null +++ b/vendor/github.com/kettek/apng/LICENSE @@ -0,0 +1,30 @@ +Original PNG code Copyright (c) 2009 The Go Authors. +APNG enhancements Copyright (c) 2018 Ketchetwahmeegwun T. Southall / +kts of kettek. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/kettek/apng/README.md b/vendor/github.com/kettek/apng/README.md new file mode 100644 index 00000000..862c3593 --- /dev/null +++ b/vendor/github.com/kettek/apng/README.md @@ -0,0 +1,114 @@ +# APNG golang library +This `apng` package provides methods for decoding and encoding APNG files. It is based upon the work in the official "image/png" package. + +See [apngr](https://github.com/kettek/apngr) for an APNG extraction and combination tool using this library. + +**NOTE**: The decoder should work for most anything you throw at it. Malformed PNGs should result in an error message. The encoder currently doesn't handle differences of Image formats and similar and has not been tested as thoroughly. + +If a regular PNG file is read, the first Frame of the APNG returned by `DecodeAll(*File)` will be the PNG data. + +## Types +### APNG +The APNG type contains the frames of a decoded `.apng` file, along with any important properties. It may also be created and used for Encoding. + +| Signature | Description | +|---------------------------|-------------------------------| +| Frames []Frame | The stored frames of the APNG.| +| LoopCount uint | The number of times an animation should be restarted during display. A value of 0 means to loop forever. | + +### Frame +The Frame type contains an individual frame of an APNG. The following table provides the important properties and methods. + +| Signature | Description | +|---------------------------|------------------| +| Image image.Image | Frame image data. | +| IsDefault bool | Indicates if this frame is a default image that should not be included as part of the animation frames. May only be true for the first Frame. | +| XOffset int | Returns the x offset of the frame. | +| YOffset int | Returns the y offset of the frame. | +| DelayNumerator int | Returns the delay numerator. | +| DelayDenominator int | Returns the delay denominator. | +| DisposeOp byte | Returns the frame disposal operation. May be `apng.DISPOSE_OP_NONE`, `apng.DISPOSE_OP_BACKGROUND`, or `apng.DISPOSE_OP_PREVIOUS`. See the [APNG Specification](https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk) for more information. | +| BlendOp byte | Returns the frame blending operation. May be `apng.BLEND_OP_SOURCE` or `apng.BLEND_OP_OVER`. See the [APNG Specification](https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk) for more information. | + +## Methods +### DecodeAll(io.Reader) (APNG, error) +This method returns an APNG type containing the frames and associated data within the passed file. + +### Example +```go +package main + +import ( + "github.com/kettek/apng" + "os" + "log" +) + +func main() { + // Open our animated PNG file + f, err := os.Open("animation.png") + if err != nil { + panic(err) + } + defer f.Close() + // Decode all frames into an APNG + a, err := apng.DecodeAll(f) + if err != nil { + panic(err) + } + // Print some information on the APNG + log.Printf("Found %d frames\n", len(a.Frames)) + for i, frame := range a.Frames { + b := frame.Image.Bounds() + log.Printf("Frame %d: %dx%d\n", i, b.Max.X, b.Max.Y) + } +} + +``` + +### Decode(io.Reader) (image.Image, error) +This method returns the Image of the default frame of an APNG file. + +### Encode(io.Writer, APNG) error +This method writes the passed APNG object to the given io.Writer as an APNG binary file. + +### Example +```go +package main + +import ( + "github.com/kettek/apng" + "image/png" + "os" +) + +func main() { + // Define our variables + output := "animation.png" + images := [4]string{"0.png", "1.png", "2.png", "3.png"} + a := apng.APNG{ + Frames: make([]apng.Frame, len(images)), + } + // Open our file for writing + out, err := os.Create(output) + if err != nil { + panic(err) + } + defer out.Close() + // Assign each decoded PNG's Image to the appropriate Frame Image + for i, s := range images { + in, err := os.Open(s) + if err != nil { + panic(err) + } + defer in.Close() + m, err := png.Decode(in) + if err != nil { + panic(err) + } + a.Frames[i].Image = m + } + // Write APNG to our output file + apng.Encode(out, a) +} +``` diff --git a/vendor/github.com/kettek/apng/apng.go b/vendor/github.com/kettek/apng/apng.go new file mode 100644 index 00000000..ee75fbd0 --- /dev/null +++ b/vendor/github.com/kettek/apng/apng.go @@ -0,0 +1,13 @@ +// Copyright 2018 kts of kettek / Ketchetwahmeegwun Tecumseh Southall. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package apng + +type APNG struct { + Frames []Frame + // LoopCount defines the number of times an animation will be + // restarted during display. + // A LoopCount of 0 means to loop forever + LoopCount uint +} diff --git a/vendor/github.com/kettek/apng/frame.go b/vendor/github.com/kettek/apng/frame.go new file mode 100644 index 00000000..31c21eda --- /dev/null +++ b/vendor/github.com/kettek/apng/frame.go @@ -0,0 +1,47 @@ +// Copyright 2018 kts of kettek / Ketchetwahmeegwun Tecumseh Southall. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package apng + +import ( + "image" +) + +// dispose_op values, as per the APNG spec. +const ( + DISPOSE_OP_NONE = 0 + DISPOSE_OP_BACKGROUND = 1 + DISPOSE_OP_PREVIOUS = 2 +) + +// blend_op values, as per the APNG spec. +const ( + BLEND_OP_SOURCE = 0 + BLEND_OP_OVER = 1 +) + +type Frame struct { + Image image.Image + width, height int + XOffset, YOffset int + DelayNumerator uint16 + DelayDenominator uint16 + DisposeOp byte + BlendOp byte + // IsDefault indicates if the Frame is a default image that + // should not be used in the animation. IsDefault can only + // be true on the first frame. + IsDefault bool +} + +// GetDelay returns the number of seconds in the frame. +func (f *Frame) GetDelay() float64 { + d := uint16(0) + if f.DelayDenominator == 0 { + d = 100 + } else { + d = f.DelayDenominator + } + return float64(f.DelayNumerator) / float64(d) +} diff --git a/vendor/github.com/kettek/apng/paeth.go b/vendor/github.com/kettek/apng/paeth.go new file mode 100644 index 00000000..adcf311d --- /dev/null +++ b/vendor/github.com/kettek/apng/paeth.go @@ -0,0 +1,71 @@ +// Copyright 2012 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 apng + +// intSize is either 32 or 64. +const intSize = 32 << (^uint(0) >> 63) + +func abs(x int) int { + // m := -1 if x < 0. m := 0 otherwise. + m := x >> (intSize - 1) + + // In two's complement representation, the negative number + // of any number (except the smallest one) can be computed + // by flipping all the bits and add 1. This is faster than + // code with a branch. + // See Hacker's Delight, section 2-4. + return (x ^ m) - m +} + +// paeth implements the Paeth filter function, as per the PNG specification. +func paeth(a, b, c uint8) uint8 { + // This is an optimized version of the sample code in the PNG spec. + // For example, the sample code starts with: + // p := int(a) + int(b) - int(c) + // pa := abs(p - int(a)) + // but the optimized form uses fewer arithmetic operations: + // pa := int(b) - int(c) + // pa = abs(pa) + pc := int(c) + pa := int(b) - pc + pb := int(a) - pc + pc = abs(pa + pb) + pa = abs(pa) + pb = abs(pb) + if pa <= pb && pa <= pc { + return a + } else if pb <= pc { + return b + } + return c +} + +// filterPaeth applies the Paeth filter to the cdat slice. +// cdat is the current row's data, pdat is the previous row's data. +func filterPaeth(cdat, pdat []byte, bytesPerPixel int) { + var a, b, c, pa, pb, pc int + for i := 0; i < bytesPerPixel; i++ { + a, c = 0, 0 + for j := i; j < len(cdat); j += bytesPerPixel { + b = int(pdat[j]) + pa = b - c + pb = a - c + pc = abs(pa + pb) + pa = abs(pa) + pb = abs(pb) + if pa <= pb && pa <= pc { + // No-op. + } else if pb <= pc { + a = b + } else { + a = c + } + a += int(cdat[j]) + a &= 0xff + cdat[j] = uint8(a) + c = b + } + } +} diff --git a/vendor/github.com/kettek/apng/reader.go b/vendor/github.com/kettek/apng/reader.go new file mode 100644 index 00000000..fd6ccee3 --- /dev/null +++ b/vendor/github.com/kettek/apng/reader.go @@ -0,0 +1,1141 @@ +// Original PNG code Copyright 2009 The Go Authors. +// Additional APNG enhancements Copyright 2018 Ketchetwahmeegwun +// Tecumseh Southall / kts of kettek. +// All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package apng implements an APNG image decoder. +// +// The PNG specification is at https://www.w3.org/TR/PNG/. +// The APNG specification is at https://wiki.mozilla.org/APNG_Specification +package apng + +import ( + "compress/zlib" + "encoding/binary" + "fmt" + "hash" + "hash/crc32" + "image" + "image/color" + "io" +) + +// Color type, as per the PNG spec. +const ( + ctGrayscale = 0 + ctTrueColor = 2 + ctPaletted = 3 + ctGrayscaleAlpha = 4 + ctTrueColorAlpha = 6 +) + +// A cb is a combination of color type and bit depth. +const ( + cbInvalid = iota + cbG1 + cbG2 + cbG4 + cbG8 + cbGA8 + cbTC8 + cbP1 + cbP2 + cbP4 + cbP8 + cbTCA8 + cbG16 + cbGA16 + cbTC16 + cbTCA16 +) + +func cbPaletted(cb int) bool { + return cbP1 <= cb && cb <= cbP8 +} + +// Filter type, as per the PNG spec. +const ( + ftNone = 0 + ftSub = 1 + ftUp = 2 + ftAverage = 3 + ftPaeth = 4 + nFilter = 5 +) + +// Interlace type. +const ( + itNone = 0 + itAdam7 = 1 +) + +// interlaceScan defines the placement and size of a pass for Adam7 interlacing. +type interlaceScan struct { + xFactor, yFactor, xOffset, yOffset int +} + +// interlacing defines Adam7 interlacing, with 7 passes of reduced images. +// See https://www.w3.org/TR/PNG/#8Interlace +var interlacing = []interlaceScan{ + {8, 8, 0, 0}, + {8, 8, 4, 0}, + {4, 8, 0, 4}, + {4, 4, 2, 0}, + {2, 4, 0, 2}, + {2, 2, 1, 0}, + {1, 2, 0, 1}, +} + +// Decoding stage. +// The PNG specification says that the IHDR, PLTE (if present), tRNS (if +// present), IDAT and IEND chunks must appear in that order. There may be +// multiple IDAT chunks, and IDAT chunks must be sequential (i.e. they may not +// have any other chunks between them). +// https://www.w3.org/TR/PNG/#5ChunkOrdering +const ( + dsStart = iota + dsSeenIHDR + dsSeenPLTE + dsSeentRNS + dsSeenacTL + dsSeenIDAT + dsSeenfdAT + dsSeenIEND +) + +const pngHeader = "\x89PNG\r\n\x1a\n" + +type decoder struct { + r io.Reader + num_frames uint32 + a APNG + frame_index int + crc hash.Hash32 + width, height int + depth int + palette color.Palette + cb int + stage int + idatLength uint32 + tmp [3 * 256]byte + interlace int + + // useTransparent and transparent are used for grayscale and truecolor + // transparency, as opposed to palette transparency. + useTransparent bool + transparent [6]byte +} + +// A FormatError reports that the input is not a valid PNG. +type FormatError string + +func (e FormatError) Error() string { return "apng: invalid format: " + string(e) } + +var chunkOrderError = FormatError("chunk out of order") + +// An UnsupportedError reports that the input uses a valid but unimplemented PNG feature. +type UnsupportedError string + +func (e UnsupportedError) Error() string { return "apng: unsupported feature: " + string(e) } + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func (d *decoder) parseIHDR(length uint32) error { + if length != 13 { + return FormatError("bad IHDR length") + } + if _, err := io.ReadFull(d.r, d.tmp[:13]); err != nil { + return err + } + d.crc.Write(d.tmp[:13]) + if d.tmp[10] != 0 { + return UnsupportedError("compression method") + } + if d.tmp[11] != 0 { + return UnsupportedError("filter method") + } + if d.tmp[12] != itNone && d.tmp[12] != itAdam7 { + return FormatError("invalid interlace method") + } + d.interlace = int(d.tmp[12]) + + w := int32(binary.BigEndian.Uint32(d.tmp[0:4])) + h := int32(binary.BigEndian.Uint32(d.tmp[4:8])) + if w <= 0 || h <= 0 { + return FormatError("non-positive dimension") + } + nPixels := int64(w) * int64(h) + if nPixels != int64(int(nPixels)) { + return UnsupportedError("dimension overflow") + } + // There can be up to 8 bytes per pixel, for 16 bits per channel RGBA. + if nPixels != (nPixels*8)/8 { + return UnsupportedError("dimension overflow") + } + + d.cb = cbInvalid + d.depth = int(d.tmp[8]) + switch d.depth { + case 1: + switch d.tmp[9] { + case ctGrayscale: + d.cb = cbG1 + case ctPaletted: + d.cb = cbP1 + } + case 2: + switch d.tmp[9] { + case ctGrayscale: + d.cb = cbG2 + case ctPaletted: + d.cb = cbP2 + } + case 4: + switch d.tmp[9] { + case ctGrayscale: + d.cb = cbG4 + case ctPaletted: + d.cb = cbP4 + } + case 8: + switch d.tmp[9] { + case ctGrayscale: + d.cb = cbG8 + case ctTrueColor: + d.cb = cbTC8 + case ctPaletted: + d.cb = cbP8 + case ctGrayscaleAlpha: + d.cb = cbGA8 + case ctTrueColorAlpha: + d.cb = cbTCA8 + } + case 16: + switch d.tmp[9] { + case ctGrayscale: + d.cb = cbG16 + case ctTrueColor: + d.cb = cbTC16 + case ctGrayscaleAlpha: + d.cb = cbGA16 + case ctTrueColorAlpha: + d.cb = cbTCA16 + } + } + if d.cb == cbInvalid { + return UnsupportedError(fmt.Sprintf("bit depth %d, color type %d", d.tmp[8], d.tmp[9])) + } + d.a.Frames[0].width, d.a.Frames[0].height = int(w), int(h) + return d.verifyChecksum() +} + +func (d *decoder) parsePLTE(length uint32) error { + np := int(length / 3) // The number of palette entries. + if length%3 != 0 || np <= 0 || np > 256 || np > 1<<uint(d.depth) { + return FormatError("bad PLTE length") + } + n, err := io.ReadFull(d.r, d.tmp[:3*np]) + if err != nil { + return err + } + d.crc.Write(d.tmp[:n]) + switch d.cb { + case cbP1, cbP2, cbP4, cbP8: + d.palette = make(color.Palette, 256) + for i := 0; i < np; i++ { + d.palette[i] = color.RGBA{d.tmp[3*i+0], d.tmp[3*i+1], d.tmp[3*i+2], 0xff} + } + for i := np; i < 256; i++ { + // Initialize the rest of the palette to opaque black. The spec (section + // 11.2.3) says that "any out-of-range pixel value found in the image data + // is an error", but some real-world PNG files have out-of-range pixel + // values. We fall back to opaque black, the same as libpng 1.5.13; + // ImageMagick 6.5.7 returns an error. + d.palette[i] = color.RGBA{0x00, 0x00, 0x00, 0xff} + } + d.palette = d.palette[:np] + case cbTC8, cbTCA8, cbTC16, cbTCA16: + // As per the PNG spec, a PLTE chunk is optional (and for practical purposes, + // ignorable) for the ctTrueColor and ctTrueColorAlpha color types (section 4.1.2). + default: + return FormatError("PLTE, color type mismatch") + } + return d.verifyChecksum() +} + +func (d *decoder) parsetRNS(length uint32) error { + switch d.cb { + case cbG1, cbG2, cbG4, cbG8, cbG16: + if length != 2 { + return FormatError("bad tRNS length") + } + n, err := io.ReadFull(d.r, d.tmp[:length]) + if err != nil { + return err + } + d.crc.Write(d.tmp[:n]) + + copy(d.transparent[:], d.tmp[:length]) + switch d.cb { + case cbG1: + d.transparent[1] *= 0xff + case cbG2: + d.transparent[1] *= 0x55 + case cbG4: + d.transparent[1] *= 0x11 + } + d.useTransparent = true + + case cbTC8, cbTC16: + if length != 6 { + return FormatError("bad tRNS length") + } + n, err := io.ReadFull(d.r, d.tmp[:length]) + if err != nil { + return err + } + d.crc.Write(d.tmp[:n]) + + copy(d.transparent[:], d.tmp[:length]) + d.useTransparent = true + + case cbP1, cbP2, cbP4, cbP8: + if length > 256 { + return FormatError("bad tRNS length") + } + n, err := io.ReadFull(d.r, d.tmp[:length]) + if err != nil { + return err + } + d.crc.Write(d.tmp[:n]) + + if len(d.palette) < n { + d.palette = d.palette[:n] + } + for i := 0; i < n; i++ { + rgba := d.palette[i].(color.RGBA) + d.palette[i] = color.NRGBA{rgba.R, rgba.G, rgba.B, d.tmp[i]} + } + + default: + return FormatError("tRNS, color type mismatch") + } + return d.verifyChecksum() +} + +// Read presents one or more IDAT chunks as one continuous stream (minus the +// intermediate chunk headers and footers). If the PNG data looked like: +// ... len0 IDAT xxx crc0 len1 IDAT yy crc1 len2 IEND crc2 +// then this reader presents xxxyy. For well-formed PNG data, the decoder state +// immediately before the first Read call is that d.r is positioned between the +// first IDAT and xxx, and the decoder state immediately after the last Read +// call is that d.r is positioned between yy and crc1. +func (d *decoder) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + for d.idatLength == 0 { + // We have exhausted an IDAT chunk. Verify the checksum of that chunk. + if err := d.verifyChecksum(); err != nil { + return 0, err + } + // Read the length and chunk type of the next chunk, and check that + // it is an IDAT chunk. + if _, err := io.ReadFull(d.r, d.tmp[:8]); err != nil { + return 0, err + } + + if d.stage < dsSeenfdAT { + d.idatLength = binary.BigEndian.Uint32(d.tmp[:4]) + if string(d.tmp[4:8]) != "IDAT" { + return 0, FormatError(fmt.Sprintf("expected IDAT, found %s", string(d.tmp[4:8]))) + } + } else { + d.idatLength = binary.BigEndian.Uint32(d.tmp[:4]) - 4 + if string(d.tmp[4:8]) != "fdAT" { + return 0, FormatError(fmt.Sprintf("expected fdAT, found %s", string(d.tmp[4:8]))) + } + } + d.crc.Reset() + d.crc.Write(d.tmp[4:8]) + if d.stage >= dsSeenfdAT { + if _, err := io.ReadFull(d.r, d.tmp[:4]); err != nil { + return 0, err + } + d.crc.Write(d.tmp[:4]) + } + } + if int(d.idatLength) < 0 { + return 0, UnsupportedError("IDAT/fdAT chunk length overflow") + } + n, err := d.r.Read(p[:min(len(p), int(d.idatLength))]) + d.crc.Write(p[:n]) + d.idatLength -= uint32(n) + return n, err +} + +// decode decodes the IDAT data into an image. +func (d *decoder) decode() (image.Image, error) { + r, err := zlib.NewReader(d) + if err != nil { + return nil, err + } + defer r.Close() + var img image.Image + if d.interlace == itNone { + img, err = d.readImagePass(r, 0, false) + if err != nil { + return nil, err + } + } else if d.interlace == itAdam7 { + // Allocate a blank image of the full size. + img, err = d.readImagePass(nil, 0, true) + if err != nil { + return nil, err + } + for pass := 0; pass < 7; pass++ { + imagePass, err := d.readImagePass(r, pass, false) + if err != nil { + return nil, err + } + if imagePass != nil { + d.mergePassInto(img, imagePass, pass) + } + } + } + + // Check for EOF, to verify the zlib checksum. + n := 0 + for i := 0; n == 0 && err == nil; i++ { + if i == 100 { + return nil, io.ErrNoProgress + } + n, err = r.Read(d.tmp[:1]) + } + if err != nil && err != io.EOF { + return nil, FormatError(err.Error()) + } + if n != 0 || d.idatLength != 0 { + return nil, FormatError("too much pixel data") + } + + return img, nil +} + +// readImagePass reads a single image pass, sized according to the pass number. +func (d *decoder) readImagePass(r io.Reader, pass int, allocateOnly bool) (image.Image, error) { + bitsPerPixel := 0 + pixOffset := 0 + var ( + gray *image.Gray + rgba *image.RGBA + paletted *image.Paletted + nrgba *image.NRGBA + gray16 *image.Gray16 + rgba64 *image.RGBA64 + nrgba64 *image.NRGBA64 + img image.Image + width int + height int + ) + width = d.a.Frames[d.frame_index].width + height = d.a.Frames[d.frame_index].height + if d.interlace == itAdam7 && !allocateOnly { + p := interlacing[pass] + // Add the multiplication factor and subtract one, effectively rounding up. + width = (width - p.xOffset + p.xFactor - 1) / p.xFactor + height = (height - p.yOffset + p.yFactor - 1) / p.yFactor + // A PNG image can't have zero width or height, but for an interlaced + // image, an individual pass might have zero width or height. If so, we + // shouldn't even read a per-row filter type byte, so return early. + if width == 0 || height == 0 { + return nil, nil + } + } + switch d.cb { + case cbG1, cbG2, cbG4, cbG8: + bitsPerPixel = d.depth + if d.useTransparent { + nrgba = image.NewNRGBA(image.Rect(0, 0, width, height)) + img = nrgba + } else { + gray = image.NewGray(image.Rect(0, 0, width, height)) + img = gray + } + case cbGA8: + bitsPerPixel = 16 + nrgba = image.NewNRGBA(image.Rect(0, 0, width, height)) + img = nrgba + case cbTC8: + bitsPerPixel = 24 + if d.useTransparent { + nrgba = image.NewNRGBA(image.Rect(0, 0, width, height)) + img = nrgba + } else { + rgba = image.NewRGBA(image.Rect(0, 0, width, height)) + img = rgba + } + case cbP1, cbP2, cbP4, cbP8: + bitsPerPixel = d.depth + paletted = image.NewPaletted(image.Rect(0, 0, width, height), d.palette) + img = paletted + case cbTCA8: + bitsPerPixel = 32 + nrgba = image.NewNRGBA(image.Rect(0, 0, width, height)) + img = nrgba + case cbG16: + bitsPerPixel = 16 + if d.useTransparent { + nrgba64 = image.NewNRGBA64(image.Rect(0, 0, width, height)) + img = nrgba64 + } else { + gray16 = image.NewGray16(image.Rect(0, 0, width, height)) + img = gray16 + } + case cbGA16: + bitsPerPixel = 32 + nrgba64 = image.NewNRGBA64(image.Rect(0, 0, width, height)) + img = nrgba64 + case cbTC16: + bitsPerPixel = 48 + if d.useTransparent { + nrgba64 = image.NewNRGBA64(image.Rect(0, 0, width, height)) + img = nrgba64 + } else { + rgba64 = image.NewRGBA64(image.Rect(0, 0, width, height)) + img = rgba64 + } + case cbTCA16: + bitsPerPixel = 64 + nrgba64 = image.NewNRGBA64(image.Rect(0, 0, width, height)) + img = nrgba64 + } + if allocateOnly { + return img, nil + } + bytesPerPixel := (bitsPerPixel + 7) / 8 + + // The +1 is for the per-row filter type, which is at cr[0]. + rowSize := 1 + (bitsPerPixel*width+7)/8 + // cr and pr are the bytes for the current and previous row. + cr := make([]uint8, rowSize) + pr := make([]uint8, rowSize) + + for y := 0; y < height; y++ { + // Read the decompressed bytes. + _, err := io.ReadFull(r, cr) + if err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + return nil, FormatError("not enough pixel data") + } + return nil, err + } + + // Apply the filter. + cdat := cr[1:] + pdat := pr[1:] + switch cr[0] { + case ftNone: + // No-op. + case ftSub: + for i := bytesPerPixel; i < len(cdat); i++ { + cdat[i] += cdat[i-bytesPerPixel] + } + case ftUp: + for i, p := range pdat { + cdat[i] += p + } + case ftAverage: + // The first column has no column to the left of it, so it is a + // special case. We know that the first column exists because we + // check above that width != 0, and so len(cdat) != 0. + for i := 0; i < bytesPerPixel; i++ { + cdat[i] += pdat[i] / 2 + } + for i := bytesPerPixel; i < len(cdat); i++ { + cdat[i] += uint8((int(cdat[i-bytesPerPixel]) + int(pdat[i])) / 2) + } + case ftPaeth: + filterPaeth(cdat, pdat, bytesPerPixel) + default: + return nil, FormatError("bad filter type") + } + + // Convert from bytes to colors. + switch d.cb { + case cbG1: + if d.useTransparent { + ty := d.transparent[1] + for x := 0; x < width; x += 8 { + b := cdat[x/8] + for x2 := 0; x2 < 8 && x+x2 < width; x2++ { + ycol := (b >> 7) * 0xff + acol := uint8(0xff) + if ycol == ty { + acol = 0x00 + } + nrgba.SetNRGBA(x+x2, y, color.NRGBA{ycol, ycol, ycol, acol}) + b <<= 1 + } + } + } else { + for x := 0; x < width; x += 8 { + b := cdat[x/8] + for x2 := 0; x2 < 8 && x+x2 < width; x2++ { + gray.SetGray(x+x2, y, color.Gray{(b >> 7) * 0xff}) + b <<= 1 + } + } + } + case cbG2: + if d.useTransparent { + ty := d.transparent[1] + for x := 0; x < width; x += 4 { + b := cdat[x/4] + for x2 := 0; x2 < 4 && x+x2 < width; x2++ { + ycol := (b >> 6) * 0x55 + acol := uint8(0xff) + if ycol == ty { + acol = 0x00 + } + nrgba.SetNRGBA(x+x2, y, color.NRGBA{ycol, ycol, ycol, acol}) + b <<= 2 + } + } + } else { + for x := 0; x < width; x += 4 { + b := cdat[x/4] + for x2 := 0; x2 < 4 && x+x2 < width; x2++ { + gray.SetGray(x+x2, y, color.Gray{(b >> 6) * 0x55}) + b <<= 2 + } + } + } + case cbG4: + if d.useTransparent { + ty := d.transparent[1] + for x := 0; x < width; x += 2 { + b := cdat[x/2] + for x2 := 0; x2 < 2 && x+x2 < width; x2++ { + ycol := (b >> 4) * 0x11 + acol := uint8(0xff) + if ycol == ty { + acol = 0x00 + } + nrgba.SetNRGBA(x+x2, y, color.NRGBA{ycol, ycol, ycol, acol}) + b <<= 4 + } + } + } else { + for x := 0; x < width; x += 2 { + b := cdat[x/2] + for x2 := 0; x2 < 2 && x+x2 < width; x2++ { + gray.SetGray(x+x2, y, color.Gray{(b >> 4) * 0x11}) + b <<= 4 + } + } + } + case cbG8: + if d.useTransparent { + ty := d.transparent[1] + for x := 0; x < width; x++ { + ycol := cdat[x] + acol := uint8(0xff) + if ycol == ty { + acol = 0x00 + } + nrgba.SetNRGBA(x, y, color.NRGBA{ycol, ycol, ycol, acol}) + } + } else { + copy(gray.Pix[pixOffset:], cdat) + pixOffset += gray.Stride + } + case cbGA8: + for x := 0; x < width; x++ { + ycol := cdat[2*x+0] + nrgba.SetNRGBA(x, y, color.NRGBA{ycol, ycol, ycol, cdat[2*x+1]}) + } + case cbTC8: + if d.useTransparent { + pix, i, j := nrgba.Pix, pixOffset, 0 + tr, tg, tb := d.transparent[1], d.transparent[3], d.transparent[5] + for x := 0; x < width; x++ { + r := cdat[j+0] + g := cdat[j+1] + b := cdat[j+2] + a := uint8(0xff) + if r == tr && g == tg && b == tb { + a = 0x00 + } + pix[i+0] = r + pix[i+1] = g + pix[i+2] = b + pix[i+3] = a + i += 4 + j += 3 + } + pixOffset += nrgba.Stride + } else { + pix, i, j := rgba.Pix, pixOffset, 0 + for x := 0; x < width; x++ { + pix[i+0] = cdat[j+0] + pix[i+1] = cdat[j+1] + pix[i+2] = cdat[j+2] + pix[i+3] = 0xff + i += 4 + j += 3 + } + pixOffset += rgba.Stride + } + case cbP1: + for x := 0; x < width; x += 8 { + b := cdat[x/8] + for x2 := 0; x2 < 8 && x+x2 < width; x2++ { + idx := b >> 7 + if len(paletted.Palette) <= int(idx) { + paletted.Palette = paletted.Palette[:int(idx)+1] + } + paletted.SetColorIndex(x+x2, y, idx) + b <<= 1 + } + } + case cbP2: + for x := 0; x < width; x += 4 { + b := cdat[x/4] + for x2 := 0; x2 < 4 && x+x2 < width; x2++ { + idx := b >> 6 + if len(paletted.Palette) <= int(idx) { + paletted.Palette = paletted.Palette[:int(idx)+1] + } + paletted.SetColorIndex(x+x2, y, idx) + b <<= 2 + } + } + case cbP4: + for x := 0; x < width; x += 2 { + b := cdat[x/2] + for x2 := 0; x2 < 2 && x+x2 < width; x2++ { + idx := b >> 4 + if len(paletted.Palette) <= int(idx) { + paletted.Palette = paletted.Palette[:int(idx)+1] + } + paletted.SetColorIndex(x+x2, y, idx) + b <<= 4 + } + } + case cbP8: + if len(paletted.Palette) != 255 { + for x := 0; x < width; x++ { + if len(paletted.Palette) <= int(cdat[x]) { + paletted.Palette = paletted.Palette[:int(cdat[x])+1] + } + } + } + copy(paletted.Pix[pixOffset:], cdat) + pixOffset += paletted.Stride + case cbTCA8: + copy(nrgba.Pix[pixOffset:], cdat) + pixOffset += nrgba.Stride + case cbG16: + if d.useTransparent { + ty := uint16(d.transparent[0])<<8 | uint16(d.transparent[1]) + for x := 0; x < width; x++ { + ycol := uint16(cdat[2*x+0])<<8 | uint16(cdat[2*x+1]) + acol := uint16(0xffff) + if ycol == ty { + acol = 0x0000 + } + nrgba64.SetNRGBA64(x, y, color.NRGBA64{ycol, ycol, ycol, acol}) + } + } else { + for x := 0; x < width; x++ { + ycol := uint16(cdat[2*x+0])<<8 | uint16(cdat[2*x+1]) + gray16.SetGray16(x, y, color.Gray16{ycol}) + } + } + case cbGA16: + for x := 0; x < width; x++ { + ycol := uint16(cdat[4*x+0])<<8 | uint16(cdat[4*x+1]) + acol := uint16(cdat[4*x+2])<<8 | uint16(cdat[4*x+3]) + nrgba64.SetNRGBA64(x, y, color.NRGBA64{ycol, ycol, ycol, acol}) + } + case cbTC16: + if d.useTransparent { + tr := uint16(d.transparent[0])<<8 | uint16(d.transparent[1]) + tg := uint16(d.transparent[2])<<8 | uint16(d.transparent[3]) + tb := uint16(d.transparent[4])<<8 | uint16(d.transparent[5]) + for x := 0; x < width; x++ { + rcol := uint16(cdat[6*x+0])<<8 | uint16(cdat[6*x+1]) + gcol := uint16(cdat[6*x+2])<<8 | uint16(cdat[6*x+3]) + bcol := uint16(cdat[6*x+4])<<8 | uint16(cdat[6*x+5]) + acol := uint16(0xffff) + if rcol == tr && gcol == tg && bcol == tb { + acol = 0x0000 + } + nrgba64.SetNRGBA64(x, y, color.NRGBA64{rcol, gcol, bcol, acol}) + } + } else { + for x := 0; x < width; x++ { + rcol := uint16(cdat[6*x+0])<<8 | uint16(cdat[6*x+1]) + gcol := uint16(cdat[6*x+2])<<8 | uint16(cdat[6*x+3]) + bcol := uint16(cdat[6*x+4])<<8 | uint16(cdat[6*x+5]) + rgba64.SetRGBA64(x, y, color.RGBA64{rcol, gcol, bcol, 0xffff}) + } + } + case cbTCA16: + for x := 0; x < width; x++ { + rcol := uint16(cdat[8*x+0])<<8 | uint16(cdat[8*x+1]) + gcol := uint16(cdat[8*x+2])<<8 | uint16(cdat[8*x+3]) + bcol := uint16(cdat[8*x+4])<<8 | uint16(cdat[8*x+5]) + acol := uint16(cdat[8*x+6])<<8 | uint16(cdat[8*x+7]) + nrgba64.SetNRGBA64(x, y, color.NRGBA64{rcol, gcol, bcol, acol}) + } + } + + // The current row for y is the previous row for y+1. + pr, cr = cr, pr + } + + return img, nil +} + +// mergePassInto merges a single pass into a full sized image. +func (d *decoder) mergePassInto(dst image.Image, src image.Image, pass int) { + p := interlacing[pass] + var ( + srcPix []uint8 + dstPix []uint8 + stride int + rect image.Rectangle + bytesPerPixel int + ) + switch target := dst.(type) { + case *image.Alpha: + srcPix = src.(*image.Alpha).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 1 + case *image.Alpha16: + srcPix = src.(*image.Alpha16).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 2 + case *image.Gray: + srcPix = src.(*image.Gray).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 1 + case *image.Gray16: + srcPix = src.(*image.Gray16).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 2 + case *image.NRGBA: + srcPix = src.(*image.NRGBA).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 4 + case *image.NRGBA64: + srcPix = src.(*image.NRGBA64).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 8 + case *image.Paletted: + srcPix = src.(*image.Paletted).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 1 + case *image.RGBA: + srcPix = src.(*image.RGBA).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 4 + case *image.RGBA64: + srcPix = src.(*image.RGBA64).Pix + dstPix, stride, rect = target.Pix, target.Stride, target.Rect + bytesPerPixel = 8 + } + s, bounds := 0, src.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + dBase := (y*p.yFactor+p.yOffset-rect.Min.Y)*stride + (p.xOffset-rect.Min.X)*bytesPerPixel + for x := bounds.Min.X; x < bounds.Max.X; x++ { + d := dBase + x*p.xFactor*bytesPerPixel + copy(dstPix[d:], srcPix[s:s+bytesPerPixel]) + s += bytesPerPixel + } + } +} + +func (d *decoder) parseacTL(length uint32) (err error) { + if length != 8 { + return FormatError("bad acTL length") + } + if _, err := io.ReadFull(d.r, d.tmp[:8]); err != nil { + return err + } + + d.num_frames = binary.BigEndian.Uint32(d.tmp[:4]) + d.a.LoopCount = uint(binary.BigEndian.Uint32(d.tmp[4:8])) + + d.crc.Write(d.tmp[:8]) + return d.verifyChecksum() +} + +func (d *decoder) parsefcTL(length uint32) (err error) { + if length != 26 { + return FormatError("bad fcTL length") + } + if _, err := io.ReadFull(d.r, d.tmp[:26]); err != nil { + return err + } + + if d.frame_index >= len(d.a.Frames) { + d.a.Frames = append(d.a.Frames, Frame{}) + } + + d.a.Frames[d.frame_index].IsDefault = false + d.a.Frames[d.frame_index].width = int(int32(binary.BigEndian.Uint32(d.tmp[4:8]))) + d.a.Frames[d.frame_index].height = int(int32(binary.BigEndian.Uint32(d.tmp[8:12]))) + d.a.Frames[d.frame_index].XOffset = int(binary.BigEndian.Uint32(d.tmp[12:16])) + d.a.Frames[d.frame_index].YOffset = int(binary.BigEndian.Uint32(d.tmp[16:20])) + d.a.Frames[d.frame_index].DelayNumerator = binary.BigEndian.Uint16(d.tmp[20:22]) + d.a.Frames[d.frame_index].DelayDenominator = binary.BigEndian.Uint16(d.tmp[22:24]) + d.a.Frames[d.frame_index].DisposeOp = byte(d.tmp[24]) + d.a.Frames[d.frame_index].BlendOp = byte(d.tmp[25]) + + d.crc.Write(d.tmp[:26]) + return d.verifyChecksum() +} + +func (d *decoder) parsefdAT(length uint32) (err error) { + if _, err := io.ReadFull(d.r, d.tmp[:4]); err != nil { + return err + } + d.crc.Write(d.tmp[:4]) + d.idatLength = length - 4 + d.a.Frames[d.frame_index].Image, err = d.decode() + if err != nil { + return err + } + return d.verifyChecksum() +} + +func (d *decoder) parseIDAT(length uint32) (err error) { + d.idatLength = length + d.a.Frames[d.frame_index].Image, err = d.decode() + if err != nil { + return err + } + return d.verifyChecksum() +} + +func (d *decoder) parseIEND(length uint32) error { + if length != 0 { + return FormatError("bad IEND length") + } + return d.verifyChecksum() +} + +func (d *decoder) parseChunk() error { + // Read the length and chunk type. + n, err := io.ReadFull(d.r, d.tmp[:8]) + if err != nil { + return err + } + length := binary.BigEndian.Uint32(d.tmp[:4]) + d.crc.Reset() + d.crc.Write(d.tmp[4:8]) + + // Read the chunk data. + switch string(d.tmp[4:8]) { + case "IHDR": + if d.stage != dsStart { + return chunkOrderError + } + d.stage = dsSeenIHDR + return d.parseIHDR(length) + case "PLTE": + if d.stage != dsSeenIHDR { + return chunkOrderError + } + d.stage = dsSeenPLTE + return d.parsePLTE(length) + case "tRNS": + if cbPaletted(d.cb) { + if d.stage != dsSeenPLTE { + return chunkOrderError + } + } else if d.stage != dsSeenIHDR { + return chunkOrderError + } + d.stage = dsSeentRNS + return d.parsetRNS(length) + case "acTL": + if d.stage >= dsSeenIDAT { + return chunkOrderError + } + return d.parseacTL(length) + case "fcTL": + if d.stage >= dsSeenIDAT { + d.frame_index = d.frame_index + 1 + } + return d.parsefcTL(length) + case "fdAT": + if d.stage < dsSeenIDAT { + return chunkOrderError + } + d.stage = dsSeenfdAT + return d.parsefdAT(length) + case "IDAT": + if d.stage < dsSeenIHDR || d.stage > dsSeenIDAT || (d.stage == dsSeenIHDR && cbPaletted(d.cb)) { + return chunkOrderError + } else if d.stage == dsSeenIDAT { + // Ignore trailing zero-length or garbage IDAT chunks. + // + // This does not affect valid PNG images that contain multiple IDAT + // chunks, since the first call to parseIDAT below will consume all + // consecutive IDAT chunks required for decoding the image. + break + } + d.stage = dsSeenIDAT + return d.parseIDAT(length) + case "IEND": + if d.stage < dsSeenIDAT { + return chunkOrderError + } + d.stage = dsSeenIEND + return d.parseIEND(length) + } + if length > 0x7fffffff { + return FormatError(fmt.Sprintf("Bad chunk length: %d", length)) + } + // Ignore this chunk (of a known length). + var ignored [4096]byte + for length > 0 { + n, err = io.ReadFull(d.r, ignored[:min(len(ignored), int(length))]) + if err != nil { + return err + } + d.crc.Write(ignored[:n]) + length -= uint32(n) + } + return d.verifyChecksum() +} + +func (d *decoder) verifyChecksum() error { + if _, err := io.ReadFull(d.r, d.tmp[:4]); err != nil { + return err + } + if binary.BigEndian.Uint32(d.tmp[:4]) != d.crc.Sum32() { + return FormatError("invalid checksum") + } + return nil +} + +func (d *decoder) checkHeader() error { + _, err := io.ReadFull(d.r, d.tmp[:len(pngHeader)]) + if err != nil { + return err + } + if string(d.tmp[:len(pngHeader)]) != pngHeader { + return FormatError("not a PNG file") + } + return nil +} + +// Decode reads an APNG file from r and returns it as an APNG +// Type. If the first frame returns true for IsDefault(), that +// frame should not be part of the a. +// The type of Image returned depends on the PNG contents. +func DecodeAll(r io.Reader) (APNG, error) { + d := &decoder{ + r: r, + crc: crc32.NewIEEE(), + frame_index: 0, + a: APNG{Frames: make([]Frame, 1)}, + } + d.a.Frames[0].IsDefault = true + if err := d.checkHeader(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return d.a, err + } + for d.stage != dsSeenIEND { + if err := d.parseChunk(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return d.a, err + } + } + return d.a, nil +} + +// Decode reads an APNG file from r and returns the default image. +func Decode(r io.Reader) (image.Image, error) { + a, err := DecodeAll(r) + if err != nil { + return nil, err + } + return a.Frames[0].Image, nil +} + +// DecodeConfig returns the color model and dimensions of a PNG image without +// decoding the entire image. +func DecodeConfig(r io.Reader) (image.Config, error) { + d := &decoder{ + r: r, + crc: crc32.NewIEEE(), + } + if err := d.checkHeader(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return image.Config{}, err + } + for { + if err := d.parseChunk(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return image.Config{}, err + } + paletted := cbPaletted(d.cb) + if d.stage == dsSeenIHDR && !paletted { + break + } + if d.stage == dsSeenPLTE && paletted { + break + } + } + var cm color.Model + switch d.cb { + case cbG1, cbG2, cbG4, cbG8: + cm = color.GrayModel + case cbGA8: + cm = color.NRGBAModel + case cbTC8: + cm = color.RGBAModel + case cbP1, cbP2, cbP4, cbP8: + cm = d.palette + case cbTCA8: + cm = color.NRGBAModel + case cbG16: + cm = color.Gray16Model + case cbGA16: + cm = color.NRGBA64Model + case cbTC16: + cm = color.RGBA64Model + case cbTCA16: + cm = color.NRGBA64Model + } + return image.Config{ + ColorModel: cm, + Width: int(d.a.Frames[0].width), + Height: int(d.a.Frames[0].height), + }, nil +} + +func init() { + image.RegisterFormat("apng", pngHeader, Decode, DecodeConfig) +} diff --git a/vendor/github.com/kettek/apng/writer.go b/vendor/github.com/kettek/apng/writer.go new file mode 100644 index 00000000..f7eee931 --- /dev/null +++ b/vendor/github.com/kettek/apng/writer.go @@ -0,0 +1,713 @@ +// Original PNG code Copyright 2009 The Go Authors. +// Additional APNG enhancements Copyright 2018 Ketchetwahmeegwun +// Tecumseh Southall / kts of kettek. +// All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package apng + +import ( + "bufio" + "compress/zlib" + "encoding/binary" + "hash/crc32" + "image" + "image/color" + "io" + "strconv" +) + +// Encoder configures encoding PNG images. +type Encoder struct { + CompressionLevel CompressionLevel + + // BufferPool optionally specifies a buffer pool to get temporary + // EncoderBuffers when encoding an image. + BufferPool EncoderBufferPool +} + +// EncoderBufferPool is an interface for getting and returning temporary +// instances of the EncoderBuffer struct. This can be used to reuse buffers +// when encoding multiple images. +type EncoderBufferPool interface { + Get() *EncoderBuffer + Put(*EncoderBuffer) +} + +// EncoderBuffer holds the buffers used for encoding PNG images. +type EncoderBuffer encoder + +type encoder struct { + enc *Encoder + w io.Writer + a APNG + write_type int // 0 = IDAT, 1 = fdAT + seq int + cb int + err error + header [8]byte + footer [4]byte + tmp [4 * 256]byte + cr [nFilter][]uint8 + pr []uint8 + zw *zlib.Writer + zwLevel int + bw *bufio.Writer +} + +type CompressionLevel int + +const ( + DefaultCompression CompressionLevel = 0 + NoCompression CompressionLevel = -1 + BestSpeed CompressionLevel = -2 + BestCompression CompressionLevel = -3 + + // Positive CompressionLevel values are reserved to mean a numeric zlib + // compression level, although that is not implemented yet. +) + +type opaquer interface { + Opaque() bool +} + +// Returns whether or not the image is fully opaque. +func opaque(m image.Image) bool { + if o, ok := m.(opaquer); ok { + return o.Opaque() + } + b := m.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + _, _, _, a := m.At(x, y).RGBA() + if a != 0xffff { + return false + } + } + } + return true +} + +// The absolute value of a byte interpreted as a signed int8. +func abs8(d uint8) int { + if d < 128 { + return int(d) + } + return 256 - int(d) +} + +func (e *encoder) writeChunk(b []byte, name string) { + if e.err != nil { + return + } + n := uint32(len(b)) + if int(n) != len(b) { + e.err = UnsupportedError(name + " chunk is too large: " + strconv.Itoa(len(b))) + return + } + binary.BigEndian.PutUint32(e.header[:4], n) + e.header[4] = name[0] + e.header[5] = name[1] + e.header[6] = name[2] + e.header[7] = name[3] + crc := crc32.NewIEEE() + crc.Write(e.header[4:8]) + crc.Write(b) + binary.BigEndian.PutUint32(e.footer[:4], crc.Sum32()) + + _, e.err = e.w.Write(e.header[:8]) + if e.err != nil { + return + } + _, e.err = e.w.Write(b) + if e.err != nil { + return + } + _, e.err = e.w.Write(e.footer[:4]) +} + +func (e *encoder) writeIHDR() { + b := e.a.Frames[0].Image.Bounds() + binary.BigEndian.PutUint32(e.tmp[0:4], uint32(b.Dx())) + binary.BigEndian.PutUint32(e.tmp[4:8], uint32(b.Dy())) + // Set bit depth and color type. + switch e.cb { + case cbG8: + e.tmp[8] = 8 + e.tmp[9] = ctGrayscale + case cbTC8: + e.tmp[8] = 8 + e.tmp[9] = ctTrueColor + case cbP8: + e.tmp[8] = 8 + e.tmp[9] = ctPaletted + case cbP4: + e.tmp[8] = 4 + e.tmp[9] = ctPaletted + case cbP2: + e.tmp[8] = 2 + e.tmp[9] = ctPaletted + case cbP1: + e.tmp[8] = 1 + e.tmp[9] = ctPaletted + case cbTCA8: + e.tmp[8] = 8 + e.tmp[9] = ctTrueColorAlpha + case cbG16: + e.tmp[8] = 16 + e.tmp[9] = ctGrayscale + case cbTC16: + e.tmp[8] = 16 + e.tmp[9] = ctTrueColor + case cbTCA16: + e.tmp[8] = 16 + e.tmp[9] = ctTrueColorAlpha + } + e.tmp[10] = 0 // default compression method + e.tmp[11] = 0 // default filter method + e.tmp[12] = 0 // non-interlaced + e.writeChunk(e.tmp[:13], "IHDR") +} + +func (e *encoder) writeacTL() { + binary.BigEndian.PutUint32(e.tmp[0:4], uint32(len(e.a.Frames))) + binary.BigEndian.PutUint32(e.tmp[4:8], uint32(e.a.LoopCount)) + e.writeChunk(e.tmp[:8], "acTL") +} + +func (e *encoder) writefcTL(f Frame) { + binary.BigEndian.PutUint32(e.tmp[0:4], uint32(e.seq)) + e.seq = e.seq + 1 + b := f.Image.Bounds() + binary.BigEndian.PutUint32(e.tmp[4:8], uint32(b.Dx())) + binary.BigEndian.PutUint32(e.tmp[8:12], uint32(b.Dy())) + binary.BigEndian.PutUint32(e.tmp[12:16], uint32(f.XOffset)) + binary.BigEndian.PutUint32(e.tmp[16:20], uint32(f.YOffset)) + binary.BigEndian.PutUint16(e.tmp[20:22], uint16(f.DelayNumerator)) + binary.BigEndian.PutUint16(e.tmp[22:24], uint16(f.DelayDenominator)) + e.tmp[24] = f.DisposeOp + e.tmp[25] = f.BlendOp + e.writeChunk(e.tmp[:26], "fcTL") +} + +func (e *encoder) writefdATs(f Frame) { + e.write_type = 1 + if e.err != nil { + return + } + if e.bw == nil { + e.bw = bufio.NewWriterSize(e, 1<<15) + } else { + e.bw.Reset(e) + } + e.err = e.writeImage(e.bw, f.Image, e.cb, levelToZlib(e.enc.CompressionLevel)) + if e.err != nil { + return + } + e.err = e.bw.Flush() +} + +func (e *encoder) writePLTEAndTRNS(p color.Palette) { + if len(p) < 1 || len(p) > 256 { + e.err = FormatError("bad palette length: " + strconv.Itoa(len(p))) + return + } + last := -1 + for i, c := range p { + c1 := color.NRGBAModel.Convert(c).(color.NRGBA) + e.tmp[3*i+0] = c1.R + e.tmp[3*i+1] = c1.G + e.tmp[3*i+2] = c1.B + if c1.A != 0xff { + last = i + } + e.tmp[3*256+i] = c1.A + } + e.writeChunk(e.tmp[:3*len(p)], "PLTE") + if last != -1 { + e.writeChunk(e.tmp[3*256:3*256+1+last], "tRNS") + } +} + +// An encoder is an io.Writer that satisfies writes by writing PNG IDAT chunks, +// including an 8-byte header and 4-byte CRC checksum per Write call. Such calls +// should be relatively infrequent, since writeIDATs uses a bufio.Writer. +// +// This method should only be called from writeIDATs (via writeImage). +// No other code should treat an encoder as an io.Writer. +func (e *encoder) Write(b []byte) (int, error) { + if e.write_type == 0 { + e.writeChunk(b, "IDAT") + } else { + c := make([]byte, 4) + binary.BigEndian.PutUint32(c[0:4], uint32(e.seq)) + e.seq = e.seq + 1 + b = append(c, b...) + e.writeChunk(b, "fdAT") + } + if e.err != nil { + return 0, e.err + } + return len(b), nil +} + +// Chooses the filter to use for encoding the current row, and applies it. +// The return value is the index of the filter and also of the row in cr that has had it applied. +func filter(cr *[nFilter][]byte, pr []byte, bpp int) int { + // We try all five filter types, and pick the one that minimizes the sum of absolute differences. + // This is the same heuristic that libpng uses, although the filters are attempted in order of + // estimated most likely to be minimal (ftUp, ftPaeth, ftNone, ftSub, ftAverage), rather than + // in their enumeration order (ftNone, ftSub, ftUp, ftAverage, ftPaeth). + cdat0 := cr[0][1:] + cdat1 := cr[1][1:] + cdat2 := cr[2][1:] + cdat3 := cr[3][1:] + cdat4 := cr[4][1:] + pdat := pr[1:] + n := len(cdat0) + + // The up filter. + sum := 0 + for i := 0; i < n; i++ { + cdat2[i] = cdat0[i] - pdat[i] + sum += abs8(cdat2[i]) + } + best := sum + filter := ftUp + + // The Paeth filter. + sum = 0 + for i := 0; i < bpp; i++ { + cdat4[i] = cdat0[i] - pdat[i] + sum += abs8(cdat4[i]) + } + for i := bpp; i < n; i++ { + cdat4[i] = cdat0[i] - paeth(cdat0[i-bpp], pdat[i], pdat[i-bpp]) + sum += abs8(cdat4[i]) + if sum >= best { + break + } + } + if sum < best { + best = sum + filter = ftPaeth + } + + // The none filter. + sum = 0 + for i := 0; i < n; i++ { + sum += abs8(cdat0[i]) + if sum >= best { + break + } + } + if sum < best { + best = sum + filter = ftNone + } + + // The sub filter. + sum = 0 + for i := 0; i < bpp; i++ { + cdat1[i] = cdat0[i] + sum += abs8(cdat1[i]) + } + for i := bpp; i < n; i++ { + cdat1[i] = cdat0[i] - cdat0[i-bpp] + sum += abs8(cdat1[i]) + if sum >= best { + break + } + } + if sum < best { + best = sum + filter = ftSub + } + + // The average filter. + sum = 0 + for i := 0; i < bpp; i++ { + cdat3[i] = cdat0[i] - pdat[i]/2 + sum += abs8(cdat3[i]) + } + for i := bpp; i < n; i++ { + cdat3[i] = cdat0[i] - uint8((int(cdat0[i-bpp])+int(pdat[i]))/2) + sum += abs8(cdat3[i]) + if sum >= best { + break + } + } + if sum < best { + best = sum + filter = ftAverage + } + + return filter +} + +func zeroMemory(v []uint8) { + for i := range v { + v[i] = 0 + } +} + +func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) error { + if e.zw == nil || e.zwLevel != level { + zw, err := zlib.NewWriterLevel(w, level) + if err != nil { + return err + } + e.zw = zw + e.zwLevel = level + } else { + e.zw.Reset(w) + } + defer e.zw.Close() + + bitsPerPixel := 0 + + switch cb { + case cbG8: + bitsPerPixel = 8 + case cbTC8: + bitsPerPixel = 24 + case cbP8: + bitsPerPixel = 8 + case cbP4: + bitsPerPixel = 4 + case cbP2: + bitsPerPixel = 2 + case cbP1: + bitsPerPixel = 1 + case cbTCA8: + bitsPerPixel = 32 + case cbTC16: + bitsPerPixel = 48 + case cbTCA16: + bitsPerPixel = 64 + case cbG16: + bitsPerPixel = 16 + } + + // cr[*] and pr are the bytes for the current and previous row. + // cr[0] is unfiltered (or equivalently, filtered with the ftNone filter). + // cr[ft], for non-zero filter types ft, are buffers for transforming cr[0] under the + // other PNG filter types. These buffers are allocated once and re-used for each row. + // The +1 is for the per-row filter type, which is at cr[*][0]. + b := m.Bounds() + sz := 1 + (bitsPerPixel*b.Dx()+7)/8 + for i := range e.cr { + if cap(e.cr[i]) < sz { + e.cr[i] = make([]uint8, sz) + } else { + e.cr[i] = e.cr[i][:sz] + } + e.cr[i][0] = uint8(i) + } + cr := e.cr + if cap(e.pr) < sz { + e.pr = make([]uint8, sz) + } else { + e.pr = e.pr[:sz] + zeroMemory(e.pr) + } + pr := e.pr + + gray, _ := m.(*image.Gray) + rgba, _ := m.(*image.RGBA) + paletted, _ := m.(*image.Paletted) + nrgba, _ := m.(*image.NRGBA) + + for y := b.Min.Y; y < b.Max.Y; y++ { + // Convert from colors to bytes. + i := 1 + switch cb { + case cbG8: + if gray != nil { + offset := (y - b.Min.Y) * gray.Stride + copy(cr[0][1:], gray.Pix[offset:offset+b.Dx()]) + } else { + for x := b.Min.X; x < b.Max.X; x++ { + c := color.GrayModel.Convert(m.At(x, y)).(color.Gray) + cr[0][i] = c.Y + i++ + } + } + case cbTC8: + // We have previously verified that the alpha value is fully opaque. + cr0 := cr[0] + stride, pix := 0, []byte(nil) + if rgba != nil { + stride, pix = rgba.Stride, rgba.Pix + } else if nrgba != nil { + stride, pix = nrgba.Stride, nrgba.Pix + } + if stride != 0 { + j0 := (y - b.Min.Y) * stride + j1 := j0 + b.Dx()*4 + for j := j0; j < j1; j += 4 { + cr0[i+0] = pix[j+0] + cr0[i+1] = pix[j+1] + cr0[i+2] = pix[j+2] + i += 3 + } + } else { + for x := b.Min.X; x < b.Max.X; x++ { + r, g, b, _ := m.At(x, y).RGBA() + cr0[i+0] = uint8(r >> 8) + cr0[i+1] = uint8(g >> 8) + cr0[i+2] = uint8(b >> 8) + i += 3 + } + } + case cbP8: + if paletted != nil { + offset := (y - b.Min.Y) * paletted.Stride + copy(cr[0][1:], paletted.Pix[offset:offset+b.Dx()]) + } else { + pi := m.(image.PalettedImage) + for x := b.Min.X; x < b.Max.X; x++ { + cr[0][i] = pi.ColorIndexAt(x, y) + i += 1 + } + } + + case cbP4, cbP2, cbP1: + pi := m.(image.PalettedImage) + + var a uint8 + var c int + for x := b.Min.X; x < b.Max.X; x++ { + a = a<<uint(bitsPerPixel) | pi.ColorIndexAt(x, y) + c++ + if c == 8/bitsPerPixel { + cr[0][i] = a + i += 1 + a = 0 + c = 0 + } + } + if c != 0 { + for c != 8/bitsPerPixel { + a = a << uint(bitsPerPixel) + c++ + } + cr[0][i] = a + } + + case cbTCA8: + if nrgba != nil { + offset := (y - b.Min.Y) * nrgba.Stride + copy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4]) + } else { + // Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied. + for x := b.Min.X; x < b.Max.X; x++ { + c := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA) + cr[0][i+0] = c.R + cr[0][i+1] = c.G + cr[0][i+2] = c.B + cr[0][i+3] = c.A + i += 4 + } + } + case cbG16: + for x := b.Min.X; x < b.Max.X; x++ { + c := color.Gray16Model.Convert(m.At(x, y)).(color.Gray16) + cr[0][i+0] = uint8(c.Y >> 8) + cr[0][i+1] = uint8(c.Y) + i += 2 + } + case cbTC16: + // We have previously verified that the alpha value is fully opaque. + for x := b.Min.X; x < b.Max.X; x++ { + r, g, b, _ := m.At(x, y).RGBA() + cr[0][i+0] = uint8(r >> 8) + cr[0][i+1] = uint8(r) + cr[0][i+2] = uint8(g >> 8) + cr[0][i+3] = uint8(g) + cr[0][i+4] = uint8(b >> 8) + cr[0][i+5] = uint8(b) + i += 6 + } + case cbTCA16: + // Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied. + for x := b.Min.X; x < b.Max.X; x++ { + c := color.NRGBA64Model.Convert(m.At(x, y)).(color.NRGBA64) + cr[0][i+0] = uint8(c.R >> 8) + cr[0][i+1] = uint8(c.R) + cr[0][i+2] = uint8(c.G >> 8) + cr[0][i+3] = uint8(c.G) + cr[0][i+4] = uint8(c.B >> 8) + cr[0][i+5] = uint8(c.B) + cr[0][i+6] = uint8(c.A >> 8) + cr[0][i+7] = uint8(c.A) + i += 8 + } + } + + // Apply the filter. + // Skip filter for NoCompression and paletted images (cbP8) as + // "filters are rarely useful on palette images" and will result + // in larger files (see http://www.libpng.org/pub/png/book/chapter09.html). + f := ftNone + if level != zlib.NoCompression && cb != cbP8 && cb != cbP4 && cb != cbP2 && cb != cbP1 { + // Since we skip paletted images we don't have to worry about + // bitsPerPixel not being a multiple of 8 + bpp := bitsPerPixel / 8 + f = filter(&cr, pr, bpp) + } + + // Write the compressed bytes. + if _, err := e.zw.Write(cr[f]); err != nil { + return err + } + + // The current row for y is the previous row for y+1. + pr, cr[0] = cr[0], pr + } + return nil +} + +// Write the actual image data to one or more IDAT chunks. +func (e *encoder) writeIDATs() { + e.write_type = 0 + if e.err != nil { + return + } + if e.bw == nil { + e.bw = bufio.NewWriterSize(e, 1<<15) + } else { + e.bw.Reset(e) + } + e.err = e.writeImage(e.bw, e.a.Frames[0].Image, e.cb, levelToZlib(e.enc.CompressionLevel)) + if e.err != nil { + return + } + e.err = e.bw.Flush() +} + +// This function is required because we want the zero value of +// Encoder.CompressionLevel to map to zlib.DefaultCompression. +func levelToZlib(l CompressionLevel) int { + switch l { + case DefaultCompression: + return zlib.DefaultCompression + case NoCompression: + return zlib.NoCompression + case BestSpeed: + return zlib.BestSpeed + case BestCompression: + return zlib.BestCompression + default: + return zlib.DefaultCompression + } +} + +func (e *encoder) writeIEND() { e.writeChunk(nil, "IEND") } + +// Encode writes the APNG a to w in PNG format. Any Image may be +// encoded, but images that are not image.NRGBA might be encoded lossily. +func Encode(w io.Writer, a APNG) error { + var e Encoder + return e.Encode(w, a) +} + +// Encode writes the Animation a to w in PNG format. +func (enc *Encoder) Encode(w io.Writer, a APNG) error { + // Obviously, negative widths and heights are invalid. Furthermore, the PNG + // spec section 11.2.2 says that zero is invalid. Excessively large images are + // also rejected. + mw, mh := int64(a.Frames[0].Image.Bounds().Dx()), int64(a.Frames[0].Image.Bounds().Dy()) + if mw <= 0 || mh <= 0 || mw >= 1<<32 || mh >= 1<<32 { + return FormatError("invalid image size: " + strconv.FormatInt(mw, 10) + "x" + strconv.FormatInt(mh, 10)) + } + + var e *encoder + if enc.BufferPool != nil { + buffer := enc.BufferPool.Get() + e = (*encoder)(buffer) + + } + if e == nil { + e = &encoder{} + } + if enc.BufferPool != nil { + defer enc.BufferPool.Put((*EncoderBuffer)(e)) + } + + e.enc = enc + e.w = w + e.a = a + + var pal color.Palette + // cbP8 encoding needs PalettedImage's ColorIndexAt method. + if _, ok := a.Frames[0].Image.(image.PalettedImage); ok { + pal, _ = a.Frames[0].Image.ColorModel().(color.Palette) + } + if pal != nil { + if len(pal) <= 2 { + e.cb = cbP1 + } else if len(pal) <= 4 { + e.cb = cbP2 + } else if len(pal) <= 16 { + e.cb = cbP4 + } else { + e.cb = cbP8 + } + } else { + switch a.Frames[0].Image.ColorModel() { + case color.GrayModel: + e.cb = cbG8 + case color.Gray16Model: + e.cb = cbG16 + case color.RGBAModel, color.NRGBAModel, color.AlphaModel: + isOpaque := true + for _, v := range a.Frames { + if !opaque(v.Image) { + isOpaque = false + break + } + } + if isOpaque { + e.cb = cbTC8 + } else { + e.cb = cbTCA8 + } + default: + isOpaque := true + for _, v := range a.Frames { + if !opaque(v.Image) { + isOpaque = false + break + } + } + if isOpaque { + e.cb = cbTC16 + } else { + e.cb = cbTCA16 + } + } + } + + _, e.err = io.WriteString(w, pngHeader) + e.writeIHDR() + if pal != nil { + e.writePLTEAndTRNS(pal) + } + if len(e.a.Frames) > 1 { + e.writeacTL() + } + if !e.a.Frames[0].IsDefault { + e.writefcTL(e.a.Frames[0]) + } + e.writeIDATs() + for i := 0; i < len(e.a.Frames); i = i + 1 { + if i != 0 && !e.a.Frames[i].IsDefault { + e.writefcTL(e.a.Frames[i]) + e.writefdATs(e.a.Frames[i]) + } + } + e.writeIEND() + return e.err +} |