mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-25 10:06:17 +00:00

An improved version of the wesnoth-optipng shell script. Supports multiple jobs at a time. ( -j ) Accepts a threshold for discarding images which were only reduced by a few bytes. ( -t 15 will only save images that were reduced in size by at least 15 %). Niceness can be set using the -n switch. Usage: cd <directory> woptipng . ../some/dir ../some/some_file.png The script should achive better compression ratios than the previous script. The previous script was running optimizations in an unconditional loop and, at the end, did not save the image if pixel colors changed. This implies if *one* of the crushers did a bad job and changed the images content in a bad way, *none* of the possibly good optimizations by the other crushed would be saved. Woptipng checks the image every time after a crusher touched it and only reverts a single optimization if it was bad leaving possibility for the other tools to still perform a good job and reduce size of the image.
224 lines
7.9 KiB
Python
Executable File
224 lines
7.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# woptipng - attempts to reduce PNGs in size using several other tools
|
|
# Copyright (C) 2016 Matthias Krüger
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2, or (at your option)
|
|
# any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA
|
|
|
|
|
|
# Please file bugs to https://github.com/matthiaskrgr/woptipng
|
|
|
|
from multiprocessing import Pool
|
|
import multiprocessing # cpu count
|
|
from PIL import Image as PIL # compare images
|
|
import subprocess # launch advdef, optipng, imagemagick
|
|
import os # os rename, niceness
|
|
import shutil # copy files
|
|
import argparse # argument parsing
|
|
import sys # sys.exit
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("inpath", help="file or path (recursively) to be searched for crushable pngss", metavar='path', nargs='+', type=str)
|
|
parser.add_argument("-d", "--debug", help="print debug information", action='store_true')
|
|
parser.add_argument("-t", "--threshold", help="size reduction below this percentage will be discarded, default: 10%", metavar='n', nargs='?', default=10, type=float)
|
|
parser.add_argument("-j", "--jobs", help="max amount of jobs/threads. If unspecified, take number of cores found", metavar='n', nargs='?', default=multiprocessing.cpu_count(), type=int)
|
|
parser.add_argument("-n", "--nice", help="niceness of all threads (must be positive)", metavar='n', nargs="?", default=19, type=int)
|
|
|
|
args = parser.parse_args()
|
|
|
|
DEBUG = args.debug
|
|
INPATHS = args.inpath
|
|
THRESHOLD = args.threshold
|
|
MAX_THREADS = args.jobs
|
|
# program executables
|
|
EXEC_OPTIPNG = shutil.which("optipng")
|
|
EXEC_IMAGEMAGICK = shutil.which("convert")
|
|
EXEC_ADVDEF = shutil.which("advdef")
|
|
|
|
os.nice(args.nice) # set niceness
|
|
|
|
input_files=[]
|
|
bad_input_files=[]
|
|
|
|
|
|
print("Collecting files... ", end="")
|
|
for path in INPATHS: # iterate over arguments
|
|
if (os.path.isfile(path)): # inpath is a file
|
|
if (path.endswith("png")):
|
|
input_files.append(path)
|
|
else: # not png?
|
|
bad_input_files.append(path)
|
|
elif (os.path.isdir(path)): # inpath is a directory
|
|
for root, directories, filenames in os.walk(path):
|
|
for filename in filenames:
|
|
if (filename.split('.')[-1] == "png"): # check for valid filetypes
|
|
input_files.append(os.path.join(root,filename)) # add to list
|
|
else: # path does not exist
|
|
bad_input_files.append(path)
|
|
|
|
bad_input_files.sort()
|
|
input_files.sort()
|
|
|
|
# get sizes
|
|
file_list = []
|
|
for file_ in input_files:
|
|
file_list.append([file_, os.path.getsize(file_), None])
|
|
|
|
print(" done")
|
|
if (bad_input_files):
|
|
print("WARNING: can't handle following files:' ")
|
|
print(', '.join(bad_input_files) + "\n")
|
|
|
|
|
|
|
|
print("Compressing " + str(len(file_list)) + " pngs...")
|
|
|
|
def debugprint(arg):
|
|
if (DEBUG):
|
|
print(arg)
|
|
|
|
def images_identical(image1, image2):
|
|
return PIL.open(image1).tobytes() == PIL.open(image2).tobytes()
|
|
|
|
def verify_images(source_img, new_img, transform):
|
|
no_change = images_identical(source_img, new_img) # image pixels values remain unaltered, we want this
|
|
image_got_smaller = os.path.getsize(source_img) > os.path.getsize(new_img)
|
|
debugprint("size reduction: " + str(os.path.getsize(source_img) - os.path.getsize(new_img)))
|
|
|
|
if (no_change and image_got_smaller):
|
|
os.rename(new_img, source_img) # move new image to old image // os.rename(src, dest)
|
|
else: # we can't os.rename(image_after, image_before) because that would leave us with no source for the next transform
|
|
shutil.copy(source_img, new_img) # override new image with old image // shutil.copy(src, dest)
|
|
debugprint(("TRANSFORMATION unsuccessfull: + " + transform + ", REVERTING " + source_img))
|
|
|
|
def run_imagemagick(image, tmpimage):
|
|
shutil.copy(image, tmpimage)
|
|
debugprint("imagemagick ")
|
|
cmd = [ EXEC_IMAGEMAGICK,
|
|
"-strip",
|
|
"-define",
|
|
"png:color-type=6",
|
|
image,
|
|
tmpimage
|
|
]
|
|
subprocess.call(cmd)
|
|
|
|
def run_optipng(image, tmpimage):
|
|
debugprint("optipng...")
|
|
shutil.copy(image, tmpimage)
|
|
cmd = [ EXEC_OPTIPNG,
|
|
"-q",
|
|
"-o5",
|
|
"-nb",
|
|
"-nc",
|
|
"-np",
|
|
tmpimage
|
|
]
|
|
subprocess.call(cmd)
|
|
|
|
def run_advdef(image, tmpimage):
|
|
debugprint("advdef")
|
|
shutil.copy(image, tmpimage)
|
|
compression_levels = [1, 2, 3, 4]
|
|
|
|
for level in compression_levels:
|
|
cmd = [
|
|
EXEC_ADVDEF,
|
|
"-z",
|
|
"-" + str(level),
|
|
tmpimage,
|
|
]
|
|
subprocess.call(cmd, stdout=open(os.devnull, 'w')) # discard stdout
|
|
|
|
def check_progs():
|
|
if (not EXEC_ADVDEF):
|
|
print("ERROR: advdef binary not found!")
|
|
if (not EXEC_IMAGEMAGICK):
|
|
print("ERROR: imagemagick/convert binary not found!")
|
|
if (not EXEC_OPTIPNG):
|
|
print("ERROR: optipng not found!")
|
|
|
|
if not (EXEC_ADVDEF and EXEC_IMAGEMAGICK and EXEC_OPTIPNG):
|
|
sys.exit(1)
|
|
|
|
|
|
def optimize_image(image):
|
|
size_initial = os.path.getsize(image)
|
|
with open(image, 'rb') as f:
|
|
initial_file_content = f.read()
|
|
|
|
|
|
size_initial = os.path.getsize(image)
|
|
it=0
|
|
size_after = 0
|
|
size_before = os.path.getsize(image)
|
|
while ((size_before > size_after) or (not it)):
|
|
it+=1
|
|
debugprint(("iteration " + str(it)))
|
|
size_before = os.path.getsize(image)
|
|
tmpimage = image + ".tmp"
|
|
|
|
run_imagemagick(image, tmpimage)
|
|
verify_images(image, tmpimage, "imagemagick")
|
|
|
|
run_optipng(image, tmpimage)
|
|
verify_images(image, tmpimage, "optipng")
|
|
|
|
run_advdef(image, tmpimage)
|
|
verify_images(image, tmpimage, "advdef")
|
|
|
|
size_after = os.path.getsize(image)
|
|
size_delta = size_after - size_initial
|
|
perc_delta = (size_delta/size_initial) *100
|
|
|
|
if (DEBUG and (size_initial < size_after)):
|
|
debugprint("WARNING: " + str(image) + "got bigger !")
|
|
if os.path.isfile(tmpimage): # clean up
|
|
os.remove(tmpimage)
|
|
|
|
if (os.path.getsize(image) > size_initial) or (perc_delta*-1 < THRESHOLD) : # got bigger, or exceeds threshold
|
|
with open(image, 'wb') as f: # write back original file
|
|
f.write(initial_file_content)
|
|
else:
|
|
print("optimized {image} from {size_initial} to {size_after}, {size_delta}b, {perc_delta}%".format(image=image, size_initial=size_initial, size_after=size_after, size_delta=size_delta, perc_delta=str(perc_delta)[0:6]))
|
|
|
|
check_progs() # all tools available? if not: exit
|
|
|
|
# do the crushing
|
|
p = Pool(MAX_THREADS)
|
|
p.map(optimize_image, set(input_files))
|
|
|
|
# update file_list
|
|
for index, file_ in enumerate(file_list):
|
|
file_list[index][2] = os.path.getsize(file_[0]) # write new filesize into list
|
|
|
|
# obtain stats
|
|
size_before = 0
|
|
size_after = 0
|
|
files_optimized = 0
|
|
for i in file_list:
|
|
if i[1] > i[2]: # file got smaller
|
|
size_before += i[1]
|
|
size_after += i[2]
|
|
files_optimized += 1
|
|
# print stats
|
|
if (files_optimized):
|
|
print("{files_optimized} of {files_processed} files optimized, {size_before} bytes reduced to {size_after} bytes; {size_diff} bytes, {percentage_delta}%".format(files_optimized = files_optimized, files_processed = len(file_list), size_before = size_before, size_after=size_after, size_diff = size_after - size_before, percentage_delta = str((size_after - size_before)/(size_before)*100)[0:6]))
|
|
|
|
print("Optimization threshold was " + str(THRESHOLD) + "%")
|
|
else:
|
|
print("Nothing optimized")
|