thomasokken / free42

Free42 : An HP-42S Calculator Simulator
https://thomasokken.com/free42/
GNU General Public License v2.0
280 stars 54 forks source link

Skin Color Shift? #41

Closed salvis closed 2 years ago

salvis commented 2 years ago

Thank you for a great program!

Looking through the Windows skins, I like the "auto" part in Nova1_360x640_auto. And I like the keyboard hints of some of the other skins, so I tried to add the hints. The resulting GIF file looks fine in Paint.Net and in Windows Explorer preview, but when loaded into Free42, the colors are completely different, somewhat similar to a hue shift of 180, but not quite:

image

Even just loading and saving the Nova1_360x640_auto.gif file produces this effect. Why does this happen? Is there a work-around?

thomasokken commented 2 years ago

Could you send me the GIF file?

salvis commented 2 years ago

Here's the GIF file: Nova1_360x640_auto_hints The LAYOUT file is the same as for Nova1_360x640_auto. Thanks for taking a look!

thomasokken commented 2 years ago

I'm afraid I can't reproduce the problem... the new gif looks fine when I load it:

screen

thomasokken commented 2 years ago

Your screen shot looks like the R and B components in the colormap are getting swapped somehow. I have no idea what could be doing that.

salvis commented 2 years ago

Your screen shot looks like the R and B components in the colormap are getting swapped somehow.

Yes, exactly. Using the 'Color Matrix' Adjustment Plugin in Paint.Net I was able to swap R and B. When I load that version, the channels are swapped back and the result comes out as intended. The strange part is that the original Nova1_360x640_auto.gif loads correctly. It's only after saving it with Paint.Net (and even Windows 10 Paint!) that the channels appear swapped in Free42 (but not in the paint programs or the Windows Explorer preview pane).

Well, I have my work-around, and I'm writing it off as a strange interaction with my video driver.

Thanks for looking into it!

thomasokken commented 2 years ago

I can't even come up with a hypothesis. The fact that that same skin works fine when I try it rules out pretty much everything.

The GIF file format is pretty rigid, there's only one way to store a colormap. Or two, if you count the local vs. global colormap thing. But if there were a bug in that aspect of GIF colormap handling, you'd expect the same effect on my machine as on yours.

Maybe the colormap is loaded with its alignment off by 2 bytes, but then again, why would that bug bite on one computer but not another?

Maybe it is a video driver bug, that seems plausible at least, but still strange, for what I would think is pretty basic functionality to be broken like that. And still a mystery why it would happen with one skin and not another.

You aren't running this under Windows-on-ARM with x86 emulation, are you? ;-)

thomasokken commented 2 years ago

I gave it another shot, and lo and behold, I'm getting a weird-looking skin when I load your GIF into Free42 on my Mac. And when I looked at a hex dump of your gif and compared it to Nova1_360x640_auto.gif, I noticed that your version has a local colormap while the original has a global one. The plot thickens!

thomasokken commented 2 years ago

Screen Shot 2022-03-31 at 14 57 46

thomasokken commented 2 years ago

There's definitely something in the GIF file that my decoder doesn't like. Time to dig up the GIF spec and compare...

thomasokken commented 2 years ago

OK, the skin loader is making a big mess of this image. Something is getting out of sync deep inside the decoder, in code that I wrote in 2004 and thought I'd never look at again. There's something nondeterministic about it, since the way it's messing up the image on my Mac right now is different from what it did on your PC, and also different from what I observed in Windows yesterday. This could take a while.

thomasokken commented 2 years ago

