Skip to content

fix(ui): optimize images#266

Open
suprstarrd wants to merge 3 commits intoPretendoNetwork:devfrom
suprstarrd:imgbot
Open

fix(ui): optimize images#266
suprstarrd wants to merge 3 commits intoPretendoNetwork:devfrom
suprstarrd:imgbot

Conversation

@suprstarrd
Copy link
Contributor

@suprstarrd suprstarrd commented Dec 27, 2025

Resolves #262. If I remember correctly, there's a few other related issues we can revisit here too.

Changes:

This is a pull request that optimizes the images Juxt offers as best as possible.

Here's what @imgbot was able to do:

Details
File Before After Percent reduction
/apps/juxtaposition-ui/src/webfiles/web/images/add-post-no-image.png 11.90kb 4.00kb 66.37%
/apps/juxtaposition-ui/src/webfiles/portal/images/add-post-no-image.png 11.90kb 4.00kb 66.37%
/apps/juxtaposition-ui/src/webfiles/ctr/images/banner.png 20.25kb 9.69kb 52.12%
/apps/juxtaposition-ui/src/webfiles/portal/images/banner.png 20.25kb 9.69kb 52.12%
/apps/juxtaposition-ui/src/webfiles/web/images/banner.png 20.25kb 9.69kb 52.12%
/apps/juxtaposition-ui/src/webfiles/portal/images/background.png 233.20kb 122.99kb 47.26%
/apps/juxtaposition-ui/src/webfiles/ctr/images/bandwidthalert.png 44.18kb 28.74kb 34.95%
/apps/juxtaposition-ui/src/webfiles/portal/images/bandwidthalert.png 44.18kb 28.74kb 34.95%
/apps/juxtaposition-ui/src/webfiles/web/images/bandwidthalert.png 44.18kb 28.74kb 34.95%
/apps/juxtaposition-ui/src/webfiles/portal/images/bandwidthlost.png 111.69kb 84.11kb 24.70%
/apps/juxtaposition-ui/src/webfiles/web/images/bandwidthlost.png 111.69kb 84.11kb 24.70%
/apps/juxtaposition-ui/src/webfiles/ctr/images/icons.png 12.70kb 10.94kb 13.89%
/apps/juxtaposition-ui/src/webfiles/portal/images/splash-background.png 43.35kb 37.50kb 13.51%
/apps/juxtaposition-ui/src/webfiles/web/images/splash-background.png 43.35kb 37.50kb 13.51%
/apps/juxtaposition-ui/src/webfiles/web/partials/assets/bin_icon.svg 0.44kb 0.41kb 6.46%
/apps/juxtaposition-ui/src/webfiles/web/partials/assets/heart_icon.svg 0.38kb 0.36kb 5.93%
/apps/juxtaposition-ui/src/webfiles/web/partials/assets/menu_icon.svg 0.34kb 0.32kb 5.48%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-72x72.png 10.75kb 10.22kb 4.93%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-96x96.png 11.01kb 10.56kb 4.04%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-144x144.png 13.02kb 12.59kb 3.28%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-128x128.png 12.35kb 11.94kb 3.27%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-152x152.png 13.04kb 12.64kb 3.03%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-192x192.png 14.62kb 14.24kb 2.60%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-384x384.png 28.09kb 27.64kb 1.61%
/apps/juxtaposition-ui/src/webfiles/web/partials/assets/flag_icon.svg 0.55kb 0.54kb 1.60%
/apps/juxtaposition-ui/src/webfiles/web/images/icons/icon-512x512.png 38.34kb 37.82kb 1.34%
/apps/juxtaposition-ui/src/webfiles/portal/fonts/Poppins-Light.svg 834.88kb 825.56kb 1.12%
Total : 1,750.87kb 1,465.28kb 16.31%

Using and integrating @lumiscosity's work can additionally grant larger savings.

@lumiscosity
Copy link

feel free to cherrypick my commit with the png optimizations here: lumiscosity@6fde3d0

@suprstarrd
Copy link
Contributor Author

I would but I'm hoping to apply the changes on top of what @imgbot did (it is open-source and it looks to all be here). AFAIK if I cherrypick then it's one or the other

@lumiscosity
Copy link

ah, got it!

@ashquarky
Copy link
Member

Worth noting that a lot of the gains in CTR were found by re-creating the assets in lower resolution or colour depth. A lot of them had highly subtle gradients or compression artefacts that ballooned out the filesize. Fixing that is where a lot of the 90% savings came from.

Automated compression like this is still valuable and free so we should absolutely do it, but I would also like the process to be at least written down somewhere so that if I come back to apply that type of manual tuning to the WiiU assets too, I can re-apply it.

@ashquarky
Copy link
Member

See #168.

I forgot a lot of those CTR assets are inlined as data URLs in stylesheets.. We will fix that to be esbuild includes at some point but in the meantime manual updates are required to actually put the improved images in the file the client gets..

@ashquarky
Copy link
Member

Would you still like to work on this? Portal and web need some love. I can do some manual work on it, but the automation is certainly nice.

@suprstarrd
Copy link
Contributor Author

I would, though college is kicking me down a bit. I should have some progress on this by Saturday.

@jonbarrow
Copy link
Member

jonbarrow commented Jan 28, 2026

Minor thing I noticed just by looking over some of the files, the image https://github.com/PretendoNetwork/juxtaposition/blob/dev/apps/juxtaposition-ui/webfiles/ctr/images/background.png is 256x256, but it's actually just a 2x2 pixel image scaled up to 256x256

How well does the 3DS support gradients? The following CSS results in an identical checkerboard background as when using the image, but it frees 437 bytes since the image is not longer being used, but I've only tested it on my real browser not on consoles. It should be using properties available at the time, but idk if Nintendo supported them in the browser used by Miiverse

https://caniuse.com/?search=linear-gradient says that there should be supported in early webkit versions, but does note:

