• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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: lode.vandevenne@gmail.com (Lode Vandevenne)
16 // Author: jyrki.alakuijala@gmail.com (Jyrki Alakuijala)
17 
18 // See zopflipng_lib.h
19 
20 #include "zopflipng_lib.h"
21 
22 #include <stdio.h>
23 #include <set>
24 #include <vector>
25 
26 #include "lodepng/lodepng.h"
27 #include "lodepng/lodepng_util.h"
28 #include "../zopfli/deflate.h"
29 
ZopfliPNGOptions()30 ZopfliPNGOptions::ZopfliPNGOptions()
31   : lossy_transparent(false)
32   , lossy_8bit(false)
33   , auto_filter_strategy(true)
34   , use_zopfli(true)
35   , num_iterations(15)
36   , num_iterations_large(5)
37   , block_split_strategy(1) {
38 }
39 
40 // Deflate compressor passed as fuction pointer to LodePNG to have it use Zopfli
41 // as its compression backend.
CustomPNGDeflate(unsigned char ** out,size_t * outsize,const unsigned char * in,size_t insize,const LodePNGCompressSettings * settings)42 unsigned CustomPNGDeflate(unsigned char** out, size_t* outsize,
43                           const unsigned char* in, size_t insize,
44                           const LodePNGCompressSettings* settings) {
45   const ZopfliPNGOptions* png_options =
46       static_cast<const ZopfliPNGOptions*>(settings->custom_context);
47   unsigned char bp = 0;
48   ZopfliOptions options;
49   ZopfliInitOptions(&options);
50 
51   options.numiterations = insize < 200000
52       ? png_options->num_iterations : png_options->num_iterations_large;
53 
54   if (png_options->block_split_strategy == 3) {
55     // Try both block splitting first and last.
56     unsigned char* out2 = 0;
57     size_t outsize2 = 0;
58     options.blocksplittinglast = 0;
59     ZopfliDeflate(&options, 2 /* Dynamic */, 1, in, insize, &bp, out, outsize);
60     bp = 0;
61     options.blocksplittinglast = 1;
62     ZopfliDeflate(&options, 2 /* Dynamic */, 1,
63                   in, insize, &bp, &out2, &outsize2);
64 
65     if (outsize2 < *outsize) {
66       free(*out);
67       *out = out2;
68       *outsize = outsize2;
69       printf("Block splitting last was better\n");
70     } else {
71       free(out2);
72     }
73   } else {
74     if (png_options->block_split_strategy == 0) options.blocksplitting = 0;
75     options.blocksplittinglast = png_options->block_split_strategy == 2;
76     ZopfliDeflate(&options, 2 /* Dynamic */, 1, in, insize, &bp, out, outsize);
77   }
78 
79   return 0;  // OK
80 }
81 
82 // Returns 32-bit integer value for RGBA color.
ColorIndex(const unsigned char * color)83 static unsigned ColorIndex(const unsigned char* color) {
84   return color[0] + 256u * color[1] + 65536u * color[1] + 16777216u * color[3];
85 }
86 
87 // Counts amount of colors in the image, up to 257. If transparent_counts_as_one
88 // is enabled, any color with alpha channel 0 is treated as a single color with
89 // index 0.
CountColors(std::set<unsigned> * unique,const unsigned char * image,unsigned w,unsigned h,bool transparent_counts_as_one)90 void CountColors(std::set<unsigned>* unique,
91                  const unsigned char* image, unsigned w, unsigned h,
92                  bool transparent_counts_as_one) {
93   unique->clear();
94   for (size_t i = 0; i < w * h; i++) {
95     unsigned index = ColorIndex(&image[i * 4]);
96     if (transparent_counts_as_one && image[i * 4 + 3] == 0) index = 0;
97     unique->insert(index);
98     if (unique->size() > 256) break;
99   }
100 }
101 
102 // Remove RGB information from pixels with alpha=0
LossyOptimizeTransparent(lodepng::State * inputstate,unsigned char * image,unsigned w,unsigned h)103 void LossyOptimizeTransparent(lodepng::State* inputstate, unsigned char* image,
104     unsigned w, unsigned h) {
105   // First check if we want to preserve potential color-key background color,
106   // or instead use the last encountered RGB value all the time to save bytes.
107   bool key = true;
108   for (size_t i = 0; i < w * h; i++) {
109     if (image[i * 4 + 3] > 0 && image[i * 4 + 3] < 255) {
110       key = false;
111       break;
112     }
113   }
114   std::set<unsigned> count;  // Color count, up to 257.
115   CountColors(&count, image, w, h, true);
116   // If true, means palette is possible so avoid using different RGB values for
117   // the transparent color.
118   bool palette = count.size() <= 256;
119 
120   // Choose the color key or first initial background color.
121   int r = 0, g = 0, b = 0;
122   if (key || palette) {
123     for (size_t i = 0; i < w * h; i++) {
124       if (image[i * 4 + 3] == 0) {
125         // Use RGB value of first encountered transparent pixel. This can be
126         // used as a valid color key, or in case of palette ensures a color
127         // existing in the input image palette is used.
128         r = image[i * 4 + 0];
129         g = image[i * 4 + 1];
130         b = image[i * 4 + 2];
131       }
132     }
133   }
134 
135   for (size_t i = 0; i < w * h; i++) {
136     // if alpha is 0, alter the RGB value to a possibly more efficient one.
137     if (image[i * 4 + 3] == 0) {
138       image[i * 4 + 0] = r;
139       image[i * 4 + 1] = g;
140       image[i * 4 + 2] = b;
141     } else {
142       if (!key && !palette) {
143         // Use the last encountered RGB value if no key or palette is used: that
144         // way more values can be 0 thanks to the PNG filter types.
145         r = image[i * 4 + 0];
146         g = image[i * 4 + 1];
147         b = image[i * 4 + 2];
148       }
149     }
150   }
151 
152   // If there are now less colors, update palette of input image to match this.
153   if (palette && inputstate->info_png.color.palettesize > 0) {
154     CountColors(&count, image, w, h, false);
155     if (count.size() < inputstate->info_png.color.palettesize) {
156       std::vector<unsigned char> palette_out;
157       unsigned char* palette_in = inputstate->info_png.color.palette;
158       for (size_t i = 0; i < inputstate->info_png.color.palettesize; i++) {
159         if (count.count(ColorIndex(&palette_in[i * 4])) != 0) {
160           palette_out.push_back(palette_in[i * 4 + 0]);
161           palette_out.push_back(palette_in[i * 4 + 1]);
162           palette_out.push_back(palette_in[i * 4 + 2]);
163           palette_out.push_back(palette_in[i * 4 + 3]);
164         }
165       }
166       inputstate->info_png.color.palettesize = palette_out.size() / 4;
167       for (size_t i = 0; i < palette_out.size(); i++) {
168         palette_in[i] = palette_out[i];
169       }
170     }
171   }
172 }
173 
174 // Tries to optimize given a single PNG filter strategy.
175 // 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,const std::vector<unsigned char> & origfile,ZopfliPNGFilterStrategy filterstrategy,bool use_zopfli,int windowsize,const ZopfliPNGOptions * png_options,std::vector<unsigned char> * out)176 unsigned TryOptimize(
177     const std::vector<unsigned char>& image, unsigned w, unsigned h,
178     const lodepng::State& inputstate, bool bit16,
179     const std::vector<unsigned char>& origfile,
180     ZopfliPNGFilterStrategy filterstrategy,
181     bool use_zopfli, int windowsize, const ZopfliPNGOptions* png_options,
182     std::vector<unsigned char>* out) {
183   unsigned error = 0;
184 
185   lodepng::State state;
186   state.encoder.zlibsettings.windowsize = windowsize;
187   if (use_zopfli && png_options->use_zopfli) {
188     state.encoder.zlibsettings.custom_deflate = CustomPNGDeflate;
189     state.encoder.zlibsettings.custom_context = png_options;
190   }
191 
192   if (inputstate.info_png.color.colortype == LCT_PALETTE) {
193     // Make it preserve the original palette order
194     lodepng_color_mode_copy(&state.info_raw, &inputstate.info_png.color);
195     state.info_raw.colortype = LCT_RGBA;
196     state.info_raw.bitdepth = 8;
197   }
198   if (bit16) {
199     state.info_raw.bitdepth = 16;
200   }
201 
202   state.encoder.filter_palette_zero = 0;
203 
204   std::vector<unsigned char> filters;
205   switch (filterstrategy) {
206     case kStrategyZero:
207       state.encoder.filter_strategy = LFS_ZERO;
208       break;
209     case kStrategyMinSum:
210       state.encoder.filter_strategy = LFS_MINSUM;
211       break;
212     case kStrategyEntropy:
213       state.encoder.filter_strategy = LFS_ENTROPY;
214       break;
215     case kStrategyBruteForce:
216       state.encoder.filter_strategy = LFS_BRUTE_FORCE;
217       break;
218     case kStrategyOne:
219     case kStrategyTwo:
220     case kStrategyThree:
221     case kStrategyFour:
222       // Set the filters of all scanlines to that number.
223       filters.resize(h, filterstrategy);
224       state.encoder.filter_strategy = LFS_PREDEFINED;
225       state.encoder.predefined_filters = &filters[0];
226       break;
227     case kStrategyPredefined:
228       lodepng::getFilterTypes(filters, origfile);
229       state.encoder.filter_strategy = LFS_PREDEFINED;
230       state.encoder.predefined_filters = &filters[0];
231       break;
232     default:
233       break;
234   }
235 
236   state.encoder.add_id = false;
237   state.encoder.text_compression = 1;
238 
239   error = lodepng::encode(*out, image, w, h, state);
240 
241   // For very small output, also try without palette, it may be smaller thanks
242   // to no palette storage overhead.
243   if (!error && out->size() < 4096) {
244     lodepng::State teststate;
245     std::vector<unsigned char> temp;
246     lodepng::decode(temp, w, h, teststate, *out);
247     LodePNGColorMode& color = teststate.info_png.color;
248     if (color.colortype == LCT_PALETTE) {
249       std::vector<unsigned char> out2;
250       state.encoder.auto_convert = LAC_ALPHA;
251       bool grey = true;
252       for (size_t i = 0; i < color.palettesize; i++) {
253         if (color.palette[i * 4 + 0] != color.palette[i * 4 + 2]
254             || color.palette[i * 4 + 1] != color.palette[i * 4 + 2]) {
255           grey = false;
256           break;
257         }
258       }
259       if (grey) state.info_png.color.colortype = LCT_GREY_ALPHA;
260 
261       error = lodepng::encode(out2, image, w, h, state);
262       if (out2.size() < out->size()) out->swap(out2);
263     }
264   }
265 
266   if (error) {
267     printf("Encoding error %u: %s\n", error, lodepng_error_text(error));
268     return error;
269   }
270 
271   return 0;
272 }
273 
274 // Use fast compression to check which PNG filter strategy gives the smallest
275 // output. This allows to then do the slow and good compression only on that
276 // filter type.
AutoChooseFilterStrategy(const std::vector<unsigned char> & image,unsigned w,unsigned h,const lodepng::State & inputstate,bool bit16,const std::vector<unsigned char> & origfile,int numstrategies,ZopfliPNGFilterStrategy * strategies,bool * enable)277 unsigned AutoChooseFilterStrategy(const std::vector<unsigned char>& image,
278                                   unsigned w, unsigned h,
279                                   const lodepng::State& inputstate, bool bit16,
280                                   const std::vector<unsigned char>& origfile,
281                                   int numstrategies,
282                                   ZopfliPNGFilterStrategy* strategies,
283                                   bool* enable) {
284   std::vector<unsigned char> out;
285   size_t bestsize = 0;
286   int bestfilter = 0;
287 
288   // A large window size should still be used to do the quick compression to
289   // try out filter strategies: which filter strategy is the best depends
290   // largely on the window size, the closer to the actual used window size the
291   // better.
292   int windowsize = 8192;
293 
294   for (int i = 0; i < numstrategies; i++) {
295     out.clear();
296     unsigned error = TryOptimize(image, w, h, inputstate, bit16, origfile,
297                                  strategies[i], false, windowsize, 0, &out);
298     if (error) return error;
299     if (bestsize == 0 || out.size() < bestsize) {
300       bestsize = out.size();
301       bestfilter = i;
302     }
303   }
304 
305   for (int i = 0; i < numstrategies; i++) {
306     enable[i] = (i == bestfilter);
307   }
308 
309   return 0;  /* OK */
310 }
311 
312 // Keeps chunks with given names from the original png by literally copying them
313 // into the new png
KeepChunks(const std::vector<unsigned char> & origpng,const std::vector<std::string> & keepnames,std::vector<unsigned char> * png)314 void KeepChunks(const std::vector<unsigned char>& origpng,
315                 const std::vector<std::string>& keepnames,
316                 std::vector<unsigned char>* png) {
317   std::vector<std::string> names[3];
318   std::vector<std::vector<unsigned char> > chunks[3];
319 
320   lodepng::getChunks(names, chunks, origpng);
321   std::vector<std::vector<unsigned char> > keepchunks[3];
322 
323   // There are 3 distinct locations in a PNG file for chunks: between IHDR and
324   // PLTE, between PLTE and IDAT, and between IDAT and IEND. Keep each chunk at
325   // its corresponding location in the new PNG.
326   for (size_t i = 0; i < 3; i++) {
327     for (size_t j = 0; j < names[i].size(); j++) {
328       for (size_t k = 0; k < keepnames.size(); k++) {
329         if (keepnames[k] == names[i][j]) {
330           keepchunks[i].push_back(chunks[i][j]);
331         }
332       }
333     }
334   }
335 
336   lodepng::insertChunks(*png, keepchunks);
337 }
338 
ZopfliPNGOptimize(const std::vector<unsigned char> & origpng,const ZopfliPNGOptions & png_options,bool verbose,std::vector<unsigned char> * resultpng)339 int ZopfliPNGOptimize(const std::vector<unsigned char>& origpng,
340     const ZopfliPNGOptions& png_options,
341     bool verbose,
342     std::vector<unsigned char>* resultpng) {
343   // Use the largest possible deflate window size
344   int windowsize = 32768;
345 
346   ZopfliPNGFilterStrategy filterstrategies[kNumFilterStrategies] = {
347     kStrategyZero, kStrategyOne, kStrategyTwo, kStrategyThree, kStrategyFour,
348     kStrategyMinSum, kStrategyEntropy, kStrategyPredefined, kStrategyBruteForce
349   };
350   bool strategy_enable[kNumFilterStrategies] = {
351     false, false, false, false, false, false, false, false, false
352   };
353   std::string strategy_name[kNumFilterStrategies] = {
354     "zero", "one", "two", "three", "four",
355     "minimum sum", "entropy", "predefined", "brute force"
356   };
357   for (size_t i = 0; i < png_options.filter_strategies.size(); i++) {
358     strategy_enable[png_options.filter_strategies[i]] = true;
359   }
360 
361   std::vector<unsigned char> image;
362   unsigned w, h;
363   unsigned error;
364   lodepng::State inputstate;
365   error = lodepng::decode(image, w, h, inputstate, origpng);
366 
367   if (error) {
368     if (verbose) {
369       printf("Decoding error %i: %s\n", error, lodepng_error_text(error));
370     }
371     return error;
372   }
373 
374   bool bit16 = false;  // Using 16-bit per channel raw image
375   if (inputstate.info_png.color.bitdepth == 16 && !png_options.lossy_8bit) {
376     // Decode as 16-bit
377     image.clear();
378     error = lodepng::decode(image, w, h, origpng, LCT_RGBA, 16);
379     bit16 = true;
380   }
381 
382   if (!error) {
383     // If lossy_transparent, remove RGB information from pixels with alpha=0
384     if (png_options.lossy_transparent && !bit16) {
385       LossyOptimizeTransparent(&inputstate, &image[0], w, h);
386     }
387 
388     if (png_options.auto_filter_strategy) {
389       error = AutoChooseFilterStrategy(image, w, h, inputstate, bit16,
390                                        origpng,
391                                        /* Don't try brute force */
392                                        kNumFilterStrategies - 1,
393                                        filterstrategies, strategy_enable);
394     }
395   }
396 
397   if (!error) {
398     size_t bestsize = 0;
399 
400     for (int i = 0; i < kNumFilterStrategies; i++) {
401       if (!strategy_enable[i]) continue;
402 
403       std::vector<unsigned char> temp;
404       error = TryOptimize(image, w, h, inputstate, bit16, origpng,
405                           filterstrategies[i], true /* use_zopfli */,
406                           windowsize, &png_options, &temp);
407       if (!error) {
408         if (verbose) {
409           printf("Filter strategy %s: %d bytes\n",
410                  strategy_name[i].c_str(), (int) temp.size());
411         }
412         if (bestsize == 0 || temp.size() < bestsize) {
413           bestsize = temp.size();
414           (*resultpng).swap(temp);  // Store best result so far in the output.
415         }
416       }
417     }
418 
419     if (!png_options.keepchunks.empty()) {
420       KeepChunks(origpng, png_options.keepchunks, resultpng);
421     }
422   }
423 
424   return error;
425 }
426