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