From f594c3aad520f2865134372911c793e00b40c14b Mon Sep 17 00:00:00 2001 From: Benny Siegert Date: Mon, 5 Mar 2012 11:45:47 +1100 Subject: [PATCH] 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 --- tiff/consts.go | 7 ++ tiff/writer.go | 194 ++++++++++++++++++++++++++++++++++++++++++++ tiff/writer_test.go | 42 ++++++++++ 3 files changed, 243 insertions(+) create mode 100644 tiff/writer.go create mode 100644 tiff/writer_test.go 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 @@ const ( 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) + } +}