]> Cypherpunks repositories - gostls13.git/commitdiff
image/png: pack image data for small bitdepth paletted images
authorIan Davis <nospam@iandavis.com>
Sat, 8 Sep 2018 16:00:00 +0000 (17:00 +0100)
committerNigel Tao <nigeltao@golang.org>
Tue, 25 Sep 2018 07:24:39 +0000 (07:24 +0000)
Bit packs image data when writing images with fewer than 16
colors in its palette. Reading of bit packed image data was
already implemented.

Fixes #19879

Change-Id: I0a06f9599a163931e20d3503fc3722e5101f0070
Reviewed-on: https://go-review.googlesource.com/134235
Reviewed-by: Nigel Tao <nigeltao@golang.org>
src/image/png/writer.go
src/image/png/writer_test.go

index de8c28e9196840a228a9e34917dad5ee03570779..c03335120eb273c813fab767a6ff506603558df2 100644 (file)
@@ -137,6 +137,15 @@ func (e *encoder) writeIHDR() {
        case cbP8:
                e.tmp[8] = 8
                e.tmp[9] = ctPaletted
+       case cbP4:
+               e.tmp[8] = 4
+               e.tmp[9] = ctPaletted
+       case cbP2:
+               e.tmp[8] = 2
+               e.tmp[9] = ctPaletted
+       case cbP1:
+               e.tmp[8] = 1
+               e.tmp[9] = ctPaletted
        case cbTCA8:
                e.tmp[8] = 8
                e.tmp[9] = ctTrueColorAlpha
@@ -305,31 +314,38 @@ func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) erro
        }
        defer e.zw.Close()
 
-       bpp := 0 // Bytes per pixel.
+       bitsPerPixel := 0
 
        switch cb {
        case cbG8:
-               bpp = 1
+               bitsPerPixel = 8
        case cbTC8:
-               bpp = 3
+               bitsPerPixel = 24
        case cbP8:
-               bpp = 1
+               bitsPerPixel = 8
+       case cbP4:
+               bitsPerPixel = 4
+       case cbP2:
+               bitsPerPixel = 2
+       case cbP1:
+               bitsPerPixel = 1
        case cbTCA8:
-               bpp = 4
+               bitsPerPixel = 32
        case cbTC16:
-               bpp = 6
+               bitsPerPixel = 48
        case cbTCA16:
-               bpp = 8
+               bitsPerPixel = 64
        case cbG16:
-               bpp = 2
+               bitsPerPixel = 16
        }
+
        // cr[*] and pr are the bytes for the current and previous row.
        // cr[0] is unfiltered (or equivalently, filtered with the ftNone filter).
        // cr[ft], for non-zero filter types ft, are buffers for transforming cr[0] under the
        // other PNG filter types. These buffers are allocated once and re-used for each row.
        // The +1 is for the per-row filter type, which is at cr[*][0].
        b := m.Bounds()
-       sz := 1 + bpp*b.Dx()
+       sz := 1 + (bitsPerPixel*b.Dx()+7)/8
        for i := range e.cr {
                if cap(e.cr[i]) < sz {
                        e.cr[i] = make([]uint8, sz)
@@ -405,6 +421,30 @@ func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) erro
                                        i += 1
                                }
                        }
+
+               case cbP4, cbP2, cbP1:
+                       pi := m.(image.PalettedImage)
+
+                       var a uint8
+                       var c int
+                       for x := b.Min.X; x < b.Max.X; x++ {
+                               a = a<<uint(bitsPerPixel) | pi.ColorIndexAt(x, y)
+                               c++
+                               if c == 8/bitsPerPixel {
+                                       cr[0][i] = a
+                                       i += 1
+                                       a = 0
+                                       c = 0
+                               }
+                       }
+                       if c != 0 {
+                               for c != 8/bitsPerPixel {
+                                       a = a << uint(bitsPerPixel)
+                                       c++
+                               }
+                               cr[0][i] = a
+                       }
+
                case cbTCA8:
                        if nrgba != nil {
                                offset := (y - b.Min.Y) * nrgba.Stride
@@ -460,7 +500,10 @@ func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) erro
                // "filters are rarely useful on palette images" and will result
                // in larger files (see http://www.libpng.org/pub/png/book/chapter09.html).
                f := ftNone
