Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tiff: Add support for JPEG-based compression (TIFF compression scheme 7) #7

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added testdata/bw-jpeg.tiff
Binary file not shown.
Binary file added testdata/video-001-jpeg.tiff
Binary file not shown.
9 changes: 9 additions & 0 deletions tiff/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -118,6 +123,7 @@ const (
mRGBA
mNRGBA
mCMYK
mYCbCr
)

// CompressionType describes the type of compression used in Options.
Expand All @@ -130,6 +136,7 @@ const (
LZW
CCITTGroup3
CCITTGroup4
JPEG
)

// specValue returns the compression type constant from the TIFF spec that
Expand All @@ -144,6 +151,8 @@ func (c CompressionType) specValue() uint32 {
return cG3
case CCITTGroup4:
return cG4
case JPEG:
return cJPEG
}
return cNone
}
125 changes: 119 additions & 6 deletions tiff/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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])))
Expand All @@ -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])
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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++ {
Expand Down Expand Up @@ -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))
Expand All @@ -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
}
Expand Down
27 changes: 27 additions & 0 deletions tiff/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"compress/zlib"
"encoding/binary"
"image"
"image/jpeg"
"io"
"sort"
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)}},
Expand Down
57 changes: 57 additions & 0 deletions tiff/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down