Implements an earlier prefixed syntax as -webkit-gradient

Though it doesn't go into detail. So it's possible some tweaks might need to be made

Edit: updated the snippet to be the 2010 version, seems to still work exactly the same on my browser but still needs console testing

#body {
	background-color: #EBEBEB;
	background-image:
		-webkit-gradient(linear, 0% 0%, 100% 100%,
			color-stop(0%, #F3F3F3),
			color-stop(25%, #F3F3F3),
			color-stop(25%, transparent),
			color-stop(75%, transparent),
			color-stop(75%, #F3F3F3),
			color-stop(100%, #F3F3F3)
		),
		-webkit-gradient(linear, 0% 0%, 100% 100%,
			color-stop(0%, #F3F3F3),
			color-stop(25%, #F3F3F3),
			color-stop(25%, transparent),
			color-stop(75%, transparent),
			color-stop(75%, #F3F3F3),
			color-stop(100%, #F3F3F3)
		);
	background-size: 100px 100px;
	background-position: 0 0, 50px 50px;
}

@jonbarrow
Copy link
Member

jonbarrow commented Jan 28, 2026

I also wonder if it might save some bytes by converting some simple images into font glyphs? Nintendo did this quite often with their UIs outside of Miiverse (unsure about Miiverse itself), they just slapped icons into the font as glyphs to reference them that way. Sprites like https://github.com/PretendoNetwork/juxtaposition/blob/dev/apps/juxtaposition-ui/webfiles/ctr/images/sprites/feeling-frustrated.png are a whole kb, I wonder if the combined size of these sprites would be smaller as a custom font?

Though that might make the pages kinda janky, unsure. Just spitballing

@ashquarky
Copy link
Member

ashquarky commented Jan 29, 2026

Minor thing I noticed just by looking over some of the files, the image https://github.com/PretendoNetwork/juxtaposition/blob/dev/apps/juxtaposition-ui/webfiles/ctr/images/background.png is 256x256, but it's actually just a 2x2 pixel image scaled up to 256x256

This isn't true, it has rounded corners on the darker squares (though it might be worthwhile to lose that design element if there's a big perf uplift)

@jonbarrow
Copy link
Member

jonbarrow commented Jan 29, 2026

Minor thing I noticed just by looking over some of the files, the image https://github.com/PretendoNetwork/juxtaposition/blob/dev/apps/juxtaposition-ui/webfiles/ctr/images/background.png is 256x256, but it's actually just a 2x2 pixel image scaled up to 256x256

This isn't true, it has rounded corners on the darker squares (though it might be worthwhile to lose that design element if there's a big perf uplift)

I had to massively zoom in on my phone to be able to see this detail, the rounding is pretty subtle and the color similarities made me entirely miss the roundness on my Mac even when I was viewing the image in gimp to color pick it

I suspect it would be similarly unnoticeable on consoles, so I agree it might be worth removing that to save on the bytes/network request. It might be possible to emulate it via the gradient method or something else, but that would likely stretch what the 3DS is capable of

That being said, this hinges on whether or not the 3DS can render these gradients at all. The properties I used were available in 2010 but it's possible Nintendo didn't support them still

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 9, 2026

Copying @lumiscosity's comment on the other issue here for easier/further analysis:

function rcomp() {
    find . -name "*.png" -type f -exec oxipng -o max -a -s {} \;
    find . -name "*.png" -type f -exec pngout {} \;
    find . -name "*.png" -type f -exec oxipng -o max --zopfli -a -s {} \;
    find . -name "*.png" -type f -exec defluff-shim {} \;
    find . -name "*.png" -type f -exec deflopt {} \;
}

where pngout is a wrapper around PNGOUT.EXE:

#!/usr/bin/env bash
# this arcane incantation converts linux paths to windows paths in the first arg
# you'll see it again in a second
set -- $(echo "$1" | sed 's/\//\\/g') "${@:2}"
# you can try different values after b, but i've found 128 to give the best results
wine /usr/bin/pngout.exe "$@" "/b128" "/k1" "/q"

and defluff-shim is a wrapper around defluff:

#!/usr/bin/env bash
if [ $# -eq 1 ]; then
  defluff < "$1" > ~/.cache/defluff-shim-tmp.png
  oldsize=$(wc -c < "$1")
  newsize=$(wc -c < ~/.cache/defluff-shim-tmp.png)
  if [[ newsize -lt oldsize ]]; then
    rm "$1"
    mv ~/.cache/defluff-shim-tmp.png "$1"
  else
    rm ~/.cache/defluff-shim-tmp.png
  fi
else
  echo 'usage: defluff-shim (path)'
fi

and deflopt is a wrapper around deflopt:

#!/usr/bin/env bash
set -- $(echo "$1" | sed 's/\//\\/g') "${@:2}"
wine /usr/bin/DeflOpt.exe "$@"

defluff and deflopt are the final steps that shave off the last tiny bytes by, as far as i understand it, optimizing the headers. pngout is a png compressor with different characteristics than opti/oxipng, it seems to excel at images with large blobs of shared color and small amounts of color from what i can tell. chaining it between the two oxipng calls gave me the best results

i'll probably make a blogpost about this sometime...

  • On the note of the shell scripts, these can probably be made entirely POSIX-compliant.
  • I was able to find a less... suspicious download for DeflOpt: https://web.archive.org/web/20140328015255/http://www.walbeehm.com/download/DeflOpt207.7z - hash cd37417818c7a8046a5b12d5630f6ab5f323747e7295354fea5c0e3749fead3f
  • Not sure if this matters, but a commenter on the defluff thread (which seems to be the original source) mentions that it will always strip ancillary metadata. I don't think that matters for us here but just as an FYI.
  • It looks like the pngout wrapper may not be needed - ports already exist: https://www.jonof.id.au/kenutils.html. Different story for DeflOpt
  • By the way - whenever you do make that blog post, let me know and I'll see what else we can throw in ^w^

Still have yet to figure out what magic @imgbot is doing, but I may need to take a closer look. It does seem to be somewhat different from just using each of the programs with standard/no extra arguments.

Will play around with some GitHub Actions and see where it gets me.

@lumiscosity
Copy link

great research! so far the blogpost just has this copied in verbatim but i'll probably tack on your findings.

good catch with the defluff thing, looks like odiff doesn't check for colorspace info for some reason??? confirmed with a hex editor that colorspace chunks get yeeted, whoops

@jonbarrow
Copy link
Member

jonbarrow commented Feb 9, 2026

Something I just found. I had some concerns over using pngout, as the authors seem to be strongly against releasing the source code and I'm not sure what the licensing rights look like here (especially with the ports), which makes trying to make this reproducible a bit annoying legally and also the downloads are just from forums/a guys personal website (and ideally this workflow would be included somewhere in the project)

A better alternative, in my opinion, would be zopfli. It's maintained by Google, open source under Apache 2.0, and hosted on GitHub, which eases all the concerns I had with pngout

I have not done extensive testing on all images, but when I tested it on https://github.com/PretendoNetwork/juxtaposition/blob/dev/apps/juxtaposition-ui/webfiles/ctr/images/bandwidthalert.png I got nearly identical results to pngout:

zopflipng bandwidthalert.png bandwidthalert-compressed.png
Optimizing bandwidthalert.png
Input size: 45238 (44K)
Result size: 26970 (26K). Percentage of original: 59.618%
Result is smaller

It's still 30 bytes larger than pngout from @lumiscosity's changes, but I also didn't test any of the other tools in that pipeline so maybe the real final result would match what's in @lumiscosity's changes (and I think an extra 30 bytes is worth the open source/reliable downloads to be honest). Samples:

bandwidthalert-original bandwidthalert-lumiscosity bandwidthalert-zopfli

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 9, 2026

From the website, this is the PNGOUT license:

PNGOUT / KZIP license:

I have gotten a lot of requests about bundling the command line executables, or releasing the source code to KZIP and PNGOUT. This is my license (updated 01/16/2021):

  • The command line versions of PNGOUT.EXE and KZIP.EXE are free for use, as are the Mac and Linux ports.
  • You may use PNGOUT- or KZIP-compressed content for non-commercial or commercial purposes.
  • Redistributing, repackaging, or reusing the PNGOUT or KZIP executable is permitted for non-commercial projects that are distributed freely.
  • For commercial requests (such as bundling with a commercial product), please write to my business partner, David Blake.
  • When bundling, my credit (both my name and website) must be clearly displayed in some reasonable fashion that can be seen by an average user.

I don't know how easily we can skirt as non-commercial given Pretendo takes donations and has some form of paid memberships. So it does look like automating with PNGOUT is a no-go.

@jonbarrow
Copy link
Member

From the website, this is the PNGOUT license:

PNGOUT / KZIP license:
I have gotten a lot of requests about bundling the command line executables, or releasing the source code to KZIP and PNGOUT. This is my license (updated 01/16/2021):

  • The command line versions of PNGOUT.EXE and KZIP.EXE are free for use, as are the Mac and Linux ports.
  • You may use PNGOUT- or KZIP-compressed content for non-commercial or commercial purposes.
  • Redistributing, repackaging, or reusing the PNGOUT or KZIP executable is permitted for non-commercial projects that are distributed freely.
  • For commercial requests (such as bundling with a commercial product), please write to my business partner, David Blake.
  • When bundling, my credit (both my name and website) must be clearly displayed in some reasonable fashion that can be seen by an average user.

I don't know how easily we can skirt as non-commercial given we take donations. So it does look like automating with PNGOUT is a no-go.

The bigger issue is that the license doesn't cover ports. I did see this on his website, but it only explictly covers the Windows versions. The website with the ports does not contain any license information that I can find, and the author also refuses to share the source code:

The source code for these tools is not public. Don't bother asking.

@jonbarrow
Copy link
Member

jonbarrow commented Feb 9, 2026

Using oxipng with --zopfli I was able to get the image down to 26,943 bytes, only 3 bytes away from the final output from lumiscosity:

oxipng -o max -a -s --zopfli bandwidthalert.png
Files processed: 1/1   
Input size: 44.2 KiB (45238 bytes)
Output size: 26.3 KiB (26943 bytes)
Total saved: 17.9 KiB (40.44%)

Using zopflipng directly with the -m flag to do multiple iterations also reduces things, this time down to 26,942 bytes (2 away from lumiscosity's changes and 1 fewer than oxipng)

zopflipng -m bandwidthalert.png bandwidthalert-compressed.png
Optimizing bandwidthalert.png
Input size: 45238 (44K)
Result size: 26942 (26K). Percentage of original: 59.556%
Result is smaller

I think just using zopflipng directly is probably the easiest way to get free gains here. It's within just a couple bytes of lumiscosity's pipeline, but only requires one open source and properly licensed tool instead of 3 closed source and poorly licensed tools (excluding oxipng, which is open source under the same license as zopfli)

zopflipng also ships as a library which we can use to do proper automation, like when making PRs and such

To be clear, forever grateful to the work lumiscosity has been doing. It's great work and is genuinely helpful. I'm simply trying to find some possibly better alternatives for these tools that would work better in our workflow

@jonbarrow
Copy link
Member

jonbarrow commented Feb 9, 2026

Sorry for the triple message. I ran a quick test to compare the compression results from lumiscosity@6fde3d0 to using zopflipng directly, and here's what I got:

  • apps/juxtaposition-ui/webfiles/ctr/images/background.png Lumi: 169b, zopfli: 169b
  • apps/juxtaposition-ui/webfiles/ctr/images/bandwidthalert.png Lumi: 26940b, zopfli: 26942b
  • apps/juxtaposition-ui/webfiles/ctr/images/banner.png Lumi: 8798b, zopfli: 8795b
  • apps/juxtaposition-ui/webfiles/ctr/images/headline-green.png Lumi: 514b, zopfli: 529b
  • apps/juxtaposition-ui/webfiles/ctr/images/headline.png Lumi: 535b, zopfli: 548b
  • apps/juxtaposition-ui/webfiles/ctr/images/icons.png Does not exist in the latest Juxtaposition commits
  • apps/juxtaposition-ui/webfiles/portal/images/add-post-no-image.png Lumi: 2886b, zopfli: 3160b
  • apps/juxtaposition-ui/webfiles/portal/images/background.png Lumi: 113749b, zopfli: 111969b
  • apps/juxtaposition-ui/webfiles/portal/images/bandwidthalert.png Lumi: 26940b, zopfli: 26942b
  • apps/juxtaposition-ui/webfiles/portal/images/bandwidthlost.png Lumi: 66539b, zopfli: 66531b
  • apps/juxtaposition-ui/webfiles/portal/images/banner.png Lumi: 8798b, zopfli: 8795b
  • apps/juxtaposition-ui/webfiles/portal/images/splash-background.png Lumi: 32166b, zopfli: 35201b
  • apps/juxtaposition-ui/webfiles/web/images/add-post-no-image.png Lumi: 2886b, zopfli: 3160b
  • apps/juxtaposition-ui/webfiles/web/images/bandwidthalert.png Lumi: 26940b, zopfli: 26942b
  • apps/juxtaposition-ui/webfiles/web/images/bandwidthlost.png Lumi: 66539b, zopfli: 66531b
  • apps/juxtaposition-ui/webfiles/web/images/banner.png Lumi: 8798b, zopfli: 8795b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-128x128.png Lumi: 4451b, zopfli: 4524b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-144x144.png Lumi: 4974b, zopfli: 4986b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-152x152.png Lumi: 4968b, zopfli: 4986b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-192x192.png Lumi: 6616b, zopfli: 6666b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-384x384.png Lumi: 18878b, zopfli: 18895b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-512x512.png Lumi: 28373b, zopfli: 28485b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-72x72.png Lumi: 2203b, zopfli: 2203b
  • apps/juxtaposition-ui/webfiles/web/images/icons/icon-96x96.png Lumi: 3105b, zopfli: 3123b
  • apps/juxtaposition-ui/webfiles/web/images/splash-background.png Lumi: 32166b, zopfli: 35201b

In most cases it looks like zopflipng is exactly identical to the multi-stage pipeline from before, or within just a few bytes of it (and sometimes even better compression, like with apps/juxtaposition-ui/webfiles/portal/images/background.png). There's definitely some outliers here where zopfli didn't perform as good for some reason, but overall these are pretty nice for a single pass of 1 tool

@jonbarrow
Copy link
Member

jonbarrow commented Feb 9, 2026

I added in ect (also open source under Apache 2.0, https://github.com/fhanau/Efficient-Compression-Tool, and also says it supports jpeg images which we haven't covered here yet) after zopflipng and got these results beating the previous pipeline in every image besides 7 (and most of those are just a few bytes different, no idea what black magic is being used for the larger differences though). Keep in mind that this pipeline is SLOW. It took almost an hour to complete all 25 (24?) images.

Commands are just these, ran in a loop for all the files:

zopflipng -m original.png compressed.png
ect -9 -strip --allfilters-b compressed.png

This looks to be the way to go moving forward. The pipeline is simpler (only 2 commands, both only called once) and the other issues with the previous pipeline are basically nonexistent as these 2 tools are both properly open source, while basically have the same/better results.

File Lumi Zopfli Zopfli+ECT
apps/juxtaposition-ui/webfiles/ctr/images/background.png 169b 169b 168b
apps/juxtaposition-ui/webfiles/ctr/images/bandwidthalert.png 26940b 26942b 26917b
apps/juxtaposition-ui/webfiles/ctr/images/banner.png 8798b 8795b 8782b
apps/juxtaposition-ui/webfiles/ctr/images/headline-green.png 514b 529b 518b
apps/juxtaposition-ui/webfiles/ctr/images/headline.png 535b 548b 541b
apps/juxtaposition-ui/webfiles/ctr/images/icons.png - - -
apps/juxtaposition-ui/webfiles/portal/images/add-post-no-image.png 2886b 3160b 2875b
apps/juxtaposition-ui/webfiles/portal/images/background.png 113749b 111969b 111949b
apps/juxtaposition-ui/webfiles/portal/images/bandwidthalert.png 26940b 26942b 26917b
apps/juxtaposition-ui/webfiles/portal/images/bandwidthlost.png 66539b 66531b 66384b
apps/juxtaposition-ui/webfiles/portal/images/banner.png 8798b 8795b 8782b
apps/juxtaposition-ui/webfiles/portal/images/splash-background.png 32166b 35201b 33941b
apps/juxtaposition-ui/webfiles/web/images/add-post-no-image.png 2886b 3160b 2875b
apps/juxtaposition-ui/webfiles/web/images/bandwidthalert.png 26940b 26942b 26917b
apps/juxtaposition-ui/webfiles/web/images/bandwidthlost.png 66539b 66531b 66384b
apps/juxtaposition-ui/webfiles/web/images/banner.png 8798b 8795b 8782b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-128x128.png 4451b 4524b 4489b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-144x144.png 4974b 4986b 4979b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-152x152.png 4968b 4986b 4956b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-192x192.png 6616b 6666b 6600b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-384x384.png 18878b 18895b 18861b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-512x512.png 28373b 28485b 28302b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-72x72.png 2203b 2203b 2192b
apps/juxtaposition-ui/webfiles/web/images/icons/icon-96x96.png 3105b 3123b 3108b
apps/juxtaposition-ui/webfiles/web/images/splash-background.png 32166b 35201b 33941b

@lumiscosity
Copy link

lumiscosity commented Feb 10, 2026

To be clear, forever grateful to the work lumiscosity has been doing. It's great work and is genuinely helpful. I'm simply trying to find some possibly better alternatives for these tools that would work better in our workflow

it's all good, i'm happy to take any improvements here and add them to my workflow as well. we're all learning!

@jonbarrow
Copy link
Member

jonbarrow commented Feb 10, 2026

New day, new tests. I think I've pushed optimization as far as it can reasonably go without getting into lossy compression.

Instead of using purely zopflipng by itself, I instead tried oxipng with the --zopfli flag and max settings, and setting it to 50 iterations rather than the default 15. After running that, plus a pass through ect, I get these reductions. This pipeline gets all images smaller than the previous best.

Commands:

oxipng -o max --zopfli --zi 50 --ng --strip all image.png
ect -9 -strip --allfilters-b image.png
File Lumi oxipng+ECT Difference
webfiles/ctr/images/background.png 169b 168b -1b (0.59%)
webfiles/ctr/images/bandwidthalert.png 26940b 26917b -23b (0.09%)
webfiles/ctr/images/banner.png 8798b 8776b -22b (0.25%)
webfiles/ctr/images/headline-green.png 514b 511b -3b (0.58%)
webfiles/ctr/images/headline.png 535b 531b -4b (0.75%)
webfiles/portal/images/add-post-no-image.png 2886b 2875b -11b (0.38%)
webfiles/portal/images/background.png 113749b 111949b -1800b (1.58%)
webfiles/portal/images/bandwidthalert.png 26940b 26917b -23b (0.09%)
webfiles/portal/images/bandwidthlost.png 66539b 66384b -155b (0.23%)
webfiles/portal/images/banner.png 8798b 8776b -22b (0.25%)
webfiles/portal/images/splash-background.png 32166b 32163b -3b (0.01%)
webfiles/web/images/add-post-no-image.png 2886b 2875b -11b (0.38%)
webfiles/web/images/bandwidthalert.png 26940b 26917b -23b (0.09%)
webfiles/web/images/bandwidthlost.png 66539b 66384b -155b (0.23%)
webfiles/web/images/banner.png 8798b 8776b -22b (0.25%)
webfiles/web/images/icons/icon-128x128.png 4451b 4449b -2b (0.04%)
webfiles/web/images/icons/icon-144x144.png 4974b 4972b -2b (0.04%)
webfiles/web/images/icons/icon-152x152.png 4968b 4956b -12b (0.24%)
webfiles/web/images/icons/icon-192x192.png 6616b 6600b -16b (0.24%)
webfiles/web/images/icons/icon-384x384.png 18878b 18861b -17b (0.09%)
webfiles/web/images/icons/icon-512x512.png 28373b 28302b -71b (0.25%)
webfiles/web/images/icons/icon-72x72.png 2203b 2192b -11b (0.50%)
webfiles/web/images/icons/icon-96x96.png 3105b 3104b -1b (0.03%)
webfiles/web/images/splash-background.png 32166b 32163b -3b (0.01%)

I reran my script to check the latest reductions for all images in the dev branch, and got these results.

File Before After Reduction
webfiles/ctr/images/background.png 437b 168b -269b (61.56%)
webfiles/ctr/images/bandwidthalert.png 45238b 26917b -18321b (40.50%)
webfiles/ctr/images/banner.png 20734b 8776b -11958b (57.67%)
webfiles/ctr/images/header.png 3592b 3208b -384b (10.69%)
webfiles/ctr/images/headline-green.png 563b 511b -52b (9.24%)
webfiles/ctr/images/headline.png 581b 531b -50b (8.61%)
webfiles/ctr/images/sprites/feeling-frustrated_checked.png 1157b 807b -350b (30.25%)
webfiles/ctr/images/sprites/feeling-frustrated.png 1156b 812b -344b (29.76%)
webfiles/ctr/images/sprites/feeling-happy_checked.png 988b 669b -319b (32.29%)
webfiles/ctr/images/sprites/feeling-happy.png 896b 631b -265b (29.58%)
webfiles/ctr/images/sprites/feeling-like_checked.png 980b 670b -310b (31.63%)
webfiles/ctr/images/sprites/feeling-like.png 891b 644b -247b (27.72%)
webfiles/ctr/images/sprites/feeling-normal_checked.png 871b 590b -281b (32.26%)
webfiles/ctr/images/sprites/feeling-normal.png 752b 529b -223b (29.65%)
webfiles/ctr/images/sprites/feeling-puzzled_checked.png 1037b 682b -355b (34.23%)
webfiles/ctr/images/sprites/feeling-puzzled.png 960b 658b -302b (31.46%)
webfiles/ctr/images/sprites/feeling-surprise_checked.png 1017b 711b -306b (30.09%)
webfiles/ctr/images/sprites/feeling-surprise.png 974b 702b -272b (27.93%)
webfiles/ctr/images/sprites/follower-count.png 464b 305b -159b (34.27%)
webfiles/ctr/images/sprites/memo-input.png 406b 278b -128b (31.53%)
webfiles/ctr/images/sprites/memo-input.selected.png 421b 287b -134b (31.83%)
webfiles/ctr/images/sprites/post-count.png 442b 287b -155b (35.07%)
webfiles/ctr/images/sprites/reply.png 539b 397b -142b (26.35%)
webfiles/ctr/images/sprites/screenshot.png 535b 333b -202b (37.76%)
webfiles/ctr/images/sprites/tag.png 342b 189b -153b (44.74%)
webfiles/ctr/images/sprites/text-input.png 553b 361b -192b (34.72%)
webfiles/ctr/images/sprites/text-input.selected.png 591b 379b -212b (35.87%)
webfiles/ctr/images/sprites/yeah-small.png 516b 350b -166b (32.17%)
webfiles/ctr/images/sprites/yeah.png 597b 390b -207b (34.67%)
webfiles/ctr/images/sprites/yeah.selected.png 553b 400b -153b (27.67%)
webfiles/ctr/images/tab.png 124b 118b -6b (4.84%)
webfiles/portal/images/add-post-no-image.png 12183b 2875b -9308b (76.40%)
webfiles/portal/images/background.png 238796b 111949b -126847b (53.12%)
webfiles/portal/images/bandwidthalert.png 45238b 26917b -18321b (40.50%)
webfiles/portal/images/bandwidthlost.png 114375b 66384b -47991b (41.96%)
webfiles/portal/images/banner.png 20734b 8776b -11958b (57.67%)
webfiles/portal/images/splash-background.png 44394b 32163b -12231b (27.55%)
webfiles/web/images/add-post-no-image.png 12183b 2875b -9308b (76.40%)
webfiles/web/images/bandwidthalert.png 45238b 26917b -18321b (40.50%)
webfiles/web/images/bandwidthlost.png 114375b 66384b -47991b (41.96%)
webfiles/web/images/banner.png 20734b 8776b -11958b (57.67%)
webfiles/web/images/icons/icon-128x128.png 12645b 4449b -8196b (64.82%)
webfiles/web/images/icons/icon-144x144.png 13334b 4972b -8362b (62.71%)
webfiles/web/images/icons/icon-152x152.png 13349b 4956b -8393b (62.87%)
webfiles/web/images/icons/icon-192x192.png 14969b 6600b -8369b (55.91%)
webfiles/web/images/icons/icon-384x384.png 28768b 18861b -9907b (34.44%)
webfiles/web/images/icons/icon-512x512.png 39257b 28302b -10955b (27.91%)
webfiles/web/images/icons/icon-72x72.png 11008b 2192b -8816b (80.09%)
webfiles/web/images/icons/icon-96x96.png 11274b 3104b -8170b (72.47%)
webfiles/web/images/splash-background.png 44394b 32163b -12231b (27.55%)

As for shipping this pipeline, I thought it might be nice to create like a apps/image-optimizer tool in this repo for this purpose. Since oxipng is written in Rust and already has a library API, I thought writing such a tool in Rust would be the easiest way to go. But turns out ECT doesn't have a library API, so there's no bindings. The author is interested in it though, and ECT is actively maintained, so maybe PRing in an API would be a good idea? That way we can get our own tool going, and other people can benefit from having bindings for ECT as well. But for now, just calling these commands in some other script is fine. That's what I've been doing, just running these commands in NodeJS using a child process.

@jonbarrow
Copy link
Member

Here is a version of my script that processes all images, using mutliprocessing. It completes all images in about 20 minutes. I can add this as a separate, or push directly to this one, if you'd prefer.

const path = require('path');
const fs = require('fs');
const child_process = require('child_process');
const cluster = require('cluster');
const os = require('os');

const baseDirectory = 'apps/juxtaposition-ui';
const filePaths = fs.readdirSync(baseDirectory, {
	recursive: true
}).filter(filePath => filePath.endsWith('.png')).map(filePath => path.join(baseDirectory, filePath));

if (cluster.isPrimary) {
	const numCPUs = os.cpus().length;
	let completed = 0;

	console.log(`Processing ${filePaths.length} files with ${numCPUs} workers...`);

	const chunkSize = Math.ceil(filePaths.length / numCPUs);
	for (let i = 0; i < numCPUs; i++) {
		const worker = cluster.fork();
		const chunk = filePaths.slice(i * chunkSize, (i + 1) * chunkSize);

		worker.on('message', (message) => {
			if (message.type === 'progress') {
				completed++;
				console.log(`[${completed}/${filePaths.length}] ${message.file}`);
			}

			if (completed === filePaths.length) {
				process.exit(0);
			}
		});

		worker.send({ chunk });
	}
} else {
	process.on('message', ({ chunk }) => {
		for (const filePath of chunk) {
			try {
				child_process.execSync(`oxipng -o max --zopfli --zi 50 --ng --strip all ${filePath}`);
				child_process.execSync(`ect -9 -strip --allfilters-b ${filePath}`);
			} catch (error) {
				console.error(`Error processing ${filePath}: ${error.message}`);
			}

			process.send({
				type: 'progress',
				file: filePath
			});
		}

		process.disconnect();
	});
}

@jonbarrow
Copy link
Member

Tis I once again here for your daily notification.

I made a PR for ECT to add a library API, so that we can make bindings for it in our pipeline rather than rawdogging CLI calls.

PR is here for those interested fhanau/Efficient-Compression-Tool#152

@suprstarrd
Copy link
Contributor Author

Will leverage your daily return to ask if you can DM me somewhere for a question I haven't been able to find an answer to

also says it supports jpeg images which we haven't covered here yet

This is something I did want to bring up. Is there a reason we went with PNG for non-transparent files? Would it be possible for us to make some of these assets into JPEG or something else that the Wii U and 3DS support?

Everything else here is sweet; thank you both! I'd like to see if we can find a way to automate the workflow (whether with Jon's script or something else) and then, for this PR, if the closed-source tools give any more savings, I can manually add those savings to this PR with a separate commit and leave it at that.

@jonbarrow
Copy link
Member

jonbarrow commented Feb 11, 2026

Will leverage your daily return to ask if you can DM me somewhere for a question I haven't been able to find an answer to

Does it need to be via DM? Iirc you're in our Discord server, could it not be asked there in an appropriate channel? Or if it's related to some of our services, as a "Question" issue on the relevant repository? Genuinely asking, since I don't really like to DM much (just in general) which is why I have them silenced on Discord and don't check message requests. I'm willing to if absolutely necessary but I'd like to not if possible (again this isn't a you thing, I just can't stand DMs 99% of the time)

This is something I did want to bring up. Is there a reason we went with PNG for non-transparent files? Would it be possible for us to make some of these assets into JPEG or something else that the Wii U and 3DS support?

Unsure, presumably it was a tradeoff between file size and image quality? I didn't make that decision, @ashquarky would have to answer that one and decide if they think JPEG is reasonable here. I will say though that having everything as PNG does make the pipeline simpler and we already have working tools for PNGs. Switching to JPEG would effectively start this whole process over again, trying to find suitable tools to optimize them

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 11, 2026

Does it need to be via DM?

Doesn't need to be a DM; but I figured you were probably the one that knew the most about it, and I didn't see an existing issue/question/etc for it anywhere (though I'm pretty sure it would apply to multiple services). I can ask it in one of those channels though. I completely understand the hesitance to DM, I'm similar when it comes to Discord in particular!

Edit: I did the thing

Switching to JPEG would effectively start this whole process over again, trying to find suitable tools to optimize them

True, but it seems like JPEG is already fairly well-compressed and we've already mentioned a tool to compress it further. As long as the JPEG is actually less bytes than the PNG I think that as long as the image quality is acceptable this would be a reasonable tradeoff. Most users won't care about anything but "it work!" at a fast rate (although I guess that statement could be in favor either way)

I can add this as a separate, or push directly to this one, if you'd prefer.

I'd appreciate this being pushed directly (or, alternatively, made as an independent repository for general use?)

@suprstarrd
Copy link
Contributor Author

Just made a commit with compressed images! @lumiscosity's work added a single additional byte of compression 🙏 Will amend and verify and add co-authors and etc in a bit.

@jonbarrow
Copy link
Member

Just made a commit with compressed images! @lumiscosity's work added a single additional byte of compression 🙏 Will amend and verify and add co-authors and etc in a bit.

I'm curious which one had the extra byte? In my test all images were smaller using the new oxipng/ect pipeline

@ashquarky
Copy link
Member

Google has some recs on JPEG compression settings: https://developers.google.com/speed/docs/insights/OptimizeImages

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 13, 2026

I'm curious which one had the extra byte? In my test all images were smaller using the new oxipng/ect pipeline

Ah, sorry for the misunderstanding. Your pipeline was smaller, which is why I ran it first. I then did Lumi's pipeline on top. None of the files were changed at all in Lumi's pipeline, except one (on my phone so will edit this in a second with the rest of the comment and a command line reference :P)

Also, yes it's been more than a bit, sorry. I'm on a roadtrip right now and God I've been so tired.


"in a second", she said.

Anyway, it's been 3 hours. Here's the single file I was referring to:

Files processed: 1/1   
Input size: 287 bytes
Output size: 286 bytes
Total saved: 1 bytes (0.35%)

I don't know what file this is for sure, but I feel very comfortable assuming it's webfiles/portal/images/bandwidthlost.png,

But I was actually wrong about it being only a single byte! The defluff output was apparently smaller for a few other files (I added some extra logging to debug something):

[defluff-shim] defluff output smaller for ./apps/juxtaposition-ui/webfiles/web/images/icons/icon-512x512.png, using
[defluff-shim] defluff output smaller for ./apps/juxtaposition-ui/webfiles/web/images/icons/icon-128x128.png, using
defluff-shim] defluff output smaller for ./apps/juxtaposition-ui/webfiles/portal/images/splash-background.png, using
[defluff-shim] defluff output smaller for ./apps/juxtaposition-ui/webfiles/web/images/splash-background.png, using

And DeflOpt got a few extra single bytes in these files too too:

"Z:/workspaces/juxtaposition/apps/juxtaposition-ui/webfiles/web/images/splash-background.png"
"Z:/workspaces/juxtaposition/apps/juxtaposition-ui/webfiles/ctr/images/sprites/feeling-surprise.png"
"Z:/workspaces/juxtaposition/apps/juxtaposition-ui/webfiles/ctr/images/sprites/text-input.selected.png"
"Z:/workspaces/juxtaposition/apps/juxtaposition-ui/webfiles/portal/images/splash-background.png"

suprstarrd added a commit to suprstarrd/juxtaposition that referenced this pull request Feb 13, 2026
This commit was built atop investigation and
research from @lumiscosity and @jonbarrow. As
such, I have made them co-authors of this commit
to give them their credit.

Further discussion in issue PretendoNetwork#262 and especially
pull request PretendoNetwork#266.

Co-authored-by: lumiscosity <averyrudelphe@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: Jonathan Barrow <jonbarrow1998@gmail.com>
Co-authored-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Signed-off-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 13, 2026

Wishing on a shooting star that you could reorder the authors of a Git commit. Figured out how to do it. Life is good again.

Heads up that I may force-push again if either of you want the Signed-off-by on this commit too - which would be great for consistency. The fix(ui): optimize images commit will stay the same regardless. (Also, if either of you don't want to be co-authors, I can amend the commit and take you off!)

Besides that irritation, the only remaining thing to figure out with this pull request is to figure out automating image optimization for the future.

I am incredibly tired at the moment, but this will be something I prioritize tomorrow after I catch up on some more college work. With that said, this is in a good enough state for review / merging if that's something you all want to do right now and skip the automation. Cheers.

@suprstarrd suprstarrd marked this pull request as ready for review February 13, 2026 05:35
suprstarrd added a commit to suprstarrd/juxtaposition that referenced this pull request Feb 13, 2026
This commit was built atop investigation and
research from @lumiscosity and @jonbarrow. As
such, I have made them co-authors of this commit
to give them their credit.

Further discussion in issue PretendoNetwork#262 and especially
pull request PretendoNetwork#266.

Co-authored-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
Co-authored-by: lumiscosity <averyrudelphe@gmail.com>
Co-authored-by: Jonathan Barrow <jonbarrow1998@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Signed-off-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
Signed-off-by: lumiscosity <averyrudelphe@gmail.com>
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
@jonbarrow
Copy link
Member

the only remaining thing to figure out with this pull request is to figure out automating image optimization for the future.

Given how small of a difference defluff seems to make (only between 1 to a few bytes per image), for our needs I don't think it's worth the hassle of trying to include it in any automation due to the platform/licensing concerns even if it does technically improve things. The oxipng+ect pipeline gets things "good enough" while fitting within our needs. I tried using advdef (from https://github.com/amadvance/advancecomp) on webfiles/web/images/icons/icon-512x512.png but it couldn't shave that extra byte off even with 1000 iterations, but I don't think that one byte matters too much here. It's possible there's some combination of settings, or some other tool entirely, that might end up matching defluff, but we're hitting what are probably the limits of compression now so I'm not hyper concerned about it at this stage. I'll open a dedicated issue for the automation side of things later, so it can be discussed without holding up this PR

@ashquarky
Copy link
Member

@suprstarrd Just to confirm you want me to review this as-is and then automation will be a seperate thing? Or should I leave you to cook on the automation front

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 15, 2026

Is yes a valid answer? (Review as-is - i.e. did I miss anything and do the images load properly; can't check on 3DS/Wii U rn. Then I'll cook on automation)


Given how small of a difference defluff seems to make (only between 1 to a few bytes per image), for our needs I don't think it's worth the hassle of trying to include it in any automation due to the platform/licensing concerns even if it does technically improve things.

I agree. Wasn't going to make it anything beyond that, just need to actually implement it is all.

@ashquarky
Copy link
Member

Quick test on a random community in modern Chrome.
CTR: 115 kB before / 113 kB after
Portal: 661 kB before / 524 kB after
Web: 806 kB before / 800 kB after

Definitely some nice free improvement, with Portal being the biggest winner. Note that this was a rough test with debug and similar enabled, so we could expect a bigger % difference in prod, too.

ashquarky
ashquarky previously approved these changes Feb 16, 2026
Copy link
Member

@ashquarky ashquarky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked Portal and CTR, everything renders fine

Copy link
Member

@binaryoverload binaryoverload left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the automation is a WIP, but could we have it documented somewhere what the compression process was that was used in the end, please?

@ashquarky
Copy link
Member

Sorry for the merge conflict ^^; Since you're re-running the script anyway, I did a bunch of other assets changes I had pending: https://github.com/PretendoNetwork/juxtaposition/tree/work/design-assets

You can rebase onto that branch or cherry-pick or merge it or whatever you'd like, works for me. (Usually we'd be a bit upset about a rebase during review, since you lose the "Viewed" status on the files on GitHub, but since this is all binary files rather than actual code, losing the diff doesn't make a difference)

@ashquarky
Copy link
Member

For any future optimisers, since this issue is becoming a bit of a hub for that info; my process for design assets in GNU IMP is:

  • Put image into RGB mode if not already
  • Check displayed size in UI and re-scale (or, for small/vector assets, re-make it at that res), crop if needed
  • Remove alpha channel if unused
  • Crank the contrast and check for noisy areas, make them solid-coloured if possible
  • Conversion to indexed colour
    • For PNG, the only meaningful palette sizes are 2, 4, 16 and 256
    • Alpha channel will always be 1bpp (2 colour) which is acceptable for some assets (2-3px radiuses on corners) and unacceptable for others (Bandwidth images)
    • Even a 256 pallette is often a 2x saving over true RGB, but see if 16 is tolerable
  • Export with maxxed settings (doesnt' really matter since ms. suprstarrd will fix it, right? ^^)

Since the indexed colour part of the process removes information (just usually too subtly to be visible) and requires some human judgement on the alpha channel point, it appears automated tools like oxipng don't apply that transform. Instead of storing better, it's storing less. Of course this still stacks with storing better, so I'll be excited to see what the script does with these :3

@suprstarrd
Copy link
Contributor Author

suprstarrd commented Feb 17, 2026

For any future optimisers, since this issue is becoming a bit of a hub for that info

Man, if only someone was writing a blog post about this

I know the automation is a WIP, but could we have it documented somewhere what the compression process was that was used in the end, please?

Yes!

@ashquarky
Copy link
Member

I'll finish this one up ^^

ashquarky and others added 3 commits February 24, 2026 15:25
This commit was built atop investigation and
research from @lumiscosity and @jonbarrow. As
such, I have made them co-authors of this commit
to give them their credit.

Further discussion in issue PretendoNetwork#262 and especially
pull request PretendoNetwork#266.

Co-authored-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
Co-authored-by: lumiscosity <averyrudelphe@gmail.com>
Co-authored-by: Jonathan Barrow <jonbarrow1998@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Signed-off-by: Sienna "suprstarrd" M. <business@suprstarrd.com>
Signed-off-by: lumiscosity <averyrudelphe@gmail.com>
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
@ashquarky
Copy link
Member

Wasn't able to quite match @jonbarrow's results despite running the same command - probably an OS thing with different versions of libpng and zlib - but still quite a good improvement and we can always have another PR with a second pass once others are more available to do so.

Dropped the script I used in as well, basically the same as Jon's just as quick-and-dirty shell so nobody gets any ideas about this being ready to automate, aha.

Still need to look at SVGs.

@lumiscosity
Copy link

For any future optimisers, since this issue is becoming a bit of a hub for that info

Man, if only someone was writing a blog post about this

will get around to it later this/next week, hopefully. i haven't been able to get ECT running on my pc, but maybe it just needs a bit more fiddling, then i'll round up the info from here, run a few more tests and let it loose

@CLAassistant
Copy link

CLAassistant commented Feb 25, 2026

CLA assistant check
All committers have signed the CLA.

@binaryoverload binaryoverload self-requested a review March 1, 2026 16:46
@binaryoverload
Copy link
Member

@ashquarky @suprstarrd What's the status of this? Is this ready to be merged?

@ashquarky
Copy link
Member

I need to final test this to make sure the applets can load the images and just haven't gotten around to it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Optimize static PNG assets

6 participants