-               if level != zlib.NoCompression && cb != cbP8 {
+               if level != zlib.NoCompression && cb != cbP8 && cb != cbP4 && cb != cbP2 && cb != cbP1 {
+                       // Since we skip paletted images we don't have to worry about
+                       // bitsPerPixel not being a multiple of 8
+                       bpp := bitsPerPixel / 8
                        f = filter(&cr, pr, bpp)
                }
 
@@ -551,7 +594,15 @@ func (enc *Encoder) Encode(w io.Writer, m image.Image) error {
                pal, _ = m.ColorModel().(color.Palette)
        }
        if pal != nil {
-               e.cb = cbP8
+               if len(pal) <= 2 {
+                       e.cb = cbP1
+               } else if len(pal) <= 4 {
+                       e.cb = cbP2
+               } else if len(pal) <= 16 {
+                       e.cb = cbP4
+               } else {
+                       e.cb = cbP8
+               }
        } else {
                switch m.ColorModel() {
                case color.GrayModel:
index 6c5e942310ecd628f9d251059ba4b2414d1254f2..5d131ff823aab1b4176695b15c72912002962c09 100644 (file)
@@ -6,9 +6,12 @@ package png
 
 import (
        "bytes"
+       "compress/zlib"
+       "encoding/binary"
        "fmt"
        "image"
        "image/color"
+       "io"
        "io/ioutil"
        "testing"
 )
@@ -77,6 +80,111 @@ func TestWriter(t *testing.T) {
        }
 }
 
+func TestWriterPaletted(t *testing.T) {
+       const width, height = 32, 16
+
+       testCases := []struct {
+               plen     int
+               bitdepth uint8
+               datalen  int
+       }{
+
+               {
+                       plen:     256,
+                       bitdepth: 8,
+                       datalen:  (1 + width) * height,
+               },
+
+               {
+                       plen:     128,
+                       bitdepth: 8,
+                       datalen:  (1 + width) * height,
+               },
+
+               {
+                       plen:     16,
+                       bitdepth: 4,
+                       datalen:  (1 + width/2) * height,
+               },
+
+               {
+                       plen:     4,
+                       bitdepth: 2,
+                       datalen:  (1 + width/4) * height,
+               },
+
+               {
+                       plen:     2,
+                       bitdepth: 1,
+                       datalen:  (1 + width/8) * height,
+               },
+       }
+
+       for _, tc := range testCases {
+               t.Run(fmt.Sprintf("plen-%d", tc.plen), func(t *testing.T) {
+                       // Create a paletted image with the correct palette length
+                       palette := make(color.Palette, tc.plen)
+                       for i := range palette {
+                               palette[i] = color.NRGBA{
+                                       R: uint8(i),
+                                       G: uint8(i),
+                                       B: uint8(i),
+                                       A: 255,
+                               }
+                       }
+                       m0 := image.NewPaletted(image.Rect(0, 0, width, height), palette)
+
+                       i := 0
+                       for y := 0; y < height; y++ {
+                               for x := 0; x < width; x++ {
+                                       m0.SetColorIndex(x, y, uint8(i%tc.plen))
+                                       i++
+                               }
+                       }
+
+                       // Encode the image
+                       var b bytes.Buffer
+                       if err := Encode(&b, m0); err != nil {
+                               t.Error(err)
+                               return
+                       }
+                       const chunkFieldsLength = 12 // 4 bytes for length, name and crc
+                       data := b.Bytes()
+                       i = len(pngHeader)
+
+                       for i < len(data)-chunkFieldsLength {
+                               length := binary.BigEndian.Uint32(data[i : i+4])
+                               name := string(data[i+4 : i+8])
+
+                               switch name {
+                               case "IHDR":
+                                       bitdepth := data[i+8+8]
+                                       if bitdepth != tc.bitdepth {
+                                               t.Errorf("got bitdepth %d, want %d", bitdepth, tc.bitdepth)
+                                       }
+                               case "IDAT":
+                                       // Uncompress the image data
+                                       r, err := zlib.NewReader(bytes.NewReader(data[i+8 : i+8+int(length)]))
+                                       if err != nil {
+                                               t.Error(err)
+                                               return
+                                       }
+                                       n, err := io.Copy(ioutil.Discard, r)
+                                       if err != nil {
+                                               t.Errorf("got error while reading image data: %v", err)
+                                       }
+                                       if n != int64(tc.datalen) {
+                                               t.Errorf("got uncompressed data length %d, want %d", n, tc.datalen)
+                                       }
+                               }
+
+                               i += chunkFieldsLength + int(length)
+                       }
+               })
+
+       }
+}
+
 func TestWriterLevels(t *testing.T) {
        m := image.NewNRGBA(image.Rect(0, 0, 100, 100))