-
Notifications
You must be signed in to change notification settings - Fork 64
[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
Conversation
There was a problem hiding this 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.
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}; |
There was a problem hiding this comment.
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 | ||
}; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 {}; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 , with a change similar to a3c01d5 it'd be now 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)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; |
There was a problem hiding this comment.
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 @@ | |||
# |
There was a problem hiding this comment.
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 rootCMakeLists.txt
, next to the WebPImporter option - a corresponding
add_subdirectory()
insrc/MagnumPlugins/CMakeLists.txt
, again next to the similar code that does it for WebPImporter - and then in all
package/ci/*.bat
andpackage/ci/*.sh
files, adding a-DMAGNUM_WITH_WEBPIMAGECONVERTER
(again next to the WebPImporter flag) that'sON
if WebPImporter isON
, andOFF
if it'sOFF
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}; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
.
…tests to the allowed pixel formats
655b823
to
d2d28dd
Compare
This is integrated in a2f8acf, together with recognizing WebP images in 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! |
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 usedWebPConfig
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 likePixelStorage{}.setSkip({0, 1, 0}).setRowLength(3)
So, I can't be sure if the conversions in tests like
rgb()
,rgba16()
,grayscale()
are correct.