ccitt: implement NewReader and DecodeIntoGray The testdata/bw-gopher.png image was derived from testdata/bw-uncompressed.tiff The testdata/*ccitt* data was generated from a custom CCITT encoder: https://go-review.googlesource.com/c/image/+/174139/10/ccitt/writer.go Updates golang/go#19443 Change-Id: I53b7acd807aa9a627517ede5bc3316d6f1fe4724 Reviewed-on: https://go-review.googlesource.com/c/image/+/182219 Reviewed-by: Benny Siegert <bsiegert@gmail.com>
diff --git a/ccitt/reader.go b/ccitt/reader.go index ef353dc..04d0302 100644 --- a/ccitt/reader.go +++ b/ccitt/reader.go
@@ -10,12 +10,22 @@ import ( "encoding/binary" "errors" + "image" "io" "math/bits" ) var ( - errInvalidCode = errors.New("ccitt: invalid code") + errInvalidBounds = errors.New("ccitt: invalid bounds") + errInvalidCode = errors.New("ccitt: invalid code") + errInvalidMode = errors.New("ccitt: invalid mode") + errInvalidOffset = errors.New("ccitt: invalid offset") + errMissingEOL = errors.New("ccitt: missing End-of-Line") + errRunLengthOverflowsWidth = errors.New("ccitt: run length overflows width") + errRunLengthTooLong = errors.New("ccitt: run length too long") + errUnsupportedMode = errors.New("ccitt: unsupported mode") + errUnsupportedSubFormat = errors.New("ccitt: unsupported sub-format") + errUnsupportedWidth = errors.New("ccitt: unsupported width") ) // Order specifies the bit ordering in a CCITT data stream. @@ -28,6 +38,35 @@ MSB ) +// SubFormat represents that the CCITT format consists of a number of +// sub-formats. Decoding or encoding a CCITT data stream requires knowing the +// sub-format context. It is not represented in the data stream per se. +type SubFormat uint32 + +const ( + Group3 SubFormat = iota + Group4 +) + +// Options are optional parameters. +type Options struct { + // Align means that some variable-bit-width codes are byte-aligned. + Align bool + // Invert means that black is the 1 bit or 0xFF byte, and white is 0. + Invert bool +} + +// maxWidth is the maximum (inclusive) supported width. This is a limitation of +// this implementation, to guard against integer overflow, and not anything +// inherent to the CCITT format. +const maxWidth = 1 << 20 + +func invertBytes(b []byte) { + for i, c := range b { + b[i] = ^c + } +} + type bitReader struct { r io.Reader @@ -36,6 +75,7 @@ // process the n > 0 bytes returned before considering the error err". readErr error + // order is whether to process r's bytes LSB first or MSB first. order Order // The low nBits bits of the bits field hold upcoming bits in LSB order. @@ -128,3 +168,493 @@ } } } + +type reader struct { + br bitReader + subFormat SubFormat + + // width is the image width in pixels. + width int + + // rowsRemaining starts at the image height in pixels, when the reader is + // driven through the io.Reader interface, and decrements to zero as rows + // are decoded. When driven through DecodeIntoGray, this field is unused. + rowsRemaining int + + // curr and prev hold the current and previous rows. Each element is either + // 0x00 (black) or 0xFF (white). + // + // prev may be nil, when processing the first row. + curr []byte + prev []byte + + // ri is the read index. curr[:ri] are those bytes of curr that have been + // passed along via the Read method. + // + // When the reader is driven through DecodeIntoGray, instead of through the + // io.Reader interface, this field is unused. + ri int + + // wi is the write index. curr[:wi] are those bytes of curr that have + // already been decoded via the decodeRow method. + // + // What this implementation calls wi is roughly equivalent to what the spec + // calls the a0 index. + wi int + + // These fields are copied from the *Options (which may be nil). + align bool + invert bool + + // atStartOfRow is whether we have just started the row. Some parts of the + // spec say to treat this situation as if "wi = -1". + atStartOfRow bool + + // penColorIsWhite is whether the next run is black or white. + penColorIsWhite bool + + // seenStartOfImage is whether we've called the startDecode method. + seenStartOfImage bool + + // readErr is a sticky error for the Read method. + readErr error +} + +func (z *reader) Read(p []byte) (int, error) { + if z.readErr != nil { + return 0, z.readErr + } + originalP := p + + for len(p) > 0 { + // Allocate buffers (and decode any start-of-image codes), if + // processing the first or second row. + if z.curr == nil { + if !z.seenStartOfImage { + if z.readErr = z.startDecode(); z.readErr != nil { + break + } + z.atStartOfRow = true + } + z.curr = make([]byte, z.width) + } + + // Decode the next row, if necessary. + if z.atStartOfRow { + if z.rowsRemaining <= 0 { + if z.readErr = z.finishDecode(); z.readErr != nil { + break + } + z.readErr = io.EOF + break + } + if z.readErr = z.decodeRow(); z.readErr != nil { + break + } + z.rowsRemaining-- + } + + // Pack from z.curr (1 byte per pixel) to p (1 bit per pixel), up to 8 + // elements per iteration. + i := 0 + for ; i < len(p); i++ { + numToPack := len(z.curr) - z.ri + if numToPack <= 0 { + break + } else if numToPack > 8 { + numToPack = 8 + } + + byteValue := byte(0) + for j := 0; j < numToPack; j++ { + byteValue |= (z.curr[z.ri] & 0x80) >> uint(j) + z.ri++ + } + p[i] = byteValue + } + p = p[i:] + + // Prepare to decode the next row, if necessary. + if z.ri == len(z.curr) { + z.ri, z.curr, z.prev = 0, z.prev, z.curr + z.atStartOfRow = true + } + } + + n := len(originalP) - len(p) + // TODO: when invert is true, should the end-of-row padding bits be 0 or 1? + if z.invert { + invertBytes(originalP[:n]) + } + return n, z.readErr +} + +func (z *reader) penColor() byte { + if z.penColorIsWhite { + return 0xFF + } + return 0x00 +} + +func (z *reader) startDecode() error { + switch z.subFormat { + case Group3: + if err := z.decodeEOL(); err != nil { + return err + } + + case Group4: + // No-op. + + default: + return errUnsupportedSubFormat + } + + z.seenStartOfImage = true + return nil +} + +func (z *reader) finishDecode() error { + numberOfEOLs := 0 + switch z.subFormat { + case Group3: + // The stream ends with a RTC (Return To Control) of 6 consecutive + // EOL's, but we should have already just seen an EOL, either in + // z.startDecode (for a zero-height image) or in z.decodeRow. + numberOfEOLs = 5 + + case Group4: + // The stream ends with two EOL's, the first of which is possibly + // byte-aligned. + numberOfEOLs = 2 + if err := z.decodeEOL(); err == nil { + numberOfEOLs-- + } else if err == errInvalidCode { + // Try again, this time starting from a byte boundary. + z.br.alignToByteBoundary() + } else { + return err + } + + default: + return errUnsupportedSubFormat + } + + for ; numberOfEOLs > 0; numberOfEOLs-- { + if err := z.decodeEOL(); err != nil { + return err + } + } + return nil +} + +func (z *reader) decodeEOL() error { + // TODO: EOL doesn't have to be in the modeDecodeTable. It could be in its + // own table, or we could just hard-code it, especially if we might need to + // cater for optional byte-alignment, or an arbitrary number (potentially + // more than 8) of 0-valued padding bits. + if mode, err := decode(&z.br, modeDecodeTable[:]); err != nil { + return err + } else if mode != modeEOL { + return errMissingEOL + } + return nil +} + +func (z *reader) decodeRow() error { + z.wi = 0 + z.atStartOfRow = true + z.penColorIsWhite = true + + switch z.subFormat { + case Group3: + for ; z.wi < len(z.curr); z.atStartOfRow = false { + if err := z.decodeRun(); err != nil { + return err + } + } + return z.decodeEOL() + + case Group4: + if z.align { + z.br.alignToByteBoundary() + } + + for ; z.wi < len(z.curr); z.atStartOfRow = false { + mode, err := decode(&z.br, modeDecodeTable[:]) + if err != nil { + return err + } + rm := readerMode{} + if mode < uint32(len(readerModes)) { + rm = readerModes[mode] + } + if rm.function == nil { + return errInvalidMode + } + if err := rm.function(z, rm.arg); err != nil { + return err + } + } + return nil + } + + return errUnsupportedSubFormat +} + +func (z *reader) decodeRun() error { + table := blackDecodeTable[:] + if z.penColorIsWhite { + table = whiteDecodeTable[:] + } + + total := 0 + for { + n, err := decode(&z.br, table) + if err != nil { + return err + } + if n > maxWidth { + panic("unreachable") + } + total += int(n) + if total > maxWidth { + return errRunLengthTooLong + } + // Anything 0x3F or below is a terminal code. + if n <= 0x3F { + break + } + } + + if total > (len(z.curr) - z.wi) { + return errRunLengthOverflowsWidth + } + dst := z.curr[z.wi : z.wi+total] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi += total + z.penColorIsWhite = !z.penColorIsWhite + + return nil +} + +// The various modes' semantics are based on determining a row of pixels' +// "changing elements": those pixels whose color differs from the one on its +// immediate left. +// +// The row above the first row is implicitly all white. Similarly, the column +// to the left of the first column is implicitly all white. +// +// For example, here's Figure 1 in "ITU-T Recommendation T.6", where the +// current and previous rows contain black (B) and white (w) pixels. The a? +// indexes point into curr, the b? indexes point into prev. +// +// b1 b2 +// v v +// prev: BBBBBwwwwwBBBwwwww +// curr: BBBwwwwwBBBBBBwwww +// ^ ^ ^ +// a0 a1 a2 +// +// a0 is the "reference element" or current decoder position, roughly +// equivalent to what this implementation calls reader.wi. +// +// a1 is the next changing element to the right of a0, on the "coding line" +// (the current row). +// +// a2 is the next changing element to the right of a1, again on curr. +// +// b1 is the first changing element on the "reference line" (the previous row) +// to the right of a0 and of opposite color to a0. +// +// b2 is the next changing element to the right of b1, again on prev. +// +// The various modes calculate a1 (and a2, for modeH): +// - modePass calculates that a1 is at or to the right of b2. +// - modeH calculates a1 and a2 without considering b1 or b2. +// - modeV* calculates a1 to be b1 plus an adjustment (between -3 and +3). + +const ( + findB1 = false + findB2 = true +) + +// findB finds either the b1 or b2 value. +func (z *reader) findB(whichB bool) int { + // The initial row is a special case. The previous row is implicitly all + // white, so that there are no changing pixel elements. We return b1 or b2 + // to be at the end of the row. + if len(z.prev) != len(z.curr) { + return len(z.curr) + } + + i := z.wi + + if z.atStartOfRow { + // a0 is implicitly at -1, on a white pixel. b1 is the first black + // pixel in the previous row. b2 is the first white pixel after that. + for ; (i < len(z.prev)) && (z.prev[i] == 0xFF); i++ { + } + if whichB == findB2 { + for ; (i < len(z.prev)) && (z.prev[i] == 0x00); i++ { + } + } + return i + } + + // As per figure 1 above, assume that the current pen color is white. + // First, walk past every contiguous black pixel in prev, starting at a0. + oppositeColor := ^z.penColor() + for ; (i < len(z.prev)) && (z.prev[i] == oppositeColor); i++ { + } + + // Then walk past every contiguous white pixel. + penColor := ^oppositeColor + for ; (i < len(z.prev)) && (z.prev[i] == penColor); i++ { + } + + // We're now at a black pixel (or at the end of the row). That's b1. + if whichB == findB2 { + // If we're looking for b2, walk past every contiguous black pixel + // again. + oppositeColor := ^penColor + for ; (i < len(z.prev)) && (z.prev[i] == oppositeColor); i++ { + } + } + + return i +} + +type readerMode struct { + function func(z *reader, arg int) error + arg int +} + +var readerModes = [...]readerMode{ + modePass: {function: readerModePass}, + modeH: {function: readerModeH}, + modeV0: {function: readerModeV, arg: +0}, + modeVR1: {function: readerModeV, arg: +1}, + modeVR2: {function: readerModeV, arg: +2}, + modeVR3: {function: readerModeV, arg: +3}, + modeVL1: {function: readerModeV, arg: -1}, + modeVL2: {function: readerModeV, arg: -2}, + modeVL3: {function: readerModeV, arg: -3}, + modeExt: {function: readerModeExt}, +} + +func readerModePass(z *reader, arg int) error { + b2 := z.findB(findB2) + if (b2 < z.wi) || (len(z.curr) < b2) { + return errInvalidOffset + } + dst := z.curr[z.wi:b2] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi = b2 + return nil +} + +func readerModeH(z *reader, arg int) error { + // The first iteration finds a1. The second finds a2. + for i := 0; i < 2; i++ { + if err := z.decodeRun(); err != nil { + return err + } + } + return nil +} + +func readerModeV(z *reader, arg int) error { + a1 := z.findB(findB1) + arg + if (a1 < z.wi) || (len(z.curr) < a1) { + return errInvalidOffset + } + dst := z.curr[z.wi:a1] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi = a1 + z.penColorIsWhite = !z.penColorIsWhite + return nil +} + +func readerModeExt(z *reader, arg int) error { + return errUnsupportedMode +} + +// DecodeIntoGray decodes the CCITT-formatted data in r into dst. +// +// It returns an error if dst's width and height don't match the implied width +// and height of CCITT-formatted data. +func DecodeIntoGray(dst *image.Gray, r io.Reader, order Order, sf SubFormat, opts *Options) error { + bounds := dst.Bounds() + if (bounds.Dx() < 0) || (bounds.Dy() < 0) { + return errInvalidBounds + } + if bounds.Dx() > maxWidth { + return errUnsupportedWidth + } + + z := reader{ + br: bitReader{r: r, order: order}, + subFormat: sf, + align: (opts != nil) && opts.Align, + invert: (opts != nil) && opts.Invert, + width: bounds.Dx(), + } + if err := z.startDecode(); err != nil { + return err + } + + width := bounds.Dx() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + p := (y - bounds.Min.Y) * dst.Stride + z.curr = dst.Pix[p : p+width] + if err := z.decodeRow(); err != nil { + return err + } + z.curr, z.prev = nil, z.curr + } + + if err := z.finishDecode(); err != nil { + return err + } + + if z.invert { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + p := (y - bounds.Min.Y) * dst.Stride + invertBytes(dst.Pix[p : p+width]) + } + } + + return nil +} + +// NewReader returns an io.Reader that decodes the CCITT-formatted data in r. +// The resultant byte stream is one bit per pixel (MSB first), with 1 meaning +// white and 0 meaning black. Each row in the result is byte-aligned. +func NewReader(r io.Reader, order Order, sf SubFormat, width int, height int, opts *Options) io.Reader { + readErr := error(nil) + if (width < 0) || (height < 0) { + readErr = errInvalidBounds + } else if width > maxWidth { + readErr = errUnsupportedWidth + } + + return &reader{ + br: bitReader{r: r, order: order}, + subFormat: sf, + align: (opts != nil) && opts.Align, + invert: (opts != nil) && opts.Invert, + width: width, + rowsRemaining: height, + readErr: readErr, + } +}
diff --git a/ccitt/reader_test.go b/ccitt/reader_test.go index a1b9451..c582f16 100644 --- a/ccitt/reader_test.go +++ b/ccitt/reader_test.go
@@ -6,12 +6,47 @@ import ( "bytes" + "fmt" + "image" + "image/png" "io" + "io/ioutil" + "os" + "path/filepath" "reflect" "testing" "unsafe" ) +func compareImages(t *testing.T, img0 image.Image, img1 image.Image) { + t.Helper() + + b0 := img0.Bounds() + b1 := img1.Bounds() + if b0 != b1 { + t.Fatalf("bounds differ: %v vs %v", b0, b1) + } + + for y := b0.Min.Y; y < b0.Max.Y; y++ { + for x := b0.Min.X; x < b0.Max.X; x++ { + c0 := img0.At(x, y) + c1 := img1.At(x, y) + if c0 != c1 { + t.Fatalf("pixel at (%d, %d) differs: %v vs %v", x, y, c0, c1) + } + } + } +} + +func decodePNG(fileName string) (image.Image, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + return png.Decode(f) +} + func TestMaxCodeLength(t *testing.T) { br := bitReader{} size := unsafe.Sizeof(br.bits) @@ -44,7 +79,7 @@ m[code.val] = code.str } - // Build the encoded form of those values. + // Build the encoded form of those values in LSB order. enc := []byte(nil) bits := uint8(0) nBits := uint32(0) @@ -163,4 +198,134 @@ } } -// TODO: more tests. +func TestReadRegular(t *testing.T) { testRead(t, false) } +func TestReadInvert(t *testing.T) { testRead(t, true) } + +func testRead(t *testing.T, invert bool) { + t.Helper() + + const width, height = 153, 55 + opts := &Options{ + Invert: invert, + } + + got := "" + { + f, err := os.Open("testdata/bw-gopher.ccitt_group3") + if err != nil { + t.Fatalf("Open: %v", err) + } + defer f.Close() + gotBytes, err := ioutil.ReadAll(NewReader(f, MSB, Group3, width, height, opts)) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + got = string(gotBytes) + } + + want := "" + { + img, err := decodePNG("testdata/bw-gopher.png") + if err != nil { + t.Fatalf("decodePNG: %v", err) + } + gray, ok := img.(*image.Gray) + if !ok { + t.Fatalf("decodePNG: got %T, want *image.Gray", img) + } + bounds := gray.Bounds() + if w := bounds.Dx(); w != width { + t.Fatalf("width: got %d, want %d", w, width) + } + if h := bounds.Dy(); h != height { + t.Fatalf("height: got %d, want %d", h, height) + } + + // Prepare to extend each row's width to a multiple of 8, to simplify + // packing from 1 byte per pixel to 1 bit per pixel. + extended := make([]byte, (width+7)&^7) + + wantBytes := []byte(nil) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + rowPix := gray.Pix[(y-bounds.Min.Y)*gray.Stride:] + rowPix = rowPix[:width] + copy(extended, rowPix) + + // Pack from 1 byte per pixel to 1 bit per pixel, MSB first. + byteValue := uint8(0) + for x, pixel := range extended { + byteValue |= (pixel & 0x80) >> uint(x&7) + if (x & 7) == 7 { + wantBytes = append(wantBytes, byteValue) + byteValue = 0 + } + } + } + if invert { + invertBytes(wantBytes) + } + want = string(wantBytes) + } + + // We expect a width of 153 pixels, which is 20 bytes per row (at 1 bit per + // pixel, plus 7 final bits of padding). Check that want is 20 * height + // bytes long, and if got != want, format them to split at every 20 bytes. + + if n := len(want); n != 20*height { + t.Fatalf("len(want): got %d, want %d", n, 20*height) + } + + format := func(s string) string { + b := []byte(nil) + for row := 0; len(s) >= 20; row++ { + b = append(b, fmt.Sprintf("row%02d: %02X\n", row, s[:20])...) + s = s[20:] + } + if len(s) > 0 { + b = append(b, fmt.Sprintf("%02X\n", s)...) + } + return string(b) + } + + if got != want { + t.Fatalf("got:\n%s\nwant:\n%s", format(got), format(want)) + } +} + +func TestDecodeIntoGray(t *testing.T) { + for _, tt := range []struct { + fileName string + sf SubFormat + w, h int + }{ + {"testdata/bw-gopher.ccitt_group3", Group3, 153, 55}, + {"testdata/bw-gopher.ccitt_group4", Group4, 153, 55}, + } { + t.Run(tt.fileName, func(t *testing.T) { + testDecodeIntoGray(t, tt.fileName, MSB, tt.sf, tt.w, tt.h, nil) + }) + } +} + +func testDecodeIntoGray(t *testing.T, fileName string, order Order, sf SubFormat, width int, height int, opts *Options) { + t.Helper() + + f, err := os.Open(filepath.FromSlash(fileName)) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer f.Close() + + got := image.NewGray(image.Rect(0, 0, width, height)) + if err := DecodeIntoGray(got, f, order, sf, opts); err != nil { + t.Fatalf("DecodeIntoGray: %v", err) + } + + baseName := fileName[:len(fileName)-len(filepath.Ext(fileName))] + want, err := decodePNG(baseName + ".png") + if err != nil { + t.Fatal(err) + } + + compareImages(t, got, want) +}
diff --git a/ccitt/testdata/bw-gopher.ccitt_group3 b/ccitt/testdata/bw-gopher.ccitt_group3 new file mode 100644 index 0000000..a461904 --- /dev/null +++ b/ccitt/testdata/bw-gopher.ccitt_group3 Binary files differ
diff --git a/ccitt/testdata/bw-gopher.ccitt_group4 b/ccitt/testdata/bw-gopher.ccitt_group4 new file mode 100644 index 0000000..05757a7 --- /dev/null +++ b/ccitt/testdata/bw-gopher.ccitt_group4 Binary files differ
diff --git a/ccitt/testdata/bw-gopher.png b/ccitt/testdata/bw-gopher.png new file mode 100644 index 0000000..6e8e957 --- /dev/null +++ b/ccitt/testdata/bw-gopher.png Binary files differ