go.image/tiff: initial support for writing TIFF images
The basic functionality works. Features to add in future CLs:
- compression
- fast paths for image formats that can be directly expressed in TIFF,
such as RGBA, NRGBA and maybe Gray and Paletted.
R=nigeltao
CC=golang-dev
https://golang.org/cl/5694051
diff --git a/tiff/consts.go b/tiff/consts.go
index 169ba27..7c458c8 100644
--- a/tiff/consts.go
+++ b/tiff/consts.go
@@ -89,6 +89,13 @@
prHorizontal = 2
)
+// Values for the tResolutionUnit tag (page 18).
+const (
+ resNone = 1
+ resPerInch = 2 // Dots per inch.
+ resPerCM = 3 // Dots per centimeter.
+)
+
// imageMode represents the mode of the image.
type imageMode int
diff --git a/tiff/writer.go b/tiff/writer.go
new file mode 100644
index 0000000..c3d71e9
--- /dev/null
+++ b/tiff/writer.go
@@ -0,0 +1,194 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+import (
+ "encoding/binary"
+ "image"
+ "io"
+ "sort"
+)
+
+// The TIFF format allows to choose the order of the different elements freely.
+// The basic structure of a TIFF file written by this package is:
+//
+// 1. Header (8 bytes).
+// 2. Image data.
+// 3. Image File Directory (IFD).
+// 4. "Pointer area" for larger entries in the IFD.
+
+// We only write little-endian TIFF files.
+var enc = binary.LittleEndian
+
+// An ifdEntry is a single entry in an Image File Directory.
+// A value of type dtRational is composed of two 32-bit values,
+// thus data contains two uints (numerator and denominator) for a single number.
+type ifdEntry struct {
+ tag int
+ datatype int
+ data []uint32
+}
+
+func (e ifdEntry) putData(p []byte) {
+ for _, d := range e.data {
+ switch e.datatype {
+ case dtByte, dtASCII:
+ p[0] = byte(d)
+ p = p[1:]
+ case dtShort:
+ enc.PutUint16(p, uint16(d))
+ p = p[2:]
+ case dtLong, dtRational:
+ enc.PutUint32(p, uint32(d))
+ p = p[4:]
+ }
+ }
+}
+
+type ifd []ifdEntry
+
+func (d ifd) Len() int {
+ return len(d)
+}
+
+func (d ifd) Less(i, j int) bool {
+ return d[i].tag < d[j].tag
+}
+
+func (d ifd) Swap(i, j int) {
+ d[i], d[j] = d[j], d[i]
+}
+
+type encoder struct {
+ ifd ifd
+ img image.Image
+ imageLen int // Length of the image in bytes.
+}
+
+func newEncoder(m image.Image) *encoder {
+ width := m.Bounds().Dx()
+ height := m.Bounds().Dy()
+ imageLen := width * height * 4
+ return &encoder{
+ img: m,
+ // For uncompressed images, imageLen is known in advance.
+ // For compressed images, we would need to write the image
+ // data in a buffer here to get its length.
+ imageLen: imageLen,
+ ifd: ifd{
+ {tImageWidth, dtShort, []uint32{uint32(width)}},
+ {tImageLength, dtShort, []uint32{uint32(height)}},
+ {tBitsPerSample, dtShort, []uint32{8, 8, 8, 8}},
+ {tCompression, dtShort, []uint32{cNone}},
+ {tPhotometricInterpretation, dtShort, []uint32{pRGB}},
+ {tStripOffsets, dtLong, []uint32{8}},
+ {tSamplesPerPixel, dtShort, []uint32{4}},
+ {tRowsPerStrip, dtShort, []uint32{uint32(height)}},
+ {tStripByteCounts, dtLong, []uint32{uint32(imageLen)}},
+ // There is currently no support for storing the image
+ // resolution, so give a bogus value of 72x72 dpi.
+ {tXResolution, dtRational, []uint32{72, 1}},
+ {tYResolution, dtRational, []uint32{72, 1}},
+ {tResolutionUnit, dtShort, []uint32{resPerInch}},
+ {tExtraSamples, dtShort, []uint32{1}}, // RGBA.
+ },
+ }
+}
+
+func (e *encoder) writeImgData(w io.Writer) error {
+ b := e.img.Bounds()
+ buf := make([]byte, 4*b.Dx())
+ for y := b.Min.Y; y < b.Max.Y; y++ {
+ i := 0
+ for x := b.Min.X; x < b.Max.X; x++ {
+ r, g, b, a := e.img.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
+ }
+ if _, err := w.Write(buf); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (e *encoder) writeIFD(w io.Writer) error {
+ var buf [ifdLen]byte
+ // Make space for "pointer area" containing IFD entry data
+ // longer than 4 bytes.
+ parea := make([]byte, 1024)
+ pstart := int(e.imageLen) + 8 + (ifdLen * len(e.ifd)) + 6
+ var o int // Current offset in parea.
+
+ // The IFD has to be written with the tags in ascending order.
+ sort.Sort(e.ifd)
+
+ // Write the number of entries in this IFD.
+ if err := binary.Write(w, enc, uint16(len(e.ifd))); err != nil {
+ return err
+ }
+ for _, ent := range e.ifd {
+ enc.PutUint16(buf[0:2], uint16(ent.tag))
+ enc.PutUint16(buf[2:4], uint16(ent.datatype))
+ count := uint32(len(ent.data))
+ if ent.datatype == dtRational {
+ count /= 2
+ }
+ enc.PutUint32(buf[4:8], count)
+ datalen := int(count * lengths[ent.datatype])
+ if datalen <= 4 {
+ ent.putData(buf[8:12])
+ } else {
+ if (o + datalen) > len(parea) {
+ newlen := len(parea) + 1024
+ for (o + datalen) > newlen {
+ newlen += 1024
+ }
+ newarea := make([]byte, newlen)
+ copy(newarea, parea)
+ parea = newarea
+ }
+ ent.putData(parea[o : o+datalen])
+ enc.PutUint32(buf[8:12], uint32(pstart+o))
+ o += datalen
+ }
+ if _, err := w.Write(buf[:]); err != nil {
+ return err
+ }
+ }
+ // The IFD ends with the offset of the next IFD in the file,
+ // or zero if it is the last one (page 14).
+ if err := binary.Write(w, enc, uint32(0)); err != nil {
+ return err
+ }
+ _, err := w.Write(parea[:o])
+ return err
+}
+
+func (e *encoder) encode(w io.Writer) error {
+ _, err := io.WriteString(w, leHeader)
+ if err != nil {
+ return err
+ }
+
+ ifdOffset := e.imageLen + 8 // 8 bytes for TIFF header.
+ err = binary.Write(w, enc, uint32(ifdOffset))
+ if err != nil {
+ return err
+ }
+ err = e.writeImgData(w)
+ if err != nil {
+ return err
+ }
+ return e.writeIFD(w)
+}
+
+// Encode writes the image m to w in uncompressed RGBA format.
+func Encode(w io.Writer, m image.Image) error {
+ return newEncoder(m).encode(w)
+}
diff --git a/tiff/writer_test.go b/tiff/writer_test.go
new file mode 100644
index 0000000..1fc2331
--- /dev/null
+++ b/tiff/writer_test.go
@@ -0,0 +1,42 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tiff
+
+import (
+ "bytes"
+ "os"
+ "testing"
+)
+
+var roundtripTests = []string{
+ "video-001.tiff",
+ "bw-packbits.tiff",
+}
+
+func TestRoundtrip(t *testing.T) {
+ for _, filename := range roundtripTests {
+ f, err := os.Open(testdataDir + filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ img, err := Decode(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out := new(bytes.Buffer)
+ err = Encode(out, img)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ img2, err := Decode(&buffer{buf: out.Bytes()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ compare(t, img, img2)
+ }
+}