Ah, I think I found it. What's happening is that when it finds a local colormap, it creates a true-color image, rather than an indexed one. The rationale behind that is that with images with local colormaps, it is possible to end up with more than 256 colors. And that is all well and good, but apparently the way those true-color images are handled is broken, it looks like there is some confusion about 4 bytes per pixel with an unused alpha, vs. 3 bytes per pixel, packed. The decoder looks OK, it's parsing and decoding the entire image with no trouble, it's just the way the resulting pixels are packaged and sent to the GUI that is broken. The nondeterministic aspect of this may be why I never noticed this before, since I'm pretty sure I did test this code with local colormaps way back when... but there are subtle variations between GIF files generated by one app and another, so maybe recent versions of Paint are just doing something I never encountered before. (I have used Paint for skins, but maybe not the latest version, and I think most skins nowadays are created with Photoshop or Gimp.) I have to run now, but I'll get back to this soon.

thomasokken commented 2 years ago

Now I'm getting the same R/B flipped colors in Windows, too. Now what I don't understand is how it is possible that I was not able to reproduce this before. I know I looked at your image, because it had the letters next to the keys. 🤔 Anyway, apart from the initial failure to reproduce, this is pretty straightforward. The GIF decoder produces true-color pixel data in a certain format, and some of the shells expect a different format. I'll check, and where necessary, fix, all five versions. I'll post another message here when I have test builds ready to try.

thomasokken commented 2 years ago

OK, fixed. I uploaded test builds to https://thomasokken.com/free42/download/test/ I'll have to dig through my commit history to find out what happened there. The only version that loaded true-color data correctly was the GTK version! I fixed things by changing the skin loader to provide packed data, i.e. 3 instead of 4 bytes per pixel, because that was what the Android, iOS, and MacOS versions needed all along; and I changed the GTK and Windows not to expect that padding, and changed the Windows version so it flips the R and B channels.

thomasokken commented 2 years ago

(Correction: the Android version didn't have a problem with local colormaps, either. Unlike the other four, the Android version doesn't use my GIF decoder, but rather the built-in one provided by the Android OS.)

thomasokken commented 2 years ago

I checked the code's history. It looks like the Windows bug has existed from the start. What it looks like is that I tested the true-color logic only in the Motif version, that is, the original Linux/Unix version, which was the predecessor of the current GTK version, and which was the very first version of Free42, together with the PalmOS version, and older even than the Windows version.

It appears that I never tested the code with GIFs with local colormaps after finishing the Motif version. Which is sloppy, but not completely surprising since it appears I simply didn't have any skins with such GIFs, so it was an easy test to miss.

The GTK version handled true-color correctly, probably because that version uses true-color bitmaps even when the GIF decoder provides indexed data; this is because GTK doesn't support indexed bitmaps. So even without having any test cases, I probably ended up coding this correctly simply because I understood the way GTK bitmaps work so well.

But with the Windows, MacOS, and iOS versions, the true-color case appears to never have been tested at all. The only way to miss the R/B reversal in Windows would have been to test with a grayscale image, or not test it at all, and missing the fact that the MacOS and iOS version expected packed RGB while the decoder provided RGBA would have been impossible with any test case.

And of course the Android version, using a completely different GIF decoder, has always been immune to this issue. The person who created the mimax3 skin undoubtedly tested it on an Android device.

(The reason I even bothered to write my own GIF codec is a story in itself, having to do with a earlier project of mine, where I needed GIF read and write capability. Libgif had had its compression logic removed because of the way Unisys was threatening to enforce the LZW patent, so I decided to just write my own. By the time I started work on Free42, the LZW patent had expired, but since I already had my own GIF codec, which was easier to embed than libgif, I just kept using it.)

As far as I can tell, all versions are OK now, so I'm re-closing this issue. But feel free to re-re-open it if you run into problems with the skin loader again!

salvis commented 2 years ago

The test build works perfectly now, and you're also stretching the display to the full size provided by the skin — thanks a lot!

It's quite amazing that this issue has been dormant for so long. :-)

thomasokken commented 2 years ago

Indeed! The Windows version has had this bug since day 1.

Of course it helped that out of all the skins in my collection, only one has a local colormap, and that skin is only suitable for high-density screens. And that the Android version is immune, because it doesn't use my GIF decoder, and the desktop versions don't support high-density skin rendering... so either nobody tried using mimax3 under iOS, or they did but didn't bother to report the problem.

