Skip to content

Commit

Permalink
Add write support for JPEG compression, and read support for YCbCr co…
Browse files Browse the repository at this point in the history
…lor model in this compression mode.
  • Loading branch information
sunshineplan committed Jul 12, 2021
1 parent 54a6525 commit 6dc5552
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 79 deletions.
Binary file added testdata/bw-jpeg.tiff
Binary file not shown.
Binary file modified testdata/video-001-jpeg.tiff
Binary file not shown.
4 changes: 4 additions & 0 deletions tiff/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const (
mRGBA
mNRGBA
mCMYK
mYCbCr
)

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

// specValue returns the compression type constant from the TIFF spec that
Expand All @@ -149,6 +151,8 @@ func (c CompressionType) specValue() uint32 {
return cG3
case CCITTGroup4:
return cG4
case JPEG:
return cJPEG
}
return cNone
}
90 changes: 56 additions & 34 deletions tiff/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ type decoder struct {
v uint32 // Buffer value for reading with arbitrary bit depths.
nbits uint // Remaining number of bits in v.

tmp image.Image // Store temporary image for jpeg compression
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 Down Expand Up @@ -406,14 +407,16 @@ 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) {
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)

Expand All @@ -435,13 +438,23 @@ func (d *decoder) decodeJPEG(dst image.Image, xmin, ymin, xmax, ymax int) {
}
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) {
Expand Down Expand Up @@ -578,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 @@ -681,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 @@ -722,43 +756,31 @@ func Decode(r io.Reader) (img image.Image, err error) {
d.buf, err = ioutil.ReadAll(r)
r.Close()
case cJPEG:
var tjpeg bool
var buf bytes.Buffer
// 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.
// When it is specified, these tables need not be duplicated in each segment.
// Start with SOI marker and end with EOI marker.
b := make([]byte, len(d.features[tJPEG]))
for i := range d.features[tJPEG] {
b[i] = uint8(d.features[tJPEG][i])
}
if len(b) > 2 {
tjpeg = true
// Write to buffer without EOI marker.
buf.Write(b[:len(b)-2])
} else if len(b) != 0 {
return nil, FormatError("bad JPEGTables field")
}
// JPEG image segment should start with SOI marker and end with EOI marker.
b, err = io.ReadAll(io.NewSectionReader(d.r, offset, n))
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")
}
if tjpeg {
// Write JPEG image segment to buffer without SOI marker.
// When this is done, buffer data will be a full JPEG format data.
buf.Write(b[2:])
} else {
// Write full JPEG image segment to buffer.
buf.Write(b)
}
// Decode as a JPEG image.
d.tmp, err = jpeg.Decode(&buf)
d.tmp, err = jpeg.Decode(bytes.NewBuffer(b))
if err != nil {
return nil, err
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
Expand All @@ -782,12 +804,12 @@ func Decode(r io.Reader) (img image.Image, err error) {
xmax := xmin + blkW
ymax := ymin + blkH
if d.firstVal(tCompression) == cJPEG {
d.decodeJPEG(img, xmin, ymin, xmax, ymax)
img, err = d.decodeJPEG(img, xmin, ymin, xmax, ymax)
} else {
err = d.decode(img, xmin, ymin, xmax, ymax)
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
}
}
Expand Down
45 changes: 0 additions & 45 deletions tiff/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,51 +229,6 @@ func TestDecodeCCITT(t *testing.T) {
}
}

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
}

// TestDecodeJPEG tests decoding an image use JPEG compression.
func TestDecodeJPEG(t *testing.T) {
img, err := openImage("video-001.tiff")
if err != nil {
t.Fatal(err)
}

img2, err := openImage("video-001-jpeg.tiff")
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)
}
}

// TestDecodeTagOrder tests that a malformed image with unsorted IFD entries is
// correctly rejected.
func TestDecodeTagOrder(t *testing.T) {
Expand Down
28 changes: 28 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,18 @@ 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.Gray:
default:
// 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

0 comments on commit 6dc5552

Please sign in to comment.