diff --git a/.gitignore b/.gitignore index 66fd13c..180e910 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.DS_Store diff --git a/README.md b/README.md index ffff877..174f109 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # imgsz Image size analyzer for jpg/png/gif/webp + +# Usage + +```go +// DecodeSize decodes the dimensions of an image that has +// been encoded in a registered format. The string returned is the format name +// used during format registration. Format registration is typically done by +// an init function in the codec-specific package. +func DecodeSize(r io.Reader) (Size, string, error) +``` diff --git a/gif.go b/gif.go new file mode 100644 index 0000000..a946dc3 --- /dev/null +++ b/gif.go @@ -0,0 +1,48 @@ +// Copyright 2011 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 gif implements a GIF image gifdecoder and encoder. +// +// The GIF specification is at https://www.w3.org/Graphics/GIF/spec-gif89a.txt. + +package imgsz + +import ( + "fmt" + "io" +) + +func readFull(r io.Reader, b []byte) error { + _, err := io.ReadFull(r, b) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err +} + +// gifdecoder is the type used to decode a GIF file. +type gifdecoder struct { + // From header. + vers string + width int + height int + + // Used when decoding. + tmp [1024]byte // must be at least 768 so we can read color table +} + +func (d *gifdecoder) readHeaderAndScreenDescriptor(r io.Reader) error { + err := readFull(r, d.tmp[:13]) + if err != nil { + return fmt.Errorf("gif: reading header: %v", err) + } + d.vers = string(d.tmp[:6]) + if d.vers != "GIF87a" && d.vers != "GIF89a" { + return fmt.Errorf("gif: can't recognize format %q", d.vers) + } + d.width = int(d.tmp[6]) + int(d.tmp[7])<<8 + d.height = int(d.tmp[8]) + int(d.tmp[9])<<8 + // d.tmp[12] is the Pixel Aspect Ratio, which is ignored. + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..00fc1c5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/fumiama/imgsz + +go 1.17 diff --git a/image.go b/image.go new file mode 100644 index 0000000..bb98710 --- /dev/null +++ b/image.go @@ -0,0 +1,108 @@ +// Copyright 2010 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 imgsz + +import ( + "bufio" + "errors" + "io" + "sync" + "sync/atomic" +) + +// A FormatError reports that the input is not a valid JPEG. +type FormatError string + +func (e FormatError) Error() string { return "invalid format: " + string(e) } + +// An UnsupportedError reports that the input uses a valid but unimplemented JPEG feature. +type UnsupportedError string + +func (e UnsupportedError) Error() string { return "unsupported feature: " + string(e) } + +// ErrFormat indicates that decoding encountered an unknown format. +var ErrFormat = errors.New("image: unknown format") + +type Size struct { + Width, Height int +} + +// A format holds an image format's name, magic header and how to decode it. +type format struct { + name, magic string + decodeSize func(io.Reader) (Size, error) +} + +// Formats is the list of registered formats. +var ( + formatsMu sync.Mutex + atomicFormats atomic.Value +) + +// RegisterFormat registers an image format for use by Decode. +// Name is the name of the format, like "jpeg" or "png". +// Magic is the magic prefix that identifies the format's encoding. The magic +// string can contain "?" wildcards that each match any one byte. +// Decode is the function that decodes the encoded image. +// DecodeSize is the function that decodes just its configuration. +func RegisterFormat(name, magic string, decodeSize func(io.Reader) (Size, error)) { + formatsMu.Lock() + formats, _ := atomicFormats.Load().([]format) + atomicFormats.Store(append(formats, format{name, magic, decodeSize})) + formatsMu.Unlock() +} + +// A reader is an io.Reader that can also peek ahead. +type reader interface { + io.Reader + Peek(int) ([]byte, error) +} + +// asReader converts an io.Reader to a reader. +func asReader(r io.Reader) reader { + if rr, ok := r.(reader); ok { + return rr + } + return bufio.NewReader(r) +} + +// Match reports whether magic matches b. Magic may contain "?" wildcards. +func match(magic string, b []byte) bool { + if len(magic) != len(b) { + return false + } + for i, c := range b { + if magic[i] != c && magic[i] != '?' { + return false + } + } + return true +} + +// Sniff determines the format of r's data. +func sniff(r reader) format { + formats, _ := atomicFormats.Load().([]format) + for _, f := range formats { + b, err := r.Peek(len(f.magic)) + if err == nil && match(f.magic, b) { + return f + } + } + return format{} +} + +// DecodeSize decodes the dimensions of an image that has +// been encoded in a registered format. The string returned is the format name +// used during format registration. Format registration is typically done by +// an init function in the codec-specific package. +func DecodeSize(r io.Reader) (Size, string, error) { + rr := asReader(r) + f := sniff(rr) + if f.decodeSize == nil { + return Size{}, "", ErrFormat + } + c, err := f.decodeSize(rr) + return c, f.name, err +} diff --git a/image_test.go b/image_test.go new file mode 100644 index 0000000..7e4a3ad --- /dev/null +++ b/image_test.go @@ -0,0 +1,60 @@ +package imgsz + +import ( + "os" + "testing" +) + +func TestSizes(t *testing.T) { + f, err := os.Open("testdata/test.webp") + if err != nil { + t.Fatal(err) + } + sz, n, err := DecodeSize(f) + if err != nil { + t.Fatal(err) + } + if sz.Width != 3507 || sz.Height != 2480 || n != "webp" { + t.Fatal(sz, n) + } + f.Close() + + f, err = os.Open("testdata/test.jpg") + if err != nil { + t.Fatal(err) + } + sz, n, err = DecodeSize(f) + if err != nil { + t.Fatal(err) + } + if sz.Width != 858 || sz.Height != 1126 || n != "jpeg" { + t.Fatal(sz, n) + } + f.Close() + + f, err = os.Open("testdata/test.png") + if err != nil { + t.Fatal(err) + } + sz, n, err = DecodeSize(f) + if err != nil { + t.Fatal(err) + } + if sz.Width != 670 || sz.Height != 717 || n != "png" { + t.Fatal(sz, n) + } + f.Close() + + f, err = os.Open("testdata/test.gif") + if err != nil { + t.Fatal(err) + } + sz, n, err = DecodeSize(f) + if err != nil { + t.Fatal(err) + } + if sz.Width != 184 || sz.Height != 166 || n != "gif" { + t.Fatal(sz, n) + } + f.Close() +} diff --git a/init.go b/init.go new file mode 100644 index 0000000..4bb2110 --- /dev/null +++ b/init.go @@ -0,0 +1,19 @@ +package imgsz + +import "io" + +func init() { + RegisterFormat("jpeg", "\xff\xd8", func(r io.Reader) (Size, error) { + var d jpgdecoder + return d.decode(r) + }) + RegisterFormat("png", pngHeader, decodepng) + RegisterFormat("gif", "GIF8?a", func(r io.Reader) (Size, error) { + var d gifdecoder + if err := d.readHeaderAndScreenDescriptor(r); err != nil { + return Size{}, err + } + return Size{d.width, d.height}, nil + }) + RegisterFormat("webp", "RIFF????WEBPVP8", decodewebp) +} diff --git a/jpg.go b/jpg.go new file mode 100644 index 0000000..868e2c3 --- /dev/null +++ b/jpg.go @@ -0,0 +1,327 @@ +// Copyright 2009 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 jpeg implements a JPEG image decoder and encoder. +// +// JPEG is defined in ITU-T T.81: https://www.w3.org/Graphics/JPEG/itu-t81.pdf. + +package imgsz + +import ( + "io" +) + +const blockSize = 64 // A DCT block is 8x8. + +const ( + sof0Marker = 0xc0 // Start Of Frame (Baseline Sequential). + sof1Marker = 0xc1 // Start Of Frame (Extended Sequential). + sof2Marker = 0xc2 // Start Of Frame (Progressive). + dhtMarker = 0xc4 // Define Huffman Table. + rst0Marker = 0xd0 // ReSTart (0). + rst7Marker = 0xd7 // ReSTart (7). + soiMarker = 0xd8 // Start Of Image. + eoiMarker = 0xd9 // End Of Image. + sosMarker = 0xda // Start Of Scan. + dqtMarker = 0xdb // Define Quantization Table. + driMarker = 0xdd // Define Restart Interval. + comMarker = 0xfe // COMment. + // "APPlication specific" markers aren't part of the JPEG spec per se, + // but in practice, their use is described at + // https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html + app0Marker = 0xe0 + app14Marker = 0xee + app15Marker = 0xef +) + +// bits holds the unprocessed bits that have been taken from the byte-stream. +// The n least significant bits of a form the unread bits, to be read in MSB to +// LSB order. +type bits struct { + a uint32 // accumulator. + m uint32 // mask. m==1<<(n-1) when n>0, with m==0 when n==0. + n int32 // the number of unread bits in a. +} + +type jpgdecoder struct { + r io.Reader + bits bits + // bytes is a byte buffer, similar to a bufio.Reader, except that it + // has to be able to unread more than 1 byte, due to byte stuffing. + // Byte stuffing is specified in section F.1.2.3. + bytes struct { + // buf[i:j] are the buffered bytes read from the underlying + // io.Reader that haven't yet been passed further on. + buf [4096]byte + i, j int + // nUnreadable is the number of bytes to back up i after + // overshooting. It can be 0, 1 or 2. + nUnreadable int + } + width, height int + + nComp int + + // As per section 4.5, there are four modes of operation (selected by the + // SOF? markers): sequential DCT, progressive DCT, lossless and + // hierarchical, although this implementation does not support the latter + // two non-DCT modes. Sequential DCT is further split into baseline and + // extended, as per section 4.11. + baseline bool + progressive bool + + jfif bool + + tmp [2 * blockSize]byte +} + +// fill fills up the d.bytes.buf buffer from the underlying io.Reader. It +// should only be called when there are no unread bytes in d.bytes. +func (d *jpgdecoder) fill() error { + if d.bytes.i != d.bytes.j { + panic("jpeg: fill called when unread bytes exist") + } + // Move the last 2 bytes to the start of the buffer, in case we need + // to call unreadByteStuffedByte. + if d.bytes.j > 2 { + d.bytes.buf[0] = d.bytes.buf[d.bytes.j-2] + d.bytes.buf[1] = d.bytes.buf[d.bytes.j-1] + d.bytes.i, d.bytes.j = 2, 2 + } + // Fill in the rest of the buffer. + n, err := d.r.Read(d.bytes.buf[d.bytes.j:]) + d.bytes.j += n + if n > 0 { + err = nil + } + return err +} + +// unreadByteStuffedByte undoes the most recent readByteStuffedByte call, +// giving a byte of data back from d.bits to d.bytes. The Huffman look-up table +// requires at least 8 bits for look-up, which means that Huffman decoding can +// sometimes overshoot and read one or two too many bytes. Two-byte overshoot +// can happen when expecting to read a 0xff 0x00 byte-stuffed byte. +func (d *jpgdecoder) unreadByteStuffedByte() { + d.bytes.i -= d.bytes.nUnreadable + d.bytes.nUnreadable = 0 + if d.bits.n >= 8 { + d.bits.a >>= 8 + d.bits.n -= 8 + d.bits.m >>= 8 + } +} + +// readByte returns the next byte, whether buffered or not buffered. It does +// not care about byte stuffing. +func (d *jpgdecoder) readByte() (x byte, err error) { + for d.bytes.i == d.bytes.j { + if err = d.fill(); err != nil { + return 0, err + } + } + x = d.bytes.buf[d.bytes.i] + d.bytes.i++ + d.bytes.nUnreadable = 0 + return x, nil +} + +// readFull reads exactly len(p) bytes into p. It does not care about byte +// stuffing. +func (d *jpgdecoder) readFull(p []byte) error { + // Unread the overshot bytes, if any. + if d.bytes.nUnreadable != 0 { + if d.bits.n >= 8 { + d.unreadByteStuffedByte() + } + d.bytes.nUnreadable = 0 + } + + for { + n := copy(p, d.bytes.buf[d.bytes.i:d.bytes.j]) + p = p[n:] + d.bytes.i += n + if len(p) == 0 { + break + } + if err := d.fill(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err + } + } + return nil +} + +// ignore ignores the next n bytes. +func (d *jpgdecoder) ignore(n int) error { + // Unread the overshot bytes, if any. + if d.bytes.nUnreadable != 0 { + if d.bits.n >= 8 { + d.unreadByteStuffedByte() + } + d.bytes.nUnreadable = 0 + } + + for { + m := d.bytes.j - d.bytes.i + if m > n { + m = n + } + d.bytes.i += m + n -= m + if n == 0 { + break + } + if err := d.fill(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err + } + } + return nil +} + +// Specified in section B.2.2. +func (d *jpgdecoder) processSOF(n int) error { + if d.nComp != 0 { + return FormatError("multiple SOF markers") + } + switch n { + case 6 + 3*1: // Grayscale image. + d.nComp = 1 + case 6 + 3*3: // YCbCr or RGB image. + d.nComp = 3 + case 6 + 3*4: // YCbCrK or CMYK image. + d.nComp = 4 + default: + return UnsupportedError("number of components") + } + if err := d.readFull(d.tmp[:n]); err != nil { + return err + } + // We only support 8-bit precision. + if d.tmp[0] != 8 { + return UnsupportedError("precision") + } + d.height = int(d.tmp[1])<<8 + int(d.tmp[2]) + d.width = int(d.tmp[3])<<8 + int(d.tmp[4]) + if int(d.tmp[5]) != d.nComp { + return FormatError("SOF has wrong length") + } + + return nil +} + +// decode reads a JPEG image from r and returns its Size +func (d *jpgdecoder) decode(r io.Reader) (Size, error) { + d.r = r + + // Check for the Start Of Image marker. + if err := d.readFull(d.tmp[:2]); err != nil { + return Size{}, err + } + if d.tmp[0] != 0xff || d.tmp[1] != soiMarker { + return Size{}, FormatError("missing SOI marker") + } + + // Process the remaining segments until the End Of Image marker. + for { + err := d.readFull(d.tmp[:2]) + if err != nil { + return Size{}, err + } + for d.tmp[0] != 0xff { + // Strictly speaking, this is a format error. However, libjpeg is + // liberal in what it accepts. As of version 9, next_marker in + // jdmarker.c treats this as a warning (JWRN_EXTRANEOUS_DATA) and + // continues to decode the stream. Even before next_marker sees + // extraneous data, jpeg_fill_bit_buffer in jdhuff.c reads as many + // bytes as it can, possibly past the end of a scan's data. It + // effectively puts back any markers that it overscanned (e.g. an + // "\xff\xd9" EOI marker), but it does not put back non-marker data, + // and thus it can silently ignore a small number of extraneous + // non-marker bytes before next_marker has a chance to see them (and + // print a warning). + // + // We are therefore also liberal in what we accept. Extraneous data + // is silently ignored. + // + // This is similar to, but not exactly the same as, the restart + // mechanism within a scan (the RST[0-7] markers). + // + // Note that extraneous 0xff bytes in e.g. SOS data are escaped as + // "\xff\x00", and so are detected a little further down below. + d.tmp[0] = d.tmp[1] + d.tmp[1], err = d.readByte() + if err != nil { + return Size{}, err + } + } + marker := d.tmp[1] + if marker == 0 { + // Treat "\xff\x00" as extraneous data. + continue + } + for marker == 0xff { + // Section B.1.1.2 says, "Any marker may optionally be preceded by any + // number of fill bytes, which are bytes assigned code X'FF'". + marker, err = d.readByte() + if err != nil { + return Size{}, err + } + } + if marker == eoiMarker { // End Of Image. + break + } + if rst0Marker <= marker && marker <= rst7Marker { + // Figures B.2 and B.16 of the specification suggest that restart markers should + // only occur between Entropy Coded Segments and not after the final ECS. + // However, some encoders may generate incorrect JPEGs with a final restart + // marker. That restart marker will be seen here instead of inside the processSOS + // method, and is ignored as a harmless error. Restart markers have no extra data, + // so we check for this before we read the 16-bit length of the segment. + continue + } + + // Read the 16-bit length of the segment. The value includes the 2 bytes for the + // length itself, so we subtract 2 to get the number of remaining bytes. + if err = d.readFull(d.tmp[:2]); err != nil { + return Size{}, err + } + n := int(d.tmp[0])<<8 + int(d.tmp[1]) - 2 + if n < 0 { + return Size{}, FormatError("short segment length") + } + + switch marker { + case sof0Marker, sof1Marker, sof2Marker: + d.baseline = marker == sof0Marker + d.progressive = marker == sof2Marker + err = d.processSOF(n) + if d.jfif { + return Size{}, err + } + return Size{d.width, d.height}, nil + case sosMarker: + return Size{}, nil + case dhtMarker, dqtMarker, driMarker, app0Marker, app14Marker: + err = d.ignore(n) + default: + if app0Marker <= marker && marker <= app15Marker || marker == comMarker { + err = d.ignore(n) + } else if marker < 0xc0 { // See Table B.1 "Marker code assignments". + err = FormatError("unknown marker") + } else { + err = UnsupportedError("unknown marker") + } + } + if err != nil { + return Size{}, err + } + } + return Size{d.width, d.height}, nil +} diff --git a/png.go b/png.go new file mode 100644 index 0000000..1688a52 --- /dev/null +++ b/png.go @@ -0,0 +1,240 @@ +// Copyright 2009 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 png implements a PNG image decoder and encoder. +// +// The PNG specification is at https://www.w3.org/TR/PNG/. + +package imgsz + +import ( + "encoding/binary" + "fmt" + "hash" + "hash/crc32" + "io" +) + +// 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 +} + +// Interlace type. +const ( + itNone = 0 + itAdam7 = 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 + dsSeenIDAT + dsSeenIEND +) + +const pngHeader = "\x89PNG\r\n\x1a\n" + +type decoder struct { + r io.Reader + crc hash.Hash32 + width, height int + cb int + stage int + idatLength uint32 + tmp [3 * 256]byte + interlace int +} + +var chunkOrderError = FormatError("chunk out of order") + +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") + } + + d.width, d.height = int(w), int(h) + 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 + } + d.idatLength = binary.BigEndian.Uint32(d.tmp[:4]) + if string(d.tmp[4:8]) != "IDAT" { + return 0, FormatError("not enough pixel data") + } + d.crc.Reset() + d.crc.Write(d.tmp[4:8]) + } + if int(d.idatLength) < 0 { + return 0, UnsupportedError("IDAT 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 +} + +func (d *decoder) parseSize() (ok bool, err error) { + // Read the length and chunk type. + if _, err = io.ReadFull(d.r, d.tmp[:8]); err != nil { + return + } + 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 false, chunkOrderError + } + d.stage = dsSeenIHDR + return true, d.parseIHDR(length) + } + if length > 0x7fffffff { + return false, 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 false, err + } + d.crc.Write(ignored[:n]) + length -= uint32(n) + } + return false, 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 +} + +// decodepng returns the color model and dimensions of a PNG image without +// decoding the entire image. +func decodepng(r io.Reader) (Size, error) { + d := &decoder{ + r: r, + crc: crc32.NewIEEE(), + } + if err := d.checkHeader(); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return Size{}, err + } + for { + ok, err := d.parseSize() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return Size{}, err + } + if ok { + return Size{d.width, d.height}, nil + } + paletted := cbPaletted(d.cb) + if d.stage == dsSeenIHDR && !paletted { + break + } + if d.stage == dsSeenPLTE && paletted { + break + } + } + return Size{d.width, d.height}, nil +} diff --git a/testdata/test.gif b/testdata/test.gif new file mode 100644 index 0000000..7c8cdb9 Binary files /dev/null and b/testdata/test.gif differ diff --git a/testdata/test.jpg b/testdata/test.jpg new file mode 100644 index 0000000..ba91ebf Binary files /dev/null and b/testdata/test.jpg differ diff --git a/testdata/test.png b/testdata/test.png new file mode 100644 index 0000000..44fbd8a Binary files /dev/null and b/testdata/test.png differ diff --git a/testdata/test.webp b/testdata/test.webp new file mode 100644 index 0000000..bb9ada1 Binary files /dev/null and b/testdata/test.webp differ diff --git a/webp.go b/webp.go new file mode 100644 index 0000000..3cd25d0 --- /dev/null +++ b/webp.go @@ -0,0 +1,365 @@ +// Copyright 2011 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 imgsz + +import ( + "bufio" + "errors" + "io" + "io/ioutil" + "math" +) + +var errInvalidFormat = errors.New("webp: invalid format") + +// FourCC is a four character code. +type FourCC [4]byte + +var ( + fccALPH = FourCC{'A', 'L', 'P', 'H'} + fccVP8 = FourCC{'V', 'P', '8', ' '} + fccVP8L = FourCC{'V', 'P', '8', 'L'} + fccVP8X = FourCC{'V', 'P', '8', 'X'} + fccWEBP = FourCC{'W', 'E', 'B', 'P'} +) + +const chunkHeaderSize = 8 + +var ( + errMissingPaddingByte = errors.New("riff: missing padding byte") + errMissingRIFFChunkHeader = errors.New("riff: missing RIFF chunk header") + errListSubchunkTooLong = errors.New("riff: list subchunk too long") + errShortChunkData = errors.New("riff: short chunk data") + errShortChunkHeader = errors.New("riff: short chunk header") + errStaleReader = errors.New("riff: stale reader") +) + +// u32 decodes the first four bytes of b as a little-endian integer. +func u32(b []byte) uint32 { + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +// Reader reads chunks from an underlying io.Reader. +type Reader struct { + r io.Reader + err error + + totalLen uint32 + chunkLen uint32 + + chunkReader *chunkReader + buf [chunkHeaderSize]byte + padded bool +} + +// NewListReader returns a LIST chunk's list type, such as "movi" or "wavl", +// and its chunks as a *Reader. +func NewListReader(chunkLen uint32, chunkData io.Reader) (listType FourCC, data *Reader, err error) { + if chunkLen < 4 { + return FourCC{}, nil, errShortChunkData + } + z := &Reader{r: chunkData} + if _, err := io.ReadFull(chunkData, z.buf[:4]); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = errShortChunkData + } + return FourCC{}, nil, err + } + z.totalLen = chunkLen - 4 + return FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]}, z, nil +} + +// NewReader returns the RIFF stream's form type, such as "AVI " or "WAVE", and +// its chunks as a *Reader. +func NewReader(r io.Reader) (formType FourCC, data *Reader, err error) { + var buf [chunkHeaderSize]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = errMissingRIFFChunkHeader + } + return FourCC{}, nil, err + } + if buf[0] != 'R' || buf[1] != 'I' || buf[2] != 'F' || buf[3] != 'F' { + return FourCC{}, nil, errMissingRIFFChunkHeader + } + return NewListReader(u32(buf[4:]), r) +} + +// Next returns the next chunk's ID, length and data. It returns io.EOF if there +// are no more chunks. The io.Reader returned becomes stale after the next Next +// call, and should no longer be used. +// +// It is valid to call Next even if all of the previous chunk's data has not +// been read. +func (z *Reader) Next() (chunkID FourCC, chunkLen uint32, chunkData io.Reader, err error) { + if z.err != nil { + return FourCC{}, 0, nil, z.err + } + + // Drain the rest of the previous chunk. + if z.chunkLen != 0 { + want := z.chunkLen + var got int64 + got, z.err = io.Copy(ioutil.Discard, z.chunkReader) + if z.err == nil && uint32(got) != want { + z.err = errShortChunkData + } + if z.err != nil { + return FourCC{}, 0, nil, z.err + } + } + z.chunkReader = nil + if z.padded { + if z.totalLen == 0 { + z.err = errListSubchunkTooLong + return FourCC{}, 0, nil, z.err + } + z.totalLen-- + _, z.err = io.ReadFull(z.r, z.buf[:1]) + if z.err != nil { + if z.err == io.EOF { + z.err = errMissingPaddingByte + } + return FourCC{}, 0, nil, z.err + } + } + + // We are done if we have no more data. + if z.totalLen == 0 { + z.err = io.EOF + return FourCC{}, 0, nil, z.err + } + + // Read the next chunk header. + if z.totalLen < chunkHeaderSize { + z.err = errShortChunkHeader + return FourCC{}, 0, nil, z.err + } + z.totalLen -= chunkHeaderSize + if _, z.err = io.ReadFull(z.r, z.buf[:chunkHeaderSize]); z.err != nil { + if z.err == io.EOF || z.err == io.ErrUnexpectedEOF { + z.err = errShortChunkHeader + } + return FourCC{}, 0, nil, z.err + } + chunkID = FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]} + z.chunkLen = u32(z.buf[4:]) + if z.chunkLen > z.totalLen { + z.err = errListSubchunkTooLong + return FourCC{}, 0, nil, z.err + } + z.padded = z.chunkLen&1 == 1 + z.chunkReader = &chunkReader{z} + return chunkID, z.chunkLen, z.chunkReader, nil +} + +type chunkReader struct { + z *Reader +} + +func (c *chunkReader) Read(p []byte) (int, error) { + if c != c.z.chunkReader { + return 0, errStaleReader + } + z := c.z + if z.err != nil { + if z.err == io.EOF { + return 0, errStaleReader + } + return 0, z.err + } + + n := int(z.chunkLen) + if n == 0 { + return 0, io.EOF + } + if n < 0 { + // Converting uint32 to int overflowed. + n = math.MaxInt32 + } + if n > len(p) { + n = len(p) + } + n, err := z.r.Read(p[:n]) + z.totalLen -= uint32(n) + z.chunkLen -= uint32(n) + if err != io.EOF { + z.err = err + } + return n, err +} + +func decodewebp(r io.Reader) (Size, error) { + formType, riffReader, err := NewReader(r) + if err != nil { + return Size{}, err + } + if formType != fccWEBP { + return Size{}, errInvalidFormat + } + + var ( + alpha []byte + wantAlpha bool + widthMinusOne uint32 + heightMinusOne uint32 + buf [10]byte + ) + for { + chunkID, chunkLen, chunkData, err := riffReader.Next() + if err == io.EOF { + err = errInvalidFormat + } + if err != nil { + return Size{}, err + } + + switch chunkID { + case fccALPH: + if !wantAlpha { + return Size{}, errInvalidFormat + } + wantAlpha = false + // Read the Pre-processing | Filter | Compression byte. + if _, err := io.ReadFull(chunkData, buf[:1]); err != nil { + if err == io.EOF { + err = errInvalidFormat + } + return Size{}, err + } + + case fccVP8: + if wantAlpha || int32(chunkLen) < 0 { + return Size{}, errInvalidFormat + } + w, h, err := decodeVP8FrameHeader(chunkData) + if err != nil { + return Size{}, err + } + return Size{w, h}, nil + + case fccVP8L: + if wantAlpha || alpha != nil { + return Size{}, errInvalidFormat + } + w, h, err := decodeVP8LHeader(chunkData) + return Size{int(w), int(h)}, err + + case fccVP8X: + if chunkLen != 10 { + return Size{}, errInvalidFormat + } + if _, err := io.ReadFull(chunkData, buf[:10]); err != nil { + return Size{}, err + } + const ( + animationBit = 1 << 1 + xmpMetadataBit = 1 << 2 + exifMetadataBit = 1 << 3 + alphaBit = 1 << 4 + iccProfileBit = 1 << 5 + ) + wantAlpha = (buf[0] & alphaBit) != 0 + widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 + heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16 + if wantAlpha { + return Size{ + Width: int(widthMinusOne) + 1, + Height: int(heightMinusOne) + 1, + }, nil + } + return Size{ + Width: int(widthMinusOne) + 1, + Height: int(heightMinusOne) + 1, + }, nil + } + } +} + +func decodeVP8FrameHeader(r io.Reader) (w, h int, err error) { + var scratch [8]byte + // All frame headers are at least 3 bytes long. + b := scratch[:3] + if _, err = io.ReadFull(r, b); err != nil { + return + } + if (b[0] & 1) != 0 { + return 0, 0, nil + } + // Frame headers for key frames are an additional 7 bytes long. + b = scratch[:7] + if _, err = io.ReadFull(r, b); err != nil { + return + } + // Check the magic sync code. + if b[0] != 0x9d || b[1] != 0x01 || b[2] != 0x2a { + err = errors.New("vp8: invalid format") + return + } + return int(b[4]&0x3f)<<8 | int(b[3]), int(b[6]&0x3f)<<8 | int(b[5]), nil +} + +// vp8ldecoder holds the bit-stream for a VP8L image. +type vp8ldecoder struct { + r io.ByteReader + bits uint32 + nBits uint32 +} + +// read reads the next n bits from the decoder's bit-stream. +func (d *vp8ldecoder) read(n uint32) (uint32, error) { + for d.nBits < n { + c, err := d.r.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return 0, err + } + d.bits |= uint32(c) << d.nBits + d.nBits += 8 + } + u := d.bits & (1<>= n + d.nBits -= n + return u, nil +} + +func decodeVP8LHeader(r io.Reader) (w int32, h int32, err error) { + rr, ok := r.(io.ByteReader) + if !ok { + rr = bufio.NewReader(r) + } + d := &vp8ldecoder{r: rr} + magic, err := d.read(8) + if err != nil { + return 0, 0, err + } + if magic != 0x2f { + return 0, 0, errors.New("vp8l: invalid header") + } + width, err := d.read(14) + if err != nil { + return 0, 0, err + } + width++ + height, err := d.read(14) + if err != nil { + return 0, 0, err + } + height++ + _, err = d.read(1) // Read and ignore the hasAlpha hint. + if err != nil { + return 0, 0, err + } + version, err := d.read(3) + if err != nil { + return 0, 0, err + } + if version != 0 { + return 0, 0, errors.New("vp8l: invalid version") + } + return int32(width), int32(height), nil +}