Hi, I'm CounterPillow. If you've ever been in our IRC channel, you've
probably seen me. I have been an irregular contributor to Overviewer
for some time now, and recently, I've rewritten the way optimizeimg
works. I thought that this would be a good opportunity to explain what
it is, and how to best use it.
First, let's explain what this is all about. In essence, this
increases the time it takes to render a map in favor of smaller
images. But how does it do that? optimizeimg allows you to run
various image optimizers against each tile image the Overviewer
outputs. As of the time of writing, we support optipng, pngcrush and
pngnq.
Lossless vs. Lossy Compression
One of the key things to understand is what lossless and lossy
compression mean. In lossless compression, the file size is reduced,
but the decompressed data is exactly the same. No data is lost in the
compression step, hence the name "lossless". Lossy compression, on the
other hand, does not preserve all information. An uncompressed output
of a lossy compression algorithm is not the same as the input.
As you may already know, PNG uses lossless compression. However, there
are a lot of different knobs to tweak when it comes to how the image
is compressed, thus reducing file size without altering the image
data, and we can even reduce the amount of colors in the image,
essentially making the process lossy! I'm not going to go into detail
as to how this all works in practice, but I will showcase the tools we
currently support to achieve this.
Lossless Image Optimization, and "crushing"
First off, I'll have to admit that I've lied in the previous
section. Some of the lossless image recompression methods are not
lossless in the sense of "data in is the same as data out
decompressed". Some tools, like optipng, discard unused alpha channels
(where images store their transparency), to reduce file size. I said
unused, which means that the visible output is still pixel-per-pixel
the same, thus lossless, but the image data is obviously
different. However, for all intents and purposes, we can call this
process "lossless", because no visible information has been changed.
Discarding unused alpha channels is one way of making the image
smaller, but there are other ways which are truly lossless. Both
pngcrush and optipng tweak the previously mentioned knobs of the
compression, trying to find the smallest possible output. The two
tools do this a bit differently, but the principle is the same: Tweak
the encoding parameters. This is what the Overviewer code refers to as
a "crusher". If you wish to learn more on how this is done internally,
you can read about it
on the optipng site,
but beware: It's quite technical.
Lossy Image Optimization
With pngnq, we also offer a way of lossy image optimization. You may
be wondering how a PNG can be lossily optimized, as the compression
method is lossless. The answer is simple: The amount of colors.
PNG supports a ton of different color modes, amongst them is an
indexed color mode, or also referred to as "Palette PNG" or
"PNG8". In images with indexed color modes, the colors of each pixel
aren't determined individually using red, green and blue
channels. Instead, it has a palette of colors, and then specifies for
each pixel the color for it by assigning it the a color of the
palette. This means less colors, in our case 256 at most, and because
we can represent those with just one byte, it means less data that
needs to be compressed. If we find the 256 colors that can best
represent the image, we get pretty good results that are just a
fraction of the original size; and since Minecraft doesn't use a lot
of colors in the default texture pack, it's barely noticeable. agrif
briefly mentioned this in the
"Introducing OIL"
blog post. In fact, if our input image has less than or exactly 256
colors anyway, the process is completely lossless!
Instead of waiting for OIL, you can use palette PNGs right now, with
the pngnq tool. However, please be aware that due to multiple bugs in
the imaging library we currently use, PIL (or Pillow, both are buggy),
this will break transparency. As stated in the docs, this is not
pngnq's fault.
Using Image Optimization In Practice
But how is this used in practice, you ask? It's not that hard. The
first step is to install the tools you wish to use and make sure they
can be called by Overviewer (i.e., are in your PATH environment
variable). Then, add them to your config.
Please be aware that this will increase render times.
An example for everyone
The following example should be satisfactory for most people who wish
to use image optimization. Please also be aware that when I say "most
people", I always actually mean "most people". If you think you are
not "most people", then you're doing so carrying the risk that you may
be wasting your time.
from .optimizeimages import optipng
renders["foo"] = {
# ... other stuff here
"optimizeimg":[optipng()],
}
This will run optipng with the default number of trials (which is the
way to go for most people).
Playing around with optimization levels
We can, of course, also specify some parameters for optipng. Please
note that you're dangerously venturing out of most people-territory
here.
from .optimizeimages import optipng
renders["foo"] = {
# ... other stuff here
"optimizeimg":[optipng(olevel=3)],
}
This will essentially execute optipng -o3 (instead of -o2), you can
see what the exact meaning of that is in the optipng manual, or by
typing optipng -h
. Please note that anything above, or most likely
even including the fifth optimization level, is pointless. The OptiPNG
developers
said so themselves:
It is important to mention that the achieved compression ratio is
less and less likely to improve when higher-level presets
(trigerring more trials) are being used. Even if the program is
capable of searching automatically over more than 200 configurations
(and the advanced users have access to more than 1000
configurations!), a preset that selects around 10 trials should be
satisfactory for most users. Furthermore, a preset that selects
between 30-40 trials should be satisfactory for all users, for it is
very, very unlikely to be beaten significantly by any wider
search. The rest of the trial configurations are offered rather as a
curiosity (but they were used in the experimentation from which we
concluded they are indeed useless!)
Getting crazy
Let's move on to something more hardcore, then. Let's say you don't
care if your map looks a bit weird thanks to PIL. Let's say you want
to use pngnq.
from .optimizeimages import optipng, pngnq
renders["foo"] = {
# ... other stuff here
"optimizeimg":[pngnq(), optipng()],
}
Note the order in which the two optimizers are specified. Here, pngnq
is run first, then optipng is run on it. This is important: If you
feed crushed output into something that is not a crusher (i.e. does
not do lossless compression tweaking), all benefits of the crusher
will be lost; essentially, you'd be wasting your time. If you choose
to use more than one optimization tool, make sure the crusher is at
the end. Overviewer will warn you if it isn't.
Don't be silly
Speaking of more than one optimization tool, let's answer a very
important question: Will the image be smaller by using multiple
crushers chained together? (i.e. optipng after pngcrush)
The short answer: No.
The long answer: Most likely not.
Crushers, by default, exit if they fail to make the image smaller, and
just admit that they have not been able to crush it. Crushers may
perform differently in different situations, so using two crushers
chained after each other gives you essentially the best output of
either; however, for most images (and I say "most images" like I say
"most people"), this will be optipng in our example. Every
optimizeimg configuration that uses both pngcrush and optipng can
most likely be reduced to just optipng, with exactly the same results
but much faster renders.
Conclusion
As you may have noticed, I often used "most likely" and "most people"
in this post. This is because we can't make certain statements without
large-scale tests, which I have not done,
but other people did. If
you're interested in utilizing image optimization, what you should
take away from this blog post is that [optipng()]
is the way to
go. If you're not sure, do tests yourself. We may add support for
better image optimizers in the future, including some for JPEG, so
frequently checking the documentation to see which are available is a
good idea.