git clone https://git.lucas.co/go_mono.git
go.image/tiff: encoder support Gray/Gray16/Paletted format
Use these commands to generate testdata:
# TIFF Tools
tiff2bw video-001.tiff video-001-gray.tiff
tiffmedian video-001.tiff video-001-paletted.tiff
# ImageMagick
convert -depth 16 video-001.tiff video-001-16bit.tiff
convert -depth 16 video-001-gray.tiff video-001-gray-16bit.tiff
R=nigeltao, bsiegert
CC=golang-dev
https://golang.org/cl/13243047
testdata/video-001-16bit.tiff | Bin 0 -> 42146 bytes
testdata/video-001-gray-16bit.tiff | Bin 0 -> 31254 bytes
testdata/video-001-gray.tiff | Bin 0 -> 15742 bytes
testdata/video-001-paletted.tiff | Bin 0 -> 11214 bytes
tiff/writer.go | 189 +++++++++++++++++++++++++++++--------
tiff/writer_test.go | 15 ++-
6 files changed, 160 insertions(+), 44 deletions(-)
diff --git a/testdata/video-001-16bit.tiff b/testdata/video-001-16bit.tiff
new file mode 100644
index 0000000..3b05ef0
Binary files /dev/null and b/testdata/video-001-16bit.tiff differ
diff --git a/testdata/video-001-gray-16bit.tiff b/testdata/video-001-gray-16bit.tiff
new file mode 100644
index 0000000..356882a
Binary files /dev/null and b/testdata/video-001-gray-16bit.tiff differ
diff --git a/testdata/video-001-gray.tiff b/testdata/video-001-gray.tiff
new file mode 100644
index 0000000..38fc9d2
Binary files /dev/null and b/testdata/video-001-gray.tiff differ
diff --git a/testdata/video-001-paletted.tiff b/testdata/video-001-paletted.tiff
new file mode 100644
index 0000000..5db84bc
Binary files /dev/null and b/testdata/video-001-paletted.tiff differ
diff --git a/tiff/writer.go b/tiff/writer.go
index 274b115..2cdd74e 100644
--- a/tiff/writer.go
+++ b/tiff/writer.go
@@ -55,13 +55,85 @@ func (d byTag) Len() int { return len(d) }
func (d byTag) Less(i, j int) bool { return d[i].tag < d[j].tag }
func (d byTag) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
-// writeImgData writes the raw data of m into w, optionally using a
-// differencing predictor.
-func writeImgData(w io.Writer, m image.Image, predictor bool) error {
+func encodeGray(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ if !predictor {
+ return writePix(w, pix, dy, dx, stride)
+ }
+ buf := make([]byte, dx)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx
+ off := 0
+ var v0 uint8
+ for i := min; i < max; i++ {
+ v1 := pix[i]
+ buf[off] = v1 - v0
+ v0 = v1
+ off++
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encodeGray16(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ buf := make([]byte, dx*2)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx*2
+ off := 0
+ var v0 uint16
+ for i := min; i < max; i += 2 {
+ // An image.Gray16's Pix is in big-endian order.
+ v1 := uint16(pix[i])<<8 | uint16(pix[i+1])
+ if predictor {
+ v0, v1 = v1, v1-v0
+ }
+ // We only write little-endian TIFF files.
+ buf[off+0] = byte(v1)
+ buf[off+1] = byte(v1 >> 8)
+ off += 2
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encodeRGBA(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error {
+ if !predictor {
+ return writePix(w, pix, dy, dx*4, stride)
+ }
+ buf := make([]byte, dx*4)
+ for y := 0; y < dy; y++ {
+ min := y*stride + 0
+ max := y*stride + dx*4
+ off := 0
+ var r0, g0, b0, a0 uint8
+ for i := min; i < max; i += 4 {
+ r1, g1, b1, a1 := pix[i+0], pix[i+1], pix[i+2], pix[i+3]
+ buf[off+0] = r1 - r0
+ buf[off+1] = g1 - g0
+ buf[off+2] = b1 - b0
+ buf[off+3] = a1 - a0
+ off += 4
+ r0, g0, b0, a0 = r1, g1, b1, a1
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func encode(w io.Writer, m image.Image, predictor bool) error {
bounds := m.Bounds()
buf := make([]byte, 4*bounds.Dx())
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
- i := 0
+ off := 0
if predictor {
var r0, g0, b0, a0 uint8
for x := bounds.Min.X; x < bounds.Max.X; x++ {
@@ -70,21 +142,21 @@ func writeImgData(w io.Writer, m image.Image, predictor bool) error {
g1 := uint8(g >> 8)
b1 := uint8(b >> 8)
a1 := uint8(a >> 8)
- buf[i+0] = r1 - r0
- buf[i+1] = g1 - g0
- buf[i+2] = b1 - b0
- buf[i+3] = a1 - a0
- i += 4
+ buf[off+0] = r1 - r0
+ buf[off+1] = g1 - g0
+ buf[off+2] = b1 - b0
+ buf[off+3] = a1 - a0
+ off += 4
r0, g0, b0, a0 = r1, g1, b1, a1
}
} else {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := m.At(x, y).RGBA()
- buf[i+0] = uint8(r >> 8)
- buf[i+1] = uint8(g >> 8)
- buf[i+2] = uint8(b >> 8)
- buf[i+3] = uint8(a >> 8)
- i += 4
+ buf[off+0] = uint8(r >> 8)
+ buf[off+1] = uint8(g >> 8)
+ buf[off+2] = uint8(b >> 8)
+ buf[off+3] = uint8(a >> 8)
+ off += 4
}
}
if _, err := w.Write(buf); err != nil {
@@ -95,7 +167,7 @@ func writeImgData(w io.Writer, m image.Image, predictor bool) error {
}
// writePix writes the internal byte array of an image to w. It is less general
-// but much faster then writeImgData. writePix is used when pix directly
+// but much faster then encode. writePix is used when pix directly
// corresponds to one of the TIFF image types.
func writePix(w io.Writer, pix []byte, nrows, length, stride int) error {
if length == stride {
@@ -180,6 +252,8 @@ type Options struct {
// encoding, such as the compression type. If opt is nil, an uncompressed
// image is written.
func Encode(w io.Writer, m image.Image, opt *Options) error {
+ d := m.Bounds().Size()
+
predictor := false
compression := uint32(cNone)
if opt != nil {
@@ -201,14 +275,21 @@ func Encode(w io.Writer, m image.Image, opt *Options) error {
// 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()
switch compression {
case cNone:
dst = w
// Write IFD offset before outputting pixel data.
- imageLen = width * height * 4
+ switch m.(type) {
+ case *image.Paletted:
+ imageLen = d.X * d.Y * 1
+ case *image.Gray:
+ imageLen = d.X * d.Y * 1
+ case *image.Gray16:
+ imageLen = d.X * d.Y * 2
+ default:
+ imageLen = d.X * d.Y * 4
+ }
err = binary.Write(w, enc, uint32(imageLen+8))
if err != nil {
return err
@@ -217,23 +298,46 @@ func Encode(w io.Writer, m image.Image, opt *Options) error {
dst = zlib.NewWriter(&buf)
}
- var pr uint32 = prNone
- var extrasamples uint32 = 1 // Associated alpha (default).
+ pr := uint32(prNone)
+ photometricInterpretation := uint32(pRGB)
+ samplesPerPixel := uint32(4)
+ bitsPerSample := []uint32{8, 8, 8, 8}
+ extrasamples := uint32(1) // Associated alpha (default).
+ colorMap := []uint32{}
+
if predictor {
pr = prHorizontal
- 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(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(dst, img.Pix[off:], img.Rect.Dy(), 4*img.Rect.Dx(), img.Stride)
- default:
- err = writeImgData(dst, m, predictor)
+ }
+ switch m := m.(type) {
+ case *image.Paletted:
+ photometricInterpretation = pPaletted
+ samplesPerPixel = 3
+ bitsPerSample = []uint32{8}
+ colorMap = make([]uint32, 256*3)
+ for i := 0; i < 256 && i < len(m.Palette); i++ {
+ r, g, b, _ := m.Palette[i].RGBA()
+ colorMap[i+0*256] = uint32(r)
+ colorMap[i+1*256] = uint32(g)
+ colorMap[i+2*256] = uint32(b)
}
+ err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.Gray:
+ photometricInterpretation = pBlackIsZero
+ samplesPerPixel = 1
+ bitsPerSample = []uint32{8}
+ err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.Gray16:
+ photometricInterpretation = pBlackIsZero
+ samplesPerPixel = 1
+ bitsPerSample = []uint32{16}
+ err = encodeGray16(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.NRGBA:
+ extrasamples = 2 // Unassociated alpha.
+ err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ case *image.RGBA:
+ err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor)
+ default:
+ err = encode(dst, m, predictor)
}
if err != nil {
return err
@@ -252,15 +356,15 @@ func Encode(w io.Writer, m image.Image, opt *Options) error {
}
}
- return writeIFD(w, imageLen+8, []ifdEntry{
- {tImageWidth, dtShort, []uint32{uint32(width)}},
- {tImageLength, dtShort, []uint32{uint32(height)}},
- {tBitsPerSample, dtShort, []uint32{8, 8, 8, 8}},
+ ifd := []ifdEntry{
+ {tImageWidth, dtShort, []uint32{uint32(d.X)}},
+ {tImageLength, dtShort, []uint32{uint32(d.Y)}},
+ {tBitsPerSample, dtShort, bitsPerSample},
{tCompression, dtShort, []uint32{compression}},
- {tPhotometricInterpretation, dtShort, []uint32{pRGB}},
+ {tPhotometricInterpretation, dtShort, []uint32{photometricInterpretation}},
{tStripOffsets, dtLong, []uint32{8}},
- {tSamplesPerPixel, dtShort, []uint32{4}},
- {tRowsPerStrip, dtShort, []uint32{uint32(height)}},
+ {tSamplesPerPixel, dtShort, []uint32{samplesPerPixel}},
+ {tRowsPerStrip, dtShort, []uint32{uint32(d.Y)}},
{tStripByteCounts, dtLong, []uint32{uint32(imageLen)}},
// There is currently no support for storing the image
// resolution, so give a bogus value of 72x72 dpi.
@@ -269,5 +373,10 @@ func Encode(w io.Writer, m image.Image, opt *Options) error {
{tResolutionUnit, dtShort, []uint32{resPerInch}},
{tPredictor, dtShort, []uint32{pr}},
{tExtraSamples, dtShort, []uint32{extrasamples}},
- })
+ }
+ if len(colorMap) != 0 {
+ ifd = append(ifd, ifdEntry{tColorMap, dtShort, colorMap})
+ }
+
+ return writeIFD(w, imageLen+8, ifd)
}
diff --git a/tiff/writer_test.go b/tiff/writer_test.go
index 500a446..644ac49 100644
--- a/tiff/writer_test.go
+++ b/tiff/writer_test.go
@@ -17,6 +17,9 @@ var roundtripTests = []struct {
opts *Options
}{
{"video-001.tiff", nil},
+ {"video-001-gray.tiff", nil},
+ {"video-001-gray-16bit.tiff", nil},
+ {"video-001-paletted.tiff", nil},
{"bw-packbits.tiff", nil},
{"video-001.tiff", &Options{Predictor: true}},
{"video-001.tiff", &Options{Compression: Deflate}},
@@ -70,16 +73,20 @@ func TestRoundtrip2(t *testing.T) {
compare(t, m0, m1)
}
-// BenchmarkEncode benchmarks the encoding of an image.
-func BenchmarkEncode(b *testing.B) {
- img, err := openImage("video-001.tiff")
+func benchmarkEncode(b *testing.B, name string, pixelSize int) {
+ img, err := openImage(name)
if err != nil {
b.Fatal(err)
}
s := img.Bounds().Size()
- b.SetBytes(int64(s.X * s.Y * 4))
+ b.SetBytes(int64(s.X * s.Y * pixelSize))
b.ResetTimer()
for i := 0; i < b.N; i++ {
Encode(ioutil.Discard, img, nil)
}
}
+
+func BenchmarkEncode(b *testing.B) { benchmarkEncode(b, "video-001.tiff", 4) }
+func BenchmarkEncodePaletted(b *testing.B) { benchmarkEncode(b, "video-001-paletted.tiff", 1) }
+func BenchmarkEncodeGray(b *testing.B) { benchmarkEncode(b, "video-001-gray.tiff", 1) }
+func BenchmarkEncodeGray16(b *testing.B) { benchmarkEncode(b, "video-001-gray-16bit.tiff", 2) }