phoboslab / qoi

The “Quite OK Image Format” for fast, lossless image compression
MIT License
6.85k stars 327 forks source link

Add 'dqoi' to the implementations list on README #205

Closed JaffaKetchup closed 2 years ago

JaffaKetchup commented 2 years ago

'dqoi' is my implementation of QOI for Dart and Flutter, based off the C code included here, and @LowLevelJavaScript's implementation.

I'm making this a draft for now, as it's still in alpha. However, it should be ready soon...

JaffaKetchup commented 2 years ago

Hi @phoboslab,

Are the test images found at https://qoiformat.org/qoi_test_images.zip up-to-date? If so, can you provide some in .bin format without any other formats' headers?

I'm having problems encoding some images (to do with the difference between RGB and RGBA), however, as far as I can see I haven't done anything wrong. It should be noted that I'm using a third party library to convert the PNGs in the ZIP to just the pixels, and this might be the issue.

When I encode this binary format image (monument.bin), I seem to get the output he got in monument.qoi.

When I encode 'testcard.qoi' I get the output seen on the left below. This does not match the example QOI file, but (QOIThumbnailProvider)[https://github.com/iOrange/QOIThumbnailProvider] still shows it as it should be. Likewise, when I use my decoder to convert it back to PNG, it doesn't match the example PNG byte-wise, but https://www.diffchecker.com/ shows them exactly the same; and Windows shows it valid. It's the same story with 'testcard_rgba.qoi'.

image (testcard.qoi; my output on left, example on right)

image (testcard_rgba.png; my output on left, example on right)

Can you shed any light onto this, as I've been debugging for hours. Many thanks :)

Alcaro commented 2 years ago

Every image format can represent the same pixel data in multiple ways. If the resulting QOI decodes to the same RGBA data, it's valid and correct, whether it's bytewise identical or not. (Though smaller is, of course, better.)

In this case, the left QOI encodes the first pixel as 0xFE 0xFF 0xFF 0xFF, aka QOI_OP_RGB(255, 255, 255). The right one encodes it as 0x55, aka QOI_OP_DIFF(-1, -1, -1); it's the first pixel, so "previous" is {r: 0, g: 0, b: 0, a: 255}. This wraps around to 255.

Both then use 0xC6 QOI_OP_RUN(7); left then uses QOI_OP_RGB again, while right uses QOI_OP_DIFF again, this time to emit a black pixel.

Looks like the left encoder gets something wrong around the wrapping rules.

Similar things happen to the PNGs, but it's a much more complicated format; unlike QOI, how to best compress one pixel depends on how previous pixels were compressed. I won't even try to compare them. If they decode to the same pixel values, they're both correct.

JaffaKetchup commented 2 years ago

Many thanks. Can you show me where the wrapping rules are? I couldn't see them in the C implementation.

phoboslab commented 2 years ago

The difference to the current channel values are using a wraparound operation, so 1 - 2 will result in 255, while 255 + 1 will result in 0.

~ from the qoi-specification.pdf

In the C implementation the difference is calculated using the char data type. This type is only 8-bit wide and will wrap around automatically. See https://github.com/phoboslab/qoi/blob/master/qoi.h#L459-L461

Strictly speaking, the C standard (afaik) says that the overflow behavior for signed char is undefined. unsigned char however is guaranteed to wrap around on overflow. It doesn't seem to be a problem in the real world - every(?) implementation wraps around signed types as well. I guess I should fix that nevertheless.

Alcaro commented 2 years ago

That'd be the types used at https://github.com/phoboslab/qoi/blob/master/qoi.h#L459. signed char can only represent values -128 to +127; if you try to insert anything out of range, like 255 or -255, it adds or subtracts 256 until it fits.

(Nitpicker's corner: There are some microcontrollers and extinct platforms where signed char has different range. But if you're targetting one of those, you know that; for everyone else, said platforms are safe to ignore.)

e: ninja'd

JaffaKetchup commented 2 years ago

Ah that would explain it, I guess I'll have to figure out the equivalent for Dart.

Although, using this line: https://github.com/kchapelier/qoijs/blob/main/src/encode.js#L126 modified to equal 128 on the left of the ternary operator doesn't seem to work for me. I guess I'll have to keep looking :)

Alcaro commented 2 years ago

Modulo acts weird with negative inputs. I'd use one of

let vr = red - prevRed; if (vr < -128) vr += 256; if (vr > 127) vr -= 256;

let vr = (red - prevRed) & 255; if (vr > 127) vr -= 256;

e: copied wrong line in the latter

JaffaKetchup commented 2 years ago

Ok, will test in a minute. Thanks for both of your fast and useful responses!

JaffaKetchup commented 2 years ago

Thanks, this seems to work a treat, and I get the exact correct output now. And my decoder also still works.

One other slight issue, but not too much of a problem. My encoder and decoder can't handle RGB images (RGBA is fine), but I'll try to discover that myself. At the moment there is an easy workaround, so it's not too bad (just changing the number of channels to 4 gives the exact correct output except the header number).

Edit: I've just hardwired RGBA input and output except in header where proper channel number is written.

JaffaKetchup commented 2 years ago

Thanks for all your helps! This is now ready to merge, as the first stable version has been published.