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