├── .github
└── workflows
│ └── codecov.yml
├── LICENSE
├── README.md
├── bitwriter.go
├── bitwriter_test.go
├── go.mod
├── go.sum
├── huffman.go
├── huffman_test.go
├── reader.go
├── reader_test.go
├── transform.go
├── transform_test.go
├── writer.go
└── writer_test.go
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | name: Codecov Coverage
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v3
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v4
21 | with:
22 | go-version: '1.x'
23 |
24 | - name: Install dependencies
25 | run: |
26 | go mod tidy
27 |
28 | - name: Run tests with coverage
29 | run: |
30 | go test -v -coverprofile=coverage.txt ./...
31 |
32 | - name: Upload coverage to Codecov
33 | uses: codecov/codecov-action@v5
34 | with:
35 | token: ${{ secrets.CODECOV_TOKEN }}
36 | slug: HugoSmits86/nativewebp
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Hugo Smits
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codecov.io/gh/HugoSmits86/nativewebp)
2 | [](https://pkg.go.dev/github.com/HugoSmits86/nativewebp)
3 | [](https://opensource.org/licenses/MIT)
4 |
5 | # Native WebP for Go
6 |
7 | This is a native WebP encoder written entirely in Go, with **no dependencies on libwebp** or other external libraries. Designed for performance and efficiency, this encoder generates smaller files than the standard Go PNG encoder and is approximately **50% faster** in execution.
8 |
9 | Currently, the encoder supports only WebP lossless images (VP8L).
10 |
11 | ## Decoding Support
12 |
13 | We provide WebP decoding through a wrapper around `golang.org/x/image/webp`, with an additional `DecodeIgnoreAlphaFlag` function to handle VP8X images where the alpha flag causes decoding issues.
14 | ## Benchmark
15 |
16 | We conducted a quick benchmark to showcase file size reduction and encoding performance. Using an image from Google’s WebP Lossless and Alpha Gallery, we compared the results of our nativewebp encoder with the standard PNG encoder.
17 | For the PNG encoder, we applied the `png.BestCompression` setting to achieve the most competitive compression outcomes.
18 |
19 |
20 |
23 | | 24 | | PNG encoder | 25 |nativeWebP encoder | 26 |reduction | 27 |
---|---|---|---|---|
file size | 31 |120 kb | 32 |96 kb | 33 |20% smaller | 34 ||
encoding time | 37 |42945049 ns/op | 38 |27716447 ns/op | 39 |35% faster | 40 ||
file size | 44 |46 kb | 45 |36 kb | 46 |22% smaller | 47 ||
encoding time | 50 |98509399 ns/op | 51 |31461759 ns/op | 52 |68% faster | 53 ||
file size | 57 |236 kb | 58 |194 kb | 59 |18% smaller | 60 ||
encoding time | 63 |178205535 ns/op | 64 |102454192 ns/op | 65 |43% faster | 66 ||
file size | 70 |53 kb | 71 |41 kb | 72 |23% smaller | 73 ||
encoding time | 76 |29088555 ns/op | 77 |14959849 ns/op | 78 |49% faster | 79 ||
file size | 83 |139 kb | 84 |123 kb | 85 |12% smaller | 86 ||
encoding time | 89 |63423995 ns/op | 90 |21717392 ns/op | 91 |66% faster | 92 |
95 | image source: https://developers.google.com/speed/webp/gallery2 96 |
97 | 98 | 99 | ## Installation 100 | 101 | To install the nativewebp package, use the following command: 102 | ```Bash 103 | go get github.com/HugoSmits86/nativewebp 104 | ``` 105 | ## Usage 106 | 107 | Here’s a simple example of how to encode an image: 108 | ```Go 109 | file, err := os.Create(name) 110 | if err != nil { 111 | log.Fatalf("Error creating file %s: %v", name, err) 112 | } 113 | defer file.Close() 114 | 115 | err = nativewebp.Encode(file, img, nil) 116 | if err != nil { 117 | log.Fatalf("Error encoding image to WebP: %v", err) 118 | } 119 | ``` 120 | 121 | Here’s a simple example of how to encode an animation: 122 | ```Go 123 | file, err := os.Create(name) 124 | if err != nil { 125 | log.Fatalf("Error creating file %s: %v", name, err) 126 | } 127 | defer file.Close() 128 | 129 | ani := nativewebp.Animation{ 130 | Images: []image.Image{ 131 | frame1, 132 | frame2, 133 | }, 134 | Durations: []uint { 135 | 100, 136 | 100, 137 | }, 138 | Disposals: []uint { 139 | 0, 140 | 0, 141 | }, 142 | LoopCount: 0, 143 | BackgroundColor: 0xffffffff, 144 | } 145 | 146 | err = nativewebp.EncodeAll(file, &ani, nil) 147 | if err != nil { 148 | log.Fatalf("Error encoding WebP animation: %v", err) 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /bitwriter.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "bytes" 8 | ) 9 | 10 | type bitWriter struct { 11 | Buffer *bytes.Buffer 12 | BitBuffer uint64 13 | BitBufferSize int 14 | } 15 | 16 | func (w *bitWriter) writeBits(value uint64, n int) { 17 | if n < 0 || n > 64 { 18 | panic("Invalid bit count: must be between 1 and 64") 19 | } 20 | 21 | if value >= (1 << n) { 22 | panic("too many bits for the given value") 23 | } 24 | 25 | w.BitBuffer |= (value << w.BitBufferSize) 26 | w.BitBufferSize += n 27 | w.writeThrough() 28 | } 29 | 30 | func (w *bitWriter) writeBytes(values []byte) { 31 | for _, v := range values { 32 | w.writeBits(uint64(v), 8) 33 | } 34 | } 35 | 36 | func (w *bitWriter) writeCode(code huffmanCode) { 37 | if code.Depth <= 0 { 38 | return 39 | } 40 | 41 | value := uint64(code.Bits) 42 | reversed := uint64(0) 43 | for i := 0; i < code.Depth; i++ { 44 | reversed = (reversed << 1) | (value & 1) 45 | value >>= 1 46 | } 47 | 48 | w.writeBits(reversed, code.Depth) 49 | } 50 | 51 | func (w *bitWriter) alignByte() { 52 | w.BitBufferSize = (w.BitBufferSize + 7) &^ 7 53 | w.writeThrough() 54 | } 55 | 56 | func (w *bitWriter) writeThrough() { 57 | for w.BitBufferSize >= 8 { 58 | w.Buffer.WriteByte(byte(w.BitBuffer & 0xFF)) 59 | w.BitBuffer >>= 8 60 | w.BitBufferSize -= 8 61 | } 62 | } -------------------------------------------------------------------------------- /bitwriter_test.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "bytes" 8 | //------------------------------ 9 | //testing 10 | //------------------------------ 11 | "testing" 12 | ) 13 | 14 | func TestWriteBits(t *testing.T) { 15 | for id, tt := range []struct { 16 | initialBuffer []byte 17 | initialBitBuf uint64 18 | initialBufSize int 19 | value uint64 20 | bitCount int 21 | expectedBuffer []byte 22 | expectedBitBuf uint64 23 | expectedBufSize int 24 | expectPanic bool 25 | }{ 26 | // Valid cases 27 | {nil, 0, 0, 0b1, 1, nil, 0b1, 1, false}, // Write 1 bit 28 | {nil, 0, 0, 0b11010101, 8, []byte{0b11010101}, 0, 0, false}, // Write 8 bits, flush to buffer 29 | {nil, 0, 0, 0xFFFF, 16, []byte{0xFF, 0xFF}, 0, 0, false}, // Write 16 bits, flush to buffer 30 | {nil, 0, 0, 0b101, 3, nil, 0b101, 3, false}, // Write 3 bits 31 | {nil, 0b1, 1, 0b10, 2, nil, 0b101, 3, false}, // Append 2 bits 32 | {nil, 0b101, 3, 0b1111, 4, nil, 0b1111101, 7, false}, // Append 4 bits 33 | {[]byte{0xFF}, 0, 0, 0b101, 3, []byte{0xFF}, 0b101, 3, false}, // Preserve buffer 34 | // Multiple writes, testing flush 35 | {nil, 0, 0, 0b1101, 4, nil, 0b1101, 4, false}, // First write 36 | {[]byte{}, 0b1101, 4, 0b1111, 4, []byte{0xFD}, 0, 0, false}, // Flush to buffer (8 bits) 37 | {[]byte{0xAB}, 0, 0, 0b1010101010101010, 16, []byte{0xAB, 0xAA, 0xAA}, 0, 0, false}, // Write 16 bits after flush 38 | // Invalid cases (expect panic) 39 | {nil, 0, 0, 0b101, 0, nil, 0, 0, true}, // Bit count is 0 40 | {nil, 0, 0, 0b101, 65, nil, 0, 0, true}, // Bit count exceeds 64 41 | {nil, 0, 0, 0b101, -1, nil, 0, 0, true}, // Bit count exceeds 64 42 | {nil, 0, 0, 0b101, 2, nil, 0, 0, true}, // Value too large for bit count 43 | } { 44 | // Use defer to catch panics 45 | func() { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | if !tt.expectPanic { 49 | t.Errorf("test %v: unexpected panic: %v", id, r) 50 | } 51 | } else if tt.expectPanic { 52 | t.Errorf("test %v: expected panic but did not occur", id) 53 | } 54 | }() 55 | 56 | buffer := &bytes.Buffer{} 57 | buffer.Write(tt.initialBuffer) 58 | writer := bitWriter{ 59 | Buffer: buffer, 60 | BitBuffer: tt.initialBitBuf, 61 | BitBufferSize: tt.initialBufSize, 62 | } 63 | 64 | writer.writeBits(tt.value, tt.bitCount) 65 | 66 | // Validate state 67 | if !tt.expectPanic { 68 | if !bytes.Equal(writer.Buffer.Bytes(), tt.expectedBuffer) { 69 | t.Errorf("test %v: buffer mismatch: expected %v, got %v", id, tt.expectedBuffer, writer.Buffer.Bytes()) 70 | } 71 | if writer.BitBuffer != tt.expectedBitBuf { 72 | t.Errorf("test %v: bit buffer mismatch: expected %v, got %v", id, tt.expectedBitBuf, writer.BitBuffer) 73 | } 74 | if writer.BitBufferSize != tt.expectedBufSize { 75 | t.Errorf("test %v: bit buffer size mismatch: expected %v, got %v", id, tt.expectedBufSize, writer.BitBufferSize) 76 | } 77 | } 78 | }() 79 | } 80 | } 81 | 82 | func TestWriteBytes(t *testing.T) { 83 | for id, tt := range []struct { 84 | initialBuffer []byte 85 | initialBitBuf uint64 86 | initialBufSize int 87 | values []byte 88 | expectedBuffer []byte 89 | expectedBitBuf uint64 90 | expectedBufSize int 91 | }{ 92 | {nil, 0, 0, []byte{0xFF}, []byte{0xFF}, 0, 0}, // Write single byte 93 | {nil, 0, 0, []byte{0x12, 0x34}, []byte{0x12, 0x34}, 0, 0}, // Write two bytes 94 | {[]byte{0xAB}, 0, 0, []byte{0xCD}, []byte{0xAB, 0xCD}, 0, 0}, // Preserve existing buffer 95 | {nil, 0b1, 1, []byte{0x80}, []byte{0x01}, 0b1, 1}, // Partial bit buffer (1 bit) + new byte 96 | {[]byte{0x00}, 0b1111, 4, []byte{0x0F}, []byte{0x00, 0xFF}, 0, 4}, // Partial + full flush 97 | {nil, 0, 0, nil, nil, 0, 0}, // No values to write 98 | } { 99 | buffer := &bytes.Buffer{} 100 | buffer.Write(tt.initialBuffer) 101 | writer := bitWriter{ 102 | Buffer: buffer, 103 | BitBuffer: tt.initialBitBuf, 104 | BitBufferSize: tt.initialBufSize, 105 | } 106 | 107 | writer.writeBytes(tt.values) 108 | 109 | if !bytes.Equal(writer.Buffer.Bytes(), tt.expectedBuffer) { 110 | t.Errorf("test %v: buffer mismatch: expected %v, got %v", id, tt.expectedBuffer, writer.Buffer.Bytes()) 111 | } 112 | 113 | if writer.BitBuffer != tt.expectedBitBuf { 114 | t.Errorf("test %v: bit buffer mismatch: expected %064b, got %064b", id, tt.expectedBitBuf, writer.BitBuffer) 115 | } 116 | 117 | if writer.BitBufferSize != tt.expectedBufSize { 118 | t.Errorf("test %v: bit buffer size mismatch: expected %v, got %v", id, tt.expectedBufSize, writer.BitBufferSize) 119 | } 120 | } 121 | } 122 | 123 | func TestWriteCode(t *testing.T) { 124 | for id, tt := range []struct { 125 | initialBuffer []byte 126 | initialBitBuf uint64 127 | initialBufSize int 128 | code huffmanCode 129 | expectedBuffer []byte 130 | expectedBitBuf uint64 131 | expectedBufSize int 132 | }{ 133 | {nil, 0, 0, huffmanCode{Bits: 0b101, Depth: 3}, nil, 0b101, 3}, // Basic 3-bit code 134 | {nil, 0, 0, huffmanCode{Bits: 0b10, Depth: 2}, nil, 0b01, 2}, // 2-bit code, reversed 135 | {nil, 0, 0, huffmanCode{Bits: 0b1011, Depth: 4}, nil, 0b1101, 4}, // 4-bit code, reversed 136 | {nil, 0b1, 1, huffmanCode{Bits: 0b10, Depth: 2}, nil, 0b011, 3}, // Append 2 bits to existing buffer 137 | {nil, 0, 0, huffmanCode{Bits: 0, Depth: 0}, nil, 0, 0}, // Zero-Depth: code, no operation 138 | {nil, 0b10101010, 8, huffmanCode{Bits: 0b1111, Depth: 4}, []byte{0b10101010}, 0b1111, 4}, // Flush full byte, 4 bits remaining 139 | {nil, 0, 0, huffmanCode{Bits: 0b10011, Depth: 5}, nil, 0b11001, 5}, // 5-bit code, reversed 140 | {nil, 0, 0, huffmanCode{Bits: 0b1, Depth: -1}, nil, 0, 0}, // Negative Depth:, no operation 141 | } { 142 | buffer := &bytes.Buffer{} 143 | buffer.Write(tt.initialBuffer) 144 | writer := bitWriter{ 145 | Buffer: buffer, 146 | BitBuffer: tt.initialBitBuf, 147 | BitBufferSize: tt.initialBufSize, 148 | } 149 | 150 | func() { 151 | defer func() { 152 | if r := recover(); r != nil { 153 | t.Errorf("test %v: unexpected panic: %v", id, r) 154 | } 155 | }() 156 | writer.writeCode(tt.code) 157 | }() 158 | 159 | if !bytes.Equal(writer.Buffer.Bytes(), tt.expectedBuffer) { 160 | t.Errorf("test %v: buffer mismatch: expected %v, got %v", id, tt.expectedBuffer, writer.Buffer.Bytes()) 161 | } 162 | 163 | if writer.BitBuffer != tt.expectedBitBuf { 164 | t.Errorf("test %v: bit buffer mismatch: expected %064b, got %064b", id, tt.expectedBitBuf, writer.BitBuffer) 165 | } 166 | 167 | if writer.BitBufferSize != tt.expectedBufSize { 168 | t.Errorf("test %v: bit buffer size mismatch: expected %v, got %v", id, tt.expectedBufSize, writer.BitBufferSize) 169 | } 170 | } 171 | } 172 | 173 | func TestWriteThrough(t *testing.T) { 174 | for id, tt := range []struct { 175 | initialBuffer []byte 176 | initialBitBuf uint64 177 | initialBufSize int 178 | expectedBuffer []byte 179 | expectedBitBuf uint64 180 | expectedBufSize int 181 | }{ 182 | {nil, 0b11010101, 8, []byte{0b11010101}, 0, 0}, // Exactly 8 bits 183 | {nil, 0b1111111111111111, 16, []byte{0xFF, 0xFF}, 0, 0}, // Multiple of 8 bits 184 | {nil, 0b1010101010101010, 12, []byte{0b10101010}, 0b10101010, 4}, // More than 8 bits, remainder in buffer 185 | {nil, 0b11110000, 4, nil, 0b11110000, 4}, // Less than 8 bits, nothing flushed 186 | {[]byte{0xAB}, 0b11010101, 8, []byte{0xAB, 0xD5}, 0, 0}, // Preserves existing buffer contents 187 | {[]byte{0xAB}, 0b1010101010101010, 12, []byte{0xAB, 0xAA}, 0b10101010, 4}, // Mixed existing buffer and partial flush 188 | } { 189 | buffer := &bytes.Buffer{} 190 | buffer.Write(tt.initialBuffer) 191 | writer := bitWriter{ 192 | Buffer: buffer, 193 | BitBuffer: tt.initialBitBuf, 194 | BitBufferSize: tt.initialBufSize, 195 | } 196 | 197 | writer.writeThrough() 198 | 199 | if !bytes.Equal(writer.Buffer.Bytes(), tt.expectedBuffer) { 200 | t.Errorf("test %v: buffer mismatch: expected %v, got %v", id, tt.expectedBuffer, writer.Buffer.Bytes()) 201 | } 202 | 203 | if writer.BitBuffer != tt.expectedBitBuf { 204 | t.Errorf("test %v: bit buffer mismatch: expected %064b, got %064b", id, tt.expectedBitBuf, writer.BitBuffer) 205 | } 206 | 207 | if writer.BitBufferSize != tt.expectedBufSize { 208 | t.Errorf("test %v: bit buffer size mismatch: expected %v, got %v", id, tt.expectedBufSize, writer.BitBufferSize) 209 | } 210 | } 211 | } 212 | 213 | func TestAlignByte(t *testing.T) { 214 | for id, tt := range []struct { 215 | initialBuffer []byte 216 | initialBitBuf uint64 217 | initialBufSize int 218 | expectedBuffer []byte 219 | expectedBitBuf uint64 220 | expectedBufSize int 221 | }{ 222 | {nil, 0b1101, 4, []byte{0x0D}, 0, 0}, // Align 4 bits, no padding 223 | {nil, 0b10101010, 8, []byte{0b10101010}, 0, 0}, // Already aligned 224 | {nil, 0b1010101010101010, 12, []byte{0xAA, 0xAA}, 0, 0}, // Align 12 bits 225 | {[]byte{0xAB}, 0b1111, 4, []byte{0xAB, 0x0F}, 0, 0}, // Existing buffer, no padding 226 | {[]byte{0xAB}, 0b1010101010101010, 10, []byte{0xAB, 0xAA, 0xAA}, 0, 0}, // Align 10 bits 227 | {nil, 0, 0, nil, 0, 0}, // Empty buffer 228 | } { 229 | buffer := &bytes.Buffer{} 230 | buffer.Write(tt.initialBuffer) 231 | writer := bitWriter{ 232 | Buffer: buffer, 233 | BitBuffer: tt.initialBitBuf, 234 | BitBufferSize: tt.initialBufSize, 235 | } 236 | 237 | writer.alignByte() 238 | 239 | if !bytes.Equal(writer.Buffer.Bytes(), tt.expectedBuffer) { 240 | t.Errorf("test %v: buffer mismatch: expected %v, got %v", id, tt.expectedBuffer, writer.Buffer.Bytes()) 241 | } 242 | 243 | if writer.BitBuffer != tt.expectedBitBuf { 244 | t.Errorf("test %v: bit buffer mismatch: expected %064b, got %064b", id, tt.expectedBitBuf, writer.BitBuffer) 245 | } 246 | 247 | if writer.BitBufferSize != tt.expectedBufSize { 248 | t.Errorf("test %v: bit buffer size mismatch: expected %v, got %v", id, tt.expectedBufSize, writer.BitBufferSize) 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/HugoSmits86/nativewebp 2 | 3 | go 1.22.2 4 | 5 | require golang.org/x/image v0.24.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= 2 | golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 3 | -------------------------------------------------------------------------------- /huffman.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "container/heap" 8 | "sort" 9 | ) 10 | 11 | type huffmanCode struct { 12 | Symbol int 13 | Bits int 14 | Depth int 15 | } 16 | 17 | type node struct { 18 | IsBranch bool 19 | Weight int 20 | Symbol int 21 | BranchLeft *node 22 | BranchRight *node 23 | } 24 | 25 | type nodeHeap []*node 26 | func (h nodeHeap) Len() int { return len(h) } 27 | func (h nodeHeap) Less(i, j int) bool { return h[i].Weight < h[j].Weight } 28 | func (h nodeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 29 | func (h *nodeHeap) Push(x interface{}) { *h = append(*h, x.(*node)) } 30 | func (h *nodeHeap) Pop() interface{} { 31 | old := *h 32 | n := len(old) 33 | x := old[n-1] 34 | *h = old[0 : n-1] 35 | return x 36 | } 37 | 38 | func buildHuffmanTree(histo []int, maxDepth int) *node { 39 | sum := 0 40 | for _, x := range histo { 41 | sum += x 42 | } 43 | 44 | minWeight := sum >> (maxDepth - 2) 45 | 46 | nHeap := &nodeHeap{} 47 | heap.Init(nHeap) 48 | 49 | for s, w := range histo { 50 | if w > 0 { 51 | if w < minWeight { 52 | w = minWeight 53 | } 54 | 55 | heap.Push(nHeap, &node{ 56 | Weight: w, 57 | Symbol: s, 58 | }) 59 | } 60 | } 61 | 62 | for nHeap.Len() < 1 { 63 | heap.Push(nHeap, &node{ 64 | Weight: minWeight, 65 | Symbol: 0, 66 | }) 67 | } 68 | 69 | for nHeap.Len() > 1 { 70 | n1 := heap.Pop(nHeap).(*node) 71 | n2 := heap.Pop(nHeap).(*node) 72 | heap.Push(nHeap, &node{ 73 | IsBranch: true, 74 | Weight: n1.Weight + n2.Weight, 75 | BranchLeft: n1, 76 | BranchRight: n2, 77 | }) 78 | } 79 | 80 | return heap.Pop(nHeap).(*node) 81 | } 82 | 83 | func buildhuffmanCodes(histo []int, maxDepth int) []huffmanCode { 84 | codes := make([]huffmanCode, len(histo)) 85 | 86 | tree := buildHuffmanTree(histo, maxDepth) 87 | if !tree.IsBranch { 88 | codes[tree.Symbol] = huffmanCode{tree.Symbol, 0, -1} 89 | return codes 90 | } 91 | 92 | var symbols []huffmanCode 93 | setBitDepths(tree, &symbols, 0) 94 | 95 | sort.Slice(symbols, func(i, j int) bool { 96 | if symbols[i].Depth == symbols[j].Depth { 97 | return symbols[i].Symbol < symbols[j].Symbol 98 | } 99 | 100 | return symbols[i].Depth < symbols[j].Depth 101 | }) 102 | 103 | bits := 0 104 | prevDepth := 0 105 | for _, sym := range symbols { 106 | bits <<= (sym.Depth - prevDepth) 107 | codes[sym.Symbol].Symbol = sym.Symbol 108 | codes[sym.Symbol].Bits = bits 109 | codes[sym.Symbol].Depth = sym.Depth 110 | bits++ 111 | 112 | prevDepth = sym.Depth 113 | } 114 | 115 | return codes 116 | } 117 | 118 | func setBitDepths(node *node, codes *[]huffmanCode, level int) { 119 | if node == nil { 120 | return 121 | } 122 | 123 | if !node.IsBranch { 124 | *codes = append(*codes, huffmanCode{ 125 | Symbol: node.Symbol, 126 | Depth: level, 127 | }) 128 | 129 | return 130 | } 131 | 132 | setBitDepths(node.BranchLeft, codes, level + 1) 133 | setBitDepths(node.BranchRight, codes, level + 1) 134 | } 135 | 136 | func writehuffmanCodes(w *bitWriter, codes []huffmanCode) { 137 | var symbols [2]int 138 | 139 | cnt := 0 140 | for _, code := range codes { 141 | if code.Depth != 0 { 142 | if cnt < 2 { 143 | symbols[cnt] = code.Symbol 144 | } 145 | 146 | cnt++ 147 | } 148 | 149 | if cnt > 2 { 150 | break 151 | } 152 | } 153 | 154 | if cnt == 0 { 155 | w.writeBits(1, 1) 156 | w.writeBits(0, 3) 157 | } else if cnt <= 2 && symbols[0] < 1 << 8 && symbols[1] < 1 << 8 { 158 | w.writeBits(1, 1) 159 | w.writeBits(uint64(cnt - 1), 1) 160 | if symbols[0] <= 1 { 161 | w.writeBits(0, 1) 162 | w.writeBits(uint64(symbols[0]), 1) 163 | } else { 164 | w.writeBits(1, 1) 165 | w.writeBits(uint64(symbols[0]), 8) 166 | } 167 | 168 | if cnt > 1 { 169 | w.writeBits(uint64(symbols[1]), 8) 170 | } 171 | } else { 172 | writeFullhuffmanCode(w, codes) 173 | } 174 | } 175 | 176 | func writeFullhuffmanCode(w *bitWriter, codes []huffmanCode) { 177 | histo := make([]int, 19) 178 | for _, c := range codes { 179 | histo[c.Depth]++ 180 | } 181 | 182 | // lengthCodeOrder comes directly from the WebP specs! 183 | var lengthCodeOrder = []int{ 184 | 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 185 | } 186 | 187 | cnt := 0 188 | for i, c := range lengthCodeOrder { 189 | if histo[c] > 0 { 190 | cnt = max(i + 1, 4) 191 | } 192 | } 193 | 194 | w.writeBits(0, 1) 195 | w.writeBits(uint64(cnt - 4), 4) 196 | 197 | lengths := buildhuffmanCodes(histo, 7) 198 | for i := 0; i < cnt; i++ { 199 | w.writeBits(uint64(lengths[lengthCodeOrder[i]].Depth), 3) 200 | } 201 | 202 | w.writeBits(0, 1) 203 | 204 | for _, c := range codes { 205 | w.writeCode(lengths[c.Depth]) 206 | } 207 | } -------------------------------------------------------------------------------- /huffman_test.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "bytes" 8 | //------------------------------ 9 | //testing 10 | //------------------------------ 11 | "testing" 12 | ) 13 | 14 | func TestBuildHuffmanTree(t *testing.T) { 15 | for id, tt := range []struct { 16 | histo []int 17 | maxDepth int 18 | expectedTree *node // Expected structure of the Huffman tree 19 | }{ 20 | // Simple case with 2 symbols 21 | { 22 | histo: []int{5, 10}, 23 | maxDepth: 4, 24 | expectedTree: &node{ 25 | IsBranch: true, 26 | Weight: 15, 27 | BranchLeft: &node{ 28 | IsBranch: false, 29 | Weight: 5, 30 | Symbol: 0, 31 | }, 32 | BranchRight: &node{ 33 | IsBranch: false, 34 | Weight: 10, 35 | Symbol: 1, 36 | }, 37 | }, 38 | }, 39 | // Histogram with more symbols 40 | { 41 | histo: []int{5, 9, 12, 13}, 42 | maxDepth: 5, 43 | expectedTree: &node{ 44 | IsBranch: true, 45 | Weight: 39, 46 | BranchLeft: &node{ 47 | IsBranch: true, 48 | Weight: 14, 49 | BranchLeft: &node{ 50 | IsBranch: false, 51 | Weight: 5, 52 | Symbol: 0, 53 | }, 54 | BranchRight: &node{ 55 | IsBranch: false, 56 | Weight: 9, 57 | Symbol: 1, 58 | }, 59 | }, 60 | BranchRight: &node{ 61 | IsBranch: true, 62 | Weight: 25, 63 | BranchLeft: &node{ 64 | IsBranch: false, 65 | Weight: 12, 66 | Symbol: 2, 67 | }, 68 | BranchRight: &node{ 69 | IsBranch: false, 70 | Weight: 13, 71 | Symbol: 3, 72 | }, 73 | }, 74 | }, 75 | }, 76 | // Test case that triggers the for nHeap.Len() < 1 loop 77 | { 78 | histo: []int{}, // Empty histogram 79 | maxDepth: 4, 80 | expectedTree: &node{ 81 | IsBranch: false, 82 | Weight: 0, 83 | Symbol: 0, 84 | }, 85 | }, 86 | // Test case with all zero weights 87 | { 88 | histo: []int{0, 0, 0}, 89 | maxDepth: 4, 90 | expectedTree: &node{ 91 | IsBranch: false, 92 | Weight: 0, 93 | Symbol: 0, 94 | }, 95 | }, 96 | } { 97 | resultTree := buildHuffmanTree(tt.histo, tt.maxDepth) 98 | 99 | var compareTrees func(a, b *node) bool 100 | compareTrees = func(a, b *node) bool { 101 | if a == nil && b == nil { 102 | return true 103 | } 104 | if a == nil || b == nil { 105 | return false 106 | } 107 | if a.IsBranch != b.IsBranch || a.Weight != b.Weight || a.Symbol != b.Symbol { 108 | return false 109 | } 110 | return compareTrees(a.BranchLeft, b.BranchLeft) && compareTrees(a.BranchRight, b.BranchRight) 111 | } 112 | 113 | if !compareTrees(resultTree, tt.expectedTree) { 114 | t.Errorf("test %v: Huffman tree mismatch: got %+v, expected %+v", id, resultTree, tt.expectedTree) 115 | } 116 | } 117 | } 118 | 119 | func TestBuildhuffmanCodes(t *testing.T) { 120 | for id, tt := range []struct { 121 | histo []int 122 | maxDepth int 123 | expectedBits map[int]huffmanCode // Expected results as a map for clarity 124 | }{ 125 | // Test case with a single symbol 126 | { 127 | histo: []int{10}, 128 | maxDepth: 4, 129 | expectedBits: map[int]huffmanCode{ 130 | 0: {Symbol: 0, Bits: 0, Depth: -1}, // Single symbol, no actual code assigned 131 | }, 132 | }, 133 | // Test case with two symbols 134 | { 135 | histo: []int{5, 15}, 136 | maxDepth: 4, 137 | expectedBits: map[int]huffmanCode{ 138 | 0: {Symbol: 0, Bits: 0b0, Depth: 1}, // Symbol 0 gets code '0' 139 | 1: {Symbol: 1, Bits: 0b1, Depth: 1}, // Symbol 1 gets code '1' 140 | }, 141 | }, 142 | // Test case with symbols requiring different depthss 143 | { 144 | histo: []int{5, 9, 12, 13, 1}, // Fifth symbol has lower weight, longer code 145 | maxDepth: 4, 146 | expectedBits: map[int]huffmanCode{ 147 | 0: {Symbol: 0, Bits: 0b110, Depth: 3}, // Symbol 0 gets code '110' 148 | 1: {Symbol: 1, Bits: 0b0, Depth: 2}, // Symbol 1 gets code '0' 149 | 2: {Symbol: 2, Bits: 0b1, Depth: 2}, // Symbol 2 gets code '1' 150 | 3: {Symbol: 3, Bits: 0b10, Depth: 2}, // Symbol 3 gets code '10' 151 | 4: {Symbol: 4, Bits: 0b111, Depth: 3}, // Symbol 4 gets code '111' 152 | }, 153 | }, 154 | } { 155 | resultCodes := buildhuffmanCodes(tt.histo, tt.maxDepth) 156 | 157 | for sym, expectedCode := range tt.expectedBits { 158 | if sym >= len(resultCodes) { 159 | t.Errorf("test %v: missing code for symbol %v", id, expectedCode.Symbol) 160 | continue 161 | } 162 | 163 | resultCode := resultCodes[sym] 164 | if resultCode.Bits != expectedCode.Bits || resultCode.Depth != expectedCode.Depth { 165 | t.Errorf("test %v: code mismatch for symbol %v: got {Bits: %b, Depth: %d}, expected {Bits: %b, Depth: %d}", 166 | id, expectedCode.Symbol, resultCode.Bits, resultCode.Depth, expectedCode.Bits, expectedCode.Depth) 167 | } 168 | } 169 | } 170 | } 171 | 172 | func TestSetBitDepths(t *testing.T) { 173 | for id, tt := range []struct { 174 | tree *node 175 | expectedCodes []huffmanCode 176 | }{ 177 | // Test case with a nil node 178 | { 179 | tree: nil, // Nil node 180 | expectedCodes: []huffmanCode{}, // No codes generated 181 | }, 182 | // Test case with a single node (no branches) 183 | { 184 | tree: &node{ 185 | IsBranch: false, 186 | Weight: 5, 187 | Symbol: 0, 188 | }, 189 | expectedCodes: []huffmanCode{ 190 | {Symbol: 0, Depth: 0}, // Root node has depth 0 191 | }, 192 | }, 193 | // Test case with a simple binary tree 194 | { 195 | tree: &node{ 196 | IsBranch: true, 197 | Weight: 15, 198 | BranchLeft: &node{ 199 | IsBranch: false, 200 | Weight: 5, 201 | Symbol: 0, 202 | }, 203 | BranchRight: &node{ 204 | IsBranch: false, 205 | Weight: 10, 206 | Symbol: 1, 207 | }, 208 | }, 209 | expectedCodes: []huffmanCode{ 210 | {Symbol: 0, Depth: 1}, // Left branch depth = 1 211 | {Symbol: 1, Depth: 1}, // Right branch depth = 1 212 | }, 213 | }, 214 | // Test case with a more complex tree 215 | { 216 | tree: &node{ 217 | IsBranch: true, 218 | Weight: 30, 219 | BranchLeft: &node{ 220 | IsBranch: true, 221 | Weight: 15, 222 | BranchLeft: &node{ 223 | IsBranch: false, 224 | Weight: 5, 225 | Symbol: 0, 226 | }, 227 | BranchRight: &node{ 228 | IsBranch: false, 229 | Weight: 10, 230 | Symbol: 1, 231 | }, 232 | }, 233 | BranchRight: &node{ 234 | IsBranch: false, 235 | Weight: 15, 236 | Symbol: 2, 237 | }, 238 | }, 239 | expectedCodes: []huffmanCode{ 240 | {Symbol: 0, Depth: 2}, 241 | {Symbol: 1, Depth: 2}, 242 | {Symbol: 2, Depth: 1}, 243 | }, 244 | }, 245 | } { 246 | var codes []huffmanCode 247 | setBitDepths(tt.tree, &codes, 0) 248 | 249 | if len(codes) != len(tt.expectedCodes) { 250 | t.Errorf("test %v: depths mismatch: got %v, expected %v", id, len(codes), len(tt.expectedCodes)) 251 | continue 252 | } 253 | 254 | for i, expectedCode := range tt.expectedCodes { 255 | if codes[i] != expectedCode { 256 | t.Errorf("test %v: mismatch at index %v: got %+v, expected %+v", id, i, codes[i], expectedCode) 257 | } 258 | } 259 | } 260 | } 261 | 262 | func TestWritehuffmanCodes(t *testing.T) { 263 | for id, tt := range []struct { 264 | codes []huffmanCode 265 | expectedBits []byte 266 | expectedBitBuf uint64 267 | expectedBufSize int 268 | }{ 269 | // No codes present 270 | { 271 | codes: []huffmanCode{}, 272 | expectedBits: []byte{}, 273 | expectedBitBuf: 0b0001, 274 | expectedBufSize: 4, 275 | }, 276 | // Single symbol, symbol[0] <= 1 277 | { 278 | codes: []huffmanCode{ 279 | {Symbol: 0, Bits: 0, Depth: 1}, 280 | }, 281 | expectedBits: []byte{}, 282 | expectedBitBuf: 0b0001, 283 | expectedBufSize: 4, 284 | }, 285 | // Single symbol, symbol[0] > 1 286 | { 287 | codes: []huffmanCode{ 288 | {Symbol: 3, Bits: 0b11, Depth: 1}, 289 | }, 290 | expectedBits: []byte{0b00011101}, 291 | expectedBitBuf: 0b0000, 292 | expectedBufSize: 3, 293 | }, 294 | // Two symbols, symbol[0] > 1 295 | { 296 | codes: []huffmanCode{ 297 | {Symbol: 2, Bits: 0b10, Depth: 1}, 298 | {Symbol: 3, Bits: 0b11, Depth: 1}, 299 | }, 300 | expectedBits: []byte{0b00010111, 0b00011000}, 301 | expectedBitBuf: 0b00, 302 | expectedBufSize: 3, 303 | }, 304 | // Write full Huffman code (trigger writeFullhuffmanCode) 305 | { 306 | codes: []huffmanCode{ 307 | {Symbol: 0, Bits: 0, Depth: 3}, 308 | {Symbol: 1, Bits: 1, Depth: 3}, 309 | {Symbol: 2, Bits: 2, Depth: 2}, 310 | }, 311 | expectedBits: []byte{0b00000100, 0b00000000, 0b00010010}, 312 | expectedBitBuf: 0b0011, 313 | expectedBufSize: 3, 314 | }, 315 | } { 316 | buffer := &bytes.Buffer{} 317 | writer := &bitWriter{ 318 | Buffer: buffer, 319 | BitBuffer: 0, 320 | BitBufferSize: 0, 321 | } 322 | 323 | writehuffmanCodes(writer, tt.codes) 324 | 325 | if !bytes.Equal(buffer.Bytes(), tt.expectedBits) { 326 | t.Errorf("test %d: buffer mismatch\nexpected: %064b\n got: %064b\n", id, tt.expectedBits, buffer.Bytes()) 327 | } 328 | 329 | if writer.BitBuffer != tt.expectedBitBuf { 330 | t.Errorf("test %d: bit buffer mismatch\nexpected: %064b\n got: %064b\n", id, tt.expectedBitBuf, writer.BitBuffer) 331 | } 332 | 333 | if writer.BitBufferSize != tt.expectedBufSize { 334 | t.Errorf("test %d: bit buffer size mismatch\nexpected: %d\n got: %d\n", id, tt.expectedBufSize, writer.BitBufferSize) 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "io" 8 | "bytes" 9 | "encoding/binary" 10 | //------------------------------ 11 | //imaging 12 | //------------------------------ 13 | "image" 14 | //------------------------------ 15 | //errors 16 | //------------------------------ 17 | decoderWebP "golang.org/x/image/webp" 18 | ) 19 | 20 | // registers the webp decoder so image.Decode can detect and use it. 21 | func init() { 22 | image.RegisterFormat("webp", "RIFF", Decode, DecodeConfig) 23 | } 24 | 25 | // Decode reads a WebP image from the provided io.Reader and returns it as an image.Image. 26 | // 27 | // This function is a wrapper around the underlying WebP decode package (golang.org/x/image/webp). 28 | // It supports both lossy and lossless WebP formats, decoding the image accordingly. 29 | // 30 | // Parameters: 31 | // r - The source io.Reader containing the WebP encoded image. 32 | // 33 | // Returns: 34 | // The decoded image as image.Image or an error if the decoding fails. 35 | func Decode(r io.Reader) (image.Image, error) { 36 | return decoderWebP.Decode(r) 37 | } 38 | 39 | // DecodeConfig reads the image configuration from the provided io.Reader without fully decoding the image. 40 | // 41 | // This function is a wrapper around the underlying WebP decode package (golang.org/x/image/webp) and 42 | // provides access to the image's metadata, such as its dimensions and color model. 43 | // It is useful for obtaining image information before performing a full decode. 44 | // 45 | // Parameters: 46 | // r - The source io.Reader containing the WebP encoded image. 47 | // 48 | // Returns: 49 | // An image.Config containing the image's dimensions and color model, or an error if the configuration cannot be retrieved 50 | func DecodeConfig(r io.Reader) (image.Config, error) { 51 | return decoderWebP.DecodeConfig(r) 52 | } 53 | 54 | // DecodeIgnoreAlphaFlag reads a WebP image from the provided io.Reader and returns it as an image.Image. 55 | // 56 | // This function fixes x/image/webp rejecting VP8L images with the VP8X alpha flag, expecting an ALPHA chunk. 57 | // VP8L handles transparency internally, and the WebP spec requires the flag for transparency. 58 | // 59 | // This function is a wrapper around the underlying WebP decode package (golang.org/x/image/webp). 60 | // It supports both lossy and lossless WebP formats, decoding the image accordingly. 61 | // 62 | // Parameters: 63 | // r - The source io.Reader containing the WebP encoded image. 64 | // 65 | // Returns: 66 | // The decoded image as image.Image or an error if the decoding fails. 67 | func DecodeIgnoreAlphaFlag(r io.Reader) (image.Image, error) { 68 | data, err := io.ReadAll(r) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | if len(data) >= 30 && string(data[8:16]) == "WEBPVP8X" { 74 | for i := 30; i + 8 < len(data); { 75 | // Detect VP8L chunk, which handles transparency internally. 76 | // The x/image/webp package misinterprets this, so we clear the alpha flag. 77 | if string(data[i: i + 4]) == "VP8L" { 78 | flags := binary.LittleEndian.Uint32(data[20:24]) 79 | flags &^= 0x00000010 80 | binary.LittleEndian.PutUint32(data[20:24], flags) 81 | break 82 | } 83 | 84 | i += 8 + int(binary.LittleEndian.Uint32(data[i + 4: i + 8])) 85 | } 86 | } 87 | 88 | return decoderWebP.Decode(bytes.NewReader(data)) 89 | } -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "bytes" 8 | "encoding/binary" 9 | //------------------------------ 10 | //imaging 11 | //------------------------------ 12 | "image" 13 | "image/color" 14 | "image/draw" 15 | //------------------------------ 16 | //testing 17 | //------------------------------ 18 | "testing" 19 | ) 20 | 21 | func TestImageDecodeRegistration(t *testing.T) { 22 | img := generateTestImageNRGBA(8, 16, 64, true) 23 | buf := new(bytes.Buffer) 24 | 25 | if err := Encode(buf, img, nil); err != nil { 26 | t.Fatalf("Encode failed: %v", err) 27 | } 28 | 29 | img, format, err := image.Decode(buf) 30 | if err != nil { 31 | t.Errorf("image.Decode error %v", err) 32 | return 33 | } 34 | 35 | if format != "webp" { 36 | t.Errorf("expected format as webp got %v", format) 37 | return 38 | } 39 | } 40 | 41 | func TestDecode(t *testing.T) { 42 | img := generateTestImageNRGBA(8, 8, 64, true) 43 | 44 | for id, tt := range []struct { 45 | img image.Image 46 | UseExtendedFormat bool 47 | expectedErr string 48 | }{ 49 | { 50 | img, 51 | false, 52 | "", 53 | }, 54 | { 55 | img, 56 | true, 57 | "webp: invalid format", 58 | }, 59 | { 60 | nil, // if nil is used create a non-webp buffer 61 | false, 62 | "riff: missing RIFF chunk header", 63 | }, 64 | }{ 65 | 66 | input := new(bytes.Buffer) 67 | 68 | if tt.img != nil { 69 | err := Encode(input, tt.img, &Options{UseExtendedFormat: tt.UseExtendedFormat}) 70 | if err != nil { 71 | t.Errorf("test %d: expected err as nil got %v", id, err) 72 | continue 73 | } 74 | } else { 75 | input.Write([]byte("not a WebP file!")) 76 | } 77 | 78 | buf := bytes.NewBuffer(input.Bytes()) 79 | result, err := Decode(buf) 80 | 81 | if err == nil && tt.expectedErr != "" { 82 | t.Errorf("test %d: expected err as %v got nil", id, tt.expectedErr) 83 | continue 84 | } 85 | 86 | if err != nil { 87 | if tt.expectedErr == "" { 88 | t.Errorf("test %d: expected err as nil got %v", id, err) 89 | continue 90 | } 91 | 92 | if tt.expectedErr != err.Error() { 93 | t.Errorf("test %d: expected err as %v got %v", id, tt.expectedErr, err) 94 | continue 95 | } 96 | 97 | continue 98 | } 99 | 100 | img1, ok := tt.img.(*image.NRGBA) 101 | if !ok { 102 | t.Errorf("test: unsupported image format for img1") 103 | return 104 | } 105 | 106 | img2 := image.NewNRGBA(result.Bounds()) 107 | draw.Draw(img2, result.Bounds(), result, result.Bounds().Min, draw.Src) 108 | 109 | if !img1.Rect.Eq(img2.Rect) || img1.Stride != img2.Stride { 110 | t.Errorf("test %d: expected image dimensions as %v got %v", id, img1.Rect, img2.Rect) 111 | continue 112 | } 113 | 114 | if !bytes.Equal(img1.Pix, img2.Pix) { 115 | t.Errorf("test %d: expected image to be equal", id) 116 | continue 117 | } 118 | } 119 | } 120 | 121 | func TestDecodeConfig(t *testing.T) { 122 | img := generateTestImageNRGBA(8, 16, 64, true) 123 | buf := new(bytes.Buffer) 124 | 125 | err := Encode(buf, img, nil) 126 | if err != nil { 127 | t.Errorf("Encode: expected err as nil got %v", err) 128 | return 129 | } 130 | 131 | for id, tt := range []struct { 132 | input []byte 133 | expectedColorModel color.Model 134 | expectedWidth int 135 | expectedHeight int 136 | expectedErr string 137 | }{ 138 | { 139 | buf.Bytes(), 140 | color.NRGBAModel, 141 | 8, 142 | 16, 143 | "", 144 | }, 145 | { 146 | []byte("invalid WebP data"), 147 | color.GrayModel, 148 | 0, 149 | 0, 150 | "riff: missing RIFF chunk header", 151 | }, 152 | }{ 153 | 154 | buf := bytes.NewBuffer(tt.input) 155 | result, err := DecodeConfig(buf) 156 | 157 | if err == nil && tt.expectedErr != "" { 158 | t.Errorf("test %d: expected err as %v got nil", id, tt.expectedErr) 159 | continue 160 | } 161 | 162 | if err != nil { 163 | if tt.expectedErr == "" { 164 | t.Errorf("test %d: expected err as nil got %v", id, err) 165 | continue 166 | } 167 | 168 | if tt.expectedErr != err.Error() { 169 | t.Errorf("test %d: expected err as %v got %v", id, tt.expectedErr, err) 170 | continue 171 | } 172 | 173 | continue 174 | } 175 | 176 | if result.ColorModel != tt.expectedColorModel { 177 | t.Errorf("test %d: expected color model as %v got %v", id, tt.expectedColorModel, result.ColorModel) 178 | continue 179 | } 180 | 181 | if result.Width != tt.expectedWidth { 182 | t.Errorf("test %d: expected width as %v got %v", id, tt.expectedWidth, result.Width) 183 | continue 184 | } 185 | 186 | if result.Height != tt.expectedHeight { 187 | t.Errorf("test %d: expected height as %v got %v", id, tt.expectedHeight, result.Height) 188 | continue 189 | } 190 | } 191 | } 192 | 193 | func TestDecodeIgnoreAlphaFlag(t *testing.T) { 194 | for id, tt := range []struct { 195 | useExtendedFormat bool 196 | useAlpha bool 197 | expectedErrorDecode string 198 | }{ 199 | { 200 | false, 201 | false, 202 | "", 203 | }, 204 | { 205 | false, 206 | true, 207 | "", 208 | }, 209 | { 210 | true, 211 | false, 212 | "", 213 | }, 214 | { 215 | true, 216 | true, 217 | "webp: invalid format", 218 | }, 219 | }{ 220 | img := generateTestImageNRGBA(8, 8, 64, tt.useAlpha) 221 | 222 | buf := new(bytes.Buffer) 223 | err := Encode(buf, img, &Options{UseExtendedFormat: tt.useExtendedFormat}) 224 | if err != nil { 225 | t.Errorf("test %v: expected err as nil got %v", id, err) 226 | continue 227 | } 228 | 229 | // TEST A: we expect the default Decode to give an error for VP8X with Alpha flag set 230 | _, err = Decode(bytes.NewReader(buf.Bytes())) 231 | if err == nil && tt.expectedErrorDecode != "" { 232 | t.Errorf("test %v: expected err as %v got %v", id, tt.expectedErrorDecode, err) 233 | continue 234 | } 235 | 236 | if err != nil && err.Error() != tt.expectedErrorDecode { 237 | t.Errorf("test %v: expected err as %v got %v", id, tt.expectedErrorDecode, err) 238 | continue 239 | } 240 | 241 | // TEST B: we expect the DecodeIgnoreAlphaFlag to correctly read VP8X with Alpha flag set 242 | _, err = DecodeIgnoreAlphaFlag(bytes.NewReader(buf.Bytes())) 243 | if err != nil { 244 | t.Errorf("test %v: expected err as nil got %v", id, err) 245 | continue 246 | } 247 | } 248 | } 249 | 250 | 251 | func TestDecodeIgnoreAlphaFlagSearchChunk(t *testing.T) { 252 | img := generateTestImageNRGBA(8, 8, 64, true) 253 | 254 | buf := new(bytes.Buffer) 255 | err := Encode(buf, img, &Options{UseExtendedFormat: true}) 256 | if err != nil { 257 | t.Errorf("expected err as nil got %v", err) 258 | return 259 | } 260 | 261 | data := buf.Bytes() 262 | data[20] |= 0x08 // set EXIF flag in VP8X header 263 | 264 | var exif bytes.Buffer 265 | exif.Write([]byte("EXIF")) 266 | binary.Write(&exif, binary.LittleEndian, uint32(6)) 267 | exif.Write([]byte("Hello!")) 268 | 269 | //TEST: test what happens if VP8L is not directly after VP8X chunk 270 | data = append(data[:30], append(exif.Bytes(), data[30:]...)...) 271 | binary.LittleEndian.PutUint32(data[4: 8], uint32(len(data) - 8)) 272 | 273 | _, err = DecodeIgnoreAlphaFlag(bytes.NewReader(data)) 274 | if err != nil { 275 | t.Errorf("expected err as nil got %v", err) 276 | return 277 | } 278 | } -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "math" 8 | "slices" 9 | //------------------------------ 10 | //imaging 11 | //------------------------------ 12 | "image/color" 13 | //------------------------------ 14 | //errors 15 | //------------------------------ 16 | //"log" 17 | "errors" 18 | ) 19 | 20 | type transform int 21 | 22 | const ( 23 | transformPredict = transform(0) 24 | transformColor = transform(1) 25 | transformSubGreen = transform(2) 26 | transformColorIndexing = transform(3) 27 | ) 28 | 29 | func applyPredictTransform(pixels []color.NRGBA, width, height int) (int, int, int, []color.NRGBA) { 30 | tileBits := 4 31 | tileSize := 1 << tileBits 32 | bw := (width + tileSize - 1) / tileSize 33 | bh := (height + tileSize - 1) / tileSize 34 | 35 | blocks := make([]color.NRGBA, bw * bh) 36 | deltas := make([]color.NRGBA, width * height) 37 | 38 | accum := [][]int{ 39 | make([]int, 256), 40 | make([]int, 256), 41 | make([]int, 256), 42 | make([]int, 256), 43 | make([]int, 40), 44 | } 45 | 46 | histos := make([][]int, len(accum)) 47 | for i := range accum { 48 | histos[i] = make([]int, len(accum[i])) 49 | } 50 | 51 | for y := 0; y < bh; y++ { 52 | for x := 0; x < bw; x++ { 53 | mx := min((x + 1) << tileBits, width) 54 | my := min((y + 1) << tileBits, height) 55 | 56 | var best int 57 | var bestEntropy float64 58 | for i := 0; i < 14; i++ { 59 | for j := range accum { 60 | copy(histos[j], accum[j]) 61 | } 62 | 63 | for tx := x << tileBits; tx < mx; tx++ { 64 | for ty := y << tileBits; ty < my; ty++ { 65 | d := applyFilter(pixels, width, tx, ty, i) 66 | 67 | off := ty * width + tx 68 | histos[0][int(uint8(pixels[off].R - d.R))]++ 69 | histos[1][int(uint8(pixels[off].G - d.G))]++ 70 | histos[2][int(uint8(pixels[off].B - d.B))]++ 71 | histos[3][int(uint8(pixels[off].A - d.A))]++ 72 | } 73 | } 74 | 75 | var total float64 76 | for _, histo := range histos { 77 | sum := 0 78 | sumSquares := 0 79 | 80 | for _, count := range histo { 81 | sum += count 82 | sumSquares += count * count 83 | } 84 | 85 | if sum == 0 { 86 | continue 87 | } 88 | 89 | total += 1.0 - float64(sumSquares) / (float64(sum) * float64(sum)) 90 | } 91 | 92 | if i == 0 || total < bestEntropy { 93 | bestEntropy = total 94 | best = i 95 | } 96 | } 97 | 98 | for tx := x << tileBits; tx < mx; tx++ { 99 | for ty := y << tileBits; ty < my; ty++ { 100 | d := applyFilter(pixels, width, tx, ty, best) 101 | 102 | off := ty * width + tx 103 | deltas[off] = color.NRGBA{ 104 | R: uint8(pixels[off].R - d.R), 105 | G: uint8(pixels[off].G - d.G), 106 | B: uint8(pixels[off].B - d.B), 107 | A: uint8(pixels[off].A - d.A), 108 | } 109 | 110 | accum[0][int(uint8(pixels[off].R - d.R))]++ 111 | accum[1][int(uint8(pixels[off].G - d.G))]++ 112 | accum[2][int(uint8(pixels[off].B - d.B))]++ 113 | accum[3][int(uint8(pixels[off].A - d.A))]++ 114 | } 115 | } 116 | 117 | blocks[y * bw + x] = color.NRGBA{0, byte(best), 0, 255} 118 | } 119 | } 120 | 121 | copy(pixels, deltas) 122 | 123 | return tileBits, bw, bh, blocks 124 | } 125 | 126 | func applyFilter(pixels []color.NRGBA, width, x, y, prediction int) color.NRGBA { 127 | if x == 0 && y == 0 { 128 | return color.NRGBA{0, 0, 0, 255} 129 | } else if x == 0 { 130 | return pixels[(y - 1) * width + x] 131 | } else if y == 0 { 132 | return pixels[y * width + (x - 1)] 133 | } 134 | 135 | t := pixels[(y - 1) * width + x] 136 | l := pixels[y * width + (x - 1)] 137 | 138 | tl := pixels[(y - 1) * width + (x - 1)] 139 | tr := pixels[(y - 1) * width + (x + 1)] 140 | 141 | avarage2 := func(a, b color.NRGBA) color.NRGBA { 142 | return color.NRGBA { 143 | uint8((int(a.R) + int(b.R)) / 2), 144 | uint8((int(a.G) + int(b.G)) / 2), 145 | uint8((int(a.B) + int(b.B)) / 2), 146 | uint8((int(a.A) + int(b.A)) / 2), 147 | } 148 | } 149 | 150 | filters := []func(t, l, tl, tr color.NRGBA) color.NRGBA { 151 | func(t, l, tl, tr color.NRGBA) color.NRGBA { return color.NRGBA{0, 0, 0, 255} }, 152 | func(t, l, tl, tr color.NRGBA) color.NRGBA { return l }, 153 | func(t, l, tl, tr color.NRGBA) color.NRGBA { return t }, 154 | func(t, l, tl, tr color.NRGBA) color.NRGBA { return tr }, 155 | func(t, l, tl, tr color.NRGBA) color.NRGBA { return tl }, 156 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 157 | return avarage2(avarage2(l, tr), t) 158 | }, 159 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 160 | return avarage2(l, tl) 161 | }, 162 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 163 | return avarage2(l, t) 164 | }, 165 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 166 | return avarage2(tl, t) 167 | }, 168 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 169 | return avarage2(t, tr) 170 | }, 171 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 172 | return avarage2(avarage2(l, tl), avarage2(t, tr)) 173 | }, 174 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 175 | pr := float64(l.R) + float64(t.R) - float64(tl.R) 176 | pg := float64(l.G) + float64(t.G) - float64(tl.G) 177 | pb := float64(l.B) + float64(t.B) - float64(tl.B) 178 | pa := float64(l.A) + float64(t.A) - float64(tl.A) 179 | 180 | // Manhattan distances to estimates for left and top pixels. 181 | pl := math.Abs(pa - float64(l.A)) + math.Abs(pr - float64(l.R)) + 182 | math.Abs(pg - float64(l.G)) + math.Abs(pb - float64(l.B)) 183 | pt := math.Abs(pa - float64(t.A)) + math.Abs(pr - float64(t.R)) + 184 | math.Abs(pg - float64(t.G)) + math.Abs(pb - float64(t.B)) 185 | 186 | if pl < pt { 187 | return l 188 | } 189 | 190 | return t 191 | }, 192 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 193 | return color.NRGBA{ 194 | uint8(max(min(int(l.R) + int(t.R) - int(tl.R), 255), 0)), 195 | uint8(max(min(int(l.G) + int(t.G) - int(tl.G), 255), 0)), 196 | uint8(max(min(int(l.B) + int(t.B) - int(tl.B), 255), 0)), 197 | uint8(max(min(int(l.A) + int(t.A) - int(tl.A), 255), 0)), 198 | } 199 | }, 200 | func(t, l, tl, tr color.NRGBA) color.NRGBA { 201 | a := avarage2(l, t) 202 | 203 | return color.NRGBA{ 204 | uint8(max(min(int(a.R) + (int(a.R) - int(tl.R)) / 2, 255), 0)), 205 | uint8(max(min(int(a.G) + (int(a.G) - int(tl.G)) / 2, 255), 0)), 206 | uint8(max(min(int(a.B) + (int(a.B) - int(tl.B)) / 2, 255), 0)), 207 | uint8(max(min(int(a.A) + (int(a.A) - int(tl.A)) / 2, 255), 0)), 208 | } 209 | }, 210 | } 211 | 212 | return filters[prediction](t, l, tl, tr) 213 | } 214 | 215 | func applyColorTransform(pixels []color.NRGBA, width, height int) (int, int, int, []color.NRGBA) { 216 | tileBits := 4 217 | tileSize := 1 << tileBits 218 | bw := (width + tileSize - 1) / tileSize 219 | bh := (height + tileSize - 1) / tileSize 220 | 221 | blocks := make([]color.NRGBA, bw * bh) 222 | deltas := make([]color.NRGBA, width * height) 223 | 224 | //TODO: analyze block and pick best Color transform Element (CTE) 225 | cte := color.NRGBA { 226 | R: 1, //red to blue 227 | G: 2, //green to blue 228 | B: 3, //green to red 229 | A: 255, 230 | } 231 | 232 | for y := 0; y < bh; y++ { 233 | for x := 0; x < bw; x++ { 234 | mx := min((x + 1) << tileBits, width) 235 | my := min((y + 1) << tileBits, height) 236 | 237 | for tx := x << tileBits; tx < mx; tx++ { 238 | for ty := y << tileBits; ty < my; ty++ { 239 | off := ty * width + tx 240 | 241 | r := int(int8(pixels[off].R)) 242 | g := int(int8(pixels[off].G)) 243 | b := int(int8(pixels[off].B)) 244 | 245 | b -= int(int8((int16(int8(cte.G)) * int16(g)) >> 5)) 246 | b -= int(int8((int16(int8(cte.R)) * int16(r)) >> 5)) 247 | r -= int(int8((int16(int8(cte.B)) * int16(g)) >> 5)) 248 | 249 | pixels[off].R = uint8(r & 0xff) 250 | pixels[off].B = uint8(b & 0xff) 251 | 252 | deltas[off] = pixels[off] 253 | } 254 | } 255 | 256 | blocks[y * bw + x] = cte 257 | } 258 | } 259 | 260 | copy(pixels, deltas) 261 | 262 | return tileBits, bw, bh, blocks 263 | } 264 | 265 | func applySubtractGreenTransform(pixels []color.NRGBA) { 266 | for i, _ := range pixels { 267 | pixels[i].R = pixels[i].R - pixels[i].G 268 | pixels[i].B = pixels[i].B - pixels[i].G 269 | } 270 | } 271 | 272 | func applyPaletteTransform(pixels *[]color.NRGBA, width, height int) ([]color.NRGBA, int, error) { 273 | var pal []color.NRGBA 274 | for _, p := range (*pixels) { 275 | if !slices.Contains(pal, p) { 276 | pal = append(pal, p) 277 | } 278 | 279 | if len(pal) > 256 { 280 | return nil, 0, errors.New("palette exceeds 256 colors") 281 | } 282 | } 283 | 284 | size := 1 285 | if len(pal) <= 2 { 286 | size = 8 287 | } else if len(pal) <= 4 { 288 | size = 4 289 | } else if len(pal) <= 16 { 290 | size = 2 291 | } 292 | 293 | pw := (width + size - 1) / size 294 | 295 | packed := make([]color.NRGBA, pw * height) 296 | for y := 0; y < height; y++ { 297 | for x := 0; x < pw; x++ { 298 | pack := 0 299 | for i := 0; i < size; i++ { 300 | px := x * size + i 301 | if px >= width { 302 | break 303 | } 304 | 305 | idx := slices.Index(pal, (*pixels)[y * width + px]) 306 | pack |= int(idx) << (i * (8 / size)) 307 | } 308 | 309 | packed[y * pw + x] = color.NRGBA{G: uint8(pack), A: 255} 310 | } 311 | } 312 | 313 | *pixels = packed 314 | 315 | for i := len(pal) - 1; i > 0; i-- { 316 | pal[i] = color.NRGBA{ 317 | R: pal[i].R - pal[i - 1].R, 318 | G: pal[i].G - pal[i - 1].G, 319 | B: pal[i].B - pal[i - 1].B, 320 | A: pal[i].A - pal[i - 1].A, 321 | } 322 | } 323 | 324 | return pal, pw, nil 325 | } 326 | -------------------------------------------------------------------------------- /transform_test.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "reflect" 8 | "encoding/hex" 9 | "crypto/sha256" 10 | //------------------------------ 11 | //imaging 12 | //------------------------------ 13 | "image/color" 14 | //------------------------------ 15 | //testing 16 | //------------------------------ 17 | "testing" 18 | ) 19 | 20 | func TestApplyPredictTransform(t *testing.T) { 21 | for id, tt := range []struct { 22 | width int 23 | height int 24 | expectedBlockWidth int 25 | expectedBlockHeight int 26 | expectedHash string 27 | expectedBlocks []color.NRGBA 28 | expectedBit int 29 | }{ 30 | { // default case 31 | 32, 32 | 32, 33 | 2, 34 | 2, 35 | "d333d3e3bea7503db703dc5608240d7919b584cfa113bb655444c3547a6b8457", 36 | []color.NRGBA{ 37 | {0, 4, 0, 255}, 38 | {0, 4, 0, 255}, 39 | {0, 4, 0, 255}, 40 | {0, 4, 0, 255}, 41 | }, 42 | 4, 43 | }, 44 | { // not power of 2 image res 45 | 33, 46 | 33, 47 | 3, 48 | 3, 49 | "a92e9e0413411cff17aec2abe8adf17c38149bd28ed3230c96ac6379e7055038", 50 | []color.NRGBA{ 51 | {0, 4, 0, 255}, 52 | {0, 4, 0, 255}, 53 | {0, 4, 0, 255}, 54 | {0, 4, 0, 255}, 55 | {0, 4, 0, 255}, 56 | {0, 4, 0, 255}, 57 | {0, 4, 0, 255}, 58 | {0, 4, 0, 255}, 59 | {0, 3, 0, 255}, 60 | }, 61 | 4, 62 | }, 63 | }{ 64 | img := generateTestImageNRGBA(tt.width, tt.height, 64, true) 65 | pixels, err := flatten(img) 66 | if err != nil { 67 | t.Errorf("test %v: unexpected error %v", id, err) 68 | continue 69 | } 70 | 71 | tileBit, bw, bh, blocks := applyPredictTransform(pixels, tt.width, tt.height) 72 | 73 | if bw != tt.expectedBlockWidth { 74 | t.Errorf("test %v: expected block width as %v got %v", id, tt.expectedBlockWidth, bw) 75 | continue 76 | } 77 | 78 | if bh != tt.expectedBlockHeight { 79 | t.Errorf("test %v: expected block height as %v got %v", id, tt.expectedBlockHeight, bh) 80 | continue 81 | } 82 | 83 | if !reflect.DeepEqual(blocks, tt.expectedBlocks) { 84 | t.Errorf("test %v: expected blocks as %v got %v", id, tt.expectedBlocks, blocks) 85 | continue 86 | } 87 | 88 | if tileBit != tt.expectedBit { 89 | t.Errorf("test %v: expected tile bit as %v got %v", id, tt.expectedBit, tileBit) 90 | continue 91 | } 92 | 93 | data := make([]byte, len(pixels) * 4) 94 | for j := 0; j < len(pixels); j++ { 95 | data[j * 4 + 0] = byte(pixels[j].R) 96 | data[j * 4 + 1] = byte(pixels[j].G) 97 | data[j * 4 + 2] = byte(pixels[j].B) 98 | data[j * 4 + 3] = byte(pixels[j].A) 99 | } 100 | 101 | hash := sha256.Sum256(data) 102 | if hex.EncodeToString(hash[:]) != tt.expectedHash { 103 | t.Errorf("test %v: expected hash as %v got %v", id, tt.expectedHash, hash) 104 | continue 105 | } 106 | } 107 | } 108 | 109 | func TestApplyFilter(t *testing.T) { 110 | pixels := []color.NRGBA{ 111 | {R: 100, G: 100, B: 100, A: 255}, {R: 50, G: 50, B: 50, A: 255}, {R: 25, G: 25, B: 25, A: 255}, 112 | {R: 200, G: 200, B: 200, A: 255}, {R: 75, G: 75, B: 75, A: 255}, {R: 0, G: 0, B: 0, A: 0}, 113 | //added extra row for filter 11 if statement check 114 | {R: 100, G: 100, B: 100, A: 255}, {R: 250, G: 250, B: 250, A: 255}, {R: 225, G: 225, B: 225, A: 255}, 115 | {R: 200, G: 200, B: 200, A: 255}, {R: 75, G: 75, B: 75, A: 255}, {R: 0, G: 0, B: 0, A: 0}, 116 | } 117 | 118 | width := 3 119 | 120 | for id, tt := range []struct { 121 | prediction int 122 | x int 123 | y int 124 | expected color.NRGBA 125 | }{ 126 | // x y edge cases 127 | {prediction: 0, x: 0, y: 0, expected: color.NRGBA{R: 0, G: 0, B: 0, A: 255}}, 128 | {prediction: 0, x: 0, y: 1, expected: color.NRGBA{R: 100, G: 100, B: 100, A: 255}}, 129 | {prediction: 0, x: 1, y: 0, expected: color.NRGBA{R: 100, G: 100, B: 100, A: 255}}, 130 | //filter predictions 131 | {prediction: 0, x: 1, y: 1, expected: color.NRGBA{R: 0, G: 0, B: 0, A: 255}}, 132 | {prediction: 1, x: 1, y: 1, expected: color.NRGBA{R: 200, G: 200, B: 200, A: 255}}, 133 | {prediction: 2, x: 1, y: 1, expected: color.NRGBA{R: 50, G: 50, B: 50, A: 255}}, 134 | {prediction: 3, x: 1, y: 1, expected: color.NRGBA{R: 25, G: 25, B: 25, A: 255}}, 135 | {prediction: 4, x: 1, y: 1, expected: color.NRGBA{R: 100, G: 100, B: 100, A: 255}}, 136 | {prediction: 5, x: 1, y: 1, expected: color.NRGBA{R: 81, G: 81, B: 81, A: 255}}, 137 | {prediction: 6, x: 1, y: 1, expected: color.NRGBA{R: 150, G: 150, B: 150, A: 255}}, 138 | {prediction: 7, x: 1, y: 1, expected: color.NRGBA{R: 125, G: 125, B: 125, A: 255}}, 139 | {prediction: 8, x: 1, y: 1, expected: color.NRGBA{R: 75, G: 75, B: 75, A: 255}}, 140 | {prediction: 9, x: 1, y: 1, expected: color.NRGBA{R: 37, G: 37, B: 37, A: 255}}, 141 | {prediction: 10, x: 1, y: 1, expected: color.NRGBA{R: 93, G: 93, B: 93, A: 255}}, 142 | {prediction: 11, x: 1, y: 1, expected: color.NRGBA{R: 200, G: 200, B: 200, A: 255}}, 143 | {prediction: 11, x: 1, y: 3, expected: color.NRGBA{R: 250, G: 250, B: 250, A: 255}}, // diff Manhattan distances 144 | {prediction: 12, x: 1, y: 1, expected: color.NRGBA{R: 150, G: 150, B: 150, A: 255}}, 145 | {prediction: 13, x: 1, y: 1, expected: color.NRGBA{R: 137, G: 137, B: 137, A: 255}}, 146 | } { 147 | got := applyFilter(pixels, width, tt.x, tt.y, tt.prediction) 148 | 149 | if !reflect.DeepEqual(got, tt.expected) { 150 | t.Errorf("test %d: mismatch\nexpected: %+v\n got: %+v", id, tt.expected, got) 151 | } 152 | } 153 | } 154 | 155 | func TestApplyColorTransform(t *testing.T) { 156 | for id, tt := range []struct { 157 | width int 158 | height int 159 | expectedBlockWidth int 160 | expectedBlockHeight int 161 | expectedHash string 162 | expectedBlocks []color.NRGBA 163 | expectedBit int 164 | }{ 165 | { // default case 166 | 32, 167 | 32, 168 | 2, 169 | 2, 170 | "7d2e490f816b7abe5f0f3dde85435a95da2a4295636cbc338689739fb1d936aa", 171 | []color.NRGBA{ 172 | {1, 2, 3, 255}, 173 | {1, 2, 3, 255}, 174 | {1, 2, 3, 255}, 175 | {1, 2, 3, 255}, 176 | }, 177 | 4, 178 | }, 179 | { // non-power-of-2 dimensions 180 | 33, 181 | 33, 182 | 3, 183 | 3, 184 | "be8a424305cc8e044a6fbb16c2d3a14c2ece1fd2733d41f6f9b452790c22ccb8", 185 | []color.NRGBA{ 186 | {1, 2, 3, 255}, 187 | {1, 2, 3, 255}, 188 | {1, 2, 3, 255}, 189 | {1, 2, 3, 255}, 190 | {1, 2, 3, 255}, 191 | {1, 2, 3, 255}, 192 | {1, 2, 3, 255}, 193 | {1, 2, 3, 255}, 194 | {1, 2, 3, 255}, 195 | }, 196 | 4, 197 | }, 198 | } { 199 | img := generateTestImageNRGBA(tt.width, tt.height, 128, true) 200 | pixels, err := flatten(img) 201 | if err != nil { 202 | t.Errorf("test %v: unexpected error %v", id, err) 203 | continue 204 | } 205 | 206 | tileBit, bw, bh, blocks := applyColorTransform(pixels, tt.width, tt.height) 207 | 208 | if bw != tt.expectedBlockWidth { 209 | t.Errorf("test %v: expected block width as %v got %v", id, tt.expectedBlockWidth, bw) 210 | continue 211 | } 212 | 213 | if bh != tt.expectedBlockHeight { 214 | t.Errorf("test %v: expected block height as %v got %v", id, tt.expectedBlockHeight, bh) 215 | continue 216 | } 217 | 218 | if !reflect.DeepEqual(blocks, tt.expectedBlocks) { 219 | t.Errorf("test %v: expected blocks as %v got %v", id, tt.expectedBlocks, blocks) 220 | continue 221 | } 222 | 223 | if tileBit != tt.expectedBit { 224 | t.Errorf("test %v: expected tile bit as %v got %v", id, tt.expectedBit, tileBit) 225 | continue 226 | } 227 | 228 | data := make([]byte, len(pixels)*4) 229 | for j := 0; j < len(pixels); j++ { 230 | data[j*4+0] = byte(pixels[j].R) 231 | data[j*4+1] = byte(pixels[j].G) 232 | data[j*4+2] = byte(pixels[j].B) 233 | data[j*4+3] = byte(pixels[j].A) 234 | } 235 | 236 | hash := sha256.Sum256(data) 237 | hashString := hex.EncodeToString(hash[:]) 238 | 239 | if hashString != tt.expectedHash { 240 | t.Errorf("test %v: expected hash as %v got %v", id, tt.expectedHash, hashString) 241 | continue 242 | } 243 | } 244 | } 245 | 246 | func TestApplySubtractGreenTransform(t *testing.T) { 247 | for id, tt := range []struct { 248 | inputPixels []color.NRGBA 249 | expectedPixels []color.NRGBA 250 | }{ 251 | { 252 | inputPixels: []color.NRGBA{ 253 | {R: 100, G: 50, B: 150}, 254 | }, 255 | expectedPixels: []color.NRGBA{ 256 | {R: 50, G: 50, B: 100}, 257 | }, 258 | }, 259 | { 260 | inputPixels: []color.NRGBA{ 261 | {R: 200, G: 200, B: 150}, 262 | }, 263 | expectedPixels: []color.NRGBA{ 264 | {R: 0, G: 200, B: 206}, 265 | }, 266 | }, 267 | { 268 | inputPixels: []color.NRGBA{ 269 | {R: 0, G: 128, B: 150}, 270 | }, 271 | expectedPixels: []color.NRGBA{ 272 | {R: 128, G: 128, B: 22}, 273 | }, 274 | }, 275 | }{ 276 | pixels := make([]color.NRGBA, len(tt.inputPixels)) 277 | copy(pixels, tt.inputPixels) 278 | 279 | applySubtractGreenTransform(pixels) 280 | 281 | if !reflect.DeepEqual(pixels, tt.expectedPixels) { 282 | t.Errorf("test %d: pixel mismatch\nexpected: %+v\n got: %+v", id, tt.expectedPixels, pixels) 283 | continue 284 | } 285 | } 286 | } 287 | 288 | func TestApplyPaletteTransform(t *testing.T) { 289 | //check for too many colors error 290 | pixels := make([]color.NRGBA, 257) 291 | for i := 0; i < 257; i++ { 292 | pixels[i] = color.NRGBA{ 293 | R: uint8(i % 16 * 16), 294 | G: uint8((i / 16) % 16 * 16), 295 | B: uint8((i / 256) % 16 * 16), 296 | A: 255, 297 | } 298 | } 299 | 300 | _, _, err := applyPaletteTransform(&pixels, 4, 4) 301 | 302 | msg := "palette exceeds 256 colors" 303 | if err == nil || err.Error() != msg { 304 | t.Errorf("test: expected error %v got %v", msg, err) 305 | } 306 | 307 | for id, tt := range []struct { 308 | width int 309 | height int 310 | pixels []color.NRGBA 311 | expectedPalette []color.NRGBA 312 | expectedPixels []color.NRGBA 313 | expectedWidth int 314 | }{ 315 | { 316 | //2 color pal - pack size = 8 317 | width: 3, 318 | height: 2, 319 | pixels: []color.NRGBA{ 320 | {R: 255, G: 0, B: 0, A: 255}, 321 | {R: 0, G: 255, B: 0, A: 255}, 322 | {R: 255, G: 0, B: 0, A: 255}, 323 | {R: 0, G: 255, B: 0, A: 255}, 324 | {R: 255, G: 0, B: 0, A: 255}, 325 | {R: 0, G: 255, B: 0, A: 255}, 326 | }, 327 | expectedPalette: []color.NRGBA{ 328 | {R: 255, G: 0, B: 0, A: 255}, 329 | {R: 1, G: 255, B: 0, A: 0}, 330 | }, 331 | expectedPixels: []color.NRGBA{ 332 | {R: 0, G: 2, B: 0, A: 255}, 333 | {R: 0, G: 5, B: 0, A: 255}, 334 | }, 335 | expectedWidth: 1, 336 | }, 337 | { 338 | //4 color pal - pack size = 4 339 | width: 3, 340 | height: 2, 341 | pixels: []color.NRGBA{ 342 | {R: 255, G: 0, B: 0, A: 255}, 343 | {R: 0, G: 255, B: 0, A: 255}, 344 | {R: 0, G: 0, B: 255, A: 255}, 345 | {R: 255, G: 255, B: 0, A: 255}, 346 | {R: 255, G: 0, B: 0, A: 255}, 347 | {R: 0, G: 255, B: 0, A: 255}, 348 | }, 349 | expectedPalette: []color.NRGBA{ 350 | {R: 255, G: 0, B: 0, A: 255}, 351 | {R: 1, G: 255, B: 0, A: 0}, 352 | {R: 0, G: 1, B: 255, A: 0}, 353 | {R: 255, G: 255, B: 1, A: 0}, 354 | 355 | }, 356 | expectedPixels: []color.NRGBA{ 357 | {R: 0, G: 36, B: 0, A: 255}, 358 | {R: 0, G: 19, B: 0, A: 255}, 359 | }, 360 | expectedWidth: 1, 361 | }, 362 | { 363 | //5 color pal - pack size = 2 364 | width: 3, 365 | height: 2, 366 | pixels: []color.NRGBA{ 367 | {R: 255, G: 0, B: 0, A: 255}, 368 | {R: 0, G: 255, B: 0, A: 255}, 369 | {R: 0, G: 0, B: 255, A: 255}, 370 | {R: 255, G: 255, B: 0, A: 255}, 371 | {R: 255, G: 0, B: 255, A: 255}, 372 | {R: 0, G: 255, B: 0, A: 255}, 373 | }, 374 | expectedPalette: []color.NRGBA{ 375 | {R: 255, G: 0, B: 0, A: 255}, 376 | {R: 1, G: 255, B: 0, A: 0}, 377 | {R: 0, G: 1, B: 255, A: 0}, 378 | {R: 255, G: 255, B: 1, A: 0}, 379 | {R: 0, G: 1, B: 255, A: 0}, 380 | }, 381 | expectedPixels: []color.NRGBA{ 382 | {R: 0, G: 16, B: 0, A: 255}, 383 | {R: 0, G: 2, B: 0, A: 255}, 384 | {R: 0, G: 67, B: 0, A: 255}, 385 | {R: 0, G: 1, B: 0, A: 255}, 386 | }, 387 | expectedWidth: 2, 388 | }, 389 | { 390 | // 16 color palette - pack size = 1 391 | width: 4, 392 | height: 5, 393 | pixels: []color.NRGBA{ 394 | {R: 255, G: 0, B: 0, A: 255}, {R: 0, G: 255, B: 0, A: 255}, {R: 0, G: 0, B: 255, A: 255}, {R: 255, G: 255, B: 0, A: 255}, 395 | {R: 255, G: 0, B: 255, A: 255}, {R: 0, G: 255, B: 255, A: 255}, {R: 128, G: 128, B: 128, A: 255}, {R: 255, G: 128, B: 0, A: 255}, 396 | {R: 128, G: 0, B: 255, A: 255}, {R: 255, G: 128, B: 128, A: 255}, {R: 0, G: 128, B: 128, A: 255}, {R: 128, G: 255, B: 0, A: 255}, 397 | {R: 128, G: 0, B: 128, A: 255}, {R: 0, G: 128, B: 0, A: 255}, {R: 255, G: 255, B: 255, A: 255}, {R: 0, G: 0, B: 0, A: 255}, 398 | {R: 128, G: 0, B: 128, A: 255}, {R: 0, G: 128, B: 0, A: 255}, {R: 255, G: 255, B: 255, A: 255}, {R: 0, G: 13, B: 37, A: 255}, 399 | }, 400 | expectedPalette: []color.NRGBA{ 401 | {R: 255, G: 0, B: 0, A: 255}, 402 | {R: 1, G: 255, B: 0, A: 0}, 403 | {R: 0, G: 1, B: 255, A: 0}, 404 | {R: 255, G: 255, B: 1, A: 0}, 405 | {R: 0, G: 1, B: 255, A: 0}, 406 | {R: 1, G: 255, B: 0, A: 0}, 407 | {R: 128, G: 129, B: 129, A: 0}, 408 | {R: 127, G: 0, B: 128, A: 0}, 409 | {R: 129, G: 128, B: 255, A: 0}, 410 | {R: 127, G: 128, B: 129, A: 0}, 411 | {R: 1, G: 0, B: 0, A: 0}, 412 | {R: 128, G: 127, B: 128, A: 0}, 413 | {R: 0, G: 1, B: 128, A: 0}, 414 | {R: 128, G: 128, B: 128, A: 0}, 415 | {R: 255, G: 127, B: 255, A: 0}, 416 | {R: 1, G: 1, B: 1, A: 0}, 417 | {R: 0, G: 13, B: 37, A: 0}, 418 | }, 419 | expectedPixels: []color.NRGBA{ 420 | {R: 0, G: 0, B: 0, A: 255}, 421 | {R: 0, G: 1, B: 0, A: 255}, 422 | {R: 0, G: 2, B: 0, A: 255}, 423 | {R: 0, G: 3, B: 0, A: 255}, 424 | {R: 0, G: 4, B: 0, A: 255}, 425 | {R: 0, G: 5, B: 0, A: 255}, 426 | {R: 0, G: 6, B: 0, A: 255}, 427 | {R: 0, G: 7, B: 0, A: 255}, 428 | {R: 0, G: 8, B: 0, A: 255}, 429 | {R: 0, G: 9, B: 0, A: 255}, 430 | {R: 0, G: 10, B: 0, A: 255}, 431 | {R: 0, G: 11, B: 0, A: 255}, 432 | {R: 0, G: 12, B: 0, A: 255}, 433 | {R: 0, G: 13, B: 0, A: 255}, 434 | {R: 0, G: 14, B: 0, A: 255}, 435 | {R: 0, G: 15, B: 0, A: 255}, 436 | {R: 0, G: 12, B: 0, A: 255}, 437 | {R: 0, G: 13, B: 0, A: 255}, 438 | {R: 0, G: 14, B: 0, A: 255}, 439 | {R: 0, G: 16, B: 0, A: 255}, 440 | }, 441 | expectedWidth: 4, 442 | }, 443 | } { 444 | // Copy inputPixels to avoid modifying the test case 445 | pixels := make([]color.NRGBA, len(tt.pixels)) 446 | copy(pixels, tt.pixels) 447 | 448 | pal, pw, err := applyPaletteTransform(&pixels, tt.width, tt.height) 449 | if err != nil { 450 | t.Errorf("test %d: unexpected error %v", id, err) 451 | continue 452 | } 453 | 454 | if pw != tt.expectedWidth { 455 | t.Errorf("test %d: expected width %v got %v", id, tt.expectedWidth, pw) 456 | continue 457 | } 458 | 459 | if !reflect.DeepEqual(pal, tt.expectedPalette) { 460 | t.Errorf("test %d: palette mismatch expected %+v got %+v", id, tt.expectedPalette, pal) 461 | continue 462 | } 463 | 464 | if !reflect.DeepEqual(pixels, tt.expectedPixels) { 465 | t.Errorf("test %d: pixel mismatch expected %+v got %+v", id, tt.expectedPixels, pixels) 466 | continue 467 | } 468 | } 469 | } -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "io" 8 | "bytes" 9 | "encoding/binary" 10 | //------------------------------ 11 | //imaging 12 | //------------------------------ 13 | "image" 14 | "image/draw" 15 | "image/color" 16 | //------------------------------ 17 | //errors 18 | //------------------------------ 19 | "errors" 20 | ) 21 | 22 | // Options holds configuration settings for WebP encoding. 23 | // 24 | // Currently, it provides a flag to enable the extended WebP format (VP8X), 25 | // which allows for metadata support such as EXIF, ICC color profiles, and XMP. 26 | // 27 | // Fields: 28 | // - UseExtendedFormat: If true, wraps the VP8L frame inside a VP8X container 29 | // to enable metadata support. This does not affect image compression or 30 | // encoding itself, as VP8L remains the encoding format. 31 | type Options struct { 32 | UseExtendedFormat bool 33 | } 34 | 35 | // Animation holds configuration settings for WebP animations. 36 | // 37 | // It allows encoding a sequence of frames with individual timing and disposal options, 38 | // supporting features like looping and background color settings. 39 | // 40 | // Fields: 41 | // - Images: A list of frames to be displayed in sequence. 42 | // - Durations: Timing for each frame in milliseconds, matching the Images slice. 43 | // - Disposals: Disposal methods for frames after display; 0 = keep, 1 = clear to background. 44 | // - LoopCount: Number of times the animation should repeat; 0 means infinite looping. 45 | // - BackgroundColor: Canvas background color in BGRA order, used for clear operations. 46 | type Animation struct { 47 | Images []image.Image 48 | Durations []uint 49 | Disposals []uint 50 | LoopCount uint16 51 | BackgroundColor uint32 52 | } 53 | 54 | // Encode writes the provided image.Image to the specified io.Writer in WebP format. 55 | // 56 | // This function always encodes the image using VP8L (lossless WebP). If `UseExtendedFormat` 57 | // is enabled, it wraps the VP8L frame inside a VP8X container, allowing the use of metadata 58 | // such as EXIF, ICC color profiles, or XMP metadata. 59 | // 60 | // Note: VP8L already supports transparency, so VP8X is **not required** for alpha support. 61 | // 62 | // Parameters: 63 | // w - The destination writer where the encoded WebP image will be written. 64 | // img - The input image to be encoded. 65 | // o - Pointer to Options containing encoding settings: 66 | // - UseExtendedFormat: If true, wraps the image in a VP8X container to enable 67 | // extended WebP features like metadata. 68 | // 69 | // Returns: 70 | // An error if encoding fails or writing to the io.Writer encounters an issue. 71 | func Encode(w io.Writer, img image.Image, o *Options) error { 72 | stream, hasAlpha, err := writeBitStream(img) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | buf := &bytes.Buffer{} 78 | 79 | if o != nil && o.UseExtendedFormat { 80 | writeChunkVP8X(buf, img.Bounds(), hasAlpha, false) 81 | } 82 | 83 | buf.Write([]byte("VP8L")) 84 | binary.Write(buf, binary.LittleEndian, uint32(stream.Len())) 85 | buf.Write(stream.Bytes()) 86 | 87 | w.Write([]byte("RIFF")) 88 | binary.Write(w, binary.LittleEndian, uint32(4 + buf.Len())) 89 | 90 | w.Write([]byte("WEBP")) 91 | w.Write(buf.Bytes()) 92 | 93 | return nil 94 | } 95 | 96 | // EncodeAll writes the provided animation sequence to the specified io.Writer in WebP format. 97 | // 98 | // This function encodes a list of frames as a WebP animation using the VP8X container, which 99 | // supports features like looping, frame timing, disposal methods, and background color settings. 100 | // Each frame is individually compressed using the VP8L (lossless) format. 101 | // 102 | // Note: Even if `UseExtendedFormat` is not explicitly set, animations always use the VP8X container 103 | // because it is required for WebP animation support. 104 | // 105 | // Parameters: 106 | // w - The destination writer where the encoded WebP animation will be written. 107 | // ani - Pointer to Animation containing the frames and animation settings: 108 | // - Images: List of frames to encode. 109 | // - Durations: Display times for each frame in milliseconds. 110 | // - Disposals: Disposal methods after frame display (keep or clear). 111 | // - LoopCount: Number of times the animation should loop (0 = infinite). 112 | // - BackgroundColor: Background color for the canvas, used when clearing. 113 | // o - Pointer to Options containing additional encoding settings: 114 | // - UseExtendedFormat: Currently unused for animations, but accepted for consistency. 115 | // 116 | // Returns: 117 | // An error if encoding fails or writing to the io.Writer encounters an issue. 118 | func EncodeAll(w io.Writer, ani *Animation, o *Options) error { 119 | frames, alpha, err := writeFrames(ani) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | var bounds image.Rectangle 125 | for _, img := range ani.Images { 126 | bounds.Max.X = max(img.Bounds().Max.X, bounds.Max.X) 127 | bounds.Max.Y = max(img.Bounds().Max.Y, bounds.Max.Y) 128 | } 129 | 130 | buf := &bytes.Buffer{} 131 | 132 | writeChunkVP8X(buf, bounds, alpha, true) 133 | 134 | buf.Write([]byte("ANIM")) 135 | binary.Write(buf, binary.LittleEndian, uint32(6)) 136 | binary.Write(buf, binary.LittleEndian, uint32(ani.BackgroundColor)) 137 | binary.Write(buf, binary.LittleEndian, uint16(ani.LoopCount)) 138 | 139 | buf.Write(frames.Bytes()) 140 | 141 | w.Write([]byte("RIFF")) 142 | binary.Write(w, binary.LittleEndian, uint32(4 + buf.Len())) 143 | 144 | w.Write([]byte("WEBP")) 145 | w.Write(buf.Bytes()) 146 | 147 | return nil 148 | } 149 | 150 | func writeChunkVP8X(buf *bytes.Buffer, bounds image.Rectangle, flagAlpha, flagAni bool) { 151 | buf.Write([]byte("VP8X")) 152 | binary.Write(buf, binary.LittleEndian, uint32(10)) 153 | 154 | var flags byte 155 | if flagAni { 156 | flags |= 1 << 1 157 | } 158 | 159 | if flagAlpha { 160 | flags |= 1 << 4 161 | } 162 | 163 | binary.Write(buf, binary.LittleEndian, flags) 164 | buf.Write([]byte{0x00, 0x00, 0x00}) 165 | 166 | dx := bounds.Dx() - 1 167 | dy := bounds.Dy() - 1 168 | 169 | buf.Write([]byte{byte(dx), byte(dx >> 8), byte(dx >> 16)}) 170 | buf.Write([]byte{byte(dy), byte(dy >> 8), byte(dy >> 16)}) 171 | } 172 | 173 | func writeFrames(ani *Animation) (*bytes.Buffer, bool, error) { 174 | if len(ani.Images) == 0 { 175 | return nil, false, errors.New("must provide at least one image") 176 | } 177 | 178 | if len(ani.Images) != len(ani.Durations) { 179 | return nil, false, errors.New("mismatched image and durations lengths") 180 | } 181 | 182 | if len(ani.Images) != len(ani.Disposals) { 183 | return nil, false, errors.New("mismatched image and disposals lengths") 184 | } 185 | 186 | for i := 0; i < len(ani.Images); i++ { 187 | ani.Durations[i] = min(ani.Durations[i], 1 << 24 - 1) 188 | ani.Disposals[i] = min(ani.Disposals[i], 1) 189 | } 190 | 191 | buf := &bytes.Buffer{} 192 | 193 | var hasAlpha bool 194 | for i, img := range ani.Images { 195 | stream, alpha, err := writeBitStream(img) 196 | if err != nil { 197 | return nil, false, err 198 | } 199 | 200 | hasAlpha = hasAlpha || alpha 201 | 202 | w := &bitWriter{Buffer: buf} 203 | w.writeBytes([]byte("ANMF")) 204 | w.writeBits(uint64(16 + 8 + stream.Len()), 32) 205 | 206 | // WebP specs requires frame offsets to be divided by 2 207 | w.writeBits(uint64(img.Bounds().Min.X / 2), 24) 208 | w.writeBits(uint64(img.Bounds().Min.Y / 2), 24) 209 | 210 | w.writeBits(uint64(img.Bounds().Dx() - 1), 24) 211 | w.writeBits(uint64(img.Bounds().Dy() - 1), 24) 212 | 213 | w.writeBits(uint64(ani.Durations[i]), 24) 214 | w.writeBits(uint64(ani.Disposals[i]), 1) 215 | w.writeBits(uint64(0), 1) 216 | w.writeBits(uint64(0), 6) 217 | 218 | w.writeBytes([]byte("VP8L")) 219 | w.writeBits(uint64(stream.Len()), 32) 220 | w.Buffer.Write(stream.Bytes()) 221 | } 222 | 223 | return buf, hasAlpha, nil 224 | } 225 | 226 | func writeBitStream(img image.Image) (*bytes.Buffer, bool, error) { 227 | if img == nil { 228 | return nil, false, errors.New("image is nil") 229 | } 230 | 231 | if img.Bounds().Dx() < 1 || img.Bounds().Dy() < 1 { 232 | return nil, false, errors.New("invalid image size") 233 | } 234 | 235 | if img.Bounds().Dx() > 1 << 14 || img.Bounds().Dy() > 1 << 14 { 236 | return nil, false, errors.New("invalid image size") 237 | } 238 | 239 | _, isIndexed := img.(*image.Paletted) 240 | 241 | rgba := image.NewNRGBA(image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy())) 242 | draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src) 243 | 244 | b := &bytes.Buffer{} 245 | s := &bitWriter{Buffer: b} 246 | 247 | writeBitStreamHeader(s, rgba.Bounds(), !rgba.Opaque()) 248 | 249 | var transforms [4]bool 250 | transforms[transformPredict] = !isIndexed 251 | transforms[transformColor] = false 252 | transforms[transformSubGreen] = !isIndexed 253 | transforms[transformColorIndexing] = isIndexed 254 | 255 | err := writeBitStreamData(s, rgba, 4, transforms) 256 | if err != nil { 257 | return nil, false, err 258 | } 259 | 260 | s.alignByte() 261 | 262 | if b.Len() % 2 != 0 { 263 | b.Write([]byte{0x00}) 264 | } 265 | 266 | return b, !rgba.Opaque(), nil 267 | } 268 | 269 | func writeBitStreamHeader(w *bitWriter, bounds image.Rectangle, hasAlpha bool) { 270 | w.writeBits(0x2f, 8) 271 | 272 | w.writeBits(uint64(bounds.Dx() - 1), 14) 273 | w.writeBits(uint64(bounds.Dy() - 1), 14) 274 | 275 | if hasAlpha { 276 | w.writeBits(1, 1) 277 | } else { 278 | w.writeBits(0, 1) 279 | } 280 | 281 | w.writeBits(0, 3) 282 | } 283 | 284 | func writeBitStreamData(w *bitWriter, img image.Image, colorCacheBits int, transforms [4]bool) error { 285 | pixels, err := flatten(img) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | width := img.Bounds().Dx() 291 | height := img.Bounds().Dy() 292 | 293 | if transforms[transformColorIndexing] { 294 | w.writeBits(1, 1) 295 | w.writeBits(3, 2) 296 | 297 | pal, pw, err := applyPaletteTransform(&pixels, width, height) 298 | if err != nil { 299 | return err 300 | } 301 | 302 | width = pw 303 | 304 | w.writeBits(uint64(len(pal) - 1), 8); 305 | writeImageData(w, pal, len(pal), 1, false, colorCacheBits); 306 | } 307 | 308 | if transforms[transformSubGreen] { 309 | w.writeBits(1, 1) 310 | w.writeBits(2, 2) 311 | 312 | applySubtractGreenTransform(pixels) 313 | } 314 | 315 | if transforms[transformColor] { 316 | w.writeBits(1, 1) 317 | w.writeBits(1, 2) 318 | 319 | bits, bw, bh, blocks := applyColorTransform(pixels, width, height) 320 | 321 | w.writeBits(uint64(bits - 2), 3); 322 | writeImageData(w, blocks, bw, bh, false, colorCacheBits) 323 | } 324 | 325 | if transforms[transformPredict] { 326 | w.writeBits(1, 1) 327 | w.writeBits(0, 2) 328 | 329 | bits, bw, bh, blocks := applyPredictTransform(pixels, width, height) 330 | 331 | w.writeBits(uint64(bits - 2), 3); 332 | writeImageData(w, blocks, bw, bh, false, colorCacheBits) 333 | } 334 | 335 | w.writeBits(0, 1) // end of transform 336 | writeImageData(w, pixels, width, height, true, colorCacheBits) 337 | 338 | return nil 339 | } 340 | 341 | func writeImageData(w *bitWriter, pixels []color.NRGBA, width, height int, isRecursive bool, colorCacheBits int) { 342 | if colorCacheBits > 0 { 343 | w.writeBits(1, 1) 344 | w.writeBits(uint64(colorCacheBits), 4) 345 | } else { 346 | w.writeBits(0, 1) 347 | } 348 | 349 | if isRecursive { 350 | w.writeBits(0, 1) 351 | } 352 | 353 | encoded := encodeImageData(pixels, width, height, colorCacheBits) 354 | histos := computeHistograms(encoded, colorCacheBits) 355 | 356 | var codes [][]huffmanCode 357 | for i := 0; i < 5; i++ { 358 | // WebP specs requires Huffman codes with maximum depth of 15 359 | c := buildhuffmanCodes(histos[i], 15) 360 | codes = append(codes, c) 361 | 362 | writehuffmanCodes(w, c) 363 | } 364 | 365 | for i := 0; i < len(encoded); i ++ { 366 | w.writeCode(codes[0][encoded[i + 0]]) 367 | if encoded[i + 0] < 256 { 368 | w.writeCode(codes[1][encoded[i + 1]]) 369 | w.writeCode(codes[2][encoded[i + 2]]) 370 | w.writeCode(codes[3][encoded[i + 3]]) 371 | i += 3 372 | } else if encoded[i + 0] < 256 + 24 { 373 | cnt := prefixEncodeBits(int(encoded[i + 0]) - 256) 374 | w.writeBits(uint64(encoded[i + 1]), cnt); 375 | 376 | w.writeCode(codes[4][encoded[i + 2]]) 377 | 378 | cnt = prefixEncodeBits(int(encoded[i + 2])) 379 | w.writeBits(uint64(encoded[i + 3]), cnt); 380 | i += 3 381 | } 382 | } 383 | } 384 | 385 | func encodeImageData(pixels []color.NRGBA, width, height, colorCacheBits int) []int { 386 | head := make([]int, 1 << 14) 387 | prev := make([]int, len(pixels)) 388 | cache := make([]color.NRGBA, 1 << colorCacheBits) 389 | 390 | encoded := make([]int, len(pixels) * 4) 391 | cnt := 0 392 | 393 | var distances = []int { 394 | 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, 395 | 101, 78, 58, 42, 26, 16, 8, 2, 0, 3, 9, 17, 27, 43, 59, 79, 396 | 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63, 87, 397 | 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91, 398 | 110, 99, 82, 66, 48, 35, 30, 24, 22, 25, 31, 36, 49, 67, 83, 100, 399 | 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65, 77, 95, 109, 400 | 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114, 401 | 119, 116, 111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117, 402 | } 403 | 404 | for i := 0; i < len(pixels); i++ { 405 | if i + 2 < len(pixels) { 406 | h := hash(pixels[i + 0], 14) 407 | h ^= hash(pixels[i + 1], 14) * 0x9e3779b9 408 | h ^= hash(pixels[i + 2], 14) * 0x85ebca6b 409 | h = h % (1 << 14) 410 | 411 | cur := head[h] - 1 412 | prev[i] = head[h] 413 | head[h] = i + 1 414 | 415 | dis := 0 416 | streak := 0 417 | for j := 0; j < 8; j++ { 418 | // 1 << 20: sliding window size is 2^20 (1,048,576) per WebP specs. 419 | // 120: reserved margin for offset adjustments. 420 | if cur == -1 || i - cur >= 1 << 20 - 120 { 421 | break 422 | } 423 | 424 | l := 0 425 | // Limit the maximum match length to 4096 pixels per WebP specs. 426 | for i + l < len(pixels) && l < 4096 { 427 | if pixels[i + l] != pixels[cur + l] { 428 | break 429 | } 430 | l++ 431 | } 432 | 433 | if l > streak { 434 | streak = l 435 | dis = i - cur 436 | } 437 | 438 | cur = prev[cur] - 1 439 | } 440 | 441 | // Only use the match if it is at least 3 pixels long per WebP specs. 442 | if streak >= 3 { 443 | for j := 0; j < streak; j++ { 444 | h := hash(pixels[i + j], colorCacheBits) 445 | cache[h] = pixels[i + j] 446 | } 447 | 448 | y := dis / width 449 | x := dis - y * width 450 | 451 | code := dis + 120 452 | if x <= 8 && y < 8 { 453 | code = distances[y * 16 + 8 - x] + 1 454 | } else if x > width - 8 && y < 7 { 455 | code = distances[(y + 1) * 16 + 8 + (width - x)] + 1 456 | } 457 | 458 | s, l := prefixEncodeCode(streak) 459 | encoded[cnt + 0] = int(s + 256) 460 | encoded[cnt + 1] = int(l) 461 | 462 | s, l = prefixEncodeCode(code) 463 | encoded[cnt + 2] = int(s) 464 | encoded[cnt + 3] = int(l) 465 | cnt += 4 466 | 467 | i += streak - 1 468 | continue 469 | } 470 | } 471 | 472 | p := pixels[i] 473 | if colorCacheBits > 0 { 474 | hash := hash(p, colorCacheBits) 475 | 476 | if cache[hash] == p { 477 | encoded[cnt] = int(hash + 256 + 24) 478 | cnt++ 479 | continue 480 | } 481 | 482 | cache[hash] = p 483 | } 484 | 485 | encoded[cnt+0] = int(p.G) 486 | encoded[cnt+1] = int(p.R) 487 | encoded[cnt+2] = int(p.B) 488 | encoded[cnt+3] = int(p.A) 489 | cnt += 4 490 | } 491 | 492 | return encoded[:cnt] 493 | } 494 | 495 | func prefixEncodeCode(n int) (int, int) { 496 | if n <= 5 { 497 | return max(0, n - 1), 0 498 | } 499 | 500 | shift := 0 501 | rem := n - 1 502 | for rem > 3 { 503 | rem >>= 1 504 | shift += 1 505 | } 506 | 507 | if rem == 2 { 508 | return 2 + 2 * shift, n - (2 << shift) - 1 509 | } 510 | 511 | return 3 + 2 * shift, n - (3 << shift) - 1 512 | } 513 | 514 | func prefixEncodeBits(prefix int) int { 515 | if prefix < 4 { 516 | return 0 517 | } 518 | 519 | return (prefix - 2) >> 1 520 | } 521 | 522 | func hash(c color.NRGBA, shifts int) uint32 { 523 | //hash formula including magic number 0x1e35a7bd comes directly from WebP specs! 524 | x := uint32(c.A) << 24 | uint32(c.R) << 16 | uint32(c.G) << 8 | uint32(c.B) 525 | return (x * 0x1e35a7bd) >> (32 - min(shifts, 32)) 526 | } 527 | 528 | func computeHistograms(pixels []int, colorCacheBits int) [][]int { 529 | c := 0 530 | if colorCacheBits > 0 { 531 | c = 1 << colorCacheBits 532 | } 533 | 534 | histos := [][]int{ 535 | make([]int, 256 + 24 + c), 536 | make([]int, 256), 537 | make([]int, 256), 538 | make([]int, 256), 539 | make([]int, 40), 540 | } 541 | 542 | for i := 0; i < len(pixels); i++ { 543 | histos[0][pixels[i]]++ 544 | if(pixels[i] < 256) { 545 | histos[1][pixels[i + 1]]++ 546 | histos[2][pixels[i + 2]]++ 547 | histos[3][pixels[i + 3]]++ 548 | i += 3 549 | } else if pixels[i] < 256 + 24 { 550 | histos[4][pixels[i + 2]]++ 551 | i += 3 552 | } 553 | } 554 | 555 | return histos 556 | } 557 | 558 | func flatten(img image.Image) ([]color.NRGBA, error) { 559 | w := img.Bounds().Dx() 560 | h := img.Bounds().Dy() 561 | 562 | rgba, ok := img.(*image.NRGBA) 563 | if !ok { 564 | return nil, errors.New("unsupported image format") 565 | } 566 | 567 | pixels := make([]color.NRGBA, w * h) 568 | for y := 0; y < h; y++ { 569 | for x := 0; x < w; x++ { 570 | i := rgba.PixOffset(x, y) 571 | s := rgba.Pix[i : i + 4 : i + 4] 572 | 573 | pixels[y * w + x].R = uint8(s[0]) 574 | pixels[y * w + x].G = uint8(s[1]) 575 | pixels[y * w + x].B = uint8(s[2]) 576 | pixels[y * w + x].A = uint8(s[3]) 577 | } 578 | } 579 | 580 | return pixels, nil 581 | } -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package nativewebp 2 | 3 | import ( 4 | //------------------------------ 5 | //general 6 | //------------------------------ 7 | "bytes" 8 | "reflect" 9 | //------------------------------ 10 | //imaging 11 | //------------------------------ 12 | "image" 13 | "image/color" 14 | //------------------------------ 15 | //testing 16 | //------------------------------ 17 | "testing" 18 | ) 19 | 20 | func generateTestImageNRGBA(width int, height int, brightness float64, hasAlpha bool) image.Image { 21 | dest := image.NewNRGBA(image.Rect(0, 0, width, height)) 22 | for y := 0; y < height; y++ { 23 | for x := 0; x < width; x++ { 24 | n := uint8(float64(x ^ y) * brightness) 25 | var c color.Color 26 | 27 | a := uint8(255) 28 | if hasAlpha { 29 | a = n 30 | } 31 | if y < height / 2 { 32 | if x < width / 2 { 33 | c = color.RGBA{n, 0, 0, a} 34 | } else { 35 | c = color.RGBA{0, n, 0, a} 36 | } 37 | } else { 38 | if x < width / 2 { 39 | c = color.RGBA{0, 0, n, a} 40 | } else { 41 | c = color.RGBA{n, n, 0, a} 42 | } 43 | } 44 | dest.Set(x, y, c) 45 | } 46 | } 47 | return dest 48 | } 49 | 50 | func TestEncodeErrors(t *testing.T) { 51 | for id, tt := range []struct { 52 | img image.Image 53 | expectedMsg string 54 | }{ 55 | { 56 | nil, 57 | "image is nil", 58 | }, 59 | { 60 | image.NewNRGBA(image.Rectangle{}), 61 | "invalid image size", 62 | }, 63 | { 64 | image.NewNRGBA(image.Rect(0, 0, 1 << 14 + 1, 1 << 14 + 1)), 65 | "invalid image size", 66 | }, 67 | }{ 68 | b := &bytes.Buffer{} 69 | 70 | err := Encode(b, tt.img, nil) 71 | if err == nil { 72 | t.Errorf("test %v: expected error %v got nil", id, tt.expectedMsg) 73 | continue 74 | } 75 | 76 | if err != nil && err.Error() != tt.expectedMsg { 77 | t.Errorf("test %v: expected error %v got %v", id, tt.expectedMsg, err) 78 | continue 79 | } 80 | } 81 | } 82 | 83 | func TestEncode(t *testing.T) { 84 | for id, tt := range []struct { 85 | img image.Image 86 | UseExtendedFormat bool 87 | expectedBytes []byte 88 | }{ 89 | { 90 | generateTestImageNRGBA(8, 8, 64, true), 91 | false, 92 | []byte { 93 | 0x52, 0x49, 0x46, 0x46, 0xd0, 0x00, 0x00, 0x00, 94 | 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4c, 95 | 0xc4, 0x00, 0x00, 0x00, 0x2f, 0x07, 0xc0, 0x01, 96 | 0x10, 0x8d, 0x52, 0x09, 0x22, 0xfa, 0x1f, 0x12, 97 | 0x06, 0x04, 0x1b, 0x89, 0x09, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x00, 0x00, 0x50, 0xee, 0x15, 0x00, 102 | 0x80, 0xb2, 0x3e, 0x37, 0x78, 0x04, 0xc8, 0x34, 103 | 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 105 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 106 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 107 | 0x70, 0x02, 0x64, 0x9a, 0x75, 0x00, 0x00, 0x00, 108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x38, 0x01, 0x32, 0xcd, 112 | 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0xe0, 0x04, 0x08, 0x70, 0x2e, 0xa8, 0x24, 0x55, 117 | 0xed, 0x0d, 0x88, 0x96, 0xf9, 0x6e, 0x56, 0x6b, 118 | 0xf3, 0x35, 0x1e, 0x1d, 0x7d, 0x5f, 0x38, 0xdc, 119 | 0x7e, 0xbc, 0x41, 0xc6, 0x5a, 0x36, 0xeb, 0x03, 120 | }, 121 | }, 122 | { 123 | generateTestImageNRGBA(8, 8, 64, true), 124 | true, 125 | []byte { 126 | 0x52, 0x49, 0x46, 0x46, 0xe2, 0x00, 0x00, 0x00, 127 | 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 128 | 0x0a, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 129 | 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, 0x56, 0x50, 130 | 0x38, 0x4c, 0xc4, 0x00, 0x00, 0x00, 0x2f, 0x07, 131 | 0xc0, 0x01, 0x10, 0x8d, 0x52, 0x09, 0x22, 0xfa, 132 | 0x1f, 0x12, 0x06, 0x04, 0x1b, 0x89, 0x09, 0x00, 133 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 134 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 135 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 136 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xee, 137 | 0x15, 0x00, 0x80, 0xb2, 0x3e, 0x37, 0x78, 0x04, 138 | 0xc8, 0x34, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00, 139 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 140 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 142 | 0x00, 0x00, 0x70, 0x02, 0x64, 0x9a, 0x75, 0x00, 143 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 144 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 145 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 146 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x01, 147 | 0x32, 0xcd, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 148 | 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 150 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 151 | 0x00, 0x00, 0xe0, 0x04, 0x08, 0x70, 0x2e, 0xa8, 152 | 0x24, 0x55, 0xed, 0x0d, 0x88, 0x96, 0xf9, 0x6e, 153 | 0x56, 0x6b, 0xf3, 0x35, 0x1e, 0x1d, 0x7d, 0x5f, 154 | 0x38, 0xdc, 0x7e, 0xbc, 0x41, 0xc6, 0x5a, 0x36, 155 | 0xeb, 0x03, 156 | }, 157 | }, 158 | }{ 159 | b := &bytes.Buffer{} 160 | Encode(b, tt.img, &Options{UseExtendedFormat: tt.UseExtendedFormat}) 161 | 162 | result := b.Bytes() 163 | 164 | if !bytes.Equal(result, tt.expectedBytes) { 165 | t.Errorf("test %v: BitStream mismatch. Got %s, expected %s", id, result, tt.expectedBytes) 166 | } 167 | } 168 | } 169 | 170 | func TestEncodeAllErrors(t *testing.T) { 171 | frame := generateTestImageNRGBA(0, 0, 64, true) 172 | 173 | for id, tt := range []struct { 174 | ani *Animation 175 | expectedMsg string 176 | }{ 177 | { 178 | &Animation { 179 | Images: []image.Image{}, 180 | }, 181 | "must provide at least one image", 182 | }, 183 | { 184 | &Animation { 185 | Images: []image.Image{ 186 | frame, 187 | }, 188 | }, 189 | "mismatched image and durations lengths", 190 | }, 191 | { 192 | &Animation { 193 | Images: []image.Image{ 194 | frame, 195 | }, 196 | Durations: []uint { 197 | 100, 198 | }, 199 | }, 200 | "mismatched image and disposals lengths", 201 | }, 202 | }{ 203 | b := &bytes.Buffer{} 204 | 205 | err := EncodeAll(b, tt.ani, nil) 206 | if err == nil { 207 | t.Errorf("test %v: expected error %v got nil", id, tt.expectedMsg) 208 | continue 209 | } 210 | 211 | if err != nil && err.Error() != tt.expectedMsg { 212 | t.Errorf("test %v: expected error %v got %v", id, tt.expectedMsg, err) 213 | continue 214 | } 215 | } 216 | } 217 | 218 | func TestEncodeAll(t *testing.T) { 219 | frame1 := generateTestImageNRGBA(4, 4, 64, true) 220 | frame2 := generateTestImageNRGBA(8, 8, 64, true) 221 | 222 | for id, tt := range []struct { 223 | ani *Animation 224 | expectedBytes []byte 225 | }{ 226 | { 227 | &Animation { 228 | Images: []image.Image{ 229 | frame1, 230 | }, 231 | Durations: []uint { 232 | 100, 233 | }, 234 | Disposals: []uint { 235 | 1, 236 | }, 237 | }, 238 | []byte { 239 | 0x52, 0x49, 0x46, 0x46, 0xf0, 0x00, 0x00, 0x00, 240 | 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 241 | 0x0a, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 242 | 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x41, 0x4e, 243 | 0x49, 0x4d, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 244 | 0x00, 0x00, 0x00, 0x00, 0x41, 0x4e, 0x4d, 0x46, 245 | 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 246 | 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 247 | 0x64, 0x00, 0x00, 0x01, 0x56, 0x50, 0x38, 0x4c, 248 | 0xac, 0x00, 0x00, 0x00, 0x2f, 0x03, 0xc0, 0x00, 249 | 0x10, 0x8d, 0x52, 0x17, 0x22, 0xfa, 0x1f, 0x12, 250 | 0x04, 0x64, 0xd8, 0x26, 0x05, 0x00, 0x00, 0x00, 251 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 252 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 253 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 254 | 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x80, 255 | 0x1e, 0xd8, 0x21, 0x40, 0xa6, 0x59, 0x07, 0x00, 256 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 257 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 258 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 259 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0b, 0x20, 260 | 0x92, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 261 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 262 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 263 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 264 | 0x00, 0x0b, 0x20, 0x12, 0x03, 0x00, 0x00, 0x00, 265 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 266 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 267 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 268 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x72, 0xba, 0xc0, 269 | 0x93, 0x87, 0xb5, 0xe5, 0xab, 0xec, 0x7e, 0x3c, 270 | }, 271 | }, 272 | { 273 | &Animation { 274 | Images: []image.Image{ 275 | frame1, 276 | frame2, 277 | }, 278 | Durations: []uint { 279 | 200, 280 | 100, 281 | }, 282 | Disposals: []uint { 283 | 0, 284 | 1, 285 | }, 286 | }, 287 | []byte { 288 | 0x52, 0x49, 0x46, 0x46, 0xd4, 0x01, 0x00, 0x00, 289 | 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 290 | 0x0a, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 291 | 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, 0x41, 0x4e, 292 | 0x49, 0x4d, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 293 | 0x00, 0x00, 0x00, 0x00, 0x41, 0x4e, 0x4d, 0x46, 294 | 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 295 | 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 296 | 0xc8, 0x00, 0x00, 0x00, 0x56, 0x50, 0x38, 0x4c, 297 | 0xac, 0x00, 0x00, 0x00, 0x2f, 0x03, 0xc0, 0x00, 298 | 0x10, 0x8d, 0x52, 0x17, 0x22, 0xfa, 0x1f, 0x12, 299 | 0x04, 0x64, 0xd8, 0x26, 0x05, 0x00, 0x00, 0x00, 300 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 301 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 302 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 303 | 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x80, 304 | 0x1e, 0xd8, 0x21, 0x40, 0xa6, 0x59, 0x07, 0x00, 305 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 306 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 307 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 308 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0b, 0x20, 309 | 0x92, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 310 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 311 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 312 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 313 | 0x00, 0x0b, 0x20, 0x12, 0x03, 0x00, 0x00, 0x00, 314 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 315 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 316 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 317 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x72, 0xba, 0xc0, 318 | 0x93, 0x87, 0xb5, 0xe5, 0xab, 0xec, 0x7e, 0x3c, 319 | 0x41, 0x4e, 0x4d, 0x46, 0xdc, 0x00, 0x00, 0x00, 320 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 321 | 0x00, 0x07, 0x00, 0x00, 0x64, 0x00, 0x00, 0x01, 322 | 0x56, 0x50, 0x38, 0x4c, 0xc4, 0x00, 0x00, 0x00, 323 | 0x2f, 0x07, 0xc0, 0x01, 0x10, 0x8d, 0x52, 0x09, 324 | 0x22, 0xfa, 0x1f, 0x12, 0x06, 0x04, 0x1b, 0x89, 325 | 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 326 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 327 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 328 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 329 | 0x50, 0xee, 0x15, 0x00, 0x80, 0xb2, 0x3e, 0x37, 330 | 0x78, 0x04, 0xc8, 0x34, 0xeb, 0x00, 0x00, 0x00, 331 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 332 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 333 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 334 | 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x64, 0x9a, 335 | 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 336 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 337 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 338 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 339 | 0x38, 0x01, 0x32, 0xcd, 0x0f, 0x00, 0x00, 0x00, 340 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 341 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 342 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 343 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0x04, 0x08, 0x70, 344 | 0x2e, 0xa8, 0x24, 0x55, 0xed, 0x0d, 0x88, 0x96, 345 | 0xf9, 0x6e, 0x56, 0x6b, 0xf3, 0x35, 0x1e, 0x1d, 346 | 0x7d, 0x5f, 0x38, 0xdc, 0x7e, 0xbc, 0x41, 0xc6, 347 | 0x5a, 0x36, 0xeb, 0x03, 348 | }, 349 | }, 350 | }{ 351 | b := &bytes.Buffer{} 352 | EncodeAll(b, tt.ani, nil) 353 | 354 | result := b.Bytes() 355 | 356 | if !bytes.Equal(result, tt.expectedBytes) { 357 | t.Errorf("test %v: BitStream mismatch. Got %s, expected %s", id, result, tt.expectedBytes) 358 | } 359 | } 360 | } 361 | 362 | func TestWriteChunkVP8X(t *testing.T) { 363 | for id, tt := range []struct { 364 | bounds image.Rectangle 365 | flagAlpha bool 366 | flagAni bool 367 | expectedBits []byte 368 | }{ 369 | { 370 | bounds: image.Rect(0, 0, 16, 16), 371 | flagAlpha: false, 372 | flagAni: false, 373 | expectedBits: []byte{ 374 | 'V', 'P', '8', 'X', 375 | 0x0a, 0x00, 0x00, 0x00, // Chunk size (10) 376 | 0x00, // Flags 377 | 0x00, 0x00, 0x00, // Reserved 378 | 0x0f, 0x00, 0x00, // Width - 1 = 15 379 | 0x0f, 0x00, 0x00, // Height - 1 = 15 380 | }, 381 | }, 382 | { 383 | bounds: image.Rect(0, 0, 32, 32), 384 | flagAlpha: true, 385 | flagAni: false, 386 | expectedBits: []byte{ 387 | 'V', 'P', '8', 'X', 388 | 0x0a, 0x00, 0x00, 0x00, 389 | 0x10, // Flags (alpha bit set) 390 | 0x00, 0x00, 0x00, 391 | 0x1f, 0x00, 0x00, 392 | 0x1f, 0x00, 0x00, 393 | }, 394 | }, 395 | { 396 | bounds: image.Rect(0, 0, 64, 128), 397 | flagAlpha: false, 398 | flagAni: true, 399 | expectedBits: []byte{ 400 | 'V', 'P', '8', 'X', 401 | 0x0a, 0x00, 0x00, 0x00, 402 | 0x02, // Flags (animation bit set) 403 | 0x00, 0x00, 0x00, 404 | 0x3f, 0x00, 0x00, // Width - 1 = 63 405 | 0x7f, 0x00, 0x00, // Height - 1 = 127 406 | }, 407 | }, 408 | { 409 | bounds: image.Rect(0, 0, 256, 256), 410 | flagAlpha: true, 411 | flagAni: true, 412 | expectedBits: []byte{ 413 | 'V', 'P', '8', 'X', 414 | 0x0a, 0x00, 0x00, 0x00, 415 | 0x12, // Flags (alpha + animation bits set) 416 | 0x00, 0x00, 0x00, 417 | 0xff, 0x00, 0x00, // Width - 1 = 255 418 | 0xff, 0x00, 0x00, // Height - 1 = 255 419 | }, 420 | }, 421 | }{ 422 | buffer := &bytes.Buffer{} 423 | writeChunkVP8X(buffer, tt.bounds, tt.flagAlpha, tt.flagAni) 424 | 425 | if !bytes.Equal(buffer.Bytes(), tt.expectedBits) { 426 | t.Errorf("test %d: buffer mismatch expected: %v got: %v\n", id, tt.expectedBits, buffer.Bytes()) 427 | continue 428 | } 429 | } 430 | } 431 | 432 | func TestWriteFramesErrors(t *testing.T) { 433 | frame := generateTestImageNRGBA(0, 0, 64, true) 434 | 435 | for id, tt := range []struct { 436 | ani *Animation 437 | expectedMsg string 438 | }{ 439 | { 440 | &Animation { 441 | Images: []image.Image{}, 442 | }, 443 | "must provide at least one image", 444 | }, 445 | { 446 | &Animation { 447 | Images: []image.Image{ 448 | frame, 449 | }, 450 | }, 451 | "mismatched image and durations lengths", 452 | }, 453 | { 454 | &Animation { 455 | Images: []image.Image{ 456 | frame, 457 | }, 458 | Durations: []uint { 459 | 100, 460 | }, 461 | }, 462 | "mismatched image and disposals lengths", 463 | }, 464 | { 465 | // Note: although this test is grouped with writeFrames error tests, 466 | // it specifically targets an error inside writeBitStream, which is called by writeFrames 467 | &Animation { 468 | Images: []image.Image{ 469 | frame, 470 | }, 471 | Durations: []uint { 472 | 100, 473 | }, 474 | Disposals: []uint { 475 | 1, 476 | }, 477 | }, 478 | "invalid image size", 479 | }, 480 | }{ 481 | _, _, err := writeFrames(tt.ani) 482 | if err == nil { 483 | t.Errorf("test %v: expected error %v got nil", id, tt.expectedMsg) 484 | continue 485 | } 486 | 487 | if err != nil && err.Error() != tt.expectedMsg { 488 | t.Errorf("test %v: expected error %v got %v", id, tt.expectedMsg, err) 489 | continue 490 | } 491 | } 492 | } 493 | 494 | 495 | func TestWriteFrames(t *testing.T) { 496 | frame1 := generateTestImageNRGBA(12, 12, 64, false) 497 | frame2 := generateTestImageNRGBA(16, 16, 64, true) 498 | 499 | for id, tt := range []struct { 500 | ani *Animation 501 | expectedAlpha bool 502 | expectedBits []byte 503 | }{ 504 | { 505 | ani: &Animation { 506 | Images: []image.Image{ 507 | frame1, 508 | }, 509 | Durations: []uint { 510 | 100, 511 | }, 512 | Disposals: []uint { 513 | 1, 514 | }, 515 | }, 516 | expectedAlpha: false, 517 | expectedBits: []byte{ 518 | 0x41, 0x4e, 0x4d, 0x46, 0xd2, 0x00, 0x00, 0x00, 519 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 520 | 0x00, 0x0b, 0x00, 0x00, 0x64, 0x00, 0x00, 0x01, 521 | 0x56, 0x50, 0x38, 0x4c, 0xba, 0x00, 0x00, 0x00, 522 | 0x2f, 0x0b, 0xc0, 0x02, 0x00, 0x8d, 0x52, 0x09, 523 | 0x22, 0xfa, 0x1f, 0x12, 0x06, 0x04, 0xd8, 0x86, 524 | 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 525 | 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 526 | 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 527 | 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 528 | 0x00, 0xa0, 0xc9, 0x06, 0x00, 0x20, 0x07, 0xce, 529 | 0xbf, 0x22, 0x40, 0xa6, 0x19, 0x00, 0x00, 0x00, 530 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 531 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 532 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 533 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0xd3, 534 | 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 535 | 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 536 | 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 537 | 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 538 | 0x00, 0xc2, 0x00, 0xe1, 0x28, 0xde, 0x8e, 0x02, 539 | 0x00, 0x00, 0x00, 0x04, 0x4b, 0xe3, 0x54, 0xd7, 540 | 0x26, 0x78, 0xed, 0xb2, 0xf0, 0xda, 0x51, 0xd7, 541 | 0xfa, 0x9d, 0xf2, 0x23, 0x44, 0xf7, 0x86, 0xbf, 542 | 0x11, 0xf4, 0x60, 0xc3, 0xa1, 0x87, 0xb9, 0x9c, 543 | 0x7c, 0xb0, 0x80, 0xb6, 0x14, 0xd2, 0xbe, 0xea, 544 | 0xa1, 0xd4, 0x38, 0x4b, 0x47, 0xac, 0x0d, 0x7f, 545 | 0x03, 0x00, 546 | }, 547 | }, 548 | { 549 | ani: &Animation { 550 | Images: []image.Image{ 551 | frame1, 552 | frame2, 553 | }, 554 | Durations: []uint { 555 | 100, 556 | 20, 557 | }, 558 | Disposals: []uint { 559 | 0, 560 | 0, 561 | }, 562 | }, 563 | expectedAlpha: true, 564 | expectedBits: []byte{ 565 | 0x41, 0x4e, 0x4d, 0x46, 0xd2, 0x00, 0x00, 0x00, 566 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 567 | 0x00, 0x0b, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 568 | 0x56, 0x50, 0x38, 0x4c, 0xba, 0x00, 0x00, 0x00, 569 | 0x2f, 0x0b, 0xc0, 0x02, 0x00, 0x8d, 0x52, 0x09, 570 | 0x22, 0xfa, 0x1f, 0x12, 0x06, 0x04, 0xd8, 0x86, 571 | 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 572 | 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 573 | 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 574 | 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 575 | 0x00, 0xa0, 0xc9, 0x06, 0x00, 0x20, 0x07, 0xce, 576 | 0xbf, 0x22, 0x40, 0xa6, 0x19, 0x00, 0x00, 0x00, 577 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 578 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 579 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 580 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0xd3, 581 | 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 582 | 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 583 | 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 584 | 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 585 | 0x00, 0xc2, 0x00, 0xe1, 0x28, 0xde, 0x8e, 0x02, 586 | 0x00, 0x00, 0x00, 0x04, 0x4b, 0xe3, 0x54, 0xd7, 587 | 0x26, 0x78, 0xed, 0xb2, 0xf0, 0xda, 0x51, 0xd7, 588 | 0xfa, 0x9d, 0xf2, 0x23, 0x44, 0xf7, 0x86, 0xbf, 589 | 0x11, 0xf4, 0x60, 0xc3, 0xa1, 0x87, 0xb9, 0x9c, 590 | 0x7c, 0xb0, 0x80, 0xb6, 0x14, 0xd2, 0xbe, 0xea, 591 | 0xa1, 0xd4, 0x38, 0x4b, 0x47, 0xac, 0x0d, 0x7f, 592 | 0x03, 0x00, 0x41, 0x4e, 0x4d, 0x46, 0xfc, 0x00, 593 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 594 | 0x0f, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x14, 0x00, 595 | 0x00, 0x00, 0x56, 0x50, 0x38, 0x4c, 0xe4, 0x00, 596 | 0x00, 0x00, 0x2f, 0x0f, 0xc0, 0x03, 0x10, 0x8d, 597 | 0x52, 0x09, 0x22, 0xfa, 0x1f, 0x12, 0x06, 0x04, 598 | 0x1b, 0x89, 0xc9, 0x00, 0x00, 0x00, 0x00, 0x00, 599 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 600 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 601 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 602 | 0x00, 0x00, 0x40, 0xf9, 0xff, 0x3a, 0x36, 0x00, 603 | 0x38, 0xd6, 0xe7, 0x07, 0x43, 0x80, 0x4c, 0xb3, 604 | 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 605 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 606 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 607 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 608 | 0x27, 0x40, 0xa6, 0x59, 0x07, 0x00, 0x00, 0x00, 609 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 610 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 611 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 612 | 0x00, 0x00, 0x00, 0x80, 0x13, 0x20, 0xd3, 0xfc, 613 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 614 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 615 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 616 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 617 | 0x0c, 0x10, 0x68, 0x83, 0x0f, 0xe1, 0x09, 0x00, 618 | 0x00, 0x00, 0xce, 0x01, 0x21, 0x26, 0x57, 0x49, 619 | 0xae, 0xdf, 0xe0, 0xbb, 0xa1, 0x65, 0xb5, 0x32, 620 | 0x41, 0xd6, 0x3b, 0x5a, 0x33, 0x41, 0xca, 0xf9, 621 | 0xcd, 0x62, 0xef, 0xce, 0x03, 0x98, 0x1e, 0x7d, 622 | 0x6f, 0x4f, 0xce, 0xc5, 0xed, 0xeb, 0xc1, 0x71, 623 | 0x66, 0x93, 0x55, 0x76, 0xcb, 0x56, 0x76, 0xfb, 624 | 0x20, 0x65, 0xf7, 0xfd, 0x18, 0x00, 625 | }, 626 | }, 627 | }{ 628 | 629 | buffer, alpha, err := writeFrames(tt.ani) 630 | if err != nil { 631 | t.Errorf("test %v: unexpected error %v", id, err) 632 | continue 633 | } 634 | 635 | if alpha != tt.expectedAlpha { 636 | t.Errorf("test %v: expected alpha as %v got %v", id, tt.expectedAlpha, alpha) 637 | continue 638 | } 639 | 640 | if !bytes.Equal(buffer.Bytes(), tt.expectedBits) { 641 | t.Errorf("test %d: buffer mismatch expected: %v got: %v\n", id, tt.expectedBits, buffer.Bytes()) 642 | continue 643 | } 644 | } 645 | } 646 | 647 | func TestWriteBitStreamHeader(t *testing.T) { 648 | for id, tt := range []struct { 649 | bounds image.Rectangle 650 | hasAlpha bool 651 | expectedBits []byte 652 | }{ 653 | // Test case with no alpha channel 654 | { 655 | bounds: image.Rect(0, 0, 16, 16), 656 | hasAlpha: false, 657 | expectedBits: []byte{ 658 | 0x2f, // Header prefix 659 | 0x0f, 0xc0, // Width - 1 (14 bits: 15) + first 6 bits of Height - 1 660 | 0x03, 0x00, // Remaining bits of Height - 1 (14 bits: 15) + no alpha + padding 661 | }, 662 | }, 663 | // Test case with alpha channel 664 | { 665 | bounds: image.Rect(0, 0, 32, 32), 666 | hasAlpha: true, 667 | expectedBits: []byte{ 668 | 0x2f, // Header prefix 669 | 0x1f, 0xc0, // Width - 1 (14 bits: 31) + first 6 bits of Height - 1 670 | 0x07, 0x10, // Remaining bits of Height - 1 (14 bits: 31) + alpha + padding 671 | }, 672 | }, 673 | // Larger rectangle with no alpha 674 | { 675 | bounds: image.Rect(0, 0, 128, 64), 676 | hasAlpha: false, 677 | expectedBits: []byte{ 678 | 0x2f, // Header prefix 679 | 0x7f, 0xc0, // Width - 1 (14 bits: 127) + first 6 bits of Height - 1 680 | 0x0f, 0x00, // Remaining bits of Height - 1 (14 bits: 63) + no alpha + padding 681 | }, 682 | }, 683 | }{ 684 | buffer := &bytes.Buffer{} 685 | writer := &bitWriter{ 686 | Buffer: buffer, 687 | BitBuffer: 0, 688 | BitBufferSize: 0, 689 | } 690 | 691 | writeBitStreamHeader(writer, tt.bounds, tt.hasAlpha) 692 | 693 | if !bytes.Equal(buffer.Bytes(), tt.expectedBits) { 694 | t.Errorf("test %d: buffer mismatch expected: %v got: %v\n", id, tt.expectedBits, buffer.Bytes()) 695 | continue 696 | } 697 | } 698 | } 699 | 700 | func TestWritBitStreamErrors(t *testing.T) { 701 | for id, tt := range []struct { 702 | img image.Image 703 | expectedMsg string 704 | }{ 705 | { 706 | nil, 707 | "image is nil", 708 | }, 709 | { 710 | image.NewNRGBA(image.Rectangle{}), 711 | "invalid image size", 712 | }, 713 | { 714 | image.NewNRGBA(image.Rect(0, 0, 1 << 14 + 1, 1 << 14 + 1)), 715 | "invalid image size", 716 | }, 717 | }{ 718 | _, _, err := writeBitStream(tt.img) 719 | if err == nil { 720 | t.Errorf("test %v: expected error %v got nil", id, tt.expectedMsg) 721 | continue 722 | } 723 | 724 | if err != nil && err.Error() != tt.expectedMsg { 725 | t.Errorf("test %v: expected error %v got %v", id, tt.expectedMsg, err) 726 | continue 727 | } 728 | } 729 | } 730 | 731 | func TestWriteBitStream(t *testing.T) { 732 | for id, tt := range []struct { 733 | img image.Image 734 | expectedAlpha bool 735 | expectedBytes []byte 736 | }{ 737 | { 738 | generateTestImageNRGBA(8, 8, 64, true), 739 | true, 740 | []byte { 741 | 0x2f, 0x07, 0xc0, 0x01, 0x10, 0x8d, 0x52, 0x09, 742 | 0x22, 0xfa, 0x1f, 0x12, 0x06, 0x04, 0x1b, 0x89, 743 | 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 744 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 745 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 746 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 747 | 0x50, 0xee, 0x15, 0x00, 0x80, 0xb2, 0x3e, 0x37, 748 | 0x78, 0x04, 0xc8, 0x34, 0xeb, 0x00, 0x00, 0x00, 749 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 750 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 751 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 752 | 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x64, 0x9a, 753 | 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 754 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 755 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 756 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 757 | 0x38, 0x01, 0x32, 0xcd, 0x0f, 0x00, 0x00, 0x00, 758 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 759 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 760 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 761 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0x04, 0x08, 0x70, 762 | 0x2e, 0xa8, 0x24, 0x55, 0xed, 0x0d, 0x88, 0x96, 763 | 0xf9, 0x6e, 0x56, 0x6b, 0xf3, 0x35, 0x1e, 0x1d, 764 | 0x7d, 0x5f, 0x38, 0xdc, 0x7e, 0xbc, 0x41, 0xc6, 765 | 0x5a, 0x36, 0xeb, 0x03, 766 | }, 767 | }, 768 | { 769 | generateTestImageNRGBA(8, 8, 64, false), 770 | false, 771 | []byte { 772 | 0x2f, 0x07, 0xc0, 0x01, 0x00, 0x8d, 0x52, 0x09, 773 | 0x22, 0xfa, 0x1f, 0x12, 0x04, 0x04, 0xdb, 0xa6, 774 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 775 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 776 | 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 777 | 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 778 | 0x80, 0x0f, 0x00, 0x00, 0x72, 0xe0, 0x58, 0x87, 779 | 0x00, 0x99, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 780 | 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 781 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 782 | 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 783 | 0x00, 0x00, 0x00, 0x40, 0x80, 0x4c, 0x13, 0x00, 784 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 785 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 786 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 787 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 788 | 0x25, 0x40, 0xeb, 0xaa, 0x3f, 0x59, 0x54, 0x94, 789 | 0xe3, 0xb0, 0x21, 0x66, 0x98, 0xee, 0x58, 0xa7, 790 | 0x4d, 0xd1, 0x8f, 0x2f, 0x1e, 0x19, 0x82, 0x12, 791 | 0x86, 0xff, 0x78, 0xd6, 0xb2, 0xdd, 0xd1, 0x0d, 792 | }, 793 | }, 794 | }{ 795 | b, alpha, err := writeBitStream(tt.img) 796 | if err != nil { 797 | t.Errorf("test %v: unexpected error %v", id, err) 798 | continue 799 | } 800 | 801 | if alpha != tt.expectedAlpha { 802 | t.Errorf("test %v: expected alpha as %v got %v", id, tt.expectedAlpha, alpha) 803 | continue 804 | } 805 | 806 | result := b.Bytes() 807 | 808 | if !bytes.Equal(result, tt.expectedBytes) { 809 | t.Errorf("test %v: BitStream mismatch. Got %s, expected %s", id, result, tt.expectedBytes) 810 | continue 811 | } 812 | } 813 | } 814 | 815 | func TestWriteBitStreamDataErrors(t *testing.T) { 816 | imgpal := image.NewNRGBA(image.Rect(0, 0, 257, 1)) 817 | for i := 0; i < 257; i++ { 818 | imgpal.Set(i, 0, color.NRGBA{ 819 | R: uint8(i % 16 * 16), 820 | G: uint8((i / 16) % 16 * 16), 821 | B: uint8((i / 256) % 16 * 16), 822 | A: 255, 823 | }) 824 | } 825 | 826 | for id, tt := range []struct { 827 | img image.Image 828 | transforms [4]bool 829 | expectedMsg string 830 | }{ 831 | { 832 | image.NewRGBA(image.Rectangle{}), 833 | [4]bool{ false, false, false, false, }, 834 | "unsupported image format", 835 | }, 836 | { 837 | imgpal, 838 | [4]bool{ false, false, false, true, }, 839 | "palette exceeds 256 colors", 840 | }, 841 | }{ 842 | b := &bytes.Buffer{} 843 | s := &bitWriter{Buffer: b} 844 | 845 | err := writeBitStreamData(s, tt.img, 0, tt.transforms) 846 | if err == nil { 847 | t.Errorf("test %v: expected error %v got nil", id, tt.expectedMsg) 848 | continue 849 | } 850 | 851 | if err != nil && err.Error() != tt.expectedMsg { 852 | t.Errorf("test %v: expected error %v got %v", id, tt.expectedMsg, err) 853 | continue 854 | } 855 | } 856 | } 857 | 858 | func TestWriteBitStreamData(t *testing.T) { 859 | img := generateTestImageNRGBA(8, 8, 64, true) 860 | 861 | for id, tt := range []struct { 862 | transforms [4]bool 863 | colorCacheBits int 864 | expectedBytes []byte 865 | }{ 866 | { 867 | [4]bool{ 868 | false, //transformPredict 869 | false, //transformColor 870 | true, //transformSubGreen 871 | false, //transformColorIndexing 872 | }, 873 | 0, 874 | []byte{ 875 | 0x85, 0x00, 0x22, 0x09, 0x00, 0x00, 0x00, 0x00, 876 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 877 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 878 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 879 | 0x00, 0x00, 0x00, 0x98, 0x01, 0x00, 0x80, 0x00, 880 | 0x22, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 881 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 882 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 883 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 884 | 0x00, 0xb0, 0x00, 0x22, 0x69, 0x00, 0x00, 0x00, 885 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 886 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 887 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 888 | 0x00, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x82, 0x08, 889 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 890 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 891 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 892 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 893 | 0x02, 0x30, 0x2d, 0x1b, 0x54, 0x56, 0x55, 0x9b, 894 | 0xc0, 0xb6, 0x2a, 0x41, 0x75, 0xd5, 0x6a, 0x56, 895 | 0x55, 0x83, 0x4a, 0xdb, 0x32, 0xd7, 0x4a, 0x00, 896 | 0x58, 0x8e, 0x07, 0xc9, 0x54, 0x9a, 0x05, 0x3c, 897 | 0x97, 0x04, 0xe9, 0xd4, 0xca, 0xa6, 0xd2, 0x20, 898 | 0xc9, 0x73, 0xec, 0x9a, 0x04, 899 | }, 900 | }, 901 | { 902 | [4]bool{ 903 | false, //transformPredict 904 | false, //transformColor 905 | true, //transformSubGreen 906 | false, //transformColorIndexing 907 | }, 908 | 8, 909 | []byte{ 910 | 0x15, 0x21, 0x20, 0xd8, 0x36, 0x03, 0x00, 0x00, 911 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 912 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 913 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 914 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 915 | 0x40, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 916 | 0x00, 0x00, 0x00, 0x1c, 0x66, 0x00, 0x00, 0x00, 917 | 0x00, 0x03, 0x00, 0x00, 0x80, 0x3b, 0x00, 0x00, 918 | 0x00, 0xc0, 0x01, 0x00, 0x00, 0x83, 0x3b, 0x00, 919 | 0x00, 0x00, 0xc0, 0x01, 0x02, 0x88, 0xa4, 0x01, 920 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 921 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 922 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 923 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x02, 924 | 0x88, 0xe4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 925 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 926 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 927 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 928 | 0x00, 0xc0, 0x02, 0x88, 0x04, 0x00, 0x00, 0x00, 929 | 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 930 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 931 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 932 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, 0xc0, 0x2e, 933 | 0x3b, 0x76, 0xa3, 0xa4, 0x50, 0x0e, 0xee, 0x2a, 934 | 0xfe, 0x71, 0x8d, 0xf3, 0xa9, 0xbb, 0xc2, 0xb5, 935 | 0xc0, 0x9c, 0x99, 0x79, 0x44, 0x82, 0x38, 0x79, 936 | 0xbb, 0x99, 0xc3, 0x35, 0xc7, 0xa4, 0xdf, 0x4e, 937 | 0xd7, 938 | }, 939 | }, 940 | { 941 | [4]bool{ 942 | false, //transformPredict 943 | true, //transformColor 944 | false, //transformSubGreen 945 | false, //transformColorIndexing 946 | }, 947 | 0, 948 | []byte{ 949 | 0x93, 0x0a, 0x64, 0x07, 0xfa, 0x1f, 0x10, 0x40, 950 | 0x24, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 951 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 952 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 953 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 954 | 0x00, 0x33, 0x00, 0x00, 0x10, 0x40, 0x24, 0x0d, 955 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 956 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 957 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 958 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 959 | 0x40, 0xa6, 0x69, 0x07, 0x00, 0x00, 0x00, 0x00, 960 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 961 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 962 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 963 | 0x00, 0x00, 0x80, 0x0b, 0x20, 0x88, 0x00, 0x00, 964 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 965 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 966 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 967 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x10, 968 | 0xa6, 0x65, 0x13, 0xc5, 0x52, 0xd9, 0x24, 0x6c, 969 | 0xab, 0x48, 0x94, 0x4b, 0xab, 0x59, 0x2a, 0x13, 970 | 0x45, 0xdb, 0x32, 0xd7, 0x22, 0x41, 0x70, 0x79, 971 | 0x7c, 0x22, 0x33, 0x2b, 0x9b, 0x4b, 0xf0, 0x79, 972 | 0x99, 0x44, 0x76, 0xd6, 0xca, 0xcd, 0xca, 0x26, 973 | 0x32, 0xf9, 0x3c, 0xee, 0x9a, 0x49, 974 | }, 975 | }, 976 | { 977 | [4]bool{ 978 | false, //transformPredict 979 | true, //transformColor 980 | false, //transformSubGreen 981 | false, //transformColorIndexing 982 | }, 983 | 8, 984 | []byte{ 985 | 0x53, 0xac, 0x40, 0x76, 0xa0, 0xff, 0x21, 0x42, 986 | 0x40, 0xb0, 0x6d, 0x06, 0x00, 0x00, 0x00, 0x00, 987 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 988 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 989 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 990 | 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x80, 0x00, 991 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 992 | 0x00, 0x00, 0x0c, 0x00, 0x00, 0x38, 0x0c, 0x06, 993 | 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x87, 994 | 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x06, 995 | 0x87, 0x03, 0x04, 0x10, 0x49, 0x03, 0x00, 0x00, 996 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 997 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 998 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 999 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x05, 0x10, 0x89, 1000 | 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1001 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1002 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1003 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 1004 | 0x05, 0x10, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 1005 | 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 1006 | 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 1007 | 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 1008 | 0x00, 0x00, 0x00, 0xba, 0x80, 0x2d, 0x1b, 0xdb, 1009 | 0x28, 0x29, 0x94, 0x93, 0xb7, 0x8b, 0x7f, 0x5c, 1010 | 0xf3, 0x7c, 0xea, 0xed, 0x74, 0x2d, 0x30, 0x67, 1011 | 0x66, 0x1e, 0x29, 0x89, 0x74, 0x70, 0x57, 0x33, 1012 | 0x87, 0x6b, 0x8c, 0x49, 0xdf, 0x15, 0xae, 1013 | }, 1014 | }, 1015 | { 1016 | [4]bool{ 1017 | true, //transformPredict 1018 | false, //transformColor 1019 | false, //transformSubGreen 1020 | false, //transformColorIndexing 1021 | }, 1022 | 0, 1023 | []byte{ 1024 | 0x91, 0x12, 0x44, 0xf4, 0x3f, 0x60, 0x80, 0x6c, 1025 | 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1026 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1027 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1028 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1029 | 0x00, 0x65, 0x77, 0x00, 0x00, 0x04, 0x10, 0x49, 1030 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1031 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1032 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1033 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 1034 | 0x05, 0x10, 0x49, 0x03, 0x00, 0x00, 0x00, 0x00, 1035 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1036 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1037 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1038 | 0x00, 0x00, 0x80, 0x09, 0x90, 0x69, 0x7e, 0x00, 1039 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 1040 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 1041 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1042 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 1043 | 0x40, 0xc0, 0x0d, 0x80, 0x08, 0x00, 0x06, 0x43, 1044 | 0x58, 0x69, 0x0f, 0x08, 0x43, 0x65, 0x7b, 0x78, 1045 | 0x08, 0x9d, 0x9e, 0x0e, 0xb3, 0x6c, 0x16, 0x1b, 1046 | 0x5d, 0xaf, 0xbb, 0xbd, 0x3e, 1047 | }, 1048 | }, 1049 | { 1050 | [4]bool{ 1051 | true, //transformPredict 1052 | false, //transformColor 1053 | false, //transformSubGreen 1054 | false, //transformColorIndexing 1055 | }, 1056 | 8, 1057 | []byte{ 1058 | 0x51, 0x2c, 0x41, 0x44, 0xff, 0x43, 0xc4, 0x80, 1059 | 0x60, 0x23, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 1060 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1061 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1062 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1063 | 0x00, 0x00, 0x00, 0xca, 0xbd, 0x02, 0x00, 0x50, 1064 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 1065 | 0x05, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 1066 | 0xe0, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 1067 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 1068 | 0x00, 0x40, 0x00, 0x91, 0x1c, 0x00, 0x00, 0x00, 1069 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1070 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1071 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1072 | 0x00, 0x00, 0x00, 0x00, 0x58, 0x00, 0x91, 0x34, 1073 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1074 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1075 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1076 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 1077 | 0x00, 0x99, 0xe6, 0x07, 0x00, 0x00, 0x00, 0x00, 1078 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 1079 | 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 1080 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1081 | 0x00, 0x00, 0x00, 0x70, 0x02, 0x04, 0x72, 0x16, 1082 | 0xa9, 0x90, 0x52, 0x7b, 0x43, 0x44, 0x98, 0xef, 1083 | 0x66, 0xc5, 0xe6, 0x6b, 0x3c, 0x0c, 0xef, 0x0b, 1084 | 0xc3, 0xf6, 0xd3, 0x0d, 0x3a, 0xd6, 0xba, 0x59, 1085 | 0x1f, 1086 | }, 1087 | }, 1088 | { 1089 | [4]bool{ 1090 | true, //transformPredict 1091 | false, //transformColor 1092 | true, //transformSubGreen 1093 | false, //transformColorIndexing 1094 | }, 1095 | 0, 1096 | []byte{ 1097 | 0x8d, 0x94, 0x20, 0xa2, 0xff, 0x01, 0x03, 0x64, 1098 | 0xdb, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1099 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1101 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1102 | 0x00, 0x28, 0xbb, 0x03, 0x00, 0x40, 0x80, 0x4c, 1103 | 0xb3, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1104 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1105 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1106 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1107 | 0x00, 0x27, 0x40, 0xa6, 0xd9, 0x0f, 0x00, 0x00, 1108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x20, 0xd3, 1112 | 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1113 | 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1114 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1116 | 0x00, 0x4e, 0x80, 0x80, 0x3b, 0x00, 0xa2, 0x06, 1117 | 0xc0, 0xc1, 0x10, 0xd6, 0xd6, 0x1e, 0x10, 0x86, 1118 | 0xda, 0xb6, 0x87, 0x87, 0x50, 0xa9, 0xa9, 0x30, 1119 | 0x97, 0x9b, 0x8b, 0x8c, 0xbc, 0xd7, 0xf9, 0x5e, 1120 | 0x1f, 1121 | }, 1122 | }, 1123 | { 1124 | [4]bool{ 1125 | true, //transformPredict 1126 | false, //transformColor 1127 | true, //transformSubGreen 1128 | false, //transformColorIndexing 1129 | }, 1130 | 8, 1131 | []byte{ 1132 | 0x8d, 0x62, 0x09, 0x22, 0xfa, 0x1f, 0x22, 0x06, 1133 | 0x04, 0x1b, 0x89, 0x09, 0x00, 0x00, 0x00, 0x00, 1134 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1135 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1136 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1137 | 0x00, 0x00, 0x00, 0x50, 0xee, 0x15, 0x00, 0x80, 1138 | 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1139 | 0x2b, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 1140 | 0x00, 0x07, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 1141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 1142 | 0x00, 0x00, 0x04, 0xc8, 0x34, 0xeb, 0x00, 0x00, 1143 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1144 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1145 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1146 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x02, 0x64, 1147 | 0x9a, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1148 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1150 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1151 | 0x00, 0x38, 0x01, 0x32, 0xcd, 0x0f, 0x00, 0x00, 1152 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 1153 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 1154 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1155 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x04, 0x08, 1156 | 0x70, 0x2e, 0xa8, 0x24, 0x55, 0xed, 0x0d, 0x88, 1157 | 0x96, 0xf9, 0x6e, 0x56, 0x6b, 0xf3, 0x35, 0x1e, 1158 | 0x1d, 0x7d, 0x5f, 0x38, 0xdc, 0x7e, 0xbc, 0x41, 1159 | 0xc6, 0x5a, 0x36, 0xeb, 1160 | }, 1161 | }, 1162 | { // paletted image 1163 | [4]bool{ 1164 | false, //transformPredict 1165 | false, //transformColor 1166 | false, //transformSubGreen 1167 | true, //transformColorIndexing 1168 | }, 1169 | 4, 1170 | []byte{ 1171 | 0x67, 0x48, 0x06, 0xc8, 0xb6, 0xd9, 0x01, 0x00, 1172 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1173 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1174 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 1176 | 0x00, 0x8e, 0x00, 0x40, 0x00, 0x91, 0x34, 0x00, 1177 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1178 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1179 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1180 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x00, 1181 | 0x91, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1182 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1183 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1184 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1185 | 0x00, 0xf8, 0x40, 0x80, 0xf1, 0x9b, 0x41, 0xc9, 1186 | 0x39, 0x5d, 0x24, 0x08, 0x08, 0x00, 0x89, 0x99, 1187 | 0x18, 0x04, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x60, 1188 | 0x00, 0x00, 0x30, 0x00, 0x30, 0x00, 0x01, 0x00, 1189 | 0x00, 0x0c, 0x00, 0x0c, 0x08, 0x00, 0x00, 0x00, 1190 | 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 1191 | 0x00, 0x00, 0x00, 0x80, 0xf8, 0xa7, 0xe7, 0x88, 1192 | 0xfe, 0x07, 0xc6, 0x54, 0x1c, 0xf9, 0x7c, 0x71, 1193 | 0x1b, 0x9d, 0xa9, 0xdc, 0x64, 0x40, 0x0d, 0x5d, 1194 | 0xf9, 0xc9, 0x07, 0x5b, 1195 | }, 1196 | }, 1197 | }{ 1198 | b := &bytes.Buffer{} 1199 | s := &bitWriter{Buffer: b} 1200 | 1201 | err := writeBitStreamData(s, img, tt.colorCacheBits, tt.transforms) 1202 | if err != nil { 1203 | t.Fatalf("test %v: writeBitStreamData returned error: %v", id, err) 1204 | } 1205 | 1206 | result := b.Bytes() 1207 | 1208 | if !bytes.Equal(result, tt.expectedBytes) { 1209 | t.Errorf("test %v: BitStream mismatch. Got %s, expected %s", id, result, tt.expectedBytes) 1210 | } 1211 | } 1212 | } 1213 | 1214 | func TestWriteImageData(t *testing.T) { 1215 | for id, tt := range []struct { 1216 | inputPixels []color.NRGBA 1217 | width int 1218 | height int 1219 | isRecursive bool 1220 | colorCacheBits int 1221 | expectedBits []byte 1222 | }{ 1223 | { 1224 | inputPixels: []color.NRGBA{ 1225 | {R: 100, G: 50, B: 150, A: 255}, 1226 | {R: 200, G: 100, B: 50, A: 255}, 1227 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1228 | }, 1229 | width: 3, 1230 | height: 1, 1231 | isRecursive: false, 1232 | colorCacheBits: 2, 1233 | expectedBits: []byte{ 1234 | 0x45, 0x00, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 1235 | 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 1236 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1237 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1238 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x4f, 1239 | 0x86, 0x7c, 0x19, 0xcb, 0xfe, 0x47, 1240 | }, 1241 | }, 1242 | { 1243 | inputPixels: []color.NRGBA{ 1244 | {R: 100, G: 50, B: 150, A: 255}, 1245 | {R: 200, G: 100, B: 50, A: 255}, 1246 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1247 | }, 1248 | width: 3, 1249 | height: 1, 1250 | isRecursive: false, 1251 | colorCacheBits: 0, 1252 | expectedBits: []byte{ 1253 | 0x2e, 0x43, 0x76, 0x32, 0xe4, 0xcb, 0x58, 0xf6, 1254 | 0x3f, 0x38, 1255 | }, 1256 | }, 1257 | { 1258 | inputPixels: []color.NRGBA{ 1259 | {R: 100, G: 50, B: 150, A: 255}, 1260 | {R: 200, G: 100, B: 50, A: 255}, 1261 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1262 | }, 1263 | width: 3, 1264 | height: 1, 1265 | isRecursive: true, 1266 | colorCacheBits: 2, 1267 | expectedBits: []byte{ 1268 | 0x85, 0x00, 0x22, 0x01, 0x00, 0x00, 0x00, 0x00, 1269 | 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1270 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1271 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 1272 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 1273 | 0x0c, 0xf9, 0x32, 0x96, 0xfd, 0x8f, 0xd4, 1274 | }, 1275 | }, 1276 | { 1277 | inputPixels: []color.NRGBA{ 1278 | {R: 100, G: 50, B: 150, A: 255}, 1279 | {R: 200, G: 100, B: 50, A: 255}, 1280 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1281 | }, 1282 | width: 3, 1283 | height: 1, 1284 | isRecursive: true, 1285 | colorCacheBits: 0, 1286 | expectedBits: []byte{ 1287 | 0x5c, 0x86, 0xec, 0x64, 0xc8, 0x97, 0xb1, 0xec, 1288 | 0x7f, 0x70, 1289 | }, 1290 | }, 1291 | } { 1292 | buffer := &bytes.Buffer{} 1293 | writer := &bitWriter{ 1294 | Buffer: buffer, 1295 | BitBuffer: 0, 1296 | BitBufferSize: 0, 1297 | } 1298 | 1299 | writeImageData(writer, tt.inputPixels, tt.width, tt.height, tt.isRecursive, tt.colorCacheBits) 1300 | 1301 | if !bytes.Equal(buffer.Bytes(), tt.expectedBits) { 1302 | t.Errorf("test %d: buffer mismatch\nexpected: %v got: %v", id, tt.expectedBits, buffer.Bytes()) 1303 | continue 1304 | } 1305 | } 1306 | } 1307 | 1308 | func TestEncodeImageData(t *testing.T) { 1309 | for id, tt := range []struct { 1310 | inputPixels []color.NRGBA 1311 | width int 1312 | height int 1313 | colorCacheBits int 1314 | expectedEncoded []int 1315 | }{ 1316 | { //cached encoding 1317 | inputPixels: []color.NRGBA{ 1318 | {R: 100, G: 50, B: 150, A: 255}, 1319 | {R: 200, G: 100, B: 50, A: 255}, 1320 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1321 | }, 1322 | width: 3, 1323 | height: 1, 1324 | colorCacheBits: 2, 1325 | expectedEncoded: []int{ 1326 | 50, 100, 150, 255, // First pixel 1327 | 100, 200, 50, 255, // Second pixel 1328 | 256 + 24 + 3, // Cached first pixel (hash index 0) 1329 | }, 1330 | }, 1331 | { //full RGBA encoding 1332 | inputPixels: []color.NRGBA{ 1333 | {R: 100, G: 50, B: 150, A: 255}, 1334 | {R: 200, G: 100, B: 50, A: 255}, 1335 | {R: 100, G: 50, B: 150, A: 255}, // Same as the first pixel 1336 | }, 1337 | width: 3, 1338 | height: 1, 1339 | colorCacheBits: 0, 1340 | expectedEncoded: []int{ 1341 | 50, 100, 150, 255, 1342 | 100, 200, 50, 255, 1343 | 50, 100, 150, 255, 1344 | }, 1345 | }, 1346 | } { 1347 | encoded := encodeImageData(tt.inputPixels, tt.width, tt.height, tt.colorCacheBits) 1348 | 1349 | if !reflect.DeepEqual(encoded, tt.expectedEncoded) { 1350 | t.Errorf("test %d: encoded data mismatch\nexpected: %+v\n got: %+v", id, tt.expectedEncoded, encoded) 1351 | continue 1352 | } 1353 | } 1354 | } 1355 | 1356 | func TestPrefixEncodeCode(t *testing.T) { 1357 | tests := []struct { 1358 | n int // input value 1359 | expectedCode int // expected prefix code 1360 | expectedRemainder int // expected remainder value 1361 | }{ 1362 | // n <= 5: code should be max(0, n-1) and remainder 0. 1363 | {-1, 0, 0}, // even negative numbers fall in this branch 1364 | {0, 0, 0}, 1365 | {1, 0, 0}, 1366 | {2, 1, 0}, 1367 | {3, 2, 0}, 1368 | {4, 3, 0}, 1369 | {5, 4, 0}, 1370 | 1371 | // n > 5: calculations using shifts. 1372 | // For n = 6: n-1 = 5, loop runs once (5 >> 1 = 2) → shift=1, rem=2, 1373 | // so returns (2 + 2*1, 6 - (2<<1) - 1) = (4, 1). 1374 | {6, 4, 1}, 1375 | 1376 | // For n = 7: n-1 = 6, loop: 6 >> 1 = 3 → shift=1, rem=3, 1377 | // so returns (3 + 2*1, 7 - (3<<1) - 1) = (5, 0). 1378 | {7, 5, 0}, 1379 | 1380 | // For n = 8: n-1 = 7, loop: 7 >> 1 = 3 → shift=1, rem=3, 1381 | // returns (3 + 2*1, 8 - (3<<1) - 1) = (5, 1). 1382 | {8, 5, 1}, 1383 | 1384 | // For n = 9: n-1 = 8, loop: 1385 | // 8 >> 1 = 4, shift becomes 1; then 4 >> 1 = 2, shift becomes 2; 1386 | // rem == 2 so returns (2 + 2*2, 9 - (2<<2) - 1) = (6, 0). 1387 | {9, 6, 0}, 1388 | 1389 | // For n = 10: returns (6, 1) 1390 | {10, 6, 1}, 1391 | 1392 | // For n = 11: returns (6, 2) 1393 | {11, 6, 2}, 1394 | 1395 | // For n = 12: returns (6, 3) 1396 | {12, 6, 3}, 1397 | 1398 | // For n = 13: n-1 = 12, loop: 12 >> 1 = 6 (shift=1), 1399 | // then 6 >> 1 = 3 (shift=2), rem becomes 3 so returns (3+2*2, 13 - (3<<2) -1) = (7, 0). 1400 | {13, 7, 0}, 1401 | 1402 | // For n = 14: returns (7, 1) 1403 | {14, 7, 1}, 1404 | 1405 | // For n = 15: returns (7, 2) 1406 | {15, 7, 2}, 1407 | 1408 | // For n = 16: returns (7, 3) 1409 | {16, 7, 3}, 1410 | } 1411 | for idx, tt := range tests { 1412 | code, remainder := prefixEncodeCode(tt.n) 1413 | 1414 | if code != tt.expectedCode { 1415 | t.Errorf("Test %d: expected code %d, got %d", idx, tt.expectedCode, code) 1416 | continue 1417 | } 1418 | if remainder != tt.expectedRemainder { 1419 | t.Errorf("Test %d: expected remainder %d, got %d", idx, tt.expectedRemainder, remainder) 1420 | continue 1421 | } 1422 | } 1423 | } 1424 | 1425 | func TestPrefixEncodeBits(t *testing.T) { 1426 | tests := []struct { 1427 | prefix int 1428 | expected int 1429 | }{ 1430 | // For prefix values less than 4, the function returns 0. 1431 | {-10, 0}, 1432 | {-1, 0}, 1433 | {0, 0}, 1434 | {1, 0}, 1435 | {2, 0}, 1436 | {3, 0}, 1437 | // For prefix values 4 and above, the function computes (prefix-2) >> 1. 1438 | // Example: For prefix = 4, (4-2) >> 1 = 2 >> 1 = 1. 1439 | {4, 1}, 1440 | // For prefix = 5, (5-2) >> 1 = 3 >> 1 = 1. 1441 | {5, 1}, 1442 | // For prefix = 6, (6-2) >> 1 = 4 >> 1 = 2. 1443 | {6, 2}, 1444 | // For prefix = 7, (7-2) >> 1 = 5 >> 1 = 2. 1445 | {7, 2}, 1446 | // For prefix = 8, (8-2) >> 1 = 6 >> 1 = 3. 1447 | {8, 3}, 1448 | // For prefix = 9, (9-2) >> 1 = 7 >> 1 = 3. 1449 | {9, 3}, 1450 | // For prefix = 10, (10-2) >> 1 = 8 >> 1 = 4. 1451 | {10, 4}, 1452 | // Additional test cases 1453 | {11, 4}, // (11-2)=9, 9 >> 1 = 4 (integer division) 1454 | {12, 5}, // (12-2)=10, 10 >> 1 = 5 1455 | } 1456 | 1457 | for idx, tt := range tests { 1458 | result := prefixEncodeBits(tt.prefix) 1459 | if result != tt.expected { 1460 | t.Errorf("Test %d: expected %d got %d", idx, tt.expected, result) 1461 | } 1462 | } 1463 | } 1464 | 1465 | func TestHash(t *testing.T) { 1466 | tests := []struct { 1467 | c color.NRGBA 1468 | shifts int 1469 | expected uint32 1470 | }{ 1471 | { 1472 | c: color.NRGBA{R: 0, G: 0, B: 0, A: 0}, 1473 | shifts: 8, 1474 | expected: 0, 1475 | }, 1476 | { 1477 | // Note: hash uses c.A as the most significant byte. 1478 | // This test uses A=0, R=0, G=0, B=1 so that: 1479 | // x = 0<<24 | 0<<16 | 0<<8 | 1 = 1, 1480 | // then hash = (1 * 0x1e35a7bd) >> (32-8) = 0x1e35a7bd >> 24. 1481 | // 0x1e35a7bd in hex is: 0x1e 0x35 0xa7 0xbd, so shifting right 24 bits yields 0x1e (30 in decimal). 1482 | c: color.NRGBA{R: 0, G: 0, B: 1, A: 0}, 1483 | shifts: 8, 1484 | expected: 30, 1485 | }, 1486 | { 1487 | // Here x = 2 and so hash = (2*0x1e35a7bd) >> 24. 1488 | // Since 0x1e35a7bd >> 24 is 30, doubling gives 60. 1489 | c: color.NRGBA{R: 0, G: 0, B: 2, A: 0}, 1490 | shifts: 8, 1491 | expected: 60, 1492 | }, 1493 | { 1494 | // For c = {255,255,255,255} we have: 1495 | // x = 0xFF<<24 | 0xFF<<16 | 0xFF<<8 | 0xFF = 0xFFFFFFFF. 1496 | // In 32-bit arithmetic, multiplying by 0x1e35a7bd gives: 1497 | // 0xFFFFFFFF * 0x1e35a7bd ≡ -0x1e35a7bd (mod 2^32) 1498 | // which equals 0x100000000 - 0x1e35a7bd = 0xE1CA5823 = 3788134467. 1499 | c: color.NRGBA{R: 255, G: 255, B: 255, A: 255}, 1500 | shifts: 32, 1501 | expected: 3788134467, 1502 | }, 1503 | { 1504 | // Here x = 1<<24 = 0x01000000. 1505 | // Multiplying by 0x1e35a7bd is equivalent to shifting the magic left 24 bits: 1506 | // (0x1e35a7bd << 24) mod 2^32. 1507 | // Only the lower 8 bits of the magic survive in the final result, 1508 | // so expected = (0x1e35a7bd & 0xFF) << 24 = 0xbd << 24 = 0xbd000000. 1509 | c: color.NRGBA{R: 0, G: 0, B: 0, A: 1}, 1510 | shifts: 32, 1511 | expected: 0xbd000000, 1512 | }, 1513 | { 1514 | // With c = {R:0, G:0, B:1, A:0}, x = 1. 1515 | // Then hash = (0x1e35a7bd) >> (32-16) = (0x1e35a7bd) >> 16. 1516 | // Shifting 0x1e35a7bd right 16 bits yields 0x1e35, which is 7733 in decimal. 1517 | c: color.NRGBA{R: 0, G: 0, B: 1, A: 0}, 1518 | shifts: 16, 1519 | expected: 7733, 1520 | }, 1521 | { 1522 | // case where shift is higher than maximum of 32 (should be set back to 32) 1523 | c: color.NRGBA{R: 255, G: 255, B: 255, A: 255}, 1524 | shifts: 33, 1525 | expected: 3788134467, 1526 | }, 1527 | } 1528 | 1529 | for id, tt := range tests { 1530 | result := hash(tt.c, tt.shifts) 1531 | if result != tt.expected { 1532 | t.Errorf("test %v: expected hash as %v got %v", id, tt.expected, result) 1533 | } 1534 | } 1535 | } 1536 | 1537 | func TestComputeHistograms(t *testing.T) { 1538 | for id, tt := range []struct { 1539 | pixels []int 1540 | colorCacheBits int 1541 | expectedSizes []int 1542 | expectedCounts []map[int]int 1543 | }{ 1544 | { 1545 | pixels: []int{ 1546 | 0xff, 0x01, 0x00, 0xff, 1547 | 0x00, 0xff, 0x00, 0xff, 1548 | 0x01, 0x01, 0xff, 0xff, 1549 | }, 1550 | colorCacheBits: 0, 1551 | expectedSizes: []int{256 + 24, 256, 256, 256, 40}, 1552 | expectedCounts: []map[int]int{ 1553 | {0: 1, 1: 1, 255: 1}, // histos[0] 1554 | {0: 0, 1: 2, 255: 1}, // histos[1] 1555 | {0: 2, 1: 0, 255: 1}, // histos[2] 1556 | {0: 0, 1: 0, 255: 3}, // histos[3] 1557 | {}, // histos[4] (unused in this case) 1558 | }, 1559 | }, 1560 | { 1561 | pixels: []int{ 1562 | 0xff, 0x01, 0x00, 0xff, 1563 | 0x00, 0xff, 0x00, 0xff, 1564 | 0x01, 0x01, 0xff, 0xff, 1565 | }, 1566 | colorCacheBits: 4, 1567 | expectedSizes: []int{256 + 24 + (1 << 4), 256, 256, 256, 40}, 1568 | expectedCounts: []map[int]int{ 1569 | {0: 1, 1: 1, 255: 1}, // histos[0] 1570 | {0: 0, 1: 2, 255: 1}, // histos[1] 1571 | {0: 2, 1: 0, 255: 1}, // histos[2] 1572 | {0: 0, 1: 0, 255: 3}, // histos[3] 1573 | {}, // histos[4] (unused in this case) 1574 | }, 1575 | }, 1576 | { 1577 | pixels: []int{ 1578 | 0x104, 0x01, 0x02, 0x03, // over 256 1579 | 0xff, 0x01, 0x00, 0xff, 1580 | 0x00, 0xff, 0x00, 0xff, 1581 | 0x01, 0x01, 0xff, 0xff, 1582 | }, 1583 | colorCacheBits: 4, 1584 | expectedSizes: []int{256 + 24 + (1 << 4), 256, 256, 256, 40}, 1585 | expectedCounts: []map[int]int{ 1586 | {0: 1, 1: 1, 255: 1}, // histos[0] 1587 | {0: 0, 1: 2, 255: 1}, // histos[1] 1588 | {0: 2, 1: 0, 255: 1}, // histos[2] 1589 | {0: 0, 1: 0, 255: 3}, // histos[3] 1590 | {2: 1}, // histos[4] (unused in this case) 1591 | }, 1592 | }, 1593 | }{ 1594 | histos := computeHistograms(tt.pixels, tt.colorCacheBits) 1595 | 1596 | for i, histo := range histos { 1597 | if len(histo) != tt.expectedSizes[i] { 1598 | t.Errorf("test %d: histos[%d] size mismatch\nexpected: %d\ngot: %d", id, i, tt.expectedSizes[i], len(histo)) 1599 | continue 1600 | } 1601 | } 1602 | 1603 | for histoIdx, expectedCounts := range tt.expectedCounts { 1604 | for value, expectedCount := range expectedCounts { 1605 | if histos[histoIdx][value] != expectedCount { 1606 | t.Errorf("test %d: histos[%d][%d] count mismatch\nexpected: %d\ngot: %d", id, histoIdx, value, expectedCount, histos[histoIdx][value]) 1607 | continue 1608 | } 1609 | } 1610 | } 1611 | } 1612 | } 1613 | 1614 | func TestFlatten(t *testing.T) { 1615 | for id, tt := range []struct { 1616 | width int 1617 | height int 1618 | brightness float64 1619 | hasAlpha bool 1620 | expectError bool 1621 | expectedErrorMsg string 1622 | }{ 1623 | // Valid NRGBA image with alpha 1624 | { 1625 | width: 16, 1626 | height: 16, 1627 | brightness: 64, 1628 | hasAlpha: true, 1629 | expectError: false, 1630 | expectedErrorMsg: "", 1631 | }, 1632 | // Valid NRGBA image without alpha 1633 | { 1634 | width: 16, 1635 | height: 16, 1636 | brightness: 64, 1637 | hasAlpha: false, 1638 | expectError: false, 1639 | expectedErrorMsg: "", 1640 | }, 1641 | // Unsupported image format 1642 | { 1643 | width: 16, 1644 | height: 16, 1645 | brightness: 64, 1646 | hasAlpha: true, 1647 | expectError: true, // Will convert to an unsupported format 1648 | expectedErrorMsg: "unsupported image format", 1649 | }, 1650 | }{ 1651 | img := generateTestImageNRGBA(tt.width, tt.height, tt.brightness, tt.hasAlpha) 1652 | 1653 | var testImage image.Image = img 1654 | if tt.expectError { 1655 | testImage = image.NewGray(img.Bounds()) 1656 | } 1657 | 1658 | pixels, err := flatten(testImage) 1659 | 1660 | if tt.expectError { 1661 | if err == nil { 1662 | t.Errorf("test %d: expected error but got nil", id) 1663 | continue 1664 | } 1665 | 1666 | if err.Error() != tt.expectedErrorMsg { 1667 | t.Errorf("test %d: expected error %v got %v", id, tt.expectedErrorMsg, err) 1668 | continue 1669 | } 1670 | 1671 | continue 1672 | } 1673 | 1674 | if err != nil { 1675 | t.Errorf("test %d: unexpected error: %v", id, err) 1676 | continue 1677 | } 1678 | 1679 | for y := 0; y < tt.height; y++ { 1680 | for x := 0; x < tt.width; x++ { 1681 | index := y*tt.width + x 1682 | expected := img.At(x, y).(color.NRGBA) 1683 | actual := pixels[index] 1684 | 1685 | if expected != actual { 1686 | t.Errorf("test %d: pixel mismatch at (%d, %d): expected %+v, got %+v", id, x, y, expected, actual) 1687 | continue 1688 | } 1689 | } 1690 | } 1691 | } 1692 | } --------------------------------------------------------------------------------