1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5 #include "tools/benchmark/benchmark_codec_webp.h"
6
7 #include <stdint.h>
8 #include <string.h>
9 #include <webp/decode.h>
10 #include <webp/encode.h>
11
12 #include <string>
13 #include <vector>
14
15 #include "lib/extras/time.h"
16 #include "lib/jxl/base/compiler_specific.h"
17 #include "lib/jxl/base/data_parallel.h"
18 #include "lib/jxl/base/padded_bytes.h"
19 #include "lib/jxl/base/span.h"
20 #include "lib/jxl/base/thread_pool_internal.h"
21 #include "lib/jxl/codec_in_out.h"
22 #include "lib/jxl/enc_external_image.h"
23 #include "lib/jxl/image.h"
24 #include "lib/jxl/image_bundle.h"
25 #include "lib/jxl/sanitizers.h"
26
27 namespace jxl {
28
29 // Sets image data from 8-bit sRGB pixel array in bytes.
30 // Amount of input bytes per pixel must be:
31 // (is_gray ? 1 : 3) + (has_alpha ? 1 : 0)
FromSRGB(const size_t xsize,const size_t ysize,const bool is_gray,const bool has_alpha,const bool alpha_is_premultiplied,const bool is_16bit,const JxlEndianness endianness,const uint8_t * pixels,const uint8_t * end,ThreadPool * pool,ImageBundle * ib)32 Status FromSRGB(const size_t xsize, const size_t ysize, const bool is_gray,
33 const bool has_alpha, const bool alpha_is_premultiplied,
34 const bool is_16bit, const JxlEndianness endianness,
35 const uint8_t* pixels, const uint8_t* end, ThreadPool* pool,
36 ImageBundle* ib) {
37 const ColorEncoding& c = ColorEncoding::SRGB(is_gray);
38 const size_t bits_per_sample = (is_16bit ? 2 : 1) * kBitsPerByte;
39 const Span<const uint8_t> span(pixels, end - pixels);
40 return ConvertFromExternal(
41 span, xsize, ysize, c, has_alpha, alpha_is_premultiplied, bits_per_sample,
42 endianness, /*flipped_y=*/false, pool, ib, /*float_in=*/false);
43 }
44
45 struct WebPArgs {
46 // Empty, no WebP-specific args currently.
47 };
48
49 static WebPArgs* const webpargs = new WebPArgs;
50
AddCommandLineOptionsWebPCodec(BenchmarkArgs * args)51 Status AddCommandLineOptionsWebPCodec(BenchmarkArgs* args) { return true; }
52
53 class WebPCodec : public ImageCodec {
54 public:
WebPCodec(const BenchmarkArgs & args)55 explicit WebPCodec(const BenchmarkArgs& args) : ImageCodec(args) {}
56
ParseParam(const std::string & param)57 Status ParseParam(const std::string& param) override {
58 // Ensure that the 'q' parameter is not used up by ImageCodec.
59 if (param[0] == 'q') {
60 if (near_lossless_) {
61 near_lossless_quality_ = ParseIntParam(param, 0, 99);
62 } else {
63 quality_ = ParseIntParam(param, 1, 100);
64 }
65 return true;
66 } else if (ImageCodec::ParseParam(param)) {
67 return true;
68 } else if (param == "ll") {
69 lossless_ = true;
70 JXL_CHECK(!near_lossless_);
71 return true;
72 } else if (param == "nl") {
73 near_lossless_ = true;
74 JXL_CHECK(!lossless_);
75 return true;
76 } else if (param[0] == 'm') {
77 method_ = ParseIntParam(param, 1, 6);
78 return true;
79 }
80 return false;
81 }
82
Compress(const std::string & filename,const CodecInOut * io,ThreadPoolInternal * pool,PaddedBytes * compressed,jpegxl::tools::SpeedStats * speed_stats)83 Status Compress(const std::string& filename, const CodecInOut* io,
84 ThreadPoolInternal* pool, PaddedBytes* compressed,
85 jpegxl::tools::SpeedStats* speed_stats) override {
86 const double start = Now();
87 const ImageBundle& ib = io->Main();
88
89 const ImageF* alpha = ib.HasAlpha() ? &ib.alpha() : nullptr;
90 if (ib.HasAlpha() && ib.metadata()->GetAlphaBits() > 8) {
91 return JXL_FAILURE("WebP alpha must be 8-bit");
92 }
93
94 // TODO: ib.CopyToSRGB to Image3B assert-fails if 16-bit alpha. Fix that,
95 // since it may be bug in external_image (alpha isn't requested here).
96 Image3B srgb;
97 JXL_RETURN_IF_ERROR(ib.CopyToSRGB(Rect(ib), &srgb, pool));
98
99 if (lossless_ || near_lossless_) {
100 // The lossless codec does not support 16-bit channels.
101 // Color models are currently not supported here and the sRGB 8-bit
102 // conversion causes loss due to clipping.
103 if (!ib.IsSRGB() || ib.metadata()->bit_depth.bits_per_sample > 8 ||
104 ib.metadata()->bit_depth.exponent_bits_per_sample > 0) {
105 return JXL_FAILURE("%s: webp:ll/nl requires 8-bit sRGB",
106 filename.c_str());
107 }
108 JXL_RETURN_IF_ERROR(CompressInternal(srgb, alpha, 100, compressed));
109 } else if (bitrate_target_ > 0.0) {
110 int quality_bad = 100;
111 int quality_good = 92;
112 size_t target_size = srgb.xsize() * srgb.ysize() * bitrate_target_ / 8.0;
113 while (quality_good > 0 &&
114 CompressInternal(srgb, alpha, quality_good, compressed) &&
115 compressed->size() > target_size) {
116 quality_bad = quality_good;
117 quality_good -= 8;
118 }
119 if (quality_good <= 0) quality_good = 1;
120 while (quality_good + 1 < quality_bad) {
121 int quality = (quality_bad + quality_good) / 2;
122 if (!CompressInternal(srgb, alpha, quality, compressed)) {
123 break;
124 }
125 if (compressed->size() <= target_size) {
126 quality_good = quality;
127 } else {
128 quality_bad = quality;
129 }
130 }
131 JXL_RETURN_IF_ERROR(
132 CompressInternal(srgb, alpha, quality_good, compressed));
133 } else if (quality_ > 0) {
134 JXL_RETURN_IF_ERROR(CompressInternal(srgb, alpha, quality_, compressed));
135 } else {
136 return false;
137 }
138 const double end = Now();
139 speed_stats->NotifyElapsed(end - start);
140 return true;
141 }
142
Decompress(const std::string & filename,const Span<const uint8_t> compressed,ThreadPoolInternal * pool,CodecInOut * io,jpegxl::tools::SpeedStats * speed_stats)143 Status Decompress(const std::string& filename,
144 const Span<const uint8_t> compressed,
145 ThreadPoolInternal* pool, CodecInOut* io,
146 jpegxl::tools::SpeedStats* speed_stats) override {
147 WebPDecoderConfig config;
148 #ifdef MEMORY_SANITIZER
149 // config is initialized by libwebp, which we are not instrumenting with
150 // msan, therefore we need to initialize it here.
151 memset(&config, 0, sizeof(config));
152 #endif
153 JXL_RETURN_IF_ERROR(WebPInitDecoderConfig(&config) == 1);
154 config.options.use_threads = 0;
155 config.options.dithering_strength = 0;
156 config.options.bypass_filtering = 0;
157 config.options.no_fancy_upsampling = 0;
158 WebPDecBuffer* const buf = &config.output;
159 buf->colorspace = MODE_RGBA;
160 const uint8_t* webp_data = compressed.data();
161 const int webp_size = compressed.size();
162 const double start = Now();
163 if (WebPDecode(webp_data, webp_size, &config) != VP8_STATUS_OK) {
164 return JXL_FAILURE("WebPDecode failed");
165 }
166 const double end = Now();
167 speed_stats->NotifyElapsed(end - start);
168 JXL_CHECK(buf->u.RGBA.stride == buf->width * 4);
169
170 const bool is_gray = false;
171 const bool has_alpha = true;
172 const uint8_t* data_begin = &buf->u.RGBA.rgba[0];
173 const uint8_t* data_end = data_begin + buf->width * buf->height * 4;
174 // The image data is initialized by libwebp, which we are not instrumenting
175 // with msan.
176 msan::UnpoisonMemory(data_begin, data_end - data_begin);
177 if (io->metadata.m.color_encoding.IsGray() != is_gray) {
178 // TODO(lode): either ensure is_gray matches what the color profile says,
179 // or set a correct color profile, e.g.
180 // io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
181 // Return a standard failure because SetFromSRGB triggers a fatal assert
182 // for this instead.
183 return JXL_FAILURE("Color profile is-gray mismatch");
184 }
185 io->metadata.m.SetAlphaBits(8);
186 const Status ok =
187 FromSRGB(buf->width, buf->height, is_gray, has_alpha,
188 /*alpha_is_premultiplied=*/false, /*is_16bit=*/false,
189 JXL_LITTLE_ENDIAN, data_begin, data_end, pool, &io->Main());
190 WebPFreeDecBuffer(buf);
191 JXL_RETURN_IF_ERROR(ok);
192 io->dec_pixels = buf->width * buf->height;
193 return true;
194 }
195
196 private:
WebPStringWrite(const uint8_t * data,size_t data_size,const WebPPicture * const picture)197 static int WebPStringWrite(const uint8_t* data, size_t data_size,
198 const WebPPicture* const picture) {
199 if (data_size) {
200 PaddedBytes* const out = static_cast<PaddedBytes*>(picture->custom_ptr);
201 const size_t pos = out->size();
202 out->resize(pos + data_size);
203 memcpy(out->data() + pos, data, data_size);
204 }
205 return 1;
206 }
207
Import(const Image3B & srgb,WebPPicture * pic)208 static void Import(const Image3B& srgb, WebPPicture* pic) {
209 const size_t xsize = srgb.xsize();
210 const size_t ysize = srgb.ysize();
211 std::vector<uint8_t> rgb(xsize * ysize * 3);
212 for (size_t y = 0; y < ysize; ++y) {
213 const uint8_t* JXL_RESTRICT row0 = srgb.ConstPlaneRow(0, y);
214 const uint8_t* JXL_RESTRICT row1 = srgb.ConstPlaneRow(1, y);
215 const uint8_t* JXL_RESTRICT row2 = srgb.ConstPlaneRow(2, y);
216 uint8_t* const JXL_RESTRICT row_rgb = &rgb[y * xsize * 3];
217 for (size_t x = 0; x < xsize; ++x) {
218 row_rgb[3 * x + 0] = row0[x];
219 row_rgb[3 * x + 1] = row1[x];
220 row_rgb[3 * x + 2] = row2[x];
221 }
222 }
223 WebPPictureImportRGB(pic, &rgb[0], 3 * srgb.xsize());
224 }
225
Import(const Image3B & srgb,const ImageF & alpha,WebPPicture * pic)226 static void Import(const Image3B& srgb, const ImageF& alpha,
227 WebPPicture* pic) {
228 const size_t xsize = srgb.xsize();
229 const size_t ysize = srgb.ysize();
230 std::vector<uint8_t> rgba(xsize * ysize * 4);
231 for (size_t y = 0; y < ysize; ++y) {
232 const uint8_t* JXL_RESTRICT row0 = srgb.ConstPlaneRow(0, y);
233 const uint8_t* JXL_RESTRICT row1 = srgb.ConstPlaneRow(1, y);
234 const uint8_t* JXL_RESTRICT row2 = srgb.ConstPlaneRow(2, y);
235 const float* JXL_RESTRICT rowa = alpha.ConstRow(y);
236 uint8_t* const JXL_RESTRICT row_rgba = &rgba[y * xsize * 4];
237 for (size_t x = 0; x < xsize; ++x) {
238 row_rgba[4 * x + 0] = row0[x];
239 row_rgba[4 * x + 1] = row1[x];
240 row_rgba[4 * x + 2] = row2[x];
241 row_rgba[4 * x + 3] = rowa[x] * 255 + .5f;
242 }
243 }
244 WebPPictureImportRGBA(pic, &rgba[0], 4 * srgb.xsize());
245 }
246
CompressInternal(const Image3B & srgb,const ImageF * alpha,int quality,PaddedBytes * compressed)247 Status CompressInternal(const Image3B& srgb, const ImageF* alpha, int quality,
248 PaddedBytes* compressed) {
249 *compressed = PaddedBytes();
250 WebPConfig config;
251 WebPConfigInit(&config);
252 JXL_ASSERT(!lossless_ || !near_lossless_); // can't have both
253 config.lossless = lossless_;
254 config.quality = quality;
255 config.method = method_;
256 #if WEBP_ENCODER_ABI_VERSION >= 0x020a
257 config.near_lossless = near_lossless_ ? near_lossless_quality_ : 100;
258 #else
259 if (near_lossless_) {
260 JXL_WARNING("Near lossless not supported by this WebP version");
261 }
262 #endif
263 JXL_CHECK(WebPValidateConfig(&config));
264
265 WebPPicture pic;
266 WebPPictureInit(&pic);
267 pic.width = static_cast<int>(srgb.xsize());
268 pic.height = static_cast<int>(srgb.ysize());
269 pic.writer = &WebPStringWrite;
270 if (lossless_ || near_lossless_) pic.use_argb = 1;
271 pic.custom_ptr = compressed;
272
273 if (alpha == nullptr) {
274 Import(srgb, &pic);
275 } else {
276 Import(srgb, *alpha, &pic);
277 }
278
279 // WebP encoding may fail, for example, if the image is more than 16384
280 // pixels high or wide.
281 bool ok = WebPEncode(&config, &pic);
282 WebPPictureFree(&pic);
283 // Compressed image data is initialized by libwebp, which we are not
284 // instrumenting with msan.
285 msan::UnpoisonMemory(compressed->data(), compressed->size());
286 return ok;
287 }
288
289 int quality_ = 90;
290 bool lossless_ = false;
291 bool near_lossless_ = false;
292 bool near_lossless_quality_ = 40; // only used if near_lossless_
293 int method_ = 6; // smallest, some speed cost
294 };
295
CreateNewWebPCodec(const BenchmarkArgs & args)296 ImageCodec* CreateNewWebPCodec(const BenchmarkArgs& args) {
297 return new WebPCodec(args);
298 }
299
300 } // namespace jxl
301