Home | History | Annotate | Download | only in zopflipng
      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