Skip to content

[WIP] First version of WebP converter #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from

Conversation

melikebatihan
Copy link
Contributor

Hey @mosra, @Squareys,
here is the first version of WebP converter.

1- I have used Advanced Encoding API instead of the Simple Encoding API, which has several simple functions for encoding:

(For lossy encoding)
size_t WebPEncodeRGB(const uint8_t* rgb, int width, int height, int stride, float quality_factor, uint8_t** output);
size_t WebPEncodeRGBA(const uint8_t* rgba, int width, int height, int stride, float quality_factor, uint8_t** output);

(For lossless encoding)
size_t WebPEncodeLosslessRGB(const uint8_t* rgb, int width, int height, int stride, uint8_t** output);
size_t WebPEncodeLosslessRGBA(const uint8_t* rgba, int width, int height, int stride, uint8_t** output);

However, the settings used by these functions disable exact feature which preserves the exact RGB values under transparent area when enabled. When this feature is disabled like in the functions above, these RGB values in transparent areas gets discarded for better compression. Hence, I have used WebPConfig structure in Advanced Encoding API to configure advanced encoding parameters to achieve the highest quality, lossless encoding, and the main encoding function below:

WEBP_EXTERN int WebPEncode(const WebPConfig* config, WebPPicture* picture);

My WebPConfig configuration might not be ideal though, considering that a lossless conversion with 100% quality decreases the conversion speed (quality/speed trade-off).

2- I have adapted most of the tests in WebPImageConverterTest.cpp from the tests of other plugins 'PngImageConverter' and 'JpegImageConverter'. However, I couldn't properly understand the purpose of the methods like

PixelStorage{}.setSkip({0, 1, 0}).setRowLength(3)

So, I can't be sure if the conversions in tests like rgb(), rgba16(), grayscale() are correct.

Copy link
Owner

@mosra mosra left a comment

Choose a reason for hiding this comment

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

Ooo, didn't expect this! :) I was contemplating writing a WebP exporter once I get bored, but the boredom didn't happen so far so I didn't.

Random comments on the essential things first, I'll have a closer look at the rest in the next iteration.

Comment on lines 207 to 217
constexpr const char OriginalRgbData16[80] = {
/* Skip */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

10, 20, 30, 10, 20, 30, 0, 0, 0, 0,
40, 50, 60, 40, 50, 60, 0, 0, 0, 0,
70, 80, 90, 70, 80, 90, 0, 0, 0, 0
};

const ImageView2D OriginalRgb16{PixelStorage{}.setSkip({0, 1, 0}),
PixelFormat::RGB16Unorm, {2, 3}, OriginalRgbData16};
Copy link
Owner

Choose a reason for hiding this comment

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

PNG supports 16-bit-per channel images, which is what the *16() tests were for. Here you however turned the data to 8-bit, while the pixel format still says RGB16. That doesn't make sense, and since -- as far as I know -- WebP supports only 8 bits per channel, you don't need any of the *16() tests. A dedicated test for (8-bit) RGBA needs to be there however.

Similarly for grayscale images, given that WebP supports only RGB and RGBA I don't think it produces anything remotely correct for single-channel input.

The plugin code should check that the PixelFormat is one of the allowed formats, and fail the conversion if not. In general, formats accepted by the converter should match the formats produced by WebPImporter.

100, 78, 74, 100, 78, 74, 0, 0,
100, 84, 41, 100, 84, 41, 0, 0,
100, 87, 25, 100, 87, 25, 0, 0
};
Copy link
Owner

Choose a reason for hiding this comment

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

Wait but given you said the conversion is meant to be lossless and exact, this doesn't really look exact :)

