153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
import zopfli
|
|
from zopfli.zopfli import png_optimize as optimize
|
|
|
|
__all__ = ["optimize"]
|
|
|
|
|
|
def main(args=None):
|
|
import argparse
|
|
import os
|
|
|
|
parser = argparse.ArgumentParser(prog="python -m zopfli.png")
|
|
parser.add_argument("infile")
|
|
parser.add_argument("outfile")
|
|
parser.add_argument("-v", "--verbose", action="store_true", help="print more info")
|
|
parser.add_argument(
|
|
"-m",
|
|
action="store_true",
|
|
dest="compress_more",
|
|
help="compress more: use more iterations (depending on file size).",
|
|
)
|
|
parser.add_argument(
|
|
"-y",
|
|
dest="overwrite",
|
|
action="store_true",
|
|
help="do not ask about overwriting files.",
|
|
)
|
|
parser.add_argument(
|
|
"--lossy_transparent",
|
|
action="store_true",
|
|
help="remove colors behind alpha channel 0. No visual difference.",
|
|
)
|
|
parser.add_argument(
|
|
"--lossy_8bit",
|
|
action="store_true",
|
|
help="convert 16-bit per channel image to 8-bit per channel.",
|
|
)
|
|
parser.add_argument(
|
|
"--always_zopflify",
|
|
action="store_true",
|
|
help="always output the image encoded by Zopfli, even if bigger than original.",
|
|
)
|
|
parser.add_argument(
|
|
"-q",
|
|
dest="use_zopfli",
|
|
action="store_false",
|
|
help="use quick, but not very good, compression.",
|
|
)
|
|
parser.add_argument(
|
|
"--iterations",
|
|
default=None,
|
|
type=int,
|
|
help=(
|
|
"number of iterations, more iterations makes it slower but provides "
|
|
"slightly better compression. Default: 15 for small files, 5 for large files."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--filters",
|
|
dest="filter_strategies",
|
|
help=(
|
|
"filter strategies to try: "
|
|
"0-4: give all scanlines PNG filter type 0-4; "
|
|
"m: minimum sum; "
|
|
"e: entropy; "
|
|
"p: predefined (keep from input, this likely overlaps another strategy); "
|
|
"b: brute force (experimental). "
|
|
"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. "
|
|
"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."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--keepchunks",
|
|
type=lambda s: s.split(","),
|
|
help=(
|
|
"keep metadata chunks with these names that would normally be removed, "
|
|
"e.g. tEXt,zTXt,iTXt,gAMA, ... Due to adding extra data, this increases "
|
|
"the result size. Keeping bKGD or sBIT chunks may cause additional worse "
|
|
"compression due to forcing a certain color type, it is advised to not "
|
|
"keep these for web images because web browsers do not use these chunks. "
|
|
"By default ZopfliPNG only keeps (and losslessly modifies) the following "
|
|
"chunks because they are essential: IHDR, PLTE, tRNS, IDAT and IEND."
|
|
),
|
|
)
|
|
|
|
options = parser.parse_args(args)
|
|
|
|
log = print if options.verbose else lambda *_: None
|
|
|
|
if options.iterations is not None:
|
|
num_iterations = num_iterations_large = options.iterations
|
|
else:
|
|
# these constants are taken from zopflipng_lib.cc, unlikely to ever change
|
|
num_iterations, num_iterations_large = 15, 5
|
|
if options.compress_more:
|
|
num_iterations *= 4
|
|
num_iterations_large *= 4
|
|
|
|
with open(options.infile, "rb") as f:
|
|
input_png = f.read()
|
|
|
|
log(f"Optimizing {options.infile}")
|
|
|
|
result_png = optimize(
|
|
input_png,
|
|
verbose=options.verbose,
|
|
lossy_transparent=options.lossy_transparent,
|
|
lossy_8bit=options.lossy_8bit,
|
|
filter_strategies=options.filter_strategies,
|
|
keepchunks=options.keepchunks,
|
|
use_zopfli=options.use_zopfli,
|
|
num_iterations=num_iterations,
|
|
num_iterations_large=num_iterations_large,
|
|
)
|
|
|
|
input_size = len(input_png)
|
|
log(f"Input size: {input_size} ({input_size // 1024}K)")
|
|
result_size = len(result_png)
|
|
percentage = round(result_size / input_size * 100, 3)
|
|
log(
|
|
f"Result size: {result_size} ({result_size // 1024}K). "
|
|
f"Percentage of original: {percentage}%"
|
|
)
|
|
|
|
if result_size < input_size:
|
|
log("Result is smaller")
|
|
elif result_size == input_size:
|
|
log("Result has exact same size")
|
|
else:
|
|
if options.always_zopflify:
|
|
log("Original was smaller")
|
|
else:
|
|
log("Preserving original PNG since it was smaller")
|
|
# Set output file to input since zopfli didn't improve it.
|
|
result_png = input_png
|
|
|
|
if (
|
|
not options.overwrite
|
|
and os.path.isfile(options.outfile)
|
|
and input(f"File {options.outfile} exists, overwrite? (y/N)\n").strip().lower()
|
|
!= "y"
|
|
):
|
|
return 0
|
|
|
|
with open(options.outfile, "wb") as f:
|
|
f.write(result_png)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|