crashinvaders / gdx-texture-packer-gui

A simple way to pack and manage texture atlases for libGDX game framework.
Apache License 2.0
597 stars 73 forks source link

PNG-8 quantization support #36

Closed tommyettinger closed 6 years ago

tommyettinger commented 6 years ago

I'm on Windows 7, running version 4.6.0 installed by the .exe installer for that release. I can compress PNGs using pngtastic, but it yields negligible size differences on the images I'm using because it won't use PNG8 (paletted files) even if the full texture atlas uses less than 256 colors. I tried using the TinyPNG option instead, but it gives what appears to be an authentication error:

Packing is started
Packing.........
Writing 2048x2048: D:\GitHub\WarpWriter\target\out\ProceduralFish1\ProceduralFish.png
Packing is done
Tinify compression started
[text-red]Exception occurred:[] Credentials are invalid (HTTP 401/Unauthorized)
[text-red]Stack trace:[] 
com.tinify.AccountException: Credentials are invalid (HTTP 401/Unauthorized)
    at com.tinify.Exception.create(Exception.java:27)
    at com.tinify.Client.request(Client.java:198)
    at com.tinify.Client.request(Client.java:96)
    at com.tinify.Source.fromBuffer(Source.java:18)
    at com.tinify.Source.fromFile(Source.java:14)
    at com.tinify.Tinify.fromFile(Tinify.java:45)
    at com.crashinvaders.texturepackergui.controllers.TinifyService.compressImageSync(TinifyService.java:73)
    at com.crashinvaders.texturepackergui.controllers.packing.processors.TinifyCompressingProcessor.processPackage(TinifyCompressingProcessor.java:44)
    at com.crashinvaders.texturepackergui.utils.packprocessing.CompositePackProcessor.processPackage(CompositePackProcessor.java:20)
    at com.crashinvaders.texturepackergui.utils.packprocessing.PackProcessingManager$1.run(PackProcessingManager.java:64)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

For reference, here's the atlas images with Pngtastic compression, no compression, and basic "save for web as PNG-8 in Photoshop" modes. fish_png_pngtastic

fish_png_uncompressed

fish_png_photoshop

There's some minor unrelated issues as well; if you give TexturePacker a directory that doesn't exist, it throws an exception when you try to pack (I'd expect it to create the directory if the path is valid). If you give it a directory that does exist and has files in it produced by a previous run (even if they have numbers appended to the filename, and even when those numbers aren't used by the current pack), it deletes those files when it packs even though it wasn't necessary to delete them.

metaphore commented 6 years ago
  1. I'm not sure what algorithm Pngtastic utilizes internally, but it appears like its general purpose is to optimize regular (not indexed) PNG images. It doesn't provide any options except for compression level and "remove gamma". So this issue should be directly addressed to @depsypher (Pngtastic maintainer) or if you know any other good compression library that deals with indexed PNGs properly, feel free to name it and I will check if this is possible to add it to the application.

  2. Regarding TinyPng, I've checked and it works fine. Are you sure you have obtained a valid API key, put it in the TinyPng settings form and validated it? image

  3. Oh, my bad. I will fix automatic directory creation in the next update.

  4. It's an original logic of TexturePacker's code to clean output dir. It's necessary to remove all matched image files on repacking due to a new packed atlas may have less pages and old ones will be left as garbage in an output folder.

tommyettinger commented 6 years ago

I didn't know an API key was needed; my main point from earlier is probably not a problem then. Maybe there should be some try/catch for an authentication error if it throws an exception, and prompt the user to check their API key settings for TinyPNG.

I'll look into PNG optimizations for Java; I mostly have to optimize images after-the-fact by quantizing (reducing color count to fit in a palette) with Photoshop, then optimizing compression with the slow, slow program pngout. Part of the issue is that most PNG optimizers do very well on indexed-color inputs, but less-well on 32-bit RGBA8888 images, and none I've found seem to correctly identify that a large image contains 256 or less colors. This means I need to use Photoshop or some similar program (GIMP would work, ImageMagick works with the right options) to actually identify the color count correctly, to a small palette of 74 for this image, and save as an indexed-color PNG. Then optimizing that indexed-color PNG goes fine, to a final size of about half the RGBA8888 image's size. I suspect that changes might be needed to TexturePacker code in libGDX to get it to recognize that the color count is small enough to save as an indexed PNG-8 losslessly, otherwise it would either save as a non-indexed PNG or would need a quantization algorithm to identify which colors can be reasonably eliminated from the atlas page. Quantization algorithms are tricky to write and use, and often produce output that doesn't look very close to the original in color usage. I'd rather use lossless 8-bits-per-channel PNG if I have more than 256 colors in an atlas page, instead of forcing colors down to 256. But if I have less than 256 colors, it would be nice for the files to use a palette, though far from necessary.

I'll close this issue because I don't think the authentication error is a bug, and any problems with too-large image files can be worked around by using 3rd-party tools without a ton of hassle.

metaphore commented 6 years ago

No, it's actualy an interesting topic and now I feel like support for PNG-8 is a good thing that probably may be just another "PNG compressor" that takes its part after texture packer done with the packing and saved regular PNG-32 atlas pages. I've looked at some libs that can be used there and probably most famous one is pngquant, but unfortunately it's under GPL3 and I would prefer something else with more permissive licence. There is some other pretty old option - https://github.com/stuart/pngnq , I will test it soon and will see if it could work. Probably imagemagic could also be of use, but I will look into it if the previous one will fail in one way or another.

tommyettinger commented 6 years ago

ImageMagick can definitely do it; there's hardly anything it can't do with images... pngnq is meant to quantize larger color counts, I think, but probably works fine on images with smaller counts too and would save them as png-8 even if it has minimal work to do. I'd lean toward trying Pngtastic though with some different usage options; it has the ability to count colors even now, but it doesn't seem to use this by default: https://github.com/depsypher/pngtastic/blob/master/src/main/java/com/googlecode/pngtastic/core/PngColorCounter.java . There may be some bug in that code which prevents accurate color counting, or it might just not be used with whatever options this project defaults to when using Pngtastic. I hope there's some easy "reduce when possible losslessly" option in Pngtastic, since it seems like that library is close to supporting it.

tommyettinger commented 6 years ago

I've written a class that allows saving Pixmaps to indexed-mode PNG8 files; it could be added to libGDX's PixmapIO alongside its PNG support, but is not currently inside libGDX (I don't know if they want it yet). There's support for palette matching, adapting a palette to fit a full-color image as best it can, and previewing a Pixmap before writing. You can compare the original PNG32 image with the various PNG8 images below on this small preview page. I'll try sending a PR here in a bit if I get this working in the texture packer.

metaphore commented 6 years ago

Yep, I clearly understand advantages of using PNG8 in some cases and would love to see that option in texture packer, the only problem was I wasn't sure about how to incorporate any of the existing PNG8 libraries, they are mostly written in C or have an incompatible licence.

Anyway, it's a good news you got java code that can do it. Do you have it publicly available anywhere so I could look?

tommyettinger commented 6 years ago

I have it working in gdx-texture-packer-gui now, on my fork. I'll send a PR now I guess; I don't know if there's anything I would want to revise later. My code is based mostly on the PixmapIO.PNG class from libGDX (Apache licensed, like this project) and was extended in SquidLib (also Apache) to get the palette and dithering support I wanted to have in it.

tommyettinger commented 6 years ago

As an example, this Mona Lisa is 230 colors (produced by the same code in PR #52 ), and is about 66% smaller than an almost-equivalent PNG32 version.