And the other thing that helped was that until recently, most image editing tools didn't write GIFs with local colormaps. I remember specifically avoiding older versions of MS Paint for working with GIFs, because they did such a terrible job of saving them. That it's using local colormaps now could be an indication that it's supporting more than 256 colors when saving GIFs, or at least that would be a good reason to do so.

(I personally prefer writing images in png or some other lossless 24-bpp format, and then using ppmquant and ppmtogif to convert them to GIFs with global, optimized colormaps. A bit old-fashioned perhaps, but for the kinds of graphics you find in skin images, it works quite well.)

thomasokken commented 2 years ago

When saving as GIF, older versions of MS Paint would use a fixed colormap, and then convert each color to whichever one was closest in that colormap. No attempt at palette optimization, and no dithering. And the result usually looked so awful that it was unusable.

I'll have to take a look at what the Windows 10 version does. One reason for using local colormaps is to be able to save a true-color image without any color reduction, by splitting it into sub-images with local colormaps, if necessary, such that each sub-image contains 256 colors or fewer.

For skins, I think creating an optimized global 256-color palette and then dithering is a better option, since even skins based on photographs tend to have a pretty limited color palette to begin with. That's also the reason why I never took the trouble to support BMP or PNG for skins: even with its limitations, GIF always seems to be good enough.

thomasokken commented 2 years ago

I tried loading the attached png image in the Windows 10 version of MS Paint and then saving it as GIF; it warned me that color information would be lost, and sure enough, it created an image with a single, fixed, global colormap. I'll be interested to see what the Windows 11 version does with it. The PNG image is 256x256, with every pixel having a unique color. It could be converted to GIF without losing color information, but that would require 256 local colormaps... so maybe this is a bit too extreme as a test case.

ramp ramp

salvis commented 2 years ago

That's pretty interesting! Your Windows 10 Paint dithered image shows 6 distinguishable bands.

Paint.NET has some settings for saving GIFs. Its "Median Cut" algorithm produces 7 bands at Dithering Level 7 (the proposed default) or 8. The file sizes are 25'602 and 26'466 respectively, and I can't see any difference between these two levels. Here's Level 8: 256x256_colors, saved by Paint NET (MedianCut  Level 8)

Paint.NET also offer the Octree algorithm, which produces more and finer bands, here at Level 8 (32'955, 32'416 at level 7): 256x256_colors, saved by Paint NET (Octree, Level 8)

Dropping to Dithering Level 0 reveals the resulting global palette and produces different results from the two algorithms: Median Cut (8'595) 256x256_colors, saved by Paint NET (MedianCut  Level 0)

Octree (12'214): 256x256_colors, saved by Paint NET (Octree, Level 0)

I don't remember which algorithm I used, but I'll use Octree in the future, even though it's still using only a global palette. It would be interesting to see whether the algorithms prioritize the other colors when blue is involved.

The infamous Windows 10 Paint 3D saves MedianCut Level 8.

thomasokken commented 2 years ago

Interesting! And GIMP appears to have yet another colormap selection algorithm. When you use it without dithering, you get somewhat irregular-shaped areas, maybe it's median cut, definitely not octree but also not the kind of hexagonal tiling you expect from nearest-neighbor algorithms, unless it's nearest neighbor with some clever metric.

I always use ppmquant for this, which apparently uses median cut, at least by default. Judging by your results, that's not actually all that great... but it is possible that I could always get away with that since I was working with images where the palettes weren't all that challenging, so even a not-so-great color selection algorithm, combined with dithering, produced results with basically no noticeable artifacts, at least as long as you didn't look too closely.

thomasokken commented 2 years ago

ppmquant has a few switches to set parameters on the algorithm, but with the color ramp image, they all produce essentially the same output, with dithering turned off:

ramp center

This is what comes out when exporting as GIF from GIMP. Curiously, there isn't even an option to enable dithering in the GIF export dialog.

ramp