git.lucas.co / go_mono
git clone https://git.lucas.co/go_mono.git

commite39b2394e57209d375d2483054624219fee70a17
parent864256f267
authorChaiShushan <[email protected]>
date2013-09-13 17:42
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) }