diff --git a/tiff/consts.go b/tiff/consts.go index 3b0a865..772e98a 100644 --- a/tiff/consts.go +++ b/tiff/consts.go @@ -109,9 +109,20 @@ const ( mNRGBA ) -// Compression describes the type of compression used in Options. +// CompressionType describes the type of compression used in Options. type CompressionType int const ( Uncompressed CompressionType = iota + Deflate ) + +// specValue returns the compression type constant from the TIFF spec that +// is equivalent to c. +func (c CompressionType) specValue() uint32 { + switch c { + case Deflate: + return cDeflate + } + return cNone +} diff --git a/tiff/writer.go b/tiff/writer.go index 0efc06f..274b115 100644 --- a/tiff/writer.go +++ b/tiff/writer.go @@ -5,6 +5,8 @@ package tiff import ( + "bytes" + "compress/zlib" "encoding/binary" "image" "io" @@ -179,57 +181,82 @@ type Options struct { // image is written. func Encode(w io.Writer, m image.Image, opt *Options) error { predictor := false - compression := Uncompressed + compression := uint32(cNone) if opt != nil { predictor = opt.Predictor - compression = opt.Compression - } - if compression != Uncompressed { - return UnsupportedError("compression type") + compression = opt.Compression.specValue() } - var extrasamples uint32 _, err := io.WriteString(w, leHeader) if err != nil { return err } + // Compressed data is written into a buffer first, so that we + // know the compressed size. + var buf bytes.Buffer + // dst holds the destination for the pixel data of the image -- + // either w or a writer to buf. + var dst io.Writer + // imageLen is the length of the pixel data in bytes. + // The offset of the IFD is imageLen + 8 header bytes. + var imageLen int bounds := m.Bounds() width, height := bounds.Dx(), bounds.Dy() - // imageLen is the length of the image data in bytes. - imageLen := width * height * 4 - ifdOffset := imageLen + 8 // 8 bytes for TIFF header. - err = binary.Write(w, enc, uint32(ifdOffset)) - if err != nil { - return err + + switch compression { + case cNone: + dst = w + // Write IFD offset before outputting pixel data. + imageLen = width * height * 4 + err = binary.Write(w, enc, uint32(imageLen+8)) + if err != nil { + return err + } + case cDeflate: + dst = zlib.NewWriter(&buf) } + var pr uint32 = prNone - extrasamples = 1 // Associated alpha (default). + var extrasamples uint32 = 1 // Associated alpha (default). if predictor { pr = prHorizontal - err = writeImgData(w, m, predictor) + err = writeImgData(dst, m, predictor) } else { switch img := m.(type) { case *image.NRGBA: extrasamples = 2 // Unassociated alpha. off := img.PixOffset(img.Rect.Min.X, img.Rect.Min.Y) - err = writePix(w, img.Pix[off:], img.Rect.Dy(), 4*img.Rect.Dx(), img.Stride) + err = writePix(dst, img.Pix[off:], img.Rect.Dy(), 4*img.Rect.Dx(), img.Stride) case *image.RGBA: off := img.PixOffset(img.Rect.Min.X, img.Rect.Min.Y) - err = writePix(w, img.Pix[off:], img.Rect.Dy(), 4*img.Rect.Dx(), img.Stride) + err = writePix(dst, img.Pix[off:], img.Rect.Dy(), 4*img.Rect.Dx(), img.Stride) default: - err = writeImgData(w, m, predictor) + err = writeImgData(dst, m, predictor) } } if err != nil { return err } - return writeIFD(w, ifdOffset, []ifdEntry{ + if compression != cNone { + if err = dst.(io.Closer).Close(); err != nil { + return err + } + imageLen = buf.Len() + if err = binary.Write(w, enc, uint32(imageLen+8)); err != nil { + return err + } + if _, err = buf.WriteTo(w); err != nil { + return err + } + } + + return writeIFD(w, imageLen+8, []ifdEntry{ {tImageWidth, dtShort, []uint32{uint32(width)}}, {tImageLength, dtShort, []uint32{uint32(height)}}, {tBitsPerSample, dtShort, []uint32{8, 8, 8, 8}}, - {tCompression, dtShort, []uint32{cNone}}, + {tCompression, dtShort, []uint32{compression}}, {tPhotometricInterpretation, dtShort, []uint32{pRGB}}, {tStripOffsets, dtLong, []uint32{8}}, {tSamplesPerPixel, dtShort, []uint32{4}}, diff --git a/tiff/writer_test.go b/tiff/writer_test.go index a82ae06..500a446 100644 --- a/tiff/writer_test.go +++ b/tiff/writer_test.go @@ -19,6 +19,8 @@ var roundtripTests = []struct { {"video-001.tiff", nil}, {"bw-packbits.tiff", nil}, {"video-001.tiff", &Options{Predictor: true}}, + {"video-001.tiff", &Options{Compression: Deflate}}, + {"video-001.tiff", &Options{Predictor: true, Compression: Deflate}}, } func openImage(filename string) (image.Image, error) {