diff --git a/testdata/gopherhat.tiff b/testdata/gopherhat.tiff new file mode 100644 index 00000000..fd578a60 Binary files /dev/null and b/testdata/gopherhat.tiff differ diff --git a/tiff/consts.go b/tiff/consts.go index 3e5f7f14..f6ca0efd 100644 --- a/tiff/consts.go +++ b/tiff/consts.go @@ -60,6 +60,7 @@ const ( tXResolution = 282 tYResolution = 283 tResolutionUnit = 296 + tPageNumber = 297 tPredictor = 317 tColorMap = 320 diff --git a/tiff/reader.go b/tiff/reader.go index de73f4b9..48155fd7 100644 --- a/tiff/reader.go +++ b/tiff/reader.go @@ -21,6 +21,32 @@ import ( "golang.org/x/image/tiff/lzw" ) +// MeasurementResolutionUnit is the units of measure for resolution +type MeasurementResolutionUnit int + +const ( + // None is no absolute unit of measurement. + None MeasurementResolutionUnit = 1 + // Inch unit of measurement. + Inch MeasurementResolutionUnit = 2 + // Centimeter unit of measurement. + Centimeter MeasurementResolutionUnit = 3 +) + +// An Info is container for image.Config, a TIFF iamge resolution and the number of the page a TIFF image. +type Info struct { + image.Config + // XResolution is the number of pixels per ResolutionUnit in the image.Config.Width direction. + XResolution int + // YResolution is the number of pixels per ResolutionUnit in the image.Config.Width direction. + YResolution int + // UnitResolution is the unit of measurement for XResolution and YResolution. + UnitResolution MeasurementResolutionUnit + //PageCount is the number of the page from which a TIFF image was scanned. + // If PageCount is 0, the total number of pages in the document is not available. + PageCount int +} + // A FormatError reports that the input is not a valid TIFF image. type FormatError string @@ -39,9 +65,14 @@ func (e UnsupportedError) Error() string { var errNoPixels = FormatError("not enough pixel data") type decoder struct { - r io.ReaderAt - byteOrder binary.ByteOrder - config image.Config + r io.ReaderAt + byteOrder binary.ByteOrder + config image.Config + resolution struct { + x, y int + unit MeasurementResolutionUnit + } + pageCount int mode imageMode bpp uint features map[int][]uint @@ -63,6 +94,15 @@ func (d *decoder) firstVal(tag int) uint { return f[0] } +func (d *decoder) secondVal(tag int) uint { + f := d.features[tag] + if len(f) < 2 { + return 0 + } + + return f[1] +} + // ifdUint decodes the IFD entry in p, which must be of the Byte, Short // or Long type, and returns the decoded uint values. func (d *decoder) ifdUint(p []byte) (u []uint, err error) { @@ -101,7 +141,7 @@ func (d *decoder) ifdUint(p []byte) (u []uint, err error) { for i := uint32(0); i < count; i++ { u[i] = uint(d.byteOrder.Uint16(raw[2*i : 2*(i+1)])) } - case dtLong: + case dtLong, dtRational: for i := uint32(0); i < count; i++ { u[i] = uint(d.byteOrder.Uint32(raw[4*i : 4*(i+1)])) } @@ -131,6 +171,10 @@ func (d *decoder) parseIFD(p []byte) (int, error) { tTileByteCounts, tImageLength, tImageWidth, + tXResolution, + tYResolution, + tResolutionUnit, + tPageNumber, tFillOrder, tT4Options, tT6Options: @@ -447,6 +491,20 @@ func newDecoder(r io.Reader) (*decoder, error) { d.config.Width = int(d.firstVal(tImageWidth)) d.config.Height = int(d.firstVal(tImageLength)) + d.resolution.x = int(d.firstVal(tXResolution)) + d.resolution.y = int(d.firstVal(tYResolution)) + + switch int(d.firstVal(tResolutionUnit)) { + case 2: + d.resolution.unit = Inch + case 3: + d.resolution.unit = Centimeter + default: + d.resolution.unit = None + } + + d.pageCount = int(d.secondVal(tPageNumber)) + if _, ok := d.features[tBitsPerSample]; !ok { // Default is 1 per specification. d.features[tBitsPerSample] = []uint{1} @@ -547,6 +605,17 @@ func DecodeConfig(r io.Reader) (image.Config, error) { return d.config, nil } +// DecodeTIFFInfo returns the page count and resolution of a TIFF image without +// decoding the entire image +func DecodeTIFFInfo(r io.Reader) (Info, error) { + d, err := newDecoder(r) + if err != nil { + return Info{}, err + } + + return Info{d.config, d.resolution.x, d.resolution.y, d.resolution.unit, d.pageCount}, nil +} + func ccittFillOrder(tiffFillOrder uint) ccitt.Order { if tiffFillOrder == 2 { return ccitt.LSB diff --git a/tiff/reader_test.go b/tiff/reader_test.go index f91fd94f..43466b4b 100644 --- a/tiff/reader_test.go +++ b/tiff/reader_test.go @@ -411,6 +411,36 @@ func TestLargeIFDEntry(t *testing.T) { } } +// TestGetResolution tests decoding an image that has XResolution, YResolution and PageNumber tags. +// Issue 20742 +func TestGetResolution(t *testing.T) { + r, err := ioutil.ReadFile(testdataDir + "gopherhat.tiff") + if err != nil { + t.Fatal(err) + } + + info, err := DecodeTIFFInfo(bytes.NewReader(r)) + if err != nil { + t.Fatal(err) + } + + if info.XResolution != 204 { + t.Fatalf("X resolution must be 204 but %d", info.XResolution) + } + + if info.YResolution != 192 { + t.Fatalf("Y resolution must be 192 but %d", info.YResolution) + } + + if info.PageCount != 1 { + t.Fatalf("PageCount must be 1 but %d", info.PageCount) + } + + if info.UnitResolution != Inch { + t.Fatalf("UnitResolution must be Inch but %d", info.UnitResolution) + } +} + // benchmarkDecode benchmarks the decoding of an image. func benchmarkDecode(b *testing.B, filename string) { b.Helper()