// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: lode.vandevenne@gmail.com (Lode Vandevenne)
// Author: jyrki.alakuijala@gmail.com (Jyrki Alakuijala)
// Command line tool to recompress and optimize PNG images, using zopflipng_lib.
#include <stdlib.h>
#include <stdio.h>
#include "lodepng/lodepng.h"
#include "zopflipng_lib.h"
// Returns directory path (including last slash) in dir, filename without
// extension in file, extension (including the dot) in ext
void GetFileNameParts(const std::string& filename,
std::string* dir, std::string* file, std::string* ext) {
size_t npos = (size_t)(-1);
size_t slashpos = filename.find_last_of("/\\");
std::string nodir;
if (slashpos == npos) {
*dir = "";
nodir = filename;
} else {
*dir = filename.substr(0, slashpos + 1);
nodir = filename.substr(slashpos + 1);
}
size_t dotpos = nodir.find_last_of('.');
if (dotpos == (size_t)(-1)) {
*file = nodir;
*ext = "";
} else {
*file = nodir.substr(0, dotpos);
*ext = nodir.substr(dotpos);
}
}
// Returns the size of the file
size_t GetFileSize(const std::string& filename) {
size_t size;
FILE* file = fopen(filename.c_str(), "rb");
if (!file) return 0;
fseek(file , 0 , SEEK_END);
size = static_cast<size_t>(ftell(file));
fclose(file);
return size;
}
void ShowHelp() {
printf("ZopfliPNG, a Portable Network Graphics (PNG) image optimizer.\n"
"\n"
"Usage: zopflipng [options]... infile.png outfile.png\n"
" zopflipng [options]... --prefix=[fileprefix] [files.png]...\n"
"\n"
"If the output file exists, it is considered a result from a"
" previous run and not overwritten if its filesize is smaller.\n"
"\n"
"Options:\n"
"-m: compress more: use more iterations (depending on file size) and"
" use block split strategy 3\n"
"--prefix=[fileprefix]: Adds a prefix to output filenames. May also"
" contain a directory path. When using a prefix, multiple input files"
" can be given and the output filenames are generated with the"
" prefix\n"
" If --prefix is specified without value, 'zopfli_' is used.\n"
" If input file names contain the prefix, they are not processed but"
" considered as output from previous runs. This is handy when using"
" *.png wildcard expansion with multiple runs.\n"
"-y: do not ask about overwriting files.\n"
"--lossy_transparent: remove colors behind alpha channel 0. No visual"
" difference, removes hidden information.\n"
"--lossy_8bit: convert 16-bit per channel image to 8-bit per"
" channel.\n"
"-d: dry run: don't save any files, just see the console output"
" (e.g. for benchmarking)\n"
"--always_zopflify: always output the image encoded by Zopfli, even if"
" it's bigger than the original, for benchmarking the algorithm. Not"
" good for real optimization.\n"
"-q: use quick, but not very good, compression"
" (e.g. for only trying the PNG filter and color types)\n"
"--iterations=[number]: number of iterations, more iterations makes it"
" slower but provides slightly better compression. Default: 15 for"
" small files, 5 for large files.\n"
"--splitting=[0-3]: block split strategy:"
" 0=none, 1=first, 2=last, 3=try both and take the best\n"
"--filters=[types]: filter strategies to try:\n"
" 0-4: give all scanlines PNG filter type 0-4\n"
" m: minimum sum\n"
" e: entropy\n"
" p: predefined (keep from input, this likely overlaps another"
" strategy)\n"
" b: brute force (experimental)\n"
" By default, if this argument is not given, one that is most likely"
" the best for this image is chosen by trying faster compression with"
" each type.\n"
" If this argument is used, all given filter types"
" are tried with slow compression and the best result retained. A good"
" set of filters to try is --filters=0me.\n"
"--keepchunks=nAME,nAME,...: keep metadata chunks with these names"
" that would normally be removed, e.g. tEXt,zTXt,iTXt,gAMA, ... \n"
" Due to adding extra data, this increases the result size. By default"
" ZopfliPNG only keeps the following chunks because they are"
" essential: IHDR, PLTE, tRNS, IDAT and IEND.\n"
"\n"
"Usage examples:\n"
"Optimize a file and overwrite if smaller: zopflipng infile.png"
" outfile.png\n"
"Compress more: zopflipng -m infile.png outfile.png\n"
"Optimize multiple files: zopflipng --prefix a.png b.png c.png\n"
"Compress really good and trying all filter strategies: zopflipng"
" --iterations=500 --splitting=3 --filters=01234mepb"
" --lossy_8bit --lossy_transparent infile.png outfile.png\n");
}
void PrintSize(const char* label, size_t size) {
printf("%s: %d (%dK)\n", label, (int) size, (int) size / 1024);
}
void PrintResultSize(const char* label, size_t oldsize, size_t newsize) {
printf("%s: %d (%dK). Percentage of original: %.3f%%\n",
label, (int) newsize, (int) newsize / 1024, newsize * 100.0 / oldsize);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
ShowHelp();
return 0;
}
ZopfliPNGOptions png_options;
// cmd line options
bool always_zopflify = false; // overwrite file even if we have bigger result
bool yes = false; // do not ask to overwrite files
bool dryrun = false; // never save anything
std::string user_out_filename; // output filename if no prefix is used
bool use_prefix = false;
std::string prefix = "zopfli_"; // prefix for output filenames
std::vector<std::string> files;
std::vector<char> options;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg[0] == '-' && arg.size() > 1 && arg[1] != '-') {
for (size_t pos = 1; pos < arg.size(); pos++) {
char c = arg[pos];
if (c == 'y') {
yes = true;
} else if (c == 'd') {
dryrun = true;
} else if (c == 'm') {
png_options.num_iterations *= 4;
png_options.num_iterations_large *= 4;
png_options.block_split_strategy = 3;
} else if (c == 'q') {
png_options.use_zopfli = false;
} else if (c == 'h') {
ShowHelp();
return 0;
} else {
printf("Unknown flag: %c\n", c);
return 0;
}
}
} else if (arg[0] == '-' && arg.size() > 1 && arg[1] == '-') {
size_t eq = arg.find('=');
std::string name = arg.substr(0, eq);
std::string value = eq >= arg.size() - 1 ? "" : arg.substr(eq + 1);
int num = atoi(value.c_str());
if (name == "--always_zopflify") {
always_zopflify = true;
} else if (name == "--lossy_transparent") {
png_options.lossy_transparent = true;
} else if (name == "--lossy_8bit") {
png_options.lossy_8bit = true;
} else if (name == "--iterations") {
if (num < 1) num = 1;
png_options.num_iterations = num;
png_options.num_iterations_large = num;
} else if (name == "--splitting") {
if (num < 0 || num > 3) num = 1;
png_options.block_split_strategy = num;
} else if (name == "--filters") {
for (size_t j = 0; j < value.size(); j++) {
ZopfliPNGFilterStrategy strategy = kStrategyZero;
char f = value[j];
switch (f) {
case '0': strategy = kStrategyZero; break;
case '1': strategy = kStrategyOne; break;
case '2': strategy = kStrategyTwo; break;
case '3': strategy = kStrategyThree; break;
case '4': strategy = kStrategyFour; break;
case 'm': strategy = kStrategyMinSum; break;
case 'e': strategy = kStrategyEntropy; break;
case 'p': strategy = kStrategyPredefined; break;
case 'b': strategy = kStrategyBruteForce; break;
default:
printf("Unknown filter strategy: %c\n", f);
return 1;
}
png_options.filter_strategies.push_back(strategy);
// Enable auto filter strategy only if no user-specified filter is
// given.
png_options.auto_filter_strategy = false;
}
} else if (name == "--keepchunks") {
bool correct = true;
if ((value.size() + 1) % 5 != 0) correct = false;
for (size_t i = 0; i + 4 <= value.size() && correct; i += 5) {
png_options.keepchunks.push_back(value.substr(i, 4));
if (i > 4 && value[i - 1] != ',') correct = false;
}
if (!correct) {
printf("Error: keepchunks format must be like for example:\n"
" --keepchunks=gAMA,cHRM,sRGB,iCCP\n");
return 0;
}
} else if (name == "--prefix") {
use_prefix = true;
if (!value.empty()) prefix = value;
} else if (name == "--help") {
ShowHelp();
return 0;
} else {
printf("Unknown flag: %s\n", name.c_str());
return 0;
}
} else {
files.push_back(argv[i]);
}
}
if (!use_prefix) {
if (files.size() == 2) {
// The second filename is the output instead of an input if no prefix is
// given.
user_out_filename = files[1];
files.resize(1);
} else {
printf("Please provide one input and output filename\n\n");
ShowHelp();
return 0;
}
}
size_t total_in_size = 0;
// Total output size, taking input size if the input file was smaller
size_t total_out_size = 0;
// Total output size that zopfli produced, even if input was smaller, for
// benchmark information
size_t total_out_size_zopfli = 0;
size_t total_errors = 0;
size_t total_files = 0;
size_t total_files_smaller = 0;
size_t total_files_saved = 0;
size_t total_files_equal = 0;
for (size_t i = 0; i < files.size(); i++) {
if (use_prefix && files.size() > 1) {
std::string dir, file, ext;
GetFileNameParts(files[i], &dir, &file, &ext);
// avoid doing filenames which were already output by this so that you
// don't get zopfli_zopfli_zopfli_... files after multiple runs.
if (file.find(prefix) == 0) continue;
}
total_files++;
printf("Optimizing %s\n", files[i].c_str());
std::vector<unsigned char> image;
unsigned w, h;
std::vector<unsigned char> origpng;
unsigned error;
lodepng::State inputstate;
std::vector<unsigned char> resultpng;
lodepng::load_file(origpng, files[i]);
error = ZopfliPNGOptimize(origpng, png_options, true, &resultpng);
if (error) {
printf("Decoding error %i: %s\n", error, lodepng_error_text(error));
}
// Verify result, check that the result causes no decoding errors
if (!error) {
error = lodepng::decode(image, w, h, inputstate, resultpng);
if (error) printf("Error: verification of result failed.\n");
}
if (error) {
printf("There was an error\n");
total_errors++;
} else {
size_t origsize = GetFileSize(files[i]);
size_t resultsize = resultpng.size();
if (resultsize < origsize) {
printf("Result is smaller\n");
} else if (resultsize == origsize) {
printf("Result has exact same size\n");
} else {
printf(always_zopflify
? "Original was smaller\n"
: "Preserving original PNG since it was smaller\n");
}
PrintSize("Input size", origsize);
PrintResultSize("Result size", origsize, resultsize);
std::string out_filename = user_out_filename;
if (use_prefix) {
std::string dir, file, ext;
GetFileNameParts(files[i], &dir, &file, &ext);
out_filename = dir + prefix + file + ext;
}
bool different_output_name = out_filename != files[i];
total_in_size += origsize;
total_out_size_zopfli += resultpng.size();
if (resultpng.size() < origsize) total_files_smaller++;
else if (resultpng.size() == origsize) total_files_equal++;
if (!always_zopflify && resultpng.size() > origsize) {
// Set output file to input since input was smaller.
resultpng = origpng;
}
size_t origoutfilesize = GetFileSize(out_filename);
bool already_exists = true;
if (origoutfilesize == 0) already_exists = false;
// When using a prefix, and the output file already exist, assume it's
// from a previous run. If that file is smaller, it may represent a
// previous run with different parameters that gave a smaller PNG image.
// In that case, do not overwrite it. This behaviour can be removed by
// adding the always_zopflify flag.
bool keep_earlier_output_file = already_exists &&
resultpng.size() >= origoutfilesize && !always_zopflify && use_prefix;
if (keep_earlier_output_file) {
// An output file from a previous run is kept, add that files' size
// to the output size statistics.
total_out_size += origoutfilesize;
if (different_output_name) {
printf(resultpng.size() == origoutfilesize
? "File not written because a previous run was as good.\n"
: "File not written because a previous run was better.\n");
}
} else {
bool confirmed = true;
if (!yes && !dryrun && already_exists) {
printf("File %s exists, overwrite? (y/N) ", out_filename.c_str());
char answer = 0;
// Read the first character, the others and enter with getchar.
while (int input = getchar()) {
if (input == '\n' || input == EOF) break;
else if (!answer) answer = input;
}
confirmed = answer == 'y' || answer == 'Y';
}
if (confirmed) {
if (!dryrun) {
lodepng::save_file(resultpng, out_filename);
total_files_saved++;
}
total_out_size += resultpng.size();
} else {
// An output file from a previous run is kept, add that files' size
// to the output size statistics.
total_out_size += origoutfilesize;
}
}
}
printf("\n");
}
if (total_files > 1) {
printf("Summary for all files:\n");
printf("Files tried: %d\n", (int) total_files);
printf("Files smaller: %d\n", (int) total_files_smaller);
if (total_files_equal) {
printf("Files equal: %d\n", (int) total_files_equal);
}
printf("Files saved: %d\n", (int) total_files_saved);
if (total_errors) printf("Errors: %d\n", (int) total_errors);
PrintSize("Total input size", total_in_size);
PrintResultSize("Total output size", total_in_size, total_out_size);
PrintResultSize("Benchmark result size",
total_in_size, total_out_size_zopfli);
}
if (dryrun) printf("No files were written because dry run was specified\n");
return total_errors;
}