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 // Command line tool to recompress and optimize PNG images, using zopflipng_lib.
19
20 #include <stdlib.h>
21 #include <stdio.h>
22
23 #include "lodepng/lodepng.h"
24 #include "lodepng/lodepng_util.h"
25 #include "zopflipng_lib.h"
26
27 // Returns directory path (including last slash) in dir, filename without
28 // extension in file, extension (including the dot) in ext
GetFileNameParts(const std::string & filename,std::string * dir,std::string * file,std::string * ext)29 void GetFileNameParts(const std::string& filename,
30 std::string* dir, std::string* file, std::string* ext) {
31 size_t npos = (size_t)(-1);
32 size_t slashpos = filename.find_last_of("/\\");
33 std::string nodir;
34 if (slashpos == npos) {
35 *dir = "";
36 nodir = filename;
37 } else {
38 *dir = filename.substr(0, slashpos + 1);
39 nodir = filename.substr(slashpos + 1);
40 }
41 size_t dotpos = nodir.find_last_of('.');
42 if (dotpos == (size_t)(-1)) {
43 *file = nodir;
44 *ext = "";
45 } else {
46 *file = nodir.substr(0, dotpos);
47 *ext = nodir.substr(dotpos);
48 }
49 }
50
51 // Returns whether the file exists and we have read permissions.
FileExists(const std::string & filename)52 bool FileExists(const std::string& filename) {
53 FILE* file = fopen(filename.c_str(), "rb");
54 if (file) {
55 fclose(file);
56 return true;
57 }
58 return false;
59 }
60
61 // Returns the size of the file, if it exists and we have read permissions.
GetFileSize(const std::string & filename)62 size_t GetFileSize(const std::string& filename) {
63 size_t size;
64 FILE* file = fopen(filename.c_str(), "rb");
65 if (!file) return 0;
66 fseek(file , 0 , SEEK_END);
67 size = static_cast<size_t>(ftell(file));
68 fclose(file);
69 return size;
70 }
71
ShowHelp()72 void ShowHelp() {
73 printf("ZopfliPNG, a Portable Network Graphics (PNG) image optimizer.\n"
74 "\n"
75 "Usage: zopflipng [options]... infile.png outfile.png\n"
76 " zopflipng [options]... --prefix=[fileprefix] [files.png]...\n"
77 "\n"
78 "If the output file exists, it is considered a result from a"
79 " previous run and not overwritten if its filesize is smaller.\n"
80 "\n"
81 "Options:\n"
82 "-m: compress more: use more iterations (depending on file size)\n"
83 "--prefix=[fileprefix]: Adds a prefix to output filenames. May also"
84 " contain a directory path. When using a prefix, multiple input files"
85 " can be given and the output filenames are generated with the"
86 " prefix\n"
87 " If --prefix is specified without value, 'zopfli_' is used.\n"
88 " If input file names contain the prefix, they are not processed but"
89 " considered as output from previous runs. This is handy when using"
90 " *.png wildcard expansion with multiple runs.\n"
91 "-y: do not ask about overwriting files.\n"
92 "--lossy_transparent: remove colors behind alpha channel 0. No visual"
93 " difference, removes hidden information.\n"
94 "--lossy_8bit: convert 16-bit per channel image to 8-bit per"
95 " channel.\n"
96 "-d: dry run: don't save any files, just see the console output"
97 " (e.g. for benchmarking)\n"
98 "--always_zopflify: always output the image encoded by Zopfli, even if"
99 " it's bigger than the original, for benchmarking the algorithm. Not"
100 " good for real optimization.\n"
101 "-q: use quick, but not very good, compression"
102 " (e.g. for only trying the PNG filter and color types)\n"
103 "--iterations=[number]: number of iterations, more iterations makes it"
104 " slower but provides slightly better compression. Default: 15 for"
105 " small files, 5 for large files.\n"
106 "--splitting=[0-3]: ignored, left for backwards compatibility\n"
107 "--filters=[types]: filter strategies to try:\n"
108 " 0-4: give all scanlines PNG filter type 0-4\n"
109 " m: minimum sum\n"
110 " e: entropy\n"
111 " p: predefined (keep from input, this likely overlaps another"
112 " strategy)\n"
113 " b: brute force (experimental)\n"
114 " By default, if this argument is not given, one that is most likely"
115 " the best for this image is chosen by trying faster compression with"
116 " each type.\n"
117 " If this argument is used, all given filter types"
118 " are tried with slow compression and the best result retained. A good"
119 " set of filters to try is --filters=0me.\n"
120 "--keepchunks=nAME,nAME,...: keep metadata chunks with these names"
121 " that would normally be removed, e.g. tEXt,zTXt,iTXt,gAMA, ... \n"
122 " Due to adding extra data, this increases the result size. Keeping"
123 " bKGD or sBIT chunks may cause additional worse compression due to"
124 " forcing a certain color type, it is advised to not keep these for"
125 " web images because web browsers do not use these chunks. By default"
126 " ZopfliPNG only keeps (and losslessly modifies) the following chunks"
127 " because they are essential: IHDR, PLTE, tRNS, IDAT and IEND.\n"
128 "--keepcolortype: Keep original color type (RGB, RGBA, gray,"
129 " gray+alpha or palette) and bit depth of the PNG.\n"
130 " This results in a loss of compression opportunities, e.g. it will no"
131 " longer convert a 4-channel RGBA image to 2-channel gray+alpha if the"
132 " image only had translucent gray pixels.\n"
133 " May be useful if a device does not support decoding PNGs of a"
134 " particular color type.\n"
135 "\n"
136 "Usage examples:\n"
137 "Optimize a file and overwrite if smaller: zopflipng infile.png"
138 " outfile.png\n"
139 "Compress more: zopflipng -m infile.png outfile.png\n"
140 "Optimize multiple files: zopflipng --prefix a.png b.png c.png\n"
141 "Compress really good and trying all filter strategies: zopflipng"
142 " --iterations=500 --filters=01234mepb --lossy_8bit"
143 " --lossy_transparent infile.png outfile.png\n");
144 }
145
PrintSize(const char * label,size_t size)146 void PrintSize(const char* label, size_t size) {
147 printf("%s: %d (%dK)\n", label, (int) size, (int) size / 1024);
148 }
149
PrintResultSize(const char * label,size_t oldsize,size_t newsize)150 void PrintResultSize(const char* label, size_t oldsize, size_t newsize) {
151 printf("%s: %d (%dK). Percentage of original: %.3f%%\n",
152 label, (int) newsize, (int) newsize / 1024, newsize * 100.0 / oldsize);
153 }
154
main(int argc,char * argv[])155 int main(int argc, char *argv[]) {
156 if (argc < 2) {
157 ShowHelp();
158 return 0;
159 }
160
161 ZopfliPNGOptions png_options;
162
163 // cmd line options
164 bool always_zopflify = false; // overwrite file even if we have bigger result
165 bool yes = false; // do not ask to overwrite files
166 bool dryrun = false; // never save anything
167
168 std::string user_out_filename; // output filename if no prefix is used
169 bool use_prefix = false;
170 std::string prefix = "zopfli_"; // prefix for output filenames
171
172 std::vector<std::string> files;
173 for (int i = 1; i < argc; i++) {
174 std::string arg = argv[i];
175 if (arg[0] == '-' && arg.size() > 1 && arg[1] != '-') {
176 for (size_t pos = 1; pos < arg.size(); pos++) {
177 char c = arg[pos];
178 if (c == 'y') {
179 yes = true;
180 } else if (c == 'd') {
181 dryrun = true;
182 } else if (c == 'm') {
183 png_options.num_iterations *= 4;
184 png_options.num_iterations_large *= 4;
185 } else if (c == 'q') {
186 png_options.use_zopfli = false;
187 } else if (c == 'h') {
188 ShowHelp();
189 return 0;
190 } else {
191 printf("Unknown flag: %c\n", c);
192 return 0;
193 }
194 }
195 } else if (arg[0] == '-' && arg.size() > 1 && arg[1] == '-') {
196 size_t eq = arg.find('=');
197 std::string name = arg.substr(0, eq);
198 std::string value = eq >= arg.size() - 1 ? "" : arg.substr(eq + 1);
199 int num = atoi(value.c_str());
200 if (name == "--always_zopflify") {
201 always_zopflify = true;
202 } else if (name == "--verbose") {
203 png_options.verbose = true;
204 } else if (name == "--lossy_transparent") {
205 png_options.lossy_transparent = true;
206 } else if (name == "--lossy_8bit") {
207 png_options.lossy_8bit = true;
208 } else if (name == "--iterations") {
209 if (num < 1) num = 1;
210 png_options.num_iterations = num;
211 png_options.num_iterations_large = num;
212 } else if (name == "--splitting") {
213 // ignored
214 } else if (name == "--filters") {
215 for (size_t j = 0; j < value.size(); j++) {
216 ZopfliPNGFilterStrategy strategy = kStrategyZero;
217 char f = value[j];
218 switch (f) {
219 case '0': strategy = kStrategyZero; break;
220 case '1': strategy = kStrategyOne; break;
221 case '2': strategy = kStrategyTwo; break;
222 case '3': strategy = kStrategyThree; break;
223 case '4': strategy = kStrategyFour; break;
224 case 'm': strategy = kStrategyMinSum; break;
225 case 'e': strategy = kStrategyEntropy; break;
226 case 'p': strategy = kStrategyPredefined; break;
227 case 'b': strategy = kStrategyBruteForce; break;
228 default:
229 printf("Unknown filter strategy: %c\n", f);
230 return 1;
231 }
232 png_options.filter_strategies.push_back(strategy);
233 // Enable auto filter strategy only if no user-specified filter is
234 // given.
235 png_options.auto_filter_strategy = false;
236 }
237 } else if (name == "--keepchunks") {
238 bool correct = true;
239 if ((value.size() + 1) % 5 != 0) correct = false;
240 for (size_t i = 0; i + 4 <= value.size() && correct; i += 5) {
241 png_options.keepchunks.push_back(value.substr(i, 4));
242 if (i > 4 && value[i - 1] != ',') correct = false;
243 }
244 if (!correct) {
245 printf("Error: keepchunks format must be like for example:\n"
246 " --keepchunks=gAMA,cHRM,sRGB,iCCP\n");
247 return 0;
248 }
249 } else if (name == "--keepcolortype") {
250 png_options.keep_colortype = true;
251 } else if (name == "--prefix") {
252 use_prefix = true;
253 if (!value.empty()) prefix = value;
254 } else if (name == "--help") {
255 ShowHelp();
256 return 0;
257 } else {
258 printf("Unknown flag: %s\n", name.c_str());
259 return 0;
260 }
261 } else {
262 files.push_back(argv[i]);
263 }
264 }
265
266 if (!use_prefix) {
267 if (files.size() == 2) {
268 // The second filename is the output instead of an input if no prefix is
269 // given.
270 user_out_filename = files[1];
271 files.resize(1);
272 } else {
273 printf("Please provide one input and output filename\n\n");
274 ShowHelp();
275 return 0;
276 }
277 }
278
279 size_t total_in_size = 0;
280 // Total output size, taking input size if the input file was smaller
281 size_t total_out_size = 0;
282 // Total output size that zopfli produced, even if input was smaller, for
283 // benchmark information
284 size_t total_out_size_zopfli = 0;
285 size_t total_errors = 0;
286 size_t total_files = 0;
287 size_t total_files_smaller = 0;
288 size_t total_files_saved = 0;
289 size_t total_files_equal = 0;
290
291 for (size_t i = 0; i < files.size(); i++) {
292 if (use_prefix && files.size() > 1) {
293 std::string dir, file, ext;
294 GetFileNameParts(files[i], &dir, &file, &ext);
295 // avoid doing filenames which were already output by this so that you
296 // don't get zopfli_zopfli_zopfli_... files after multiple runs.
297 if (file.find(prefix) == 0) continue;
298 }
299
300 total_files++;
301
302 printf("Optimizing %s\n", files[i].c_str());
303 std::vector<unsigned char> image;
304 unsigned w, h;
305 std::vector<unsigned char> origpng;
306 unsigned error;
307 lodepng::State inputstate;
308 std::vector<unsigned char> resultpng;
309
310 error = lodepng::load_file(origpng, files[i]);
311 if (!error) {
312 error = ZopfliPNGOptimize(origpng, png_options,
313 png_options.verbose, &resultpng);
314 }
315
316 if (error) {
317 if (error == 1) {
318 printf("Decoding error\n");
319 } else {
320 printf("Decoding error %u: %s\n", error, lodepng_error_text(error));
321 }
322 }
323
324 // Verify result, check that the result causes no decoding errors
325 if (!error) {
326 error = lodepng::decode(image, w, h, resultpng);
327 if (!error) {
328 std::vector<unsigned char> origimage;
329 unsigned origw, origh;
330 lodepng::decode(origimage, origw, origh, origpng);
331 if (origw != w || origh != h || origimage.size() != image.size()) {
332 error = 1;
333 } else {
334 for (size_t i = 0; i < image.size(); i += 4) {
335 bool same_alpha = image[i + 3] == origimage[i + 3];
336 bool same_rgb =
337 (png_options.lossy_transparent && image[i + 3] == 0) ||
338 (image[i + 0] == origimage[i + 0] &&
339 image[i + 1] == origimage[i + 1] &&
340 image[i + 2] == origimage[i + 2]);
341 if (!same_alpha || !same_rgb) {
342 error = 1;
343 break;
344 }
345 }
346 }
347 }
348 if (error) {
349 printf("Error: verification of result failed, keeping original."
350 " Error: %u.\n", error);
351 // Reset the error to 0, instead set output back to the original. The
352 // input PNG is valid, zopfli failed on it so treat as if it could not
353 // make it smaller.
354 error = 0;
355 resultpng = origpng;
356 }
357 }
358
359 if (error) {
360 total_errors++;
361 } else {
362 size_t origsize = origpng.size();
363 size_t resultsize = resultpng.size();
364
365 if (!png_options.keepchunks.empty()) {
366 std::vector<std::string> names;
367 std::vector<size_t> sizes;
368 lodepng::getChunkInfo(names, sizes, resultpng);
369 for (size_t i = 0; i < names.size(); i++) {
370 if (names[i] == "bKGD" || names[i] == "sBIT") {
371 printf("Forced to keep original color type due to keeping bKGD or"
372 " sBIT chunk. Try without --keepchunks for better"
373 " compression.\n");
374 break;
375 }
376 }
377 }
378
379 PrintSize("Input size", origsize);
380 PrintResultSize("Result size", origsize, resultsize);
381 if (resultsize < origsize) {
382 printf("Result is smaller\n");
383 } else if (resultsize == origsize) {
384 printf("Result has exact same size\n");
385 } else {
386 printf(always_zopflify
387 ? "Original was smaller\n"
388 : "Preserving original PNG since it was smaller\n");
389 }
390
391 std::string out_filename = user_out_filename;
392 if (use_prefix) {
393 std::string dir, file, ext;
394 GetFileNameParts(files[i], &dir, &file, &ext);
395 out_filename = dir + prefix + file + ext;
396 }
397 bool different_output_name = out_filename != files[i];
398
399 total_in_size += origsize;
400 total_out_size_zopfli += resultpng.size();
401 if (resultpng.size() < origsize) total_files_smaller++;
402 else if (resultpng.size() == origsize) total_files_equal++;
403
404 if (!always_zopflify && resultpng.size() >= origsize) {
405 // Set output file to input since zopfli didn't improve it.
406 resultpng = origpng;
407 }
408
409 bool already_exists = FileExists(out_filename);
410 size_t origoutfilesize = GetFileSize(out_filename);
411
412 // When using a prefix, and the output file already exist, assume it's
413 // from a previous run. If that file is smaller, it may represent a
414 // previous run with different parameters that gave a smaller PNG image.
415 // This also applies when not using prefix but same input as output file.
416 // In that case, do not overwrite it. This behaviour can be removed by
417 // adding the always_zopflify flag.
418 bool keep_earlier_output_file = already_exists &&
419 resultpng.size() >= origoutfilesize && !always_zopflify &&
420 (use_prefix || !different_output_name);
421
422 if (keep_earlier_output_file) {
423 // An output file from a previous run is kept, add that files' size
424 // to the output size statistics.
425 total_out_size += origoutfilesize;
426 if (use_prefix) {
427 printf(resultpng.size() == origoutfilesize
428 ? "File not written because a previous run was as good.\n"
429 : "File not written because a previous run was better.\n");
430 }
431 } else {
432 bool confirmed = true;
433 if (!yes && !dryrun && already_exists) {
434 printf("File %s exists, overwrite? (y/N) ", out_filename.c_str());
435 char answer = 0;
436 // Read the first character, the others and enter with getchar.
437 while (int input = getchar()) {
438 if (input == '\n' || input == EOF) break;
439 else if (!answer) answer = input;
440 }
441 confirmed = answer == 'y' || answer == 'Y';
442 }
443 if (confirmed) {
444 if (!dryrun) {
445 if (lodepng::save_file(resultpng, out_filename) != 0) {
446 printf("Failed to write to file %s\n", out_filename.c_str());
447 } else {
448 total_files_saved++;
449 }
450 }
451 total_out_size += resultpng.size();
452 } else {
453 // An output file from a previous run is kept, add that files' size
454 // to the output size statistics.
455 total_out_size += origoutfilesize;
456 }
457 }
458 }
459 printf("\n");
460 }
461
462 if (total_files > 1) {
463 printf("Summary for all files:\n");
464 printf("Files tried: %d\n", (int) total_files);
465 printf("Files smaller: %d\n", (int) total_files_smaller);
466 if (total_files_equal) {
467 printf("Files equal: %d\n", (int) total_files_equal);
468 }
469 printf("Files saved: %d\n", (int) total_files_saved);
470 if (total_errors) printf("Errors: %d\n", (int) total_errors);
471 PrintSize("Total input size", total_in_size);
472 PrintResultSize("Total output size", total_in_size, total_out_size);
473 PrintResultSize("Benchmark result size",
474 total_in_size, total_out_size_zopfli);
475 }
476
477 if (dryrun) printf("No files were written because dry run was specified\n");
478
479 return total_errors;
480 }
481