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 (at) gmail.com (Lode Vandevenne) 16 // Author: jyrki.alakuijala (at) 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 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 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 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 127 void PrintSize(const char* label, size_t size) { 128 printf("%s: %d (%dK)\n", label, (int) size, (int) size / 1024); 129 } 130 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 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