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