mirror of
https://github.com/fumiama/imgsz.git
synced 2026-06-05 00:32:53 +08:00
🎉 完成
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.DS_Store
|
||||
|
||||
10
README.md
10
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)
|
||||
```
|
||||
|
||||
48
gif.go
Normal file
48
gif.go
Normal file
@@ -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
|
||||
}
|
||||
108
image.go
Normal file
108
image.go
Normal file
@@ -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
|
||||
}
|
||||
60
image_test.go
Normal file
60
image_test.go
Normal file
@@ -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()
|
||||
}
|
||||
19
init.go
Normal file
19
init.go
Normal file
@@ -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)
|
||||
}
|
||||
327
jpg.go
Normal file
327
jpg.go
Normal file
@@ -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
|
||||
}
|
||||
240
png.go
Normal file
240
png.go
Normal file
@@ -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
|
||||
}
|
||||
BIN
testdata/test.gif
vendored
Normal file
BIN
testdata/test.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
testdata/test.jpg
vendored
Normal file
BIN
testdata/test.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
testdata/test.png
vendored
Normal file
BIN
testdata/test.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 635 KiB |
BIN
testdata/test.webp
vendored
Normal file
BIN
testdata/test.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
365
webp.go
Normal file
365
webp.go
Normal file
@@ -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 - 1)
|
||||
d.bits >>= 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
|
||||
}
|
||||
Reference in New Issue
Block a user