1 // Copyright 2013 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15 // Author: [email protected] (Lode Vandevenne)
16 // Author: [email protected] (Jyrki Alakuijala)
17
18 // See zopflipng_lib.h
19
20 #include "zopflipng_lib.h"
21
22 #include <errno.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <set>
27 #include <vector>
28
29 #include "lodepng/lodepng.h"
30 #include "lodepng/lodepng_util.h"
31 #include "../zopfli/deflate.h"
32
ZopfliPNGOptions()33 ZopfliPNGOptions::ZopfliPNGOptions()
34 : verbose(false)
35 , lossy_transparent(false)
36 , lossy_8bit(false)
37 , auto_filter_strategy(true)
38 , keep_colortype(false)
39 , use_zopfli(true)
40 , num_iterations(15)
41 , num_iterations_large(5)
42 , block_split_strategy(1) {
43 }
44
45 // Deflate compressor passed as fuction pointer to LodePNG to have it use Zopfli
46 // as its compression backend.
CustomPNGDeflate(unsigned char ** out,size_t * outsize,const unsigned char * in,size_t insize,const LodePNGCompressSettings * settings)47 unsigned CustomPNGDeflate(unsigned char** out, size_t* outsize,
48 const unsigned char* in, size_t insize,
49 const LodePNGCompressSettings* settings) {
50 const ZopfliPNGOptions* png_options =
51 static_cast<const ZopfliPNGOptions*>(settings->custom_context);
52 unsigned char bp = 0;
53 ZopfliOptions options;
54 ZopfliInitOptions(&options);
55
56 options.verbose = png_options->verbose;
57 options.numiterations = insize < 200000
58 ? png_options->num_iterations : png_options->num_iterations_large;
59
60 ZopfliDeflate(&options, 2 /* Dynamic */, 1, in, insize, &bp, out, outsize);
61
62 return 0; // OK
63 }
64
65 // Returns 32-bit integer value for RGBA color.
ColorIndex(const unsigned char * color)66 static unsigned ColorIndex(const unsigned char* color) {
67 return color[0] + 256u * color[1] + 65536u * color[2] + 16777216u * color[3];
68 }
69
70 // Counts amount of colors in the image, up to 257. If transparent_counts_as_one
71 // is enabled, any color with alpha channel 0 is treated as a single color with
72 // index 0.
CountColors(std::set<unsigned> * unique,const unsigned char * image,unsigned w,unsigned h,bool transparent_counts_as_one)73 void CountColors(std::set<unsigned>* unique,
74 const unsigned char* image, unsigned w, unsigned h,
75 bool transparent_counts_as_one) {
76 unique->clear();
77 for (size_t i = 0; i < w * h; i++) {
78 unsigned index = ColorIndex(&image[i * 4]);
79 if (transparent_counts_as_one && image[i * 4 + 3] == 0) index = 0;
80 unique->insert(index);
81 if (unique->size() > 256) break;
82 }
83 }
84
85 // Remove RGB information from pixels with alpha=0
LossyOptimizeTransparent(lodepng::State * inputstate,unsigned char * image,unsigned w,unsigned h)86 void LossyOptimizeTransparent(lodepng::State* inputstate, unsigned char* image,
87 unsigned w, unsigned h) {
88 // First check if we want to preserve potential color-key background color,
89 // or instead use the last encountered RGB value all the time to save bytes.
90 bool key = true;
91 for (size_t i = 0; i < w * h; i++) {
92 if (image[i * 4 + 3] > 0 && image[i * 4 + 3] < 255) {
93 key = false;
94 break;
95 }
96 }
97 std::set<unsigned> count; // Color count, up to 257.
98 CountColors(&count, image, w, h, true);
99 // If true, means palette is possible so avoid using different RGB values for
100 // the transparent color.
101 bool palette = count.size() <= 256;
102
103 // Choose the color key or first initial background color.
104 int r = 0, g = 0, b = 0;
105 if (key || palette) {
106 for (size_t i = 0; i < w * h; i++) {
107 if (image[i * 4 + 3] == 0) {
108 // Use RGB value of first encountered transparent pixel. This can be
109 // used as a valid color key, or in case of palette ensures a color
110 // existing in the input image palette is used.
111 r = image[i * 4 + 0];
112 g = image[i * 4 + 1];
113 b = image[i * 4 + 2];
114 break;
115 }
116 }
117 }
118
119 for (size_t i = 0; i < w * h; i++) {
120 // if alpha is 0, alter the RGB value to a possibly more efficient one.
121 if (image[i * 4 + 3] == 0) {
122 image[i * 4 + 0] = r;
123 image[i * 4 + 1] = g;
124 image[i * 4 + 2] = b;
125 } else {
126 if (!key && !palette) {
127 // Use the last encountered RGB value if no key or palette is used: that
128 // way more values can be 0 thanks to the PNG filter types.
129 r = image[i * 4 + 0];
130 g = image[i * 4 + 1];
131 b = image[i * 4 + 2];
132 }
133 }
134 }
135
136 // If there are now less colors, update palette of input image to match this.
137 if (palette && inputstate->info_png.color.palettesize > 0) {
138 CountColors(&count, image, w, h, false);
139 if (count.size() < inputstate->info_png.color.palettesize) {
140 std::vector<unsigned char> palette_out;
141 unsigned char* palette_in = inputstate->info_png.color.palette;
142 for (size_t i = 0; i < inputstate->info_png.color.palettesize; i++) {
143 if (count.count(ColorIndex(&palette_in[i * 4])) != 0) {
144 palette_out.push_back(palette_in[i * 4 + 0]);
145 palette_out.push_back(palette_in[i * 4 + 1]);
146 palette_out.push_back(palette_in[i * 4 + 2]);
147 palette_out.push_back(palette_in[i * 4 + 3]);
148 }
149 }
150 inputstate->info_png.color.palettesize = palette_out.size() / 4;
151 for (size_t i = 0; i < palette_out.size(); i++) {
152 palette_in[i] = palette_out[i];
153 }
154 }
155 }
156 }
157
158 // Tries to optimize given a single PNG filter strategy.
159 // Returns 0 if ok, other value for error
TryOptimize(const std::vector<unsigned char> & image,unsigned w,unsigned h,const lodepng::State & inputstate,bool bit16,bool keep_colortype,const std::vector<unsigned char> & origfile,ZopfliPNGFilterStrategy filterstrategy,bool use_zopfli,int windowsize,const ZopfliPNGOptions * png_options,std::vector<unsigned char> * out)160 unsigned TryOptimize(
161 const std::vector<unsigned char>& image, unsigned w, unsigned h,
162 const lodepng::State& inputstate, bool bit16, bool keep_colortype,
163 const std::vector<unsigned char>& origfile,
164 ZopfliPNGFilterStrategy filterstrategy,
165 bool use_zopfli, int windowsize, const ZopfliPNGOptions* png_options,
166 std::vector<unsigned char>* out) {
167 unsigned error = 0;
168
169 lodepng::State state;
170 state.encoder.zlibsettings.windowsize = windowsize;
171 if (use_zopfli && png_options->use_zopfli) {
172 state.encoder.zlibsettings.custom_deflate = CustomPNGDeflate;
173 state.encoder.zlibsettings.custom_context = png_options;
174 }
175
176 if (keep_colortype) {
177 state.encoder.auto_convert = 0;
178 lodepng_color_mode_copy(&state.info_png.color, &inputstate.info_png.color);
179 }
180 if (inputstate.info_png.color.colortype == LCT_PALETTE) {
181 // Make it preserve the original palette order
182 lodepng_color_mode_copy(&state.info_raw, &inputstate.info_png.color);
183 state.info_raw.colortype = LCT_RGBA;
184 state.info_raw.bitdepth = 8;
185 }
186 if (bit16) {
187 state.info_raw.bitdepth = 16;
188 }
189
190 state.encoder.filter_palette_zero = 0;
191
192 std::vector<unsigned char> filters;
193 switch (filterstrategy) {
194 case kStrategyZero:
195 state.encoder.filter_strategy = LFS_ZERO;
196 break;
197 case kStrategyMinSum:
198 state.encoder.filter_strategy = LFS_MINSUM;
199 break;
200 case kStrategyEntropy:
201 state.encoder.filter_strategy = LFS_ENTROPY;
202 break;
203 case kStrategyBruteForce:
204 state.encoder.filter_strategy = LFS_BRUTE_FORCE;
205 break;
206 case kStrategyOne:
207 state.encoder.filter_strategy = LFS_ONE;
208 break;
209 case kStrategyTwo:
210 state.encoder.filter_strategy = LFS_TWO;
211 break;
212 case kStrategyThree:
213 state.encoder.filter_strategy = LFS_THREE;
214 break;
215 case kStrategyFour:
216 state.encoder.filter_strategy = LFS_FOUR;
217 break;
218 case kStrategyPredefined:
219 lodepng::getFilterTypes(filters, origfile);
220 if (filters.size() != h) return 1; // Error getting filters
221 state.encoder.filter_strategy = LFS_PREDEFINED;
222 state.encoder.predefined_filters = &filters[0];
223 break;
224 default:
225 break;
226 }
227
228 state.encoder.add_id = false;
229 state.encoder.text_compression = 1;
230
231 error = lodepng::encode(*out, image, w, h, state);
232
233 // For very small output, also try without palette, it may be smaller thanks
234 // to no palette storage overhead.
235 if (!error && out->size() < 4096 && !keep_colortype) {
236 if (lodepng::getPNGHeaderInfo(*out).color.colortype == LCT_PALETTE) {
237 LodePNGColorStats stats;
238 lodepng_color_stats_init(&stats);
239 lodepng_compute_color_stats(&stats, &image[0], w, h, &state.info_raw);
240 // Too small for tRNS chunk overhead.
241 if (w * h <= 16 && stats.key) stats.alpha = 1;
242 state.encoder.auto_convert = 0;
243 state.info_png.color.colortype = (stats.alpha ? LCT_RGBA : LCT_RGB);
244 state.info_png.color.bitdepth = 8;
245 state.info_png.color.key_defined = (stats.key && !stats.alpha);
246 if (state.info_png.color.key_defined) {
247 state.info_png.color.key_defined = 1;
248 state.info_png.color.key_r = (stats.key_r & 255u);
249 state.info_png.color.key_g = (stats.key_g & 255u);
250 state.info_png.color.key_b = (stats.key_b & 255u);
251 }
252
253 std::vector<unsigned char> out2;
254 error = lodepng::encode(out2, image, w, h, state);
255 if (out2.size() < out->size()) out->swap(out2);
256 }
257 }
258
259 if (error) {
260 printf("Encoding error %u: %s\n", error, lodepng_error_text(error));
261 return error;
262 }
263
264 return 0;
265 }
266
267 // Use fast compression to check which PNG filter strategy gives the smallest
268 // output. This allows to then do the slow and good compression only on that
269 // filter type.
AutoChooseFilterStrategy(const std::vector<unsigned char> & image,unsigned w,unsigned h,const lodepng::State & inputstate,bool bit16,bool keep_colortype,const std::vector<unsigned char> & origfile,int numstrategies,ZopfliPNGFilterStrategy * strategies,bool * enable)270 unsigned AutoChooseFilterStrategy(const std::vector<unsigned char>& image,
271 unsigned w, unsigned h,
272 const lodepng::State& inputstate,
273 bool bit16, bool keep_colortype,
274 const std::vector<unsigned char>& origfile,
275 int numstrategies,
276 ZopfliPNGFilterStrategy* strategies,
277 bool* enable) {
278 std::vector<unsigned char> out;
279 size_t bestsize = 0;
280 int bestfilter = 0;
281
282 // A large window size should still be used to do the quick compression to
283 // try out filter strategies: which filter strategy is the best depends
284 // largely on the window size, the closer to the actual used window size the
285 // better.
286 int windowsize = 8192;
287
288 for (int i = 0; i < numstrategies; i++) {
289 out.clear();
290 unsigned error = TryOptimize(image, w, h, inputstate, bit16, keep_colortype,
291 origfile, strategies[i], false, windowsize, 0,
292 &out);
293 if (error) return error;
294 if (bestsize == 0 || out.size() < bestsize) {
295 bestsize = out.size();
296 bestfilter = i;
297 }
298 }
299
300 for (int i = 0; i < numstrategies; i++) {
301 enable[i] = (i == bestfilter);
302 }
303
304 return 0; /* OK */
305 }
306
307 // Outputs the intersection of keepnames and non-essential chunks which are in
308 // the PNG image.
ChunksToKeep(const std::vector<unsigned char> & origpng,const std::vector<std::string> & keepnames,std::set<std::string> * result)309 void ChunksToKeep(const std::vector<unsigned char>& origpng,
310 const std::vector<std::string>& keepnames,
311 std::set<std::string>* result) {
312 std::vector<std::string> names[3];
313 std::vector<std::vector<unsigned char> > chunks[3];
314
315 lodepng::getChunks(names, chunks, origpng);
316
317 for (size_t i = 0; i < 3; i++) {
318 for (size_t j = 0; j < names[i].size(); j++) {
319 for (size_t k = 0; k < keepnames.size(); k++) {
320 if (keepnames[k] == names[i][j]) {
321 result->insert(names[i][j]);
322 }
323 }
324 }
325 }
326 }
327
328 // Keeps chunks with given names from the original png by literally copying them
329 // into the new png
KeepChunks(const std::vector<unsigned char> & origpng,const std::vector<std::string> & keepnames,std::vector<unsigned char> * png)330 void KeepChunks(const std::vector<unsigned char>& origpng,
331 const std::vector<std::string>& keepnames,
332 std::vector<unsigned char>* png) {
333 std::vector<std::string> names[3];
334 std::vector<std::vector<unsigned char> > chunks[3];
335
336 lodepng::getChunks(names, chunks, origpng);
337 std::vector<std::vector<unsigned char> > keepchunks[3];
338
339 // There are 3 distinct locations in a PNG file for chunks: between IHDR and
340 // PLTE, between PLTE and IDAT, and between IDAT and IEND. Keep each chunk at
341 // its corresponding location in the new PNG.
342 for (size_t i = 0; i < 3; i++) {
343 for (size_t j = 0; j < names[i].size(); j++) {
344 for (size_t k = 0; k < keepnames.size(); k++) {
345 if (keepnames[k] == names[i][j]) {
346 keepchunks[i].push_back(chunks[i][j]);
347 }
348 }
349 }
350 }
351
352 lodepng::insertChunks(*png, keepchunks);
353 }
354
ZopfliPNGOptimize(const std::vector<unsigned char> & origpng,const ZopfliPNGOptions & png_options,bool verbose,std::vector<unsigned char> * resultpng)355 int ZopfliPNGOptimize(const std::vector<unsigned char>& origpng,
356 const ZopfliPNGOptions& png_options,
357 bool verbose,
358 std::vector<unsigned char>* resultpng) {
359 // Use the largest possible deflate window size
360 int windowsize = 32768;
361
362 ZopfliPNGFilterStrategy filterstrategies[kNumFilterStrategies] = {
363 kStrategyZero, kStrategyOne, kStrategyTwo, kStrategyThree, kStrategyFour,
364 kStrategyMinSum, kStrategyEntropy, kStrategyPredefined, kStrategyBruteForce
365 };
366 bool strategy_enable[kNumFilterStrategies] = {
367 false, false, false, false, false, false, false, false, false
368 };
369 std::string strategy_name[kNumFilterStrategies] = {
370 "zero", "one", "two", "three", "four",
371 "minimum sum", "entropy", "predefined", "brute force"
372 };
373 for (size_t i = 0; i < png_options.filter_strategies.size(); i++) {
374 strategy_enable[png_options.filter_strategies[i]] = true;
375 }
376
377 std::vector<unsigned char> image;
378 unsigned w, h;
379 unsigned error;
380 lodepng::State inputstate;
381 error = lodepng::decode(image, w, h, inputstate, origpng);
382
383 bool keep_colortype = png_options.keep_colortype;
384
385 if (!png_options.keepchunks.empty()) {
386 // If the user wants to keep the non-essential chunks bKGD or sBIT, the
387 // input color type has to be kept since the chunks format depend on it.
388 // This may severely hurt compression if it is not an ideal color type.
389 // Ideally these chunks should not be kept for web images. Handling of bKGD
390 // chunks could be improved by changing its color type but not done yet due
391 // to its additional complexity, for sBIT such improvement is usually not
392 // possible.
393 std::set<std::string> keepchunks;
394 ChunksToKeep(origpng, png_options.keepchunks, &keepchunks);
395 if (keepchunks.count("bKGD") || keepchunks.count("sBIT")) {
396 if (!keep_colortype && verbose) {
397 printf("Forced to keep original color type due to keeping bKGD or sBIT"
398 " chunk.\n");
399 }
400 keep_colortype = true;
401 }
402 }
403
404 if (error) {
405 if (verbose) {
406 if (error == 1) {
407 printf("Decoding error\n");
408 } else {
409 printf("Decoding error %u: %s\n", error, lodepng_error_text(error));
410 }
411 }
412 return error;
413 }
414
415 bool bit16 = false; // Using 16-bit per channel raw image
416 if (inputstate.info_png.color.bitdepth == 16 &&
417 (keep_colortype || !png_options.lossy_8bit)) {
418 // Decode as 16-bit
419 image.clear();
420 error = lodepng::decode(image, w, h, origpng, LCT_RGBA, 16);
421 bit16 = true;
422 }
423
424 if (!error) {
425 // If lossy_transparent, remove RGB information from pixels with alpha=0
426 if (png_options.lossy_transparent && !bit16) {
427 LossyOptimizeTransparent(&inputstate, &image[0], w, h);
428 }
429
430 if (png_options.auto_filter_strategy) {
431 error = AutoChooseFilterStrategy(image, w, h, inputstate, bit16,
432 keep_colortype, origpng,
433 /* Don't try brute force */
434 kNumFilterStrategies - 1,
435 filterstrategies, strategy_enable);
436 }
437 }
438
439 if (!error) {
440 size_t bestsize = 0;
441
442 for (int i = 0; i < kNumFilterStrategies; i++) {
443 if (!strategy_enable[i]) continue;
444
445 std::vector<unsigned char> temp;
446 error = TryOptimize(image, w, h, inputstate, bit16, keep_colortype,
447 origpng, filterstrategies[i], true /* use_zopfli */,
448 windowsize, &png_options, &temp);
449 if (!error) {
450 if (verbose) {
451 printf("Filter strategy %s: %d bytes\n",
452 strategy_name[i].c_str(), (int) temp.size());
453 }
454 if (bestsize == 0 || temp.size() < bestsize) {
455 bestsize = temp.size();
456 (*resultpng).swap(temp); // Store best result so far in the output.
457 }
458 }
459 }
460
461 if (!png_options.keepchunks.empty()) {
462 KeepChunks(origpng, png_options.keepchunks, resultpng);
463 }
464 }
465
466 return error;
467 }
468
CZopfliPNGSetDefaults(CZopfliPNGOptions * png_options)469 extern "C" void CZopfliPNGSetDefaults(CZopfliPNGOptions* png_options) {
470
471 memset(png_options, 0, sizeof(*png_options));
472 // Constructor sets the defaults
473 ZopfliPNGOptions opts;
474
475 png_options->lossy_transparent = opts.lossy_transparent;
476 png_options->lossy_8bit = opts.lossy_8bit;
477 png_options->auto_filter_strategy = opts.auto_filter_strategy;
478 png_options->use_zopfli = opts.use_zopfli;
479 png_options->num_iterations = opts.num_iterations;
480 png_options->num_iterations_large = opts.num_iterations_large;
481 png_options->block_split_strategy = opts.block_split_strategy;
482 }
483
CZopfliPNGOptimize(const unsigned char * origpng,const size_t origpng_size,const CZopfliPNGOptions * png_options,int verbose,unsigned char ** resultpng,size_t * resultpng_size)484 extern "C" int CZopfliPNGOptimize(const unsigned char* origpng,
485 const size_t origpng_size,
486 const CZopfliPNGOptions* png_options,
487 int verbose,
488 unsigned char** resultpng,
489 size_t* resultpng_size) {
490 ZopfliPNGOptions opts;
491
492 // Copy over to the C++-style struct
493 opts.lossy_transparent = !!png_options->lossy_transparent;
494 opts.lossy_8bit = !!png_options->lossy_8bit;
495 opts.auto_filter_strategy = !!png_options->auto_filter_strategy;
496 opts.use_zopfli = !!png_options->use_zopfli;
497 opts.num_iterations = png_options->num_iterations;
498 opts.num_iterations_large = png_options->num_iterations_large;
499 opts.block_split_strategy = png_options->block_split_strategy;
500
501 for (int i = 0; i < png_options->num_filter_strategies; i++) {
502 opts.filter_strategies.push_back(png_options->filter_strategies[i]);
503 }
504
505 for (int i = 0; i < png_options->num_keepchunks; i++) {
506 opts.keepchunks.push_back(png_options->keepchunks[i]);
507 }
508
509 const std::vector<unsigned char> origpng_cc(origpng, origpng + origpng_size);
510 std::vector<unsigned char> resultpng_cc;
511
512 int ret = ZopfliPNGOptimize(origpng_cc, opts, !!verbose, &resultpng_cc);
513 if (ret) {
514 return ret;
515 }
516
517 *resultpng_size = resultpng_cc.size();
518 *resultpng = (unsigned char*) malloc(resultpng_cc.size());
519 if (!(*resultpng)) {
520 return ENOMEM;
521 }
522
523 memcpy(*resultpng,
524 reinterpret_cast<unsigned char*>(&resultpng_cc[0]),
525 resultpng_cc.size());
526
527 return 0;
528 }
529