constexpr const char ConvertedRgba8Data[] = {
70, 80, 90, 100, 70, 80, 90, 100,
40, 50, 60, 90, 40, 50, 60, 90,
10, 20, 30, 80, 10, 20, 30, 80
Copy link
Owner

@mosra mosra Jul 4, 2023

Choose a reason for hiding this comment

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

It also looks like the converter doesn't Y-flip on export. The WebPImporter does (all importer plugins do), which means the converter has to flip as well, to arrive back at the Y up same orientation.

For decoding there was a flip option, the encoder options don't seem to have anything similar. But maybe passing a pointer to the last row instead of the first and a negative stride could work instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Scan line order from the webp sepcification:

A processing order of pixels (left to right and top to bottom), starting from the left-hand-top pixel. Once a row is completed, continue from the left-hand column of the next row.

If that does indeed need to be Y-flipped, flipping the image like PngImageConverter does before encoding would work, right?

Copy link
Owner

Choose a reason for hiding this comment

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

That comment may have been confusing, sorry, updated it. WebP stores images Y down, while the in-memory data in Magnum is Y up (because they're all imported that way, currently). So the exporter has to flip back, yes.

In case of libpng it's easier because the encoder supports supplying data row-by-row. WebP doesn't, and doesn't have any explicit option for this, so it has to be either

[...] maybe passing a pointer to the last row instead of the first and a negative stride could work

as I said above, or making a copy of the input data and running Utility::flipInPlace() on it before passing it to libwebp.

Comment on lines 95 to 96
const std::pair<Math::Vector2<std::size_t>, Math::Vector2<std::size_t>> dataProperties = image.dataProperties();
auto inputData = Containers::arrayCast<const unsigned char>(image.data()).exceptPrefix(dataProperties.first.sum());
Copy link
Owner

Choose a reason for hiding this comment

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

Ugh, I have to clean this up in the PngImageConverter and other plugins, the dataProperties() API is just horrific, sorry. It's far easier to access the data properties through the image.pixels() view than this. I'll let you know once that's done so you can mirror that change here.

Copy link
Owner

Choose a reason for hiding this comment

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

This is cleaned up as of a3c01d5. Hopefully significantly easier to grasp this way.

Comment on lines 135 to 143
if(flags() & ImageConverterFlag::Quiet)
Error() << "Trade::WebPImageConverter::convertToData(): invalid WebPPicture size";
else
Warning{} << "Trade::WebPImageConverter::convertToData(): width or height of WebPPicture structure exceeds the maximum allowed:" << WEBP_MAX_DIMENSION;

return {};
Copy link
Owner

Choose a reason for hiding this comment

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

I don't understand this logic. Given the conversion fails in any case, why is it a warning if the Quiet flag is not set, and an error if it is? Why one of the messages is more verbose than the other?

I think this whole check shouldn't need to be here at all, it's libwebp's responsibility to check that image dimensions are in allowed bounds.

int width = image.size().x();
int height = image.size().y();
UnsignedInt channel_count = pixelFormatChannelCount(image.format());
const std::size_t stride = width * channel_count; // in pixels units, not bytes.
Copy link
Owner

@mosra mosra Jul 4, 2023

Choose a reason for hiding this comment

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

Gotta admit I don't really understand what this comment in libwebp's encode.h means :D But you're using the stride in WebPPictureImport*(), where it is meant to be bytes I think.

To handle the input image stride correctly, it has to be be dataProperties.second.x() instead (for now, until I do the modifications to use image.pixels() instead -- then it would simply be a stride of the view), with a change similar to a3c01d5 it'd be now pixels.stride()[0].

I couldn't properly understand the purpose of the methods like setSkip, setRowLength

To answer this, these are usually for taking slices of large images. For example you have a 64x64 image and then you want the top-right 32x32 corner. So you skip the first 32 rows (assuming Y up coordinate system, with origin bottom left), the first 32 pixels of the first row, and then you have extra 32 pixels between each row, which means you set the row length to 64.

In the tests those are tested with the extra zeros in the image data. Also, this API isn't really great (same as the dataProperties() was), it's much easier to understand with sliced strided array views. It's a legacy from GL that I still have to clean up.

return {};
}

pic.use_argb = (channel_count == 4) ? 1 : 0;
Copy link
Owner

Choose a reason for hiding this comment

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

IIUC, if this is set to 0, then you're supposed to pass in a YUV input or something? That's not what's done here, it's RGB(A) always, so I think this should be 1 always.

@@ -0,0 +1,70 @@
#
Copy link
Owner

Choose a reason for hiding this comment

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

I'd like to have the CIs compile & run the new code, can you make them do it? It'd be mainly this:

  • adding a MAGNUM_WITH_WEBPIMAGECONVERTER option to the root CMakeLists.txt, next to the WebPImporter option
  • a corresponding add_subdirectory() in src/MagnumPlugins/CMakeLists.txt, again next to the similar code that does it for WebPImporter
  • and then in all package/ci/*.bat and package/ci/*.sh files, adding a -DMAGNUM_WITH_WEBPIMAGECONVERTER (again next to the WebPImporter flag) that's ON if WebPImporter is ON, and OFF if it's OFF

If you then look at the CI builds in the PR status, they should be building & running tests for the new plugin if everything is alright.

Other places (docs, CMake Find modules, packages...) I can do myself after.

}

auto output = writer.mem;
Containers::Array<char> fileData{reinterpret_cast<char *>(output), writer.max_size};
Copy link
Owner

@mosra mosra Jul 4, 2023

Choose a reason for hiding this comment

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

This means Array will call delete[] on the output. I wonder if that's correct, given the docs say WebPMemoryWriterClear() should be used.

You'll probably need to allocate a new (NoInit'd) Array instead and then Utility::copy() to it.

Also I think it should be writer.size, not writer.max_size? Which sounds a bit similar to the Acropalypse kind of bugs, and would make sense to explicitly test for by having a ground truth output file to compare against. Such test unfortunately wasn't done for the PNG/JPEG converters, but as of 386fb15 it is. Hopefully this change is easy enough to do, the expected output files can be saved with --save-diagnostics when running the test.

auto output = writer.mem;
Containers::Array<char> fileData{reinterpret_cast<char *>(output), writer.max_size};

WebPPictureFree(&pic);
Copy link
Owner

Choose a reason for hiding this comment

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

To avoid leaks in early returns above, use a Containers::ScopeGuard. Same for the writer.

@mosra mosra added this to the 2023.0a milestone Jul 4, 2023
@melikebatihan
Copy link
Contributor Author

Hi @mosra, @Squareys,

I've made all the fixes and changes mentioned in the previous review, hopefully in an ideal way. After fixing some memory leak issues, all the CI checks seem to be successful, except for AppVeyor, which is still pending as I am writing.

@mosra
Copy link
Owner

mosra commented Mar 31, 2024

This is integrated in a2f8acf, together with recognizing WebP images in AnyImageConverter in mosra/magnum@a2f8acf and support in GltfSceneConverter in 730133d.

Because the plugin now additionally exposes various options for controlling whether the output is lossy or lossless, quality / compression level knobs etc., the code and tests are quite different from what was originally in this PR. Nevertheless, you're kept as the original author. Thanks for doing the first 90% of the work!

@mosra mosra closed this Mar 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants