Commit 57964db3 authored by Bill Thiede's avatar Bill Thiede Committed by Nigel Tao

image/jpeg: encode *image.Gray as grayscale JPEGs.

Fixes #8201.

LGTM=nigeltao
R=nigeltao
CC=golang-codereviews
https://golang.org/cl/105990046
parent 705a028d
...@@ -312,32 +312,44 @@ func (e *encoder) writeDQT() { ...@@ -312,32 +312,44 @@ func (e *encoder) writeDQT() {
} }
// writeSOF0 writes the Start Of Frame (Baseline) marker. // writeSOF0 writes the Start Of Frame (Baseline) marker.
func (e *encoder) writeSOF0(size image.Point) { func (e *encoder) writeSOF0(size image.Point, nComponent int) {
const markerlen = 8 + 3*nColorComponent markerlen := 8 + 3*nComponent
e.writeMarkerHeader(sof0Marker, markerlen) e.writeMarkerHeader(sof0Marker, markerlen)
e.buf[0] = 8 // 8-bit color. e.buf[0] = 8 // 8-bit color.
e.buf[1] = uint8(size.Y >> 8) e.buf[1] = uint8(size.Y >> 8)
e.buf[2] = uint8(size.Y & 0xff) e.buf[2] = uint8(size.Y & 0xff)
e.buf[3] = uint8(size.X >> 8) e.buf[3] = uint8(size.X >> 8)
e.buf[4] = uint8(size.X & 0xff) e.buf[4] = uint8(size.X & 0xff)
e.buf[5] = nColorComponent e.buf[5] = uint8(nComponent)
for i := 0; i < nColorComponent; i++ { if nComponent == 1 {
e.buf[3*i+6] = uint8(i + 1) e.buf[6] = 1
// We use 4:2:0 chroma subsampling. // No subsampling for grayscale image.
e.buf[3*i+7] = "\x22\x11\x11"[i] e.buf[7] = 0x11
e.buf[3*i+8] = "\x00\x01\x01"[i] e.buf[8] = 0x00
} else {
for i := 0; i < nComponent; i++ {
e.buf[3*i+6] = uint8(i + 1)
// We use 4:2:0 chroma subsampling.
e.buf[3*i+7] = "\x22\x11\x11"[i]
e.buf[3*i+8] = "\x00\x01\x01"[i]
}
} }
e.write(e.buf[:3*(nColorComponent-1)+9]) e.write(e.buf[:3*(nComponent-1)+9])
} }
// writeDHT writes the Define Huffman Table marker. // writeDHT writes the Define Huffman Table marker.
func (e *encoder) writeDHT() { func (e *encoder) writeDHT(nComponent int) {
markerlen := 2 markerlen := 2
for _, s := range theHuffmanSpec { specs := theHuffmanSpec[:]
if nComponent == 1 {
// Drop the Chrominance tables.
specs = specs[:2]
}
for _, s := range specs {
markerlen += 1 + 16 + len(s.value) markerlen += 1 + 16 + len(s.value)
} }
e.writeMarkerHeader(dhtMarker, markerlen) e.writeMarkerHeader(dhtMarker, markerlen)
for i, s := range theHuffmanSpec { for i, s := range specs {
e.writeByte("\x00\x10\x01\x11"[i]) e.writeByte("\x00\x10\x01\x11"[i])
e.write(s.count[:]) e.write(s.count[:])
e.write(s.value) e.write(s.value)
...@@ -345,8 +357,8 @@ func (e *encoder) writeDHT() { ...@@ -345,8 +357,8 @@ func (e *encoder) writeDHT() {
} }
// writeBlock writes a block of pixel data using the given quantization table, // writeBlock writes a block of pixel data using the given quantization table,
// returning the post-quantized DC value of the DCT-transformed block. // returning the post-quantized DC value of the DCT-transformed block. b is in
// b is in natural (not zig-zag) order. // natural (not zig-zag) order.
func (e *encoder) writeBlock(b *block, q quantIndex, prevDC int32) int32 { func (e *encoder) writeBlock(b *block, q quantIndex, prevDC int32) int32 {
fdct(b) fdct(b)
// Emit the DC delta. // Emit the DC delta.
...@@ -390,6 +402,20 @@ func toYCbCr(m image.Image, p image.Point, yBlock, cbBlock, crBlock *block) { ...@@ -390,6 +402,20 @@ func toYCbCr(m image.Image, p image.Point, yBlock, cbBlock, crBlock *block) {
} }
} }
// grayToY stores the 8x8 region of m whose top-left corner is p in yBlock.
func grayToY(m *image.Gray, p image.Point, yBlock *block) {
b := m.Bounds()
xmax := b.Max.X - 1
ymax := b.Max.Y - 1
pix := m.Pix
for j := 0; j < 8; j++ {
for i := 0; i < 8; i++ {
idx := m.PixOffset(min(p.X+i, xmax), min(p.Y+j, ymax))
yBlock[8*j+i] = int32(pix[idx])
}
}
}
// rgbaToYCbCr is a specialized version of toYCbCr for image.RGBA images. // rgbaToYCbCr is a specialized version of toYCbCr for image.RGBA images.
func rgbaToYCbCr(m *image.RGBA, p image.Point, yBlock, cbBlock, crBlock *block) { func rgbaToYCbCr(m *image.RGBA, p image.Point, yBlock, cbBlock, crBlock *block) {
b := m.Bounds() b := m.Bounds()
...@@ -430,7 +456,18 @@ func scale(dst *block, src *[4]block) { ...@@ -430,7 +456,18 @@ func scale(dst *block, src *[4]block) {
} }
} }
// sosHeader is the SOS marker "\xff\xda" followed by 12 bytes: // sosHeaderY is the SOS marker "\xff\xda" followed by 8 bytes:
// - the marker length "\x00\x08",
// - the number of components "\x01",
// - component 1 uses DC table 0 and AC table 0 "\x01\x00",
// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
// should be 0x00, 0x3f, 0x00<<4 | 0x00.
var sosHeaderY = []byte{
0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00,
}
// sosHeaderYCbCr is the SOS marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c", // - the marker length "\x00\x0c",
// - the number of components "\x03", // - the number of components "\x03",
// - component 1 uses DC table 0 and AC table 0 "\x01\x00", // - component 1 uses DC table 0 and AC table 0 "\x01\x00",
...@@ -439,14 +476,19 @@ func scale(dst *block, src *[4]block) { ...@@ -439,14 +476,19 @@ func scale(dst *block, src *[4]block) {
// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for // - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al) // sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
// should be 0x00, 0x3f, 0x00<<4 | 0x00. // should be 0x00, 0x3f, 0x00<<4 | 0x00.
var sosHeader = []byte{ var sosHeaderYCbCr = []byte{
0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02,
0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
} }
// writeSOS writes the StartOfScan marker. // writeSOS writes the StartOfScan marker.
func (e *encoder) writeSOS(m image.Image) { func (e *encoder) writeSOS(m image.Image) {
e.write(sosHeader) switch m.(type) {
case *image.Gray:
e.write(sosHeaderY)
default:
e.write(sosHeaderYCbCr)
}
var ( var (
// Scratch buffers to hold the YCbCr values. // Scratch buffers to hold the YCbCr values.
// The blocks are in natural (not zig-zag) order. // The blocks are in natural (not zig-zag) order.
...@@ -456,24 +498,36 @@ func (e *encoder) writeSOS(m image.Image) { ...@@ -456,24 +498,36 @@ func (e *encoder) writeSOS(m image.Image) {
prevDCY, prevDCCb, prevDCCr int32 prevDCY, prevDCCb, prevDCCr int32
) )
bounds := m.Bounds() bounds := m.Bounds()
rgba, _ := m.(*image.RGBA) switch m := m.(type) {
for y := bounds.Min.Y; y < bounds.Max.Y; y += 16 { // TODO(wathiede): switch on m.ColorModel() instead of type.
for x := bounds.Min.X; x < bounds.Max.X; x += 16 { case *image.Gray:
for i := 0; i < 4; i++ { for y := bounds.Min.Y; y < bounds.Max.Y; y += 8 {
xOff := (i & 1) * 8 for x := bounds.Min.X; x < bounds.Max.X; x += 8 {
yOff := (i & 2) * 4 p := image.Pt(x, y)
p := image.Pt(x+xOff, y+yOff) grayToY(m, p, &b)
if rgba != nil {
rgbaToYCbCr(rgba, p, &b, &cb[i], &cr[i])
} else {
toYCbCr(m, p, &b, &cb[i], &cr[i])
}
prevDCY = e.writeBlock(&b, 0, prevDCY) prevDCY = e.writeBlock(&b, 0, prevDCY)
} }
scale(&b, &cb) }
prevDCCb = e.writeBlock(&b, 1, prevDCCb) default:
scale(&b, &cr) rgba, _ := m.(*image.RGBA)
prevDCCr = e.writeBlock(&b, 1, prevDCCr) for y := bounds.Min.Y; y < bounds.Max.Y; y += 16 {
for x := bounds.Min.X; x < bounds.Max.X; x += 16 {
for i := 0; i < 4; i++ {
xOff := (i & 1) * 8
yOff := (i & 2) * 4
p := image.Pt(x+xOff, y+yOff)
if rgba != nil {
rgbaToYCbCr(rgba, p, &b, &cb[i], &cr[i])
} else {
toYCbCr(m, p, &b, &cb[i], &cr[i])
}
prevDCY = e.writeBlock(&b, 0, prevDCY)
}
scale(&b, &cb)
prevDCCb = e.writeBlock(&b, 1, prevDCCb)
scale(&b, &cr)
prevDCCr = e.writeBlock(&b, 1, prevDCCr)
}
} }
} }
// Pad the last byte with 1's. // Pad the last byte with 1's.
...@@ -532,6 +586,13 @@ func Encode(w io.Writer, m image.Image, o *Options) error { ...@@ -532,6 +586,13 @@ func Encode(w io.Writer, m image.Image, o *Options) error {
e.quant[i][j] = uint8(x) e.quant[i][j] = uint8(x)
} }
} }
// Compute number of components based on input image type.
nComponent := 3
switch m.(type) {
// TODO(wathiede): switch on m.ColorModel() instead of type.
case *image.Gray:
nComponent = 1
}
// Write the Start Of Image marker. // Write the Start Of Image marker.
e.buf[0] = 0xff e.buf[0] = 0xff
e.buf[1] = 0xd8 e.buf[1] = 0xd8
...@@ -539,9 +600,9 @@ func Encode(w io.Writer, m image.Image, o *Options) error { ...@@ -539,9 +600,9 @@ func Encode(w io.Writer, m image.Image, o *Options) error {
// Write the quantization tables. // Write the quantization tables.
e.writeDQT() e.writeDQT()
// Write the image dimensions. // Write the image dimensions.
e.writeSOF0(b.Size()) e.writeSOF0(b.Size(), nComponent)
// Write the Huffman tables. // Write the Huffman tables.
e.writeDHT() e.writeDHT(nComponent)
// Write the image data. // Write the image data.
e.writeSOS(m) e.writeSOS(m)
// Write the End Of Image marker. // Write the End Of Image marker.
......
...@@ -160,6 +160,34 @@ func TestWriter(t *testing.T) { ...@@ -160,6 +160,34 @@ func TestWriter(t *testing.T) {
} }
} }
// TestWriteGrayscale tests that a grayscale images survives a round-trip
// through encode/decode cycle.
func TestWriteGrayscale(t *testing.T) {
m0 := image.NewGray(image.Rect(0, 0, 32, 32))
for i := range m0.Pix {
m0.Pix[i] = uint8(i)
}
var buf bytes.Buffer
if err := Encode(&buf, m0, nil); err != nil {
t.Fatal(err)
}
m1, err := Decode(&buf)
if err != nil {
t.Fatal(err)
}
if m0.Bounds() != m1.Bounds() {
t.Fatalf("bounds differ: %v and %v", m0.Bounds(), m1.Bounds())
}
if _, ok := m1.(*image.Gray); !ok {
t.Errorf("got %T, want *image.Gray", m1)
}
// Compare the average delta to the tolerance level.
want := int64(2 << 8)
if got := averageDelta(m0, m1); got > want {
t.Errorf("average delta too high; got %d, want <= %d", got, want)
}
}
// averageDelta returns the average delta in RGB space. The two images must // averageDelta returns the average delta in RGB space. The two images must
// have the same bounds. // have the same bounds.
func averageDelta(m0, m1 image.Image) int64 { func averageDelta(m0, m1 image.Image) int64 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment