diff --git a/testdata/bw-jpeg.tiff b/testdata/bw-jpeg.tiff new file mode 100644 index 00000000..fe6f0aa2 Binary files /dev/null and b/testdata/bw-jpeg.tiff differ diff --git a/testdata/video-001-jpeg.tiff b/testdata/video-001-jpeg.tiff new file mode 100644 index 00000000..31a0e8c4 Binary files /dev/null and b/testdata/video-001-jpeg.tiff differ diff --git a/tiff/consts.go b/tiff/consts.go index 3e5f7f14..31b7b8b3 100644 --- a/tiff/consts.go +++ b/tiff/consts.go @@ -29,6 +29,8 @@ const ( dtShort = 3 dtLong = 4 dtRational = 5 + + dtUndefined = 7 // JPEGTables field is required to have type code UNDEFINED ) // The length of one instance of each data type in bytes. @@ -65,6 +67,9 @@ const ( tColorMap = 320 tExtraSamples = 338 tSampleFormat = 339 + + // https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf + tJPEG = 347 // page 7 ) // Compression types (defined in various places in the spec and supplements). @@ -118,6 +123,7 @@ const ( mRGBA mNRGBA mCMYK + mYCbCr ) // CompressionType describes the type of compression used in Options. @@ -130,6 +136,7 @@ const ( LZW CCITTGroup3 CCITTGroup4 + JPEG ) // specValue returns the compression type constant from the TIFF spec that @@ -144,6 +151,8 @@ func (c CompressionType) specValue() uint32 { return cG3 case CCITTGroup4: return cG4 + case JPEG: + return cJPEG } return cNone } diff --git a/tiff/reader.go b/tiff/reader.go index de73f4b9..60545f6a 100644 --- a/tiff/reader.go +++ b/tiff/reader.go @@ -8,11 +8,14 @@ package tiff // import "golang.org/x/image/tiff" import ( + "bytes" "compress/zlib" "encoding/binary" "fmt" "image" "image/color" + "image/draw" + "image/jpeg" "io" "io/ioutil" "math" @@ -51,6 +54,9 @@ type decoder struct { off int // Current offset in buf. v uint32 // Buffer value for reading with arbitrary bit depths. nbits uint // Remaining number of bits in v. + + jpegTables []byte // Store JPEGTables data + tmp image.Image // Store temporary image for jpeg compression } // firstVal returns the first uint of the features entry with the given tag, @@ -71,16 +77,25 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { return nil, FormatError("bad IFD entry") } + tag := d.byteOrder.Uint16(p[0:2]) datatype := d.byteOrder.Uint16(p[2:4]) - if dt := int(datatype); dt <= 0 || dt >= len(lengths) { + if dt := int(datatype); dt <= 0 || dt >= len(lengths) && tag != tJPEG { return nil, UnsupportedError("IFD entry datatype") } + // tJPEG's type is dtUndefined which size is same as dtByte. + var length uint32 + if tag != tJPEG { + length = lengths[datatype] + } else { + length = 1 + } + count := d.byteOrder.Uint32(p[4:8]) - if count > math.MaxInt32/lengths[datatype] { + if count > math.MaxInt32/length { return nil, FormatError("IFD data too large") } - if datalen := lengths[datatype] * count; datalen > 4 { + if datalen := length * count; datalen > 4 { // The IFD contains a pointer to the real value. raw = make([]byte, datalen) _, err = d.r.ReadAt(raw, int64(d.byteOrder.Uint32(p[8:12]))) @@ -93,7 +108,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { u = make([]uint, count) switch datatype { - case dtByte: + case dtByte, dtUndefined: for i := uint32(0); i < count; i++ { u[i] = uint(raw[i]) } @@ -133,7 +148,8 @@ func (d *decoder) parseIFD(p []byte) (int, error) { tImageWidth, tFillOrder, tT4Options, - tT6Options: + tT6Options, + tJPEG: val, err := d.ifdUint(p) if err != nil { return 0, err @@ -391,11 +407,56 @@ func (d *decoder) decode(dst image.Image, xmin, ymin, xmax, ymax int) error { copy(img.Pix[min:max], d.buf[i0:i1]) } } + case mYCbCr: + return UnsupportedError("color model YCbCr not in JPEG compression") } return nil } +// decodeJPEG decodes the jpeg data of an image. +// It reads from d.tmp and writes the strip or tile into dst. +func (d *decoder) decodeJPEG(dst image.Image, xmin, ymin, xmax, ymax int) (image.Image, error) { + rMaxX := minInt(xmax, dst.Bounds().Max.X) + rMaxY := minInt(ymax, dst.Bounds().Max.Y) + + var img draw.Image + switch d.mode { + case mGray, mGrayInvert: + if d.bpp == 16 { + img = dst.(*image.Gray16) + } else { + img = dst.(*image.Gray) + } + case mPaletted: + img = dst.(*image.Paletted) + case mRGB, mNRGBA, mRGBA: + if d.bpp == 16 { + img = dst.(*image.RGBA64) + } else { + img = dst.(*image.RGBA) + } + case mCMYK: + img = dst.(*image.CMYK) + case mYCbCr: + // only support for single segment. + if dst.Bounds() == d.tmp.Bounds() { + dst = d.tmp + } else { + return nil, UnsupportedError("color model YCbCr with multiple segments in JPEG compression") + } + return dst, nil + } + + for y := 0; y+ymin < rMaxY; y++ { + for x := 0; x+xmin < rMaxX; x++ { + img.Set(x+xmin, y+ymin, d.tmp.At(x, y)) + } + } + + return dst, nil +} + func newDecoder(r io.Reader) (*decoder, error) { d := &decoder{ r: newReaderAt(r), @@ -530,6 +591,12 @@ func newDecoder(r io.Reader) (*decoder, error) { } else { d.config.ColorModel = color.GrayModel } + case pYCbCr: + d.mode = mYCbCr + if d.bpp == 16 { + return nil, UnsupportedError(fmt.Sprintf("YCbCr BitsPerSample of %d", d.bpp)) + } + d.config.ColorModel = color.YCbCrModel default: return nil, UnsupportedError("color model") } @@ -633,6 +700,21 @@ func Decode(r io.Reader) (img image.Image, err error) { } else { img = image.NewRGBA(imgRect) } + case mYCbCr: + img = image.NewYCbCr(imgRect, 0) + } + + // According to the spec, JPEGTables is an optional field. The purpose of it is to + // predefine JPEG quantization and/or Huffman tables for subsequent use by JPEG image segments. + // Start with SOI marker and end with EOI marker. + if d.firstVal(tCompression) == cJPEG { + d.jpegTables = make([]byte, len(d.features[tJPEG])) + for i := range d.features[tJPEG] { + d.jpegTables[i] = uint8(d.features[tJPEG][i]) + } + if l := len(d.jpegTables); l != 0 && l < 4 { + return nil, FormatError("bad JPEGTables field") + } } for i := 0; i < blocksAcross; i++ { @@ -673,6 +755,33 @@ func Decode(r io.Reader) (img image.Image, err error) { r := lzw.NewReader(io.NewSectionReader(d.r, offset, n), lzw.MSB, 8) d.buf, err = ioutil.ReadAll(r) r.Close() + case cJPEG: + // JPEG image segment should start with SOI marker and end with EOI marker. + b, err := io.ReadAll(io.NewSectionReader(d.r, offset, n)) + if err != nil { + return nil, err + } + if len(b) < 4 { + return nil, FormatError("bad JPEG image segment") + } + // Decode as a JPEG image. + d.tmp, err = jpeg.Decode(bytes.NewBuffer(b)) + if err != nil { + var buf bytes.Buffer + if len(d.jpegTables) != 0 { + // Write JPEGTables data to buffer without EOI marker. + buf.Write(d.jpegTables[:len(d.jpegTables)-2]) + } else { + return nil, err + } + // Write JPEG image segment to buffer without SOI marker. + // When this is done, buffer data should be a full JPEG format data. + buf.Write(b[2:]) + d.tmp, err = jpeg.Decode(&buf) + if err != nil { + return nil, err + } + } case cDeflate, cDeflateOld: var r io.ReadCloser r, err = zlib.NewReader(io.NewSectionReader(d.r, offset, n)) @@ -694,7 +803,11 @@ func Decode(r io.Reader) (img image.Image, err error) { ymin := j * blockHeight xmax := xmin + blkW ymax := ymin + blkH - err = d.decode(img, xmin, ymin, xmax, ymax) + if d.firstVal(tCompression) == cJPEG { + img, err = d.decodeJPEG(img, xmin, ymin, xmax, ymax) + } else { + err = d.decode(img, xmin, ymin, xmax, ymax) + } if err != nil { return nil, err } diff --git a/tiff/writer.go b/tiff/writer.go index c8a01cea..abbdae84 100644 --- a/tiff/writer.go +++ b/tiff/writer.go @@ -9,6 +9,7 @@ import ( "compress/zlib" "encoding/binary" "image" + "image/jpeg" "io" "sort" ) @@ -285,6 +286,15 @@ type Options struct { Predictor bool } +type discard struct{} + +func (discard) Write(p []byte) (int, error) { + return len(p), nil +} +func (discard) Close() error { + return nil +} + // Encode writes the image m to w. opt determines the options used for // encoding, such as the compression type. If opt is nil, an uncompressed // image is written. @@ -338,6 +348,12 @@ func Encode(w io.Writer, m image.Image, opt *Options) error { } case cDeflate: dst = zlib.NewWriter(&buf) + case cJPEG: + dst = discard{} + err = jpeg.Encode(&buf, m, nil) + if err != nil { + return err + } } pr := uint32(prNone) @@ -408,6 +424,17 @@ func Encode(w io.Writer, m image.Image, opt *Options) error { } } + // JPEG compression uses jpeg.Encode to encoding image which writes Gray or YCbCr image. + if compression == cJPEG { + switch m.(type) { + case *image.YCbCr: + // Minimum Requirements for YCbCr Images. (See page 94). + photometricInterpretation = uint32(pYCbCr) + samplesPerPixel = 3 + bitsPerSample = []uint32{8, 8, 8} + } + } + ifd := []ifdEntry{ {tImageWidth, dtShort, []uint32{uint32(d.X)}}, {tImageLength, dtShort, []uint32{uint32(d.Y)}}, diff --git a/tiff/writer_test.go b/tiff/writer_test.go index 0650df3d..7347ab7c 100644 --- a/tiff/writer_test.go +++ b/tiff/writer_test.go @@ -75,6 +75,63 @@ func TestRoundtrip2(t *testing.T) { compare(t, m0, m1) } +func delta(u0, u1 uint32) int64 { + d := int64(u0) - int64(u1) + if d < 0 { + return -d + } + return d +} + +// averageDelta returns the average delta in RGB space. The two images must +// have the same bounds. +func averageDelta(m0, m1 image.Image) int64 { + b := m0.Bounds() + var sum, n int64 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + c0 := m0.At(x, y) + c1 := m1.At(x, y) + r0, g0, b0, _ := c0.RGBA() + r1, g1, b1, _ := c1.RGBA() + sum += delta(r0, r1) + sum += delta(g0, g1) + sum += delta(b0, b1) + n += 3 + } + } + return sum / n +} + +// TestRoundtrip3 tests that encoding and decoding an image use JPEG compression. +func TestRoundtrip3(t *testing.T) { + roundtripTests := []string{ + "bw-jpeg.tiff", + "video-001-jpeg.tiff", + } + for _, rt := range roundtripTests { + img, err := openImage(rt) + if err != nil { + t.Fatal(err) + } + + out := new(bytes.Buffer) + err = Encode(out, img, &Options{Compression: JPEG}) + if err != nil { + t.Fatal(err) + } + + img2, err := Decode(&buffer{buf: out.Bytes()}) + if err != nil { + t.Fatal(err) + } + want := int64(6 << 8) + if got := averageDelta(img, img2); got > want { + t.Errorf("average delta too high; got %d, want <= %d", got, want) + } + } +} + func benchmarkEncode(b *testing.B, name string, pixelSize int) { b.Helper() img, err := openImage(name)