Rewrote image optimisation stuff.
The old one was broken anyway.
This commit is contained in:
@@ -545,18 +545,92 @@ values. The valid configuration keys are listed below.
|
||||
**Default:** ``95``
|
||||
|
||||
``optimizeimg``
|
||||
|
||||
.. warning::
|
||||
Using image optimizers will increase render times significantly.
|
||||
|
||||
This option specifies which additional tools overviewer should use to
|
||||
optimize the filesize of png tiles.
|
||||
The tools used must be placed somewhere, where overviewer can find them, for
|
||||
example the "PATH" environment variable or a directory like /usr/bin.
|
||||
This should be an integer between 0 and 3.
|
||||
* ``1 - Use pngcrush``
|
||||
* ``2 - Use advdef``
|
||||
* ``3 - Use pngcrush and advdef (Not recommended)``
|
||||
Using this option may significantly increase render time, but will make
|
||||
the resulting tiles smaller, with lossless image quality.
|
||||
|
||||
**Default:** ``0``
|
||||
The option is a list of Optimizer objects, which are then executed in
|
||||
the order in which they're specified::
|
||||
|
||||
from optimizeimages import pngnq, optipng
|
||||
worlds["world"] = "/path/to/world"
|
||||
|
||||
renders["daytime"] = {
|
||||
"world":"world",
|
||||
"title":"day",
|
||||
"rendermode":smooth_lighting,
|
||||
"optimizeimg":[pngnq(sampling=1), optipng(olevel=3)],
|
||||
}
|
||||
|
||||
Here is a list of supported image optimization programs:
|
||||
|
||||
``pngnq``
|
||||
pngnq quantizes 32-bit RGBA images into 8-bit RGBA palette PNGs. This is
|
||||
lossy, but reduces filesize significantly. Available settings:
|
||||
|
||||
``sampling``
|
||||
An integer between ``1`` and ``10``, ``1`` samples all pixels, is slow and yields
|
||||
the best quality. Higher values sample less of the image, which makes
|
||||
the process faster, but less accurate.
|
||||
|
||||
**Default:** ``3``
|
||||
|
||||
``dither``
|
||||
Either the string ``"n"`` for no dithering, or ``"f"`` for Floyd
|
||||
Steinberg dithering. Dithering helps eliminate colorbanding, sometimes
|
||||
increasing visual quality.
|
||||
|
||||
.. warning::
|
||||
With pngnq version 1.0 (which is what Ubuntu 12.04 ships), the
|
||||
dithering option is broken. Only the default, no dithering,
|
||||
can be specified on those systems.
|
||||
|
||||
**Default:** ``"n"``
|
||||
|
||||
.. warning::
|
||||
Because of several PIL bugs, only the most zoomed in level has transparency
|
||||
when using pngnq. The other zoom levels have all transparency replaced by
|
||||
black. This is *not* pngnq's fault, as pngnq supports multiple levels of
|
||||
transparency just fine, it's PIL's fault for not even reading indexed
|
||||
PNGs correctly.
|
||||
|
||||
``optipng``
|
||||
optipng tunes the deflate algorithm and removes unneeded channels from the PNG,
|
||||
producing a smaller, lossless output image. It was inspired by pngcrush.
|
||||
Available settings:
|
||||
|
||||
``olevel``
|
||||
An integer between ``0`` (few optimizations) and ``7`` (many optimizations).
|
||||
The default should be satisfactory for everyone, higher levels than the default
|
||||
see almost no benefit.
|
||||
|
||||
**Default:** ``2``
|
||||
|
||||
``pngcrush``
|
||||
pngcrush is very slow and not very good, you should use optipng in probably all cases.
|
||||
However, Overviewer still allows you to use it because we're nice people like that.
|
||||
Available settings:
|
||||
|
||||
``brute``
|
||||
Either ``True`` or ``False``. Cycles through all compression methods, and is very slow.
|
||||
|
||||
.. note::
|
||||
There is practically no reason to ever use this. optipng will beat pngcrush, and
|
||||
throwing more CPU time at pngcrush most likely won't help. If you think you need
|
||||
this option, then you are most likely wrong.
|
||||
|
||||
**Default:** ``False``
|
||||
|
||||
.. note::
|
||||
Don't forget to import the optimizers you use in your settings file, as shown in the
|
||||
example above.
|
||||
|
||||
**Default:** ``[]``
|
||||
|
||||
``bgcolor``
|
||||
This is the background color to be displayed behind the map. Its value
|
||||
|
||||
@@ -17,36 +17,96 @@ import os
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
pngcrush = "pngcrush"
|
||||
optipng = "optipng"
|
||||
advdef = "advdef"
|
||||
class Optimizer:
|
||||
binaryname = ""
|
||||
|
||||
def check_programs(level):
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
def optimize(self, img):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
def fire_and_forget(self, args):
|
||||
subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
|
||||
|
||||
class NonAtomicOptimizer(Optimizer):
|
||||
def cleanup(self, img):
|
||||
os.rename(img + ".tmp", img)
|
||||
|
||||
def fire_and_forget(self, args, img):
|
||||
subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
|
||||
self.cleanup(img)
|
||||
|
||||
class PNGOptimizer:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
class JPEGOptimizer:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("I can't let you do that, Dave.")
|
||||
|
||||
class pngnq(NonAtomicOptimizer, PNGOptimizer):
|
||||
binaryname = "pngnq"
|
||||
|
||||
def __init__(self, sampling=3, dither="n"):
|
||||
if sampling < 1 or sampling > 10:
|
||||
raise Exception("Invalid sampling value '%d' for pngnq!" % sampling)
|
||||
|
||||
if dither not in ["n", "f"]:
|
||||
raise Exception("Invalid dither method '%s' for pngnq!" % dither)
|
||||
|
||||
self.sampling = sampling
|
||||
self.dither = dither
|
||||
|
||||
def optimize(self, img):
|
||||
if img.endswith(".tmp"):
|
||||
extension = ".tmp"
|
||||
else:
|
||||
extension = ".png.tmp"
|
||||
|
||||
NonAtomicOptimizer.fire_and_forget(self, [self.binaryname, "-s", str(self.sampling),
|
||||
"-Q", self.dither, "-e", extension, img], img)
|
||||
|
||||
class pngcrush(NonAtomicOptimizer, PNGOptimizer):
|
||||
binaryname = "pngcrush"
|
||||
# really can't be bothered to add some interface for all
|
||||
# the pngcrush options, it sucks anyway
|
||||
def __init__(self, brute=False):
|
||||
self.brute = brute
|
||||
|
||||
def optimize(self, img):
|
||||
args = [self.binaryname, img, img + ".tmp"]
|
||||
if self.brute == True: # Was the user an idiot?
|
||||
args.insert(1, "-brute")
|
||||
|
||||
NonAtomicOptimizer.fire_and_forget(self, args, img)
|
||||
|
||||
class optipng(Optimizer, PNGOptimizer):
|
||||
binaryname = "optipng"
|
||||
|
||||
def __init__(self, olevel=2):
|
||||
self.olevel = olevel
|
||||
|
||||
def optimize(self, img):
|
||||
Optimizer.fire_and_forget(self, [self.binaryname, "-o" + str(self.olevel), "-quiet", img])
|
||||
|
||||
|
||||
def check_programs(optimizers):
|
||||
path = os.environ.get("PATH").split(os.pathsep)
|
||||
|
||||
def exists_in_path(prog):
|
||||
result = filter(lambda x: os.path.exists(os.path.join(x, prog)), path)
|
||||
return len(result) != 0
|
||||
|
||||
for prog,l in [(pngcrush,1), (advdef,2)]:
|
||||
if l <= level:
|
||||
if (not exists_in_path(prog)) and (not exists_in_path(prog + ".exe")):
|
||||
raise Exception("Optimization prog %s for level %d not found!" % (prog, l))
|
||||
|
||||
def optimize_image(imgpath, imgformat, optimizeimg):
|
||||
if imgformat == 'png':
|
||||
if optimizeimg >= 1:
|
||||
# we can't do an atomic replace here because windows is terrible
|
||||
# so instead, we make temp files, delete the old ones, and rename
|
||||
# the temp files. go windows!
|
||||
subprocess.Popen([pngcrush, imgpath, imgpath + ".tmp"],
|
||||
stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0]
|
||||
os.remove(imgpath)
|
||||
os.rename(imgpath+".tmp", imgpath)
|
||||
|
||||
if optimizeimg >= 2:
|
||||
# the "-nc" it's needed to no broke the transparency of tiles
|
||||
recompress_option = "-z2" if optimizeimg == 2 else "-z4"
|
||||
subprocess.Popen([advdef, recompress_option,imgpath], stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE).communicate()[0]
|
||||
for opt in optimizers:
|
||||
if (not exists_in_path(opt.binaryname)) and (not exists_in_path(opt.binaryname + ".exe")):
|
||||
raise Exception("Optimization program '%s' was not found!" % opt.binaryname)
|
||||
|
||||
def optimize_image(imgpath, imgformat, optimizers):
|
||||
for opt in optimizers:
|
||||
if imgformat == 'png':
|
||||
if isinstance(opt, PNGOptimizer):
|
||||
opt.optimize(imgpath)
|
||||
elif imgformat == 'jpg':
|
||||
if isinstance(opt, JPEGOptimizer):
|
||||
opt.optimize(imgpath)
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
from settingsValidators import *
|
||||
import util
|
||||
from observer import ProgressBarObserver, LoggingObserver, JSObserver
|
||||
from optimizeimages import pngnq, optipng, pngcrush
|
||||
import platform
|
||||
import sys
|
||||
|
||||
@@ -72,7 +73,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
|
||||
"imgquality": Setting(required=False, validator=validateImgQuality, default=95),
|
||||
"bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"),
|
||||
"defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1),
|
||||
"optimizeimg": Setting(required=True, validator=validateOptImg, default=0),
|
||||
"optimizeimg": Setting(required=True, validator=validateOptImg, default=[]),
|
||||
"nomarkers": Setting(required=False, validator=validateBool, default=None),
|
||||
"texturepath": Setting(required=False, validator=validateTexturePath, default=None),
|
||||
"renderchecks": Setting(required=False, validator=validateInt, default=None),
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections import namedtuple
|
||||
|
||||
import rendermodes
|
||||
import util
|
||||
from optimizeimages import Optimizer
|
||||
from world import UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT
|
||||
|
||||
class ValidationException(Exception):
|
||||
@@ -155,8 +156,13 @@ def validateBGColor(color):
|
||||
return color
|
||||
|
||||
|
||||
def validateOptImg(opt):
|
||||
return bool(opt)
|
||||
def validateOptImg(optimizers):
|
||||
if isinstance(optimizers, (int, long)):
|
||||
raise ValidationException("You are using a deprecated method of specifying optimizeimg!")
|
||||
for opt in optimizers:
|
||||
if not isinstance(opt, Optimizer):
|
||||
raise ValidationException("Invalid Optimizer!")
|
||||
return optimizers
|
||||
|
||||
def validateTexturePath(path):
|
||||
# Expand user dir in directories strings
|
||||
|
||||
@@ -246,11 +246,7 @@ class TileSet(object):
|
||||
relevant in jpeg mode.
|
||||
|
||||
optimizeimg
|
||||
an integer indiating optimizations to perform on png outputs. 0
|
||||
indicates no optimizations. Only relevant in png mode.
|
||||
1 indicates pngcrush is run on all output images
|
||||
2 indicates pngcrush and advdef are run on all output images with advdef -z2
|
||||
3 indicates pngcrush and advdef are run on all output images with advdef -z4
|
||||
A list of optimizer instances to use.
|
||||
|
||||
rendermode
|
||||
Perhaps the most important/relevant option: a string indicating the
|
||||
@@ -892,7 +888,11 @@ class TileSet(object):
|
||||
try:
|
||||
#quad = Image.open(path[1]).resize((192,192), Image.ANTIALIAS)
|
||||
src = Image.open(path[1])
|
||||
# optimizeimg may have converted them to a palette image in the meantime
|
||||
if src.mode != "RGB" and src.mode != "RGBA":
|
||||
src = src.convert("RGBA")
|
||||
src.load()
|
||||
|
||||
quad = Image.new("RGBA", (192, 192), self.options['bgcolor'])
|
||||
resize_half(quad, src)
|
||||
img.paste(quad, path[0])
|
||||
@@ -1017,7 +1017,7 @@ class TileSet(object):
|
||||
|
||||
if self.options['optimizeimg']:
|
||||
optimize_image(tmppath, self.imgextension, self.options['optimizeimg'])
|
||||
|
||||
|
||||
os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime))
|
||||
|
||||
def _iterate_and_check_tiles(self, path):
|
||||
|
||||
Reference in New Issue
Block a user