From: uazo Date: Tue, 24 Oct 2023 10:02:00 +0000 Subject: Add support to jxl Partial revert of https://chromium-review.googlesource.com/c/chromium/src/+/4095497 Enabled by default --- DEPS | 7 + build/linux/unbundle/libjxl.gn | 34 + build/linux/unbundle/replace_gn_files.py | 1 + cc/base/devtools_instrumentation.cc | 3 + cc/base/devtools_instrumentation.h | 2 +- cc/paint/paint_image.h | 2 +- cc/tiles/image_decode_cache.h | 2 + chrome/browser/about_flags.cc | 6 + chrome/browser/flag-metadata.json | 5 + chrome/browser/flag_descriptions.cc | 7 + chrome/browser/flag_descriptions.h | 5 + content/common/content_constants_internal.cc | 11 +- content/common/content_constants_internal.h | 3 +- content/public/browser/frame_accept_header.cc | 13 +- media/BUILD.gn | 1 + media/media_options.gni | 3 + net/base/mime_util.cc | 2 + net/base/mime_util_unittest.cc | 3 + third_party/.gitignore | 1 + third_party/blink/common/features.cc | 3 + .../blink/common/loader/network_utils.cc | 16 +- .../blink/common/mime_util/mime_util.cc | 7 + .../common/mime_util/mime_util_unittest.cc | 6 + third_party/blink/public/common/features.h | 2 + .../devtools_protocol/browser_protocol.pdl | 1 + .../inspector/inspector_emulation_agent.cc | 7 +- .../inspector_emulation_agent_test.cc | 37 + .../generate_image_corpus.py | 1 + .../modules/webcodecs/image_decoder_fuzzer.cc | 5 + third_party/blink/renderer/platform/BUILD.gn | 5 + .../platform/graphics/bitmap_image_metrics.cc | 9 +- .../platform/graphics/bitmap_image_metrics.h | 4 +- .../renderer/platform/image-decoders/BUILD.gn | 9 + .../platform/image-decoders/image_decoder.cc | 23 + .../image-decoders/jxl/jxl_image_decoder.cc | 683 ++++++++++++++++++ .../image-decoders/jxl/jxl_image_decoder.h | 123 ++++ .../jxl/jxl_image_decoder_test.cc | 626 ++++++++++++++++ .../blink/tools/commit_stats/git-dirs.txt | 1 + third_party/blink/web_tests/TestExpectations | 6 + third_party/blink/web_tests/VirtualTestSuites | 9 + ...-set-disabled-image-types-jxl-expected.txt | 13 + .../emulation-set-disabled-image-types-jxl.js | 50 ++ .../resources/image-jxl-fallback-img.html | 1 + .../resources/image-jxl-fallback-picture.html | 4 + .../web_tests/images/jxl/jxl-images.html | 22 + .../web_tests/images/jxl/progressive.html | 6 + .../web_tests/images/resources/jxl/README.md | 79 ++ .../web_tests/virtual/jxl-enabled/README.md | 5 + third_party/libjxl/BUILD.gn | 79 ++ third_party/libjxl/DIR_METADATA | 4 + third_party/libjxl/LICENSE | 27 + third_party/libjxl/OWNERS | 9 + third_party/libjxl/README.chromium | 15 + .../libjxl/gen_headers/jxl/jxl_export.h | 11 + tools/metrics/histograms/enums.xml | 3 +- 55 files changed, 2005 insertions(+), 17 deletions(-) create mode 100644 build/linux/unbundle/libjxl.gn create mode 100644 third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.cc create mode 100644 third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h create mode 100644 third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder_test.cc create mode 100644 third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl-expected.txt create mode 100644 third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js create mode 100644 third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-img.html create mode 100644 third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-picture.html create mode 100644 third_party/blink/web_tests/images/jxl/jxl-images.html create mode 100644 third_party/blink/web_tests/images/jxl/progressive.html create mode 100644 third_party/blink/web_tests/images/resources/jxl/README.md create mode 100644 third_party/blink/web_tests/virtual/jxl-enabled/README.md create mode 100644 third_party/libjxl/BUILD.gn create mode 100644 third_party/libjxl/DIR_METADATA create mode 100644 third_party/libjxl/LICENSE create mode 100644 third_party/libjxl/OWNERS create mode 100644 third_party/libjxl/README.chromium create mode 100644 third_party/libjxl/gen_headers/jxl/jxl_export.h diff --git a/DEPS b/DEPS --- a/DEPS +++ b/DEPS @@ -515,6 +515,10 @@ vars = { # Three lines of non-changing comments so that # the commit queue can handle CLs rolling feed # and whatever else without interference from each other. + 'libjxl_revision': '954b460768c08a147abf47689ad69b0e7beff65e', + # Three lines of non-changing comments so that + # the commit queue can handle CLs rolling feed + # and whatever else without interference from each other. 'highway_revision': '8f20644eca693cfb74aa795b0006b6779c370e7a', # Three lines of non-changing comments so that # the commit queue can handle CLs rolling ffmpeg @@ -1205,6 +1209,9 @@ deps = { 'src/third_party/dawn': Var('dawn_git') + '/dawn.git' + '@' + Var('dawn_revision'), + 'src/third_party/libjxl/src': + Var('chromium_git') + '/external/github.com/libjxl/libjxl.git' + '@' + Var('libjxl_revision'), + 'src/third_party/highway/src': Var('chromium_git') + '/external/github.com/google/highway.git' + '@' + Var('highway_revision'), diff --git a/build/linux/unbundle/libjxl.gn b/build/linux/unbundle/libjxl.gn new file mode 100644 --- /dev/null +++ b/build/linux/unbundle/libjxl.gn @@ -0,0 +1,34 @@ +import("//build/config/linux/pkg_config.gni") +import("//build/shim_headers.gni") + +pkg_config("system_libjxl") { + packages = [ "libjxl" ] +} + +shim_headers("jxl_shim") { + root_path = "src/lib/include" + headers = [ + "jxl/butteraugli.h", + "jxl/butteraugli_cxx.h", + "jxl/codestream_header.h", + "jxl/color_encoding.h", + "jxl/decode.h", + "jxl/decode_cxx.h", + "jxl/encode.h", + "jxl/encode_cxx.h", + "jxl/jxl_export.h", + "jxl/jxl_threads_export.h", + "jxl/memory_manager.h", + "jxl/parallel_runner.h", + "jxl/resizable_parallel_runner.h", + "jxl/resizable_parallel_runner_cxx.h", + "jxl/thread_parallel_runner.h", + "jxl/thread_parallel_runner_cxx.h", + "jxl/types.h", + ] +} + +source_set("libjxl") { + deps = [ ":jxl_shim" ] + public_configs = [ ":system_libjxl" ] +} diff --git a/build/linux/unbundle/replace_gn_files.py b/build/linux/unbundle/replace_gn_files.py --- a/build/linux/unbundle/replace_gn_files.py +++ b/build/linux/unbundle/replace_gn_files.py @@ -53,6 +53,7 @@ REPLACEMENTS = { 'libdrm': 'third_party/libdrm/BUILD.gn', 'libevent': 'third_party/libevent/BUILD.gn', 'libjpeg': 'third_party/libjpeg.gni', + 'libjxl' : 'third_party/libjxl/BUILD.gn', 'libpng': 'third_party/libpng/BUILD.gn', 'libvpx': 'third_party/libvpx/BUILD.gn', 'libwebp': 'third_party/libwebp/BUILD.gn', diff --git a/cc/base/devtools_instrumentation.cc b/cc/base/devtools_instrumentation.cc --- a/cc/base/devtools_instrumentation.cc +++ b/cc/base/devtools_instrumentation.cc @@ -90,6 +90,9 @@ ScopedImageDecodeTask::~ScopedImageDecodeTask() { auto duration = base::TimeTicks::Now() - start_time_; const char* histogram_name = nullptr; switch (image_type_) { + case ImageType::kJxl: + histogram_name = "Renderer4.ImageUploadTaskDurationUs.Jxl"; + break; case ImageType::kAvif: histogram_name = "Renderer4.ImageDecodeTaskDurationUs.Avif"; break; diff --git a/cc/base/devtools_instrumentation.h b/cc/base/devtools_instrumentation.h --- a/cc/base/devtools_instrumentation.h +++ b/cc/base/devtools_instrumentation.h @@ -72,7 +72,7 @@ class CC_BASE_EXPORT ScopedLayerTask { class CC_BASE_EXPORT ScopedImageTask { public: - enum ImageType { kAvif, kBmp, kGif, kIco, kJpeg, kPng, kWebP, kOther }; + enum ImageType { kJxl, kAvif, kBmp, kGif, kIco, kJpeg, kPng, kWebP, kOther }; explicit ScopedImageTask(ImageType image_type) : image_type_(image_type), start_time_(base::TimeTicks::Now()) {} diff --git a/cc/paint/paint_image.h b/cc/paint/paint_image.h --- a/cc/paint/paint_image.h +++ b/cc/paint/paint_image.h @@ -40,7 +40,7 @@ class PaintImageGenerator; class PaintWorkletInput; class TextureBacking; -enum class ImageType { kPNG, kJPEG, kWEBP, kGIF, kICO, kBMP, kAVIF, kInvalid }; +enum class ImageType { kPNG, kJPEG, kWEBP, kGIF, kICO, kBMP, kAVIF, kJXL, kInvalid }; enum class AuxImage : size_t { kDefault = 0, kGainmap = 1 }; static constexpr std::array kAllAuxImages = {AuxImage::kDefault, diff --git a/cc/tiles/image_decode_cache.h b/cc/tiles/image_decode_cache.h --- a/cc/tiles/image_decode_cache.h +++ b/cc/tiles/image_decode_cache.h @@ -84,6 +84,8 @@ class CC_EXPORT ImageDecodeCache { using ScopedImageType = devtools_instrumentation::ScopedImageDecodeTask::ImageType; switch (image_type) { + case ImageType::kJXL: + return ScopedImageType::kJxl; case ImageType::kAVIF: return ScopedImageType::kAvif; case ImageType::kBMP: diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc @@ -8918,6 +8918,12 @@ const FeatureEntry kFeatureEntries[] = { FEATURE_VALUE_TYPE(download::features::kSmartSuggestionForLargeDownloads)}, #endif // BUILDFLAG(IS_ANDROID) +#if BUILDFLAG(ENABLE_JXL_DECODER) + {"enable-jxl", flag_descriptions::kEnableJXLName, + flag_descriptions::kEnableJXLDescription, kOsAll, + FEATURE_VALUE_TYPE(blink::features::kJXL)}, +#endif // BUILDFLAG(ENABLE_JXL_DECODER) + #if BUILDFLAG(IS_ANDROID) {"messages-for-android-ads-blocked", flag_descriptions::kMessagesForAndroidAdsBlockedName, diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json @@ -3095,6 +3095,11 @@ // This flag is used by web developers to test upcoming javascript features. "expiry_milestone": -1 }, + { + "name": "enable-jxl", + "owners": [ "eustas@chromium.org", "firsching", "sboukortt", "veluca" ], + "expiry_milestone": 150 + }, { "name": "enable-keyboard-backlight-toggle", "owners": [ "rtinkoff" ], diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc @@ -7714,6 +7714,13 @@ const char kDcheckIsFatalDescription[] = "rather than crashing. If enabled, DCHECKs will crash the calling process."; #endif // BUILDFLAG(DCHECK_IS_CONFIGURABLE) +#if BUILDFLAG(ENABLE_JXL_DECODER) +const char kEnableJXLName[] = "Enable JXL image format"; +const char kEnableJXLDescription[] = + "Adds image decoding support for the JPEG XL image format. NOTE: JPEG XL " + "format will be removed in Chrome 110 release."; +#endif // BUILDFLAG(ENABLE_JXL_DECODER) + #if BUILDFLAG(ENABLE_CARDBOARD) const char kEnableCardboardName[] = "Enable Cardboard VR WebXR Runtime"; const char kEnableCardboardDescription[] = diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -4459,6 +4459,11 @@ extern const char kDcheckIsFatalName[]; extern const char kDcheckIsFatalDescription[]; #endif // BUILDFLAG(DCHECK_IS_CONFIGURABLE) +#if BUILDFLAG(ENABLE_JXL_DECODER) +extern const char kEnableJXLName[]; +extern const char kEnableJXLDescription[]; +#endif // BUILDFLAG(ENABLE_JXL_DECODER) + #if BUILDFLAG(ENABLE_CARDBOARD) extern const char kEnableCardboardName[]; extern const char kEnableCardboardDescription[]; diff --git a/content/common/content_constants_internal.cc b/content/common/content_constants_internal.cc --- a/content/common/content_constants_internal.cc +++ b/content/common/content_constants_internal.cc @@ -19,13 +19,16 @@ const int kTraceEventGpuProcessSortIndex = -1; const int kTraceEventRendererMainThreadSortIndex = -1; +const char kFrameAcceptHeaderValue_Prefix[] = + "text/html,application/xhtml+xml,application/xml;q=0.9,"; + #if BUILDFLAG(ENABLE_AV1_DECODER) -const char kFrameAcceptHeaderValue[] = - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," +const char kFrameAcceptHeaderValue_Suffix[] = + "image/avif," "image/webp,image/apng,*/*;q=0.8"; #else -const char kFrameAcceptHeaderValue[] = - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp," +const char kFrameAcceptHeaderValue_Suffix[] = + "image/webp," "image/apng,*/*;q=0.8"; #endif diff --git a/content/common/content_constants_internal.h b/content/common/content_constants_internal.h --- a/content/common/content_constants_internal.h +++ b/content/common/content_constants_internal.h @@ -39,7 +39,8 @@ CONTENT_EXPORT extern const int kTraceEventGpuProcessSortIndex; CONTENT_EXPORT extern const int kTraceEventRendererMainThreadSortIndex; // Accept header used for frame requests. -CONTENT_EXPORT extern const char kFrameAcceptHeaderValue[]; +CONTENT_EXPORT extern const char kFrameAcceptHeaderValue_Prefix[]; +CONTENT_EXPORT extern const char kFrameAcceptHeaderValue_Suffix[]; // Constants for attaching message pipes to the mojo invitation used to // initialize child processes. diff --git a/content/public/browser/frame_accept_header.cc b/content/public/browser/frame_accept_header.cc --- a/content/public/browser/frame_accept_header.cc +++ b/content/public/browser/frame_accept_header.cc @@ -7,13 +7,22 @@ #include "content/browser/web_package/signed_exchange_consts.h" #include "content/browser/web_package/signed_exchange_utils.h" #include "content/common/content_constants_internal.h" +#include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" namespace content { std::string FrameAcceptHeaderValue(bool allow_sxg_responses, BrowserContext* browser_context) { - std::string header_value = kFrameAcceptHeaderValue; - + std::string header_value = kFrameAcceptHeaderValue_Prefix; +#if BUILDFLAG(ENABLE_JXL_DECODER) + // In case the buildflag and the runtime flag are enables, we insert + // "image/jxl," into the header value at the correct place. + if (base::FeatureList::IsEnabled(blink::features::kJXL)) { + header_value.append("image/jxl,"); + } +#endif // BUILDFLAG(ENABLE_JXL_DECODER) + header_value.append(kFrameAcceptHeaderValue_Suffix); if (allow_sxg_responses && content::signed_exchange_utils::IsSignedExchangeHandlingEnabled( browser_context)) { diff --git a/media/BUILD.gn b/media/BUILD.gn --- a/media/BUILD.gn +++ b/media/BUILD.gn @@ -37,6 +37,7 @@ buildflag_header("media_buildflags") { "ENABLE_CAST_AUDIO_RENDERER=$enable_cast_audio_renderer", "ENABLE_DAV1D_DECODER=$enable_dav1d_decoder", "ENABLE_AV1_DECODER=$enable_av1_decoder", + "ENABLE_JXL_DECODER=$enable_jxl_decoder", "ENABLE_PLATFORM_DOLBY_VISION=$enable_platform_dolby_vision", "ENABLE_PLATFORM_ENCRYPTED_DOLBY_VISION=$enable_platform_encrypted_dolby_vision", "ENABLE_FFMPEG=$media_use_ffmpeg", diff --git a/media/media_options.gni b/media/media_options.gni --- a/media/media_options.gni +++ b/media/media_options.gni @@ -125,6 +125,9 @@ declare_args() { # `enable_libaom` should likely also be overriddent to false. enable_av1_decoder = enable_dav1d_decoder + # If true, adds support for JPEG XL image decoding. + enable_jxl_decoder = is_android || is_win + # Enable HEVC/H265 demuxing. Actual decoding must be provided by the # platform. Always enable this for Lacros, it determines support at runtime. # TODO(crbug.com/1336055): Revisit the default value for this setting as it diff --git a/net/base/mime_util.cc b/net/base/mime_util.cc --- a/net/base/mime_util.cc +++ b/net/base/mime_util.cc @@ -163,6 +163,7 @@ static const MimeInfo kPrimaryMappings[] = { {"image/avif", "avif"}, {"image/gif", "gif"}, {"image/jpeg", "jpeg,jpg"}, + {"image/jxl", "jxl"}, {"image/png", "png"}, {"image/apng", "png,apng"}, {"image/svg+xml", "svg,svgz"}, @@ -666,6 +667,7 @@ static const char* const kStandardImageTypes[] = {"image/avif", "image/gif", "image/ief", "image/jpeg", + "image/jxl", "image/webp", "image/pict", "image/pipeg", diff --git a/net/base/mime_util_unittest.cc b/net/base/mime_util_unittest.cc --- a/net/base/mime_util_unittest.cc +++ b/net/base/mime_util_unittest.cc @@ -39,6 +39,7 @@ TEST(MimeUtilTest, GetWellKnownMimeTypeFromExtension) { {FILE_PATH_LITERAL("webm"), "video/webm"}, {FILE_PATH_LITERAL("weba"), "audio/webm"}, {FILE_PATH_LITERAL("avif"), "image/avif"}, + {FILE_PATH_LITERAL("jxl"), "image/jxl"}, {FILE_PATH_LITERAL("epub"), "application/epub+zip"}, {FILE_PATH_LITERAL("apk"), "application/vnd.android.package-archive"}, {FILE_PATH_LITERAL("cer"), "application/x-x509-ca-cert"}, @@ -80,6 +81,7 @@ TEST(MimeUtilTest, ExtensionTest) { {FILE_PATH_LITERAL("webm"), {"video/webm"}}, {FILE_PATH_LITERAL("weba"), {"audio/webm"}}, {FILE_PATH_LITERAL("avif"), {"image/avif"}}, + {FILE_PATH_LITERAL("jxl"), {"image/jxl"}}, #if BUILDFLAG(IS_CHROMEOS_ASH) // These are test cases for testing platform mime types on ChromeOS. {FILE_PATH_LITERAL("epub"), {"application/epub+zip"}}, @@ -497,6 +499,7 @@ TEST(MimeUtilTest, TestGetExtensionsForMimeType) { {"MeSsAge/*", 1, "eml"}, {"message/", 0, nullptr, true}, {"image/avif", 1, "avif"}, + {"image/jxl", 1, "jxl"}, {"image/bmp", 1, "bmp"}, {"video/*", 6, "mp4"}, {"video/*", 6, "mpeg"}, diff --git a/third_party/.gitignore b/third_party/.gitignore --- a/third_party/.gitignore +++ b/third_party/.gitignore @@ -97,6 +97,7 @@ /libgifcodec /libjingle/source /libupnp +#/libjxl/src /llvm /llvm-allocated-type /llvm-bootstrap diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc --- a/third_party/blink/common/features.cc +++ b/third_party/blink/common/features.cc @@ -422,6 +422,9 @@ BASE_FEATURE(kCORSErrorsIssueOnly, "CORSErrorsIssueOnly", base::FEATURE_DISABLED_BY_DEFAULT); +// Enables the JPEG XL Image File Format (JXL). +BASE_FEATURE(kJXL, "JXL", base::FEATURE_ENABLED_BY_DEFAULT); + // When enabled, code cache is produced asynchronously from the script execution // (https://crbug.com/1260908). BASE_FEATURE(kCacheCodeOnIdle, diff --git a/third_party/blink/common/loader/network_utils.cc b/third_party/blink/common/loader/network_utils.cc --- a/third_party/blink/common/loader/network_utils.cc +++ b/third_party/blink/common/loader/network_utils.cc @@ -9,6 +9,7 @@ #include "services/network/public/cpp/constants.h" #include "services/network/public/mojom/fetch_api.mojom.h" #include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" namespace blink { namespace network_utils { @@ -33,7 +34,20 @@ bool AlwaysAccessNetwork( } const char* ImageAcceptHeader() { -#if BUILDFLAG(ENABLE_AV1_DECODER) +#if BUILDFLAG(ENABLE_JXL_DECODER) && BUILDFLAG(ENABLE_AV1_DECODER) + if (base::FeatureList::IsEnabled(blink::features::kJXL)) { + return "image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/" + "*;q=0.8"; + } else { + return "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + } +#elif BUILDFLAG(ENABLE_JXL_DECODER) + if (base::FeatureList::IsEnabled(blink::features::kJXL)) { + return "image/jxl,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + } else { + return "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + } +#elif BUILDFLAG(ENABLE_AV1_DECODER) return "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; #else return "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; diff --git a/third_party/blink/common/mime_util/mime_util.cc b/third_party/blink/common/mime_util/mime_util.cc --- a/third_party/blink/common/mime_util/mime_util.cc +++ b/third_party/blink/common/mime_util/mime_util.cc @@ -14,6 +14,7 @@ #include "media/media_buildflags.h" #include "net/base/mime_util.h" #include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" #if !BUILDFLAG(IS_IOS) // iOS doesn't use and must not depend on //media @@ -144,6 +145,12 @@ MimeUtil::MimeUtil() { non_image_types_.insert(type); for (const char* type : kSupportedImageTypes) image_types_.insert(type); +#if BUILDFLAG(ENABLE_JXL_DECODER) + // TODO(firsching): Add "image/jxl" to the kSupportedImageTypes array when the + // JXL feature is shipped. + if (base::FeatureList::IsEnabled(features::kJXL)) + image_types_.insert("image/jxl"); +#endif for (const char* type : kUnsupportedTextTypes) unsupported_text_types_.insert(type); for (const char* type : kSupportedJavascriptTypes) { diff --git a/third_party/blink/common/mime_util/mime_util_unittest.cc b/third_party/blink/common/mime_util/mime_util_unittest.cc --- a/third_party/blink/common/mime_util/mime_util_unittest.cc +++ b/third_party/blink/common/mime_util/mime_util_unittest.cc @@ -9,6 +9,7 @@ #include "net/base/mime_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" namespace blink { @@ -18,7 +19,12 @@ TEST(MimeUtilTest, LookupTypes) { EXPECT_TRUE(IsSupportedImageMimeType("image/jpeg")); EXPECT_TRUE(IsSupportedImageMimeType("Image/JPEG")); +#if BUILDFLAG(ENABLE_JXL_DECODER) + EXPECT_EQ(IsSupportedImageMimeType("image/jxl"), + base::FeatureList::IsEnabled(features::kJXL)); +#else EXPECT_FALSE(IsSupportedImageMimeType("image/jxl")); +#endif EXPECT_EQ(IsSupportedImageMimeType("image/avif"), BUILDFLAG(ENABLE_AV1_DECODER)); EXPECT_FALSE(IsSupportedImageMimeType("image/lolcat")); diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h --- a/third_party/blink/public/common/features.h +++ b/third_party/blink/public/common/features.h @@ -145,6 +145,8 @@ BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBackgroundResourceFetch); BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kShowAlwaysContextMenuOnLinks); +BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kJXL); + // Used to configure a per-origin allowlist of performance.mark events that are // permitted to be included in slow reports traces. See crbug.com/1181774. BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBackgroundTracingPerformanceMark); diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl --- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl +++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl @@ -4133,6 +4133,7 @@ domain Emulation experimental type DisabledImageType extends string enum avif + jxl webp experimental command setDisabledImageTypes diff --git a/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc b/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc --- a/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc +++ b/third_party/blink/renderer/core/inspector/inspector_emulation_agent.cc @@ -495,10 +495,10 @@ AtomicString InspectorEmulationAgent::OverrideAcceptImageHeader( String header(network_utils::ImageAcceptHeader()); for (String type : *disabled_image_types) { // The header string is expected to be like - // `image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8` + // `image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8` // and is expected to be always ending with `image/*,*/*;q=xxx`, therefore, - // to remove a type we replace `image/x,` with empty string. Only webp and - // avif types can be disabled. + // to remove a type we replace `image/x,` with empty string. Only webp, avif + // and jxl types can be disabled. header.Replace(String(type + ","), ""); } return AtomicString(header); @@ -842,6 +842,7 @@ protocol::Response InspectorEmulationAgent::setDisabledImageTypes( namespace DisabledImageTypeEnum = protocol::Emulation::DisabledImageTypeEnum; for (protocol::Emulation::DisabledImageType type : *disabled_types) { if (DisabledImageTypeEnum::Avif == type || + DisabledImageTypeEnum::Jxl == type || DisabledImageTypeEnum::Webp == type) { disabled_image_types_.Set(prefix + type, true); continue; diff --git a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc --- a/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc +++ b/third_party/blink/renderer/core/inspector/inspector_emulation_agent_test.cc @@ -7,6 +7,7 @@ #include "media/media_buildflags.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" namespace blink { @@ -22,6 +23,8 @@ TEST_F(InspectorEmulationAgentTest, ModifiesAcceptHeader) { "image/apng,image/svg+xml,image/*,*/*;q=0.8"; String expected_no_avif = "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + String expected_no_jxl = + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; #else String expected_default = "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; @@ -30,8 +33,38 @@ TEST_F(InspectorEmulationAgentTest, ModifiesAcceptHeader) { "image/apng,image/svg+xml,image/*,*/*;q=0.8"; String expected_no_avif = "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + String expected_no_jxl = + "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; #endif +#if BUILDFLAG(ENABLE_JXL_DECODER) + bool jxl_enabled = base::FeatureList::IsEnabled(features::kJXL); + if (jxl_enabled) { +#if BUILDFLAG(ENABLE_AV1_DECODER) + expected_default = + "image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/" + "*;q=0.8"; + expected_no_webp = + "image/jxl,image/avif,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_webp_and_avif = + "image/jxl,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_avif = + "image/jxl,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_jxl = + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; +#else // BUILDFLAG(ENABLE_AV1_DECODER) + expected_default = + "image/jxl,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_webp = "image/jxl,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_webp_and_avif = + "image/jxl,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_avif = + "image/jxl,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; + expected_no_jxl = "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"; +#endif // BUILDFLAG(ENABLE_AV1_DECODER) + } +#endif // BUILDFLAG(ENABLE_JXL_DECODER) + HashSet disabled_types; EXPECT_EQ(InspectorEmulationAgent::OverrideAcceptImageHeader(&disabled_types), expected_default); @@ -44,6 +77,10 @@ TEST_F(InspectorEmulationAgentTest, ModifiesAcceptHeader) { disabled_types.erase("image/webp"); EXPECT_EQ(InspectorEmulationAgent::OverrideAcceptImageHeader(&disabled_types), expected_no_avif); + disabled_types.erase("image/avif"); + disabled_types.insert("image/jxl"); + EXPECT_EQ(InspectorEmulationAgent::OverrideAcceptImageHeader(&disabled_types), + expected_no_jxl); } } // namespace blink diff --git a/third_party/blink/renderer/modules/webcodecs/fuzzer_seed_corpus/generate_image_corpus.py b/third_party/blink/renderer/modules/webcodecs/fuzzer_seed_corpus/generate_image_corpus.py --- a/third_party/blink/renderer/modules/webcodecs/fuzzer_seed_corpus/generate_image_corpus.py +++ b/third_party/blink/renderer/modules/webcodecs/fuzzer_seed_corpus/generate_image_corpus.py @@ -32,6 +32,7 @@ EXTENSIONS_MAP = { "ico": "image/x-icon", "bmp": "image/bmp", "jpg": "image/jpeg", + "jxl": "image/jxl", "gif": "image/gif", "cur": "image/x-icon", "webp": "image/webp", diff --git a/third_party/blink/renderer/modules/webcodecs/image_decoder_fuzzer.cc b/third_party/blink/renderer/modules/webcodecs/image_decoder_fuzzer.cc --- a/third_party/blink/renderer/modules/webcodecs/image_decoder_fuzzer.cc +++ b/third_party/blink/renderer/modules/webcodecs/image_decoder_fuzzer.cc @@ -3,8 +3,10 @@ // found in the LICENSE file. #include "base/run_loop.h" +#include "base/test/scoped_feature_list.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/libfuzzer/proto/lpm_interface.h" +#include "third_party/blink/public/common/features.h" #include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" #include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybufferallowshared_arraybufferviewallowshared_readablestream.h" @@ -89,6 +91,9 @@ DEFINE_BINARY_PROTO_FUZZER( auto scoped_gc = MakeScopedGarbageCollectionRequest(test_support.GetIsolate()); + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(features::kJXL); + // // NOTE: GC objects that need to survive iterations of the loop below // must be Persistent<>! diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn --- a/third_party/blink/renderer/platform/BUILD.gn +++ b/third_party/blink/renderer/platform/BUILD.gn @@ -2232,6 +2232,10 @@ source_set("blink_platform_unittests_sources") { sources += [ "text/locale_icu_test.cc" ] } + if (enable_jxl_decoder) { + sources += [ "image-decoders/jxl/jxl_image_decoder_test.cc" ] + } + sources += [ "testing/run_all_tests.cc" ] configs += [ @@ -2275,6 +2279,7 @@ source_set("blink_platform_unittests_sources") { "//third_party/blink/renderer/platform/scheduler:unit_tests", "//third_party/blink/renderer/platform/wtf", "//third_party/libavif:libavif", + "//third_party/libjxl:libjxl", "//third_party/libyuv", "//third_party/webrtc/api/task_queue:task_queue_test", "//third_party/webrtc_overrides:metronome_like_task_queue_test", diff --git a/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.cc b/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.cc --- a/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.cc +++ b/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.cc @@ -6,7 +6,6 @@ #include "base/metrics/histogram_base.h" #include "base/metrics/histogram_macros.h" -#include "base/notreached.h" #include "base/numerics/safe_conversions.h" #include "media/media_buildflags.h" #include "third_party/blink/public/common/buildflags.h" @@ -37,6 +36,10 @@ BitmapImageMetrics::StringToDecodedImageType(const String& type) { #if BUILDFLAG(ENABLE_AV1_DECODER) if (type == "avif") return BitmapImageMetrics::DecodedImageType::kAVIF; +#endif +#if BUILDFLAG(ENABLE_JXL_DECODER) + if (type == "jxl") + return BitmapImageMetrics::DecodedImageType::kJXL; #endif return BitmapImageMetrics::DecodedImageType::kUnknown; } @@ -55,6 +58,10 @@ void BitmapImageMetrics::CountDecodedImageType(const String& type, } else if (type == "avif") { use_counter->CountUse(WebFeature::kAVIFImage); #endif +// #if BUILDFLAG(ENABLE_JXL_DECODER) +// } else if (type == "jxl") { +// use_counter->CountUse(WebFeature::kJXLImage); +// #endif } } } diff --git a/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.h b/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.h --- a/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.h +++ b/third_party/blink/renderer/platform/graphics/bitmap_image_metrics.h @@ -29,8 +29,8 @@ class PLATFORM_EXPORT BitmapImageMetrics { kICO = 5, kBMP = 6, kAVIF = 7, - kREMOVED_JXL = 8, - kMaxValue = kREMOVED_JXL, + kJXL = 8, + kMaxValue = kJXL, }; // Categories for the JPEG color space histogram. Synced with 'JpegColorSpace' diff --git a/third_party/blink/renderer/platform/image-decoders/BUILD.gn b/third_party/blink/renderer/platform/image-decoders/BUILD.gn --- a/third_party/blink/renderer/platform/image-decoders/BUILD.gn +++ b/third_party/blink/renderer/platform/image-decoders/BUILD.gn @@ -70,6 +70,15 @@ component("image_decoders") { "//third_party/libyuv", ] + if (enable_jxl_decoder) { + sources += [ + "jxl/jxl_image_decoder.cc", + "jxl/jxl_image_decoder.h", + ] + + deps += [ "//third_party/libjxl:libjxl" ] + } + if (enable_av1_decoder) { sources += [ "avif/avif_image_decoder.cc", diff --git a/third_party/blink/renderer/platform/image-decoders/image_decoder.cc b/third_party/blink/renderer/platform/image-decoders/image_decoder.cc --- a/third_party/blink/renderer/platform/image-decoders/image_decoder.cc +++ b/third_party/blink/renderer/platform/image-decoders/image_decoder.cc @@ -23,6 +23,7 @@ #include #include "base/logging.h" +#include "base/feature_list.h" #include "base/numerics/safe_conversions.h" #include "base/sys_byteorder.h" #include "base/trace_event/trace_event.h" @@ -30,6 +31,7 @@ #include "media/media_buildflags.h" #include "skia/ext/cicp.h" #include "third_party/blink/public/common/buildflags.h" +#include "third_party/blink/public/common/features.h" #include "third_party/blink/public/platform/platform.h" #include "third_party/blink/renderer/platform/image-decoders/bmp/bmp_image_decoder.h" #include "third_party/blink/renderer/platform/image-decoders/exif_reader.h" @@ -47,6 +49,9 @@ #include "third_party/blink/renderer/platform/image-decoders/avif/avif_image_decoder.h" #endif +#if BUILDFLAG(ENABLE_JXL_DECODER) +#include "third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h" +#endif namespace blink { namespace { @@ -74,6 +79,11 @@ cc::ImageType FileExtensionToImageType(String image_extension) { if (image_extension == "avif") { return cc::ImageType::kAVIF; } +#endif +#if BUILDFLAG(ENABLE_JXL_DECODER) + if (image_extension == "jxl") { + return cc::ImageType::kJXL; + } #endif return cc::ImageType::kInvalid; } @@ -191,6 +201,12 @@ String SniffMimeTypeInternal(scoped_refptr reader) { return "image/avif"; } #endif +#if BUILDFLAG(ENABLE_JXL_DECODER) + if (base::FeatureList::IsEnabled(blink::features::kJXL) && + JXLImageDecoder::MatchesJXLSignature(fast_reader)) { + return "image/jxl"; + } +#endif return String(); } @@ -296,6 +312,13 @@ std::unique_ptr ImageDecoder::CreateByMimeType( decoder = std::make_unique( alpha_option, high_bit_depth_decoding_option, color_behavior, max_decoded_bytes, animation_option); +#endif +#if BUILDFLAG(ENABLE_JXL_DECODER) + } else if (base::FeatureList::IsEnabled(blink::features::kJXL) && + mime_type == "image/jxl") { + decoder = std::make_unique( + alpha_option, high_bit_depth_decoding_option, color_behavior, + max_decoded_bytes); #endif } diff --git a/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.cc b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.cc new file mode 100644 --- /dev/null +++ b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.cc @@ -0,0 +1,683 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h" +#include "base/logging.h" +#include "base/time/time.h" +#include "third_party/blink/renderer/platform/image-decoders/fast_shared_buffer_reader.h" +#include "third_party/skia/include/core/SkColorSpace.h" + +namespace blink { + +namespace { +// Returns transfer function which approximates HLG with linear range 0..1, +// while skcms_TransferFunction_makeHLGish uses linear range 0..12. +void MakeTransferFunctionHLG01(skcms_TransferFunction* tf) { + skcms_TransferFunction_makeScaledHLGish( + tf, 1 / 12.0f, 2.0f, 2.0f, 1 / 0.17883277f, 0.28466892f, 0.55991073f); +} + +// The input profile must outlive the output one as they will share their +// buffers. +skcms_ICCProfile ReplaceTransferFunction(skcms_ICCProfile profile, + const skcms_TransferFunction& tf) { + // Override the transfer function with a known parametric curve. + profile.has_trc = true; + for (int c = 0; c < 3; c++) { + profile.trc[c].table_entries = 0; + profile.trc[c].parametric = tf; + } + return profile; +} + +// Computes whether the transfer function from the ColorProfile, that was +// created from a parsed ICC profile, approximately matches the given parametric +// transfer function. +bool ApproximatelyMatchesTF(const ColorProfile& profile, + const skcms_TransferFunction& tf) { + skcms_ICCProfile parsed_copy = + ReplaceTransferFunction(*profile.GetProfile(), tf); + return skcms_ApproximatelyEqualProfiles(profile.GetProfile(), &parsed_copy); +} + +std::unique_ptr NewColorProfileWithSameBuffer( + const ColorProfile& buffer_donor, + skcms_ICCProfile new_profile) { + // The input ColorProfile owns the buffer memory, make a new copy for + // the newly created one and pass the ownership of the new copy to the new + // color profile. + std::unique_ptr owned_buffer( + new uint8_t[buffer_donor.GetProfile()->size]); + memcpy(owned_buffer.get(), buffer_donor.GetProfile()->buffer, + buffer_donor.GetProfile()->size); + new_profile.buffer = owned_buffer.get(); + return std::make_unique(new_profile, std::move(owned_buffer)); +} +} // namespace + +JXLImageDecoder::JXLImageDecoder( + AlphaOption alpha_option, + HighBitDepthDecodingOption high_bit_depth_decoding_option, + const ColorBehavior& color_behavior, + wtf_size_t max_decoded_bytes) + : ImageDecoder(alpha_option, + high_bit_depth_decoding_option, + color_behavior, + max_decoded_bytes) { + info_.have_animation = false; +} + +// Use the provisional Mime type "image/jxl" for JPEG XL images. See +// https://www.iana.org/assignments/provisional-standard-media-types/provisional-standard-media-types.xhtml. +const AtomicString& JXLImageDecoder::MimeType() const { + DEFINE_STATIC_LOCAL(const AtomicString, jxl_mime_type, ("image/jxl")); + return jxl_mime_type; +} + +bool JXLImageDecoder::ReadBytes(size_t remaining, + wtf_size_t* offset, + WTF::Vector* segment, + FastSharedBufferReader* reader, + const uint8_t** jxl_data, + size_t* jxl_size) { + *offset -= remaining; + if (*offset + remaining >= reader->size()) { + segment->clear(); + if (IsAllDataReceived()) { + DVLOG(1) << "need more input but all data received"; + SetFailed(); + return false; + } + // Return because we need more input from the reader, to continue + // decoding in the next call. + return false; + } + const char* buffer = nullptr; + size_t read = reader->GetSomeData(buffer, *offset); + + if (read > remaining) { + // Sufficient data present in the segment from the + // FastSharedBufferReader, no need to copy to segment_. + *jxl_data = reinterpret_cast(buffer); + *jxl_size = read; + *offset += read; + segment->clear(); + } else { + if (segment->size() == remaining) { + // Keep reading from the end of the segment_ we already are + // appending to. The above read is ignored, and start reading after the + // end of the data we already have. + *offset += remaining; + read = 0; + } else { + // segment_->size() could be greater than or smaller than remaining. + // Typically, it'll be smaller than. If it is greater than, then we could + // do something similar as in the segment->size() == remaining case but + // remove the non-remaining bytes from the beginning of the segment_ + // vector. This would avoid re-reading, however the case where + // segment->size() > remaining is rare since normally if the JXL decoder + // returns a positive value for remaining, it will be consistent, making + // the sizes match exactly, so this more complex case is not implemented. + // Clear the segment, the bytes from the GetSomeData above will be + // appended and then we continue reading from the position after the + // above GetSomeData read. + segment->clear(); + } + + for (;;) { + if (read) { + *offset += read; + segment->Append(buffer, base::checked_cast(read)); + } + if (segment->size() > remaining) { + *jxl_data = segment->data(); + *jxl_size = segment->size(); + // Have enough data, break and continue JXL decoding, rather than + // copy more input than needed into segment_. + break; + } + read = reader->GetSomeData(buffer, *offset); + if (read == 0) { + // We tested above that *offset + remaining >= reader.size() so + // should be able to read all data. + DVLOG(1) << "couldn't read all available data"; + SetFailed(); + return false; + } + } + } + return true; +} + +void JXLImageDecoder::DecodeImpl(wtf_size_t index, bool only_size) { + if (Failed()) + return; + + if (IsDecodedSizeAvailable() && only_size) { + // Also SetEmbeddedProfile is done already if the size was set. + return; + } + + DCHECK_LE(num_decoded_frames_, frame_buffer_cache_.size()); + if (num_decoded_frames_ > index && + frame_buffer_cache_[index].GetStatus() == ImageFrame::kFrameComplete) { + // Frame already complete + return; + } + if ((index < num_decoded_frames_) && dec_ && + frame_buffer_cache_[index].GetStatus() != ImageFrame::kFramePartial) { + // An animation frame that already has been decoded, but does not have + // status ImageFrame::kFrameComplete, was requested. + // This can mean two things: + // (1) an earlier animation frame was purged but is to be re-decoded now. + // Rewind the decoder and skip to the requested frame. + // (2) During progressive decoding the frame has the status + // ImageFrame::kFramePartial. + JxlDecoderRewind(dec_.get()); + offset_ = 0; + // No longer subscribe to JXL_DEC_BASIC_INFO or JXL_DEC_COLOR_ENCODING. + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents( + dec_.get(), JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION)) { + SetFailed(); + return; + } + JxlDecoderSkipFrames(dec_.get(), index); + num_decoded_frames_ = index; + } + + if (!dec_) { + dec_ = JxlDecoderMake(nullptr); + // Subscribe to color encoding event even when only getting size, because + // SetSize must be called after SetEmbeddedColorProfile + const int events = JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION; + + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec_.get(), events)) { + SetFailed(); + return; + } + if (JXL_DEC_SUCCESS != + JxlDecoderSetProgressiveDetail(dec_.get(), JxlProgressiveDetail::kDC)) { + SetFailed(); + return; + } + } else { + offset_ -= JxlDecoderReleaseInput(dec_.get()); + } + + FastSharedBufferReader reader(data_.get()); + + const JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + + const bool size_available = IsDecodedSizeAvailable(); + + if (have_color_info_) { + xform_ = ColorTransform(); + } + + // The JXL API guarantees that we eventually get JXL_DEC_ERROR, + // JXL_DEC_SUCCESS or JXL_DEC_NEED_MORE_INPUT, and we exit the loop below in + // each case. + for (;;) { + if (only_size && have_color_info_) + return; + JxlDecoderStatus status = JxlDecoderProcessInput(dec_.get()); + switch (status) { + case JXL_DEC_ERROR: { + DVLOG(1) << "Decoder error " << status; + SetFailed(); + return; + } + case JXL_DEC_NEED_MORE_INPUT: { + // The decoder returns how many bytes it has not yet processed, and + // must be included in the next JxlDecoderSetInput call. + const size_t remaining = JxlDecoderReleaseInput(dec_.get()); + const uint8_t* jxl_data = nullptr; + size_t jxl_size = 0; + if (!ReadBytes(remaining, &offset_, &segment_, &reader, &jxl_data, + &jxl_size)) { + if (IsAllDataReceived()) { + // Happens only if a partial image file was transferred, otherwise + // status will be JXL_DEC_FULL_IMAGE or JXL_DEC_SUCCESS. In + // this case we flush one more time in order to get the progressive + // image plus everything known so far. The progressive image was not + // flushed when status was JXL_DEC_FRAME_PROGRESSION because all + // data seemed to have been received (not knowing then that it was + // only a partial file). + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec_.get())) { + DVLOG(1) << "JxlDecoderSetImageOutCallback failed"; + SetFailed(); + return; + } + ImageFrame& frame = frame_buffer_cache_[num_decoded_frames_ - 1]; + frame.SetPixelsChanged(true); + frame.SetStatus(ImageFrame::kFramePartial); + } + return; + } + + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(dec_.get(), jxl_data, jxl_size)) { + DVLOG(1) << "JxlDecoderSetInput failed"; + SetFailed(); + return; + } + break; + } + case JXL_DEC_BASIC_INFO: { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec_.get(), &info_)) { + DVLOG(1) << "JxlDecoderGetBasicInfo failed"; + SetFailed(); + return; + } + if (!size_available && !SetSize(info_.xsize, info_.ysize)) { + return; + } + break; + } + case JXL_DEC_COLOR_ENCODING: { + if (IgnoresColorSpace()) { + have_color_info_ = true; + continue; + } + + // If the decoder was used before with only_size == true, the color + // encoding is already decoded as well, and SetEmbeddedColorProfile + // should not be called a second time anymore. + if (size_available) { + continue; + } + + // Detect whether the JXL image is intended to be an HDR image: when it + // uses more than 8 bits per pixel, or when it has explicitly marked + // PQ or HLG color profile. + if (info_.bits_per_sample > 8) { + is_hdr_ = true; + } + JxlColorEncoding color_encoding; + if (JXL_DEC_SUCCESS == JxlDecoderGetColorAsEncodedProfile( + dec_.get(), &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + &color_encoding)) { + if (color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_PQ || + color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_HLG) { + is_hdr_ = true; + } + } + + std::unique_ptr profile; + + if (is_hdr_ && + high_bit_depth_decoding_option_ == kHighBitDepthToHalfFloat) { + decode_to_half_float_ = true; + } + + bool have_data_profile = false; + if (JXL_DEC_SUCCESS == + JxlDecoderGetColorAsEncodedProfile(dec_.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, + &color_encoding)) { + bool known_transfer_function = true; + bool known_gamut = true; + gfx::ColorSpace::PrimaryID gamut; + gfx::ColorSpace::TransferID transfer; + if (color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_PQ) { + transfer = gfx::ColorSpace::TransferID::PQ; + } else if (color_encoding.transfer_function == + JXL_TRANSFER_FUNCTION_HLG) { + transfer = gfx::ColorSpace::TransferID::HLG; + } else if (color_encoding.transfer_function == + JXL_TRANSFER_FUNCTION_LINEAR) { + transfer = gfx::ColorSpace::TransferID::LINEAR; + } else if (color_encoding.transfer_function == + JXL_TRANSFER_FUNCTION_SRGB) { + transfer = gfx::ColorSpace::TransferID::SRGB; + } else { + known_transfer_function = false; + } + + if (color_encoding.white_point == JXL_WHITE_POINT_D65 && + color_encoding.primaries == JXL_PRIMARIES_2100) { + gamut = gfx::ColorSpace::PrimaryID::BT2020; + } else if (color_encoding.white_point == JXL_WHITE_POINT_D65 && + color_encoding.primaries == JXL_PRIMARIES_SRGB) { + gamut = gfx::ColorSpace::PrimaryID::BT709; + } else if (color_encoding.white_point == JXL_WHITE_POINT_D65 && + color_encoding.primaries == JXL_PRIMARIES_P3) { + gamut = gfx::ColorSpace::PrimaryID::P3; + } else { + known_gamut = false; + } + + have_data_profile = known_transfer_function && known_gamut; + + if (have_data_profile) { + skcms_ICCProfile dataProfile; + gfx::ColorSpace(gamut, transfer) + .ToSkColorSpace() + ->toProfile(&dataProfile); + profile = std::make_unique(dataProfile); + } + } + + // Did not handle exact enum values, get as ICC profile instead. + if (!have_data_profile) { + size_t icc_size; + bool got_size = + JXL_DEC_SUCCESS == JxlDecoderGetICCProfileSize( + dec_.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, &icc_size); + std::vector icc_profile(icc_size); + if (got_size && + JXL_DEC_SUCCESS == JxlDecoderGetColorAsICCProfile( + dec_.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile.data(), icc_profile.size())) { + profile = + ColorProfile::Create(icc_profile.data(), icc_profile.size()); + have_data_profile = true; + + // Detect whether the ICC profile approximately equals PQ or HLG, + // and set the profile to one that indicates this transfer function + // more clearly than a raw ICC profile does, so Chrome considers + // the profile as HDR. + skcms_TransferFunction tf_pq; + skcms_TransferFunction tf_hlg01; + skcms_TransferFunction tf_hlg12; + skcms_TransferFunction_makePQ(&tf_pq); + MakeTransferFunctionHLG01(&tf_hlg01); + skcms_TransferFunction_makeHLG(&tf_hlg12); + + if (ApproximatelyMatchesTF(*profile, tf_pq)) { + is_hdr_ = true; + auto hdr10 = gfx::ColorSpace::CreateHDR10().ToSkColorSpace(); + skcms_TransferFunction pq; + hdr10->transferFn(&pq); + profile = NewColorProfileWithSameBuffer( + *profile, + ReplaceTransferFunction(*profile->GetProfile(), pq)); + } else { + for (skcms_TransferFunction tf : {tf_hlg01, tf_hlg12}) { + if (ApproximatelyMatchesTF(*profile, tf)) { + is_hdr_ = true; + auto hlg_colorspace = + gfx::ColorSpace::CreateHLG().ToSkColorSpace(); + skcms_TransferFunction hlg; + hlg_colorspace->transferFn(&hlg); + profile = NewColorProfileWithSameBuffer( + *profile, + ReplaceTransferFunction(*profile->GetProfile(), hlg)); + break; + } + } + } + } + } + + if (is_hdr_ && + high_bit_depth_decoding_option_ == kHighBitDepthToHalfFloat) { + decode_to_half_float_ = true; + } + + if (have_data_profile) { + if (profile->GetProfile()->data_color_space == skcms_Signature_RGB) { + SetEmbeddedColorProfile(std::move(profile)); + } + } + have_color_info_ = true; + break; + } + case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { + const wtf_size_t frame_index = num_decoded_frames_++; + ImageFrame& frame = frame_buffer_cache_[frame_index]; + // This is guaranteed to occur after JXL_DEC_BASIC_INFO so the size + // is correct. + if (!InitFrameBuffer(frame_index)) { + DVLOG(1) << "InitFrameBuffer failed"; + SetFailed(); + return; + } + frame.SetHasAlpha(info_.alpha_bits != 0); + + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec_.get(), &format, &buffer_size)) { + DVLOG(1) << "JxlDecoderImageOutBufferSize failed"; + SetFailed(); + return; + } + if (buffer_size != info_.xsize * info_.ysize * 16) { + DVLOG(1) << "Unexpected buffer size"; + SetFailed(); + return; + } + + // TODO(http://crbug.com/1210465): Add Munsell chart color accuracy + // tests for JXL + xform_ = ColorTransform(); + auto callback = [](void* opaque, size_t x, size_t y, size_t num_pixels, + const void* pixels) { + JXLImageDecoder* self = reinterpret_cast(opaque); + ImageFrame& frame = + self->frame_buffer_cache_[self->num_decoded_frames_ - 1]; + void* row_dst = self->decode_to_half_float_ + ? reinterpret_cast(frame.GetAddrF16( + static_cast(x), static_cast(y))) + : reinterpret_cast(frame.GetAddr( + static_cast(x), static_cast(y))); + + bool dst_premultiply = frame.PremultiplyAlpha(); + + const skcms_PixelFormat kSrcFormat = skcms_PixelFormat_RGBA_ffff; + const skcms_PixelFormat kDstFormat = self->decode_to_half_float_ + ? skcms_PixelFormat_RGBA_hhhh + : XformColorFormat(); + + if (self->xform_ || (kDstFormat != kSrcFormat) || + (dst_premultiply && frame.HasAlpha())) { + skcms_AlphaFormat src_alpha = skcms_AlphaFormat_Unpremul; + skcms_AlphaFormat dst_alpha = + (dst_premultiply && self->info_.alpha_bits) + ? skcms_AlphaFormat_PremulAsEncoded + : skcms_AlphaFormat_Unpremul; + const auto* src_profile = + self->xform_ ? self->xform_->SrcProfile() : nullptr; + const auto* dst_profile = + self->xform_ ? self->xform_->DstProfile() : nullptr; + bool color_conversion_successful = skcms_Transform( + pixels, kSrcFormat, src_alpha, src_profile, row_dst, kDstFormat, + dst_alpha, dst_profile, num_pixels); + DCHECK(color_conversion_successful); + } + }; + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutCallback( + dec_.get(), &format, callback, this)) { + DVLOG(1) << "JxlDecoderSetImageOutCallback failed"; + SetFailed(); + return; + } + break; + } + case JXL_DEC_FRAME_PROGRESSION: { + if (IsAllDataReceived()) { + break; + } else { + if (JXL_DEC_SUCCESS != JxlDecoderFlushImage(dec_.get())) { + DVLOG(1) << "JxlDecoderSetImageOutCallback failed"; + SetFailed(); + return; + } + ImageFrame& frame = frame_buffer_cache_[num_decoded_frames_ - 1]; + frame.SetPixelsChanged(true); + frame.SetStatus(ImageFrame::kFramePartial); + break; + } + } + case JXL_DEC_FULL_IMAGE: { + ImageFrame& frame = frame_buffer_cache_[num_decoded_frames_ - 1]; + frame.SetPixelsChanged(true); + frame.SetStatus(ImageFrame::kFrameComplete); + // All required frames were decoded. + if (num_decoded_frames_ > index) { + return; + } + break; + } + case JXL_DEC_SUCCESS: { + // Finished decoding entire image, with all frames in case of animation. + // Don't reset dec_, since we may want to rewind it if an earlier + // animation frame has to be decoded again. + segment_.clear(); + return; + } + default: { + DVLOG(1) << "Unexpected decoder status " << status; + SetFailed(); + return; + } + } + } +} + +bool JXLImageDecoder::MatchesJXLSignature( + const FastSharedBufferReader& fast_reader) { + char buffer[12]; + if (fast_reader.size() < sizeof(buffer)) + return false; + const char* contents = reinterpret_cast( + fast_reader.GetConsecutiveData(0, sizeof(buffer), buffer)); + // Direct codestream + if (!memcmp(contents, "\xFF\x0A", 2)) + return true; + // Box format container + if (!memcmp(contents, "\0\0\0\x0CJXL \x0D\x0A\x87\x0A", 12)) + return true; + return false; +} + +void JXLImageDecoder::InitializeNewFrame(wtf_size_t index) { + auto& buffer = frame_buffer_cache_[index]; + if (decode_to_half_float_) + buffer.SetPixelFormat(ImageFrame::PixelFormat::kRGBA_F16); + buffer.SetHasAlpha(info_.alpha_bits != 0); + buffer.SetPremultiplyAlpha(premultiply_alpha_); +} + +bool JXLImageDecoder::FrameIsReceivedAtIndex(wtf_size_t index) const { + return IsAllDataReceived() || + (index < num_decoded_frames_ && + frame_buffer_cache_[index].GetStatus() == ImageFrame::kFrameComplete); +} + +int JXLImageDecoder::RepetitionCount() const { + if (!info_.have_animation) + return kAnimationNone; + + if (info_.animation.num_loops == 0) + return kAnimationLoopInfinite; + + if (info_.animation.num_loops == 1) + return kAnimationLoopOnce; + + return info_.animation.num_loops; +} + +base::TimeDelta JXLImageDecoder::FrameDurationAtIndex(wtf_size_t index) const { + if (index < frame_durations_.size()) + return base::Seconds(frame_durations_[index]); + + return base::TimeDelta(); +} + +wtf_size_t JXLImageDecoder::DecodeFrameCount() { + DecodeSize(); + if (!info_.have_animation) { + frame_durations_.resize(1); + frame_durations_[0] = 0; + return 1; + } + + FastSharedBufferReader reader(data_.get()); + if (has_full_frame_count_ || size_at_last_frame_count_ == reader.size()) { + return frame_buffer_cache_.size(); + } + size_at_last_frame_count_ = reader.size(); + + // Decode the metadata of every frame that is available. + if (frame_count_dec_ == nullptr) { + frame_durations_.clear(); + frame_count_dec_ = JxlDecoderMake(nullptr); + frame_count_offset_ = 0; + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(frame_count_dec_.get(), JXL_DEC_FRAME)) { + SetFailed(); + return frame_buffer_cache_.size(); + } + } + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(frame_count_dec_.get()); + switch (status) { + case JXL_DEC_ERROR: { + DVLOG(1) << "Decoder error " << status; + SetFailed(); + return frame_buffer_cache_.size(); + } + case JXL_DEC_NEED_MORE_INPUT: { + // The decoder returns how many bytes it has not yet processed, and + // must be included in the next JxlDecoderSetInput call. + const size_t remaining = JxlDecoderReleaseInput(frame_count_dec_.get()); + const uint8_t* jxl_data = nullptr; + size_t jxl_size = 0; + if (!ReadBytes(remaining, &frame_count_offset_, &frame_count_segment_, + &reader, &jxl_data, &jxl_size)) { + if (Failed()) { + return frame_buffer_cache_.size(); + } + return frame_durations_.size(); + } + + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(frame_count_dec_.get(), jxl_data, jxl_size)) { + DVLOG(1) << "JxlDecoderSetInput failed"; + SetFailed(); + return frame_buffer_cache_.size(); + } + break; + } + case JXL_DEC_FRAME: { + JxlFrameHeader frame_header; + if (JxlDecoderGetFrameHeader(frame_count_dec_.get(), &frame_header) != + JXL_DEC_SUCCESS) { + DVLOG(1) << "GetFrameHeader failed"; + SetFailed(); + return frame_buffer_cache_.size(); + } + if (frame_header.is_last) { + has_full_frame_count_ = true; + } + frame_durations_.push_back(1.0f * frame_header.duration * + info_.animation.tps_denominator / + info_.animation.tps_numerator); + break; + } + case JXL_DEC_SUCCESS: { + // If the file is fully processed, we won't need to run the decoder + // anymore: we can free the memory. + frame_count_dec_ = nullptr; + DCHECK(has_full_frame_count_); + frame_count_segment_.clear(); + return frame_durations_.size(); + } + default: { + DVLOG(1) << "Unexpected decoder status " << status; + SetFailed(); + return frame_buffer_cache_.size(); + } + } + } +} + +} // namespace blink diff --git a/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h new file mode 100644 --- /dev/null +++ b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2021, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_IMAGE_DECODERS_JXL_JXL_IMAGE_DECODER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_IMAGE_DECODERS_JXL_JXL_IMAGE_DECODER_H_ + +#include "third_party/blink/renderer/platform/image-decoders/fast_shared_buffer_reader.h" +#include "third_party/blink/renderer/platform/image-decoders/image_decoder.h" + +#include "third_party/libjxl/src/lib/include/jxl/decode.h" +#include "third_party/libjxl/src/lib/include/jxl/decode_cxx.h" + +namespace blink { + +// This class decodes the JXL image format. +class PLATFORM_EXPORT JXLImageDecoder final : public ImageDecoder { + public: + JXLImageDecoder(AlphaOption, + HighBitDepthDecodingOption high_bit_depth_decoding_option, + const ColorBehavior&, + wtf_size_t max_decoded_bytes); + + // ImageDecoder: + String FilenameExtension() const override { return "jxl"; } + const AtomicString& MimeType() const override; + bool ImageIsHighBitDepth() override { return is_hdr_; } + + // Returns true if the data in fast_reader begins with + static bool MatchesJXLSignature(const FastSharedBufferReader& fast_reader); + + private: + // ImageDecoder: + void DecodeSize() override { DecodeImpl(0, true); } + wtf_size_t DecodeFrameCount() override; + void Decode(wtf_size_t frame) override { DecodeImpl(frame); } + void InitializeNewFrame(wtf_size_t) override; + + // Decodes up to a given frame. If |only_size| is true, stops decoding after + // calculating the image size. If decoding fails but there is no more + // data coming, sets the "decode failure" flag. + void DecodeImpl(wtf_size_t frame, bool only_size = false); + + bool FrameIsReceivedAtIndex(wtf_size_t) const override; + base::TimeDelta FrameDurationAtIndex(wtf_size_t) const override; + int RepetitionCount() const override; + bool CanReusePreviousFrameBuffer(wtf_size_t) const override { return false; } + + // Reads bytes from the segment reader, after releasing input from the JXL + // decoder, which required `remaining` previous bytes to still be available. + // Starts reading from *offset - remaining, and ensures more than remaining + // bytes are read, if possible. Returns false if not enough bytes are + // available or if Failed() was set. + bool ReadBytes(size_t remaining, + wtf_size_t* offset, + WTF::Vector* segment, + FastSharedBufferReader* reader, + const uint8_t** jxl_data, + size_t* jxl_size); + + JxlDecoderPtr dec_ = nullptr; + wtf_size_t offset_ = 0; + + JxlDecoderPtr frame_count_dec_ = nullptr; + wtf_size_t frame_count_offset_ = 0; + + // The image is considered to be HDR, such as using PQ or HLG transfer + // function in the color space. + bool is_hdr_ = false; + bool decode_to_half_float_ = false; + + JxlBasicInfo info_; + bool have_color_info_ = false; + + // Preserved for JXL pixel callback. Not owned. + ColorProfileTransform* xform_; + + // Fields for animation support. + + // The amount of frames the JXL decoder has decoded. This can be reset to + // an earlier amount if frame buffers were cleared and decoding was + // restarted from an earlier frame. This is used to keep track of the index + // in the frame_buffer_cache_. + wtf_size_t num_decoded_frames_ = 0; + bool has_full_frame_count_ = false; + size_t size_at_last_frame_count_ = 0; + WTF::Vector frame_durations_; + // Multiple concatenated segments from the FastSharedBufferReader, these are + // only used when a single segment did not contain enough data for the JXL + // parser. + WTF::Vector segment_; + WTF::Vector frame_count_segment_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_IMAGE_DECODERS_JXL_JXL_IMAGE_DECODER_H_ diff --git a/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder_test.cc b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder_test.cc new file mode 100644 --- /dev/null +++ b/third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder_test.cc @@ -0,0 +1,626 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/platform/image-decoders/jxl/jxl_image_decoder.h" + +#include +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/platform/image-decoders/image_decoder_test_helpers.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "ui/gfx/geometry/point.h" + +namespace blink { + +namespace { + +std::unique_ptr CreateJXLDecoderWithArguments( + const char* jxl_file, + ImageDecoder::AlphaOption alpha_option, + ImageDecoder::HighBitDepthDecodingOption high_bit_depth_decoding_option, + ColorBehavior color_behavior) { + auto decoder = std::make_unique( + alpha_option, high_bit_depth_decoding_option, color_behavior, + ImageDecoder::kNoDecodedImageByteLimit); + scoped_refptr data = ReadFile(jxl_file); + EXPECT_FALSE(data->empty()); + decoder->SetData(data.get(), true); + return decoder; +} + +std::unique_ptr CreateJXLDecoder() { + return std::make_unique( + ImageDecoder::kAlphaNotPremultiplied, ImageDecoder::kDefaultBitDepth, + ColorBehavior::Tag(), ImageDecoder::kNoDecodedImageByteLimit); +} + +std::unique_ptr CreateJXLDecoderWithData(const char* jxl_file) { + auto decoder = CreateJXLDecoder(); + scoped_refptr data = ReadFile(jxl_file); + EXPECT_FALSE(data->empty()); + decoder->SetData(data.get(), true); + return decoder; +} + +// expected_color must match the expected top left pixel +void TestColorProfile(const char* jxl_file, + ColorBehavior color_behavior, + SkColor expected_color) { + auto decoder = CreateJXLDecoderWithArguments( + jxl_file, ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ImageDecoder::kDefaultBitDepth, color_behavior); + EXPECT_EQ(1u, decoder->FrameCount()); + ImageFrame* frame = decoder->DecodeFrameBufferAtIndex(0); + ASSERT_TRUE(frame); + EXPECT_EQ(ImageFrame::kFrameComplete, frame->GetStatus()); + EXPECT_FALSE(decoder->Failed()); + const SkBitmap& bitmap = frame->Bitmap(); + SkColor frame_color = bitmap.getColor(0, 0); + for (int i = 0; i < 4; ++i) { + int frame_comp = (frame_color >> (8 * i)) & 255; + int expected_comp = (expected_color >> (8 * i)) & 255; + EXPECT_GE(1, abs(frame_comp - expected_comp)); + } +} + +// Convert from float16 bits in a uint16_t, to 32-bit float, for testing +static float FromFloat16(uint16_t a) { + // 5 bits exponent + int exp = (a >> 10) & 31; + // 10 bits fractional part + float frac = a & 1023; + // 1 bit sign + int sign = (a & 32768) ? 1 : 0; + bool subnormal = exp == 0; + // Infinity and NaN are not supported here. + exp -= 15; + if (subnormal) + exp++; + frac /= 1024.0; + if (!subnormal) + frac++; + frac *= std::pow(2, exp); + if (sign) + frac = -frac; + return frac; +} + +// expected_color must match the expected top left pixel +void TestHDR(const char* jxl_file, + ColorBehavior color_behavior, + bool expect_f16, + float expected_r, + float expected_g, + float expected_b, + float expected_a) { + auto decoder = CreateJXLDecoderWithArguments( + jxl_file, ImageDecoder::AlphaOption::kAlphaPremultiplied, + ImageDecoder::kHighBitDepthToHalfFloat, color_behavior); + EXPECT_TRUE(decoder->IsSizeAvailable()); + EXPECT_EQ(1u, decoder->FrameCount()); + ImageFrame* frame = decoder->DecodeFrameBufferAtIndex(0); + ASSERT_TRUE(frame); + EXPECT_EQ(ImageFrame::kFrameComplete, frame->GetStatus()); + EXPECT_FALSE(decoder->Failed()); + float r, g, b, a; + if (expect_f16) { + EXPECT_EQ(ImageFrame::kRGBA_F16, frame->GetPixelFormat()); + } else { + EXPECT_EQ(ImageFrame::kN32, frame->GetPixelFormat()); + } + if (ImageFrame::kRGBA_F16 == frame->GetPixelFormat()) { + uint64_t first_pixel = *frame->GetAddrF16(0, 0); + r = FromFloat16(first_pixel >> 0); + g = FromFloat16(first_pixel >> 16); + b = FromFloat16(first_pixel >> 32); + a = FromFloat16(first_pixel >> 48); + } else { + uint32_t first_pixel = *frame->GetAddr(0, 0); + a = ((first_pixel >> SK_A32_SHIFT) & 255) / 255.0; + r = ((first_pixel >> SK_R32_SHIFT) & 255) / 255.0; + g = ((first_pixel >> SK_G32_SHIFT) & 255) / 255.0; + b = ((first_pixel >> SK_B32_SHIFT) & 255) / 255.0; + } + constexpr float eps = 0.01; + EXPECT_NEAR(expected_r, r, eps); + EXPECT_NEAR(expected_g, g, eps); + EXPECT_NEAR(expected_b, b, eps); + EXPECT_NEAR(expected_a, a, eps); +} + +void TestSize(const char* jxl_file, gfx::Size expected_size) { + auto decoder = CreateJXLDecoderWithData(jxl_file); + EXPECT_TRUE(decoder->IsSizeAvailable()); + EXPECT_EQ(expected_size, decoder->Size()); +} + +struct FramePoint { + size_t frame; + gfx::Point point; +}; + +void TestPixel(const char* jxl_file, + gfx::Size expected_size, + const WTF::Vector& coordinates, + const WTF::Vector& expected_colors, + ImageDecoder::AlphaOption alpha_option, + ColorBehavior color_behavior, + int accuracy, + size_t num_frames = 1) { + SCOPED_TRACE(testing::Message() + << "TestPixel jxl_file: " << jxl_file + << ", alpha_option:" << static_cast(alpha_option)); + EXPECT_EQ(coordinates.size(), expected_colors.size()); + auto decoder = CreateJXLDecoderWithArguments( + jxl_file, alpha_option, ImageDecoder::kDefaultBitDepth, color_behavior); + EXPECT_TRUE(decoder->IsSizeAvailable()); + EXPECT_EQ(expected_size, decoder->Size()); + ASSERT_EQ(num_frames, decoder->FrameCount()); + for (size_t i = 0; i < num_frames; ++i) { + ImageFrame* frame = decoder->DecodeFrameBufferAtIndex(i); + ASSERT_TRUE(frame); + EXPECT_EQ(ImageFrame::kFrameComplete, frame->GetStatus()); + } + EXPECT_FALSE(decoder->Failed()); + for (size_t i = 0; i < coordinates.size(); ++i) { + SCOPED_TRACE(testing::Message() << "Coordinate: " << i); + const SkBitmap& bitmap = + decoder->DecodeFrameBufferAtIndex(coordinates[i].frame)->Bitmap(); + EXPECT_TRUE(SkColorSpace::Equals(bitmap.colorSpace(), + decoder->ColorSpaceForSkImages().get())); + int x = coordinates[i].point.x(); + int y = coordinates[i].point.y(); + SkColor frame_color = bitmap.getColor(x, y); + int r_expected = (expected_colors[i] >> 16) & 255; + int g_expected = (expected_colors[i] >> 8) & 255; + int b_expected = (expected_colors[i] >> 0) & 255; + int a_expected = (expected_colors[i] >> 24) & 255; + int r_actual = (frame_color >> 16) & 255; + int g_actual = (frame_color >> 8) & 255; + int b_actual = (frame_color >> 0) & 255; + int a_actual = (frame_color >> 24) & 255; + EXPECT_NEAR(r_expected, r_actual, accuracy); + EXPECT_NEAR(g_expected, g_actual, accuracy); + EXPECT_NEAR(b_expected, b_actual, accuracy); + // Alpha is always lossless. + EXPECT_EQ(a_expected, a_actual); + } +} + +// SegmentReader implementation for testing, which always returns segments +// of size 1. This allows to test whether the decoder handles streaming +// correctly in the most fine-grained case. +class PerByteSegmentReader : public SegmentReader { + public: + PerByteSegmentReader(SharedBuffer& buffer) : buffer_(buffer) {} + size_t size() const override { return buffer_.size(); } + size_t GetSomeData(const char*& data, size_t position) const override { + if (position >= buffer_.size()) { + return 0; + } + data = buffer_.Data() + position; + return 1; + } + sk_sp GetAsSkData() const override { return nullptr; } + + private: + SharedBuffer& buffer_; +}; + +// Tests whether the decoder successfully parses the file without errors or +// infinite loop in the worst case of the reader returning 1-byte segments. +void TestSegmented(const char* jxl_file, gfx::Size expected_size) { + auto decoder = std::make_unique( + ImageDecoder::kAlphaNotPremultiplied, ImageDecoder::kDefaultBitDepth, + ColorBehavior::Tag(), ImageDecoder::kNoDecodedImageByteLimit); + scoped_refptr data = ReadFile(jxl_file); + EXPECT_FALSE(data->empty()); + + scoped_refptr reader = + base::AdoptRef(new PerByteSegmentReader(*data.get())); + decoder->SetData(reader, true); + + ImageFrame* frame; + for (;;) { + frame = decoder->DecodeFrameBufferAtIndex(0); + if (decoder->Failed()) + break; + if (frame) + break; + } + + EXPECT_TRUE(decoder->IsSizeAvailable()); + EXPECT_LE(1u, decoder->FrameCount()); + EXPECT_TRUE(!!frame); + EXPECT_EQ(ImageFrame::kFrameComplete, frame->GetStatus()); + EXPECT_FALSE(decoder->Failed()); + EXPECT_EQ(expected_size, decoder->Size()); +} + +TEST(JXLTests, SegmentedTest) { + TestSegmented("/images/resources/jxl/alpha-lossless.jxl", gfx::Size(2, 10)); + TestSegmented("/images/resources/jxl/3x3_srgb_lossy.jxl", gfx::Size(3, 3)); + TestSegmented("/images/resources/jxl/pq_gradient_icc_lossy.jxl", + gfx::Size(16, 16)); + TestSegmented("/images/resources/jxl/animated.jxl", gfx::Size(16, 16)); +} + +TEST(JXLTests, SizeTest) { + TestSize("/images/resources/jxl/alpha-lossless.jxl", gfx::Size(2, 10)); +} + +TEST(JXLTests, PixelTest) { + TestPixel("/images/resources/jxl/red-10-default.jxl", gfx::Size(10, 10), + {{0, {0, 0}}}, {SkColorSetARGB(255, 255, 0, 0)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/red-10-lossless.jxl", gfx::Size(10, 10), + {{0, {0, 1}}}, {SkColorSetARGB(255, 255, 0, 0)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/red-10-container.jxl", gfx::Size(10, 10), + {{0, {1, 0}}}, {SkColorSetARGB(255, 255, 0, 0)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/green-10-lossless.jxl", gfx::Size(10, 10), + {{0, {2, 3}}}, {SkColorSetARGB(255, 0, 255, 0)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/blue-10-lossless.jxl", gfx::Size(10, 10), + {{0, {9, 9}}}, {SkColorSetARGB(255, 0, 0, 255)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/alpha-lossless.jxl", gfx::Size(2, 10), + {{0, {0, 1}}}, {SkColorSetARGB(0, 255, 255, 255)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, + ColorBehavior::Tag(), 0); + TestPixel("/images/resources/jxl/alpha-lossless.jxl", gfx::Size(2, 10), + {{0, {0, 1}}}, {SkColorSetARGB(0, 0, 0, 0)}, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 0); + + WTF::Vector coordinates_3x3 = { + {0, {0, 0}}, {0, {1, 0}}, {0, {2, 0}}, {0, {0, 1}}, {0, {1, 1}}, + {0, {2, 1}}, {0, {0, 2}}, {0, {1, 2}}, {0, {2, 2}}, + }; + + TestPixel("/images/resources/jxl/3x3_srgb_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 0); + + TestPixel("/images/resources/jxl/3x3_srgb_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 15); + + TestPixel("/images/resources/jxl/3x3a_srgb_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 0); + + TestPixel("/images/resources/jxl/3x3a_srgb_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 15); + + // Lossless, but allow some inaccuracy due to the color profile conversion. + TestPixel("/images/resources/jxl/3x3_gbr_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 3); + + TestPixel("/images/resources/jxl/3x3_gbr_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 35); + + // Lossless, but allow some inaccuracy due to the color profile conversion. + TestPixel("/images/resources/jxl/3x3a_gbr_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 3); + + TestPixel("/images/resources/jxl/3x3a_gbr_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::TransformToSRGB(), 35); + + // Lossless, but allow some inaccuracy due to the color profile conversion. + TestPixel("/images/resources/jxl/3x3_pq_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 2); + + TestPixel("/images/resources/jxl/3x3_pq_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 64, 255, 64), + SkColorSetARGB(255, 39, 76, 255), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 15); + + TestPixel("/images/resources/jxl/3x3a_pq_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 2); + + TestPixel("/images/resources/jxl/3x3a_pq_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 64, 255, 64), + SkColorSetARGB(128, 40, 82, 255), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 64, 64, 128), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 15); + + TestPixel("/images/resources/jxl/3x3_hlg_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 0, 0), + SkColorSetARGB(255, 0, 255, 0), + SkColorSetARGB(255, 0, 0, 255), + SkColorSetARGB(255, 86, 46, 46), + SkColorSetARGB(255, 46, 86, 46), + SkColorSetARGB(255, 46, 46, 86), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 85, 85, 85), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 2); + + TestPixel("/images/resources/jxl/3x3_hlg_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(255, 255, 13, 13), + SkColorSetARGB(255, 13, 255, 13), + SkColorSetARGB(255, 13, 13, 255), + SkColorSetARGB(255, 128, 64, 64), + SkColorSetARGB(255, 64, 128, 64), + SkColorSetARGB(255, 64, 64, 128), + SkColorSetARGB(255, 255, 255, 255), + SkColorSetARGB(255, 128, 128, 128), + SkColorSetARGB(255, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 15); + + TestPixel("/images/resources/jxl/3x3a_hlg_lossless.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 0, 0), + SkColorSetARGB(128, 0, 255, 0), + SkColorSetARGB(128, 0, 0, 255), + SkColorSetARGB(128, 86, 46, 46), + SkColorSetARGB(128, 46, 86, 46), + SkColorSetARGB(128, 46, 46, 86), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 85, 85, 85), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 6); + + TestPixel("/images/resources/jxl/3x3a_hlg_lossy.jxl", gfx::Size(3, 3), + coordinates_3x3, + { + SkColorSetARGB(128, 255, 13, 13), + SkColorSetARGB(128, 13, 255, 13), + SkColorSetARGB(128, 13, 13, 255), + SkColorSetARGB(128, 128, 64, 64), + SkColorSetARGB(128, 64, 128, 64), + SkColorSetARGB(128, 74, 64, 128), + SkColorSetARGB(128, 255, 255, 255), + SkColorSetARGB(128, 128, 128, 128), + SkColorSetARGB(128, 0, 0, 0), + }, + ImageDecoder::AlphaOption::kAlphaPremultiplied, + ColorBehavior::Tag(), 15); +} + +TEST(JXLTests, ColorProfileTest) { + TestColorProfile("/images/resources/jxl/icc-v2-gbr.jxl", ColorBehavior::Tag(), + SkColorSetARGB(255, 0xaf, 0xfe, 0x6b)); + TestColorProfile("/images/resources/jxl/icc-v2-gbr.jxl", + ColorBehavior::TransformToSRGB(), + SkColorSetARGB(255, 0x6b, 0xb1, 0xfe)); + TestColorProfile("/images/resources/jxl/icc-v2-gbr.jxl", + ColorBehavior::Ignore(), + SkColorSetARGB(255, 0xaf, 0xfe, 0x6b)); +} + +TEST(JXLTests, AnimatedPixelTest) { + TestPixel( + "/images/resources/jxl/animated.jxl", gfx::Size(16, 16), + {{0, {0, 0}}, {1, {0, 0}}}, + {SkColorSetARGB(255, 204, 0, 153), SkColorSetARGB(255, 0, 102, 102)}, + ImageDecoder::AlphaOption::kAlphaNotPremultiplied, ColorBehavior::Tag(), + 0, 2); +} + +TEST(JXLTests, JXLHDRTest) { + // PQ tests + // PQ values, as expected + TestHDR("/images/resources/jxl/pq_gradient_lossy.jxl", + ColorBehavior::Ignore(), false, 0.58039218187332153, + 0.73333334922790527, 0.43921568989753723, 1); + // sRGB as expected, but not an exact match + TestHDR("/images/resources/jxl/pq_gradient_lossy.jxl", + ColorBehavior::TransformToSRGB(), true, -0.9248046875, 1.943359375, + -0.4443359375, 1); + + // linear sRGB as expected. + TestHDR("/images/resources/jxl/pq_gradient_lossy.jxl", ColorBehavior::Tag(), + true, 0.58039218187332153, 0.73333334922790527, 0.43921568989753723, + 1); + + // correct, original PQ values + TestHDR("/images/resources/jxl/pq_gradient_lossless.jxl", + ColorBehavior::Ignore(), false, 0.58039218187332153, + 0.73725491762161255, 0.45098039507865906, 1); + TestHDR("/images/resources/jxl/pq_gradient_lossless.jxl", + ColorBehavior::TransformToSRGB(), true, -0.95751953125, 1.9677734375, + -0.416748046875, 1); + // correct, original PQ values + TestHDR("/images/resources/jxl/pq_gradient_lossless.jxl", + ColorBehavior::Tag(), true, 0.58056640625, 0.7373046875, + 0.450927734375, 1); + + // with ICC + // clipped linear sRGB, as expected from current JXL implementation + TestHDR("/images/resources/jxl/pq_gradient_icc_lossy.jxl", + ColorBehavior::Ignore(), false, 0, 0.0930381, 0, 1); + + TestHDR("/images/resources/jxl/pq_gradient_icc_lossy.jxl", + ColorBehavior::TransformToSRGB(), false, 0, 0.338623046875, 0, 1); + TestHDR("/images/resources/jxl/pq_gradient_icc_lossy.jxl", + ColorBehavior::Tag(), false, 0, 0.0930381, 0, 1); + + TestHDR("/images/resources/jxl/pq_gradient_icc_lossless.jxl", + ColorBehavior::Ignore(), false, 0.58039218187332153, + 0.73725491762161255, 0.45098039507865906, 1); + TestHDR("/images/resources/jxl/pq_gradient_icc_lossless.jxl", + ColorBehavior::TransformToSRGB(), true, -0.95751953125, 1.9677734375, + -0.416748046875, 1); + TestHDR("/images/resources/jxl/pq_gradient_icc_lossless.jxl", + ColorBehavior::Tag(), true, 0.58039218187332153, 0.73725491762161255, + 0.45098039507865906, 1); +} + +TEST(JXLTests, RandomFrameDecode) { + TestRandomFrameDecode(&CreateJXLDecoder, "/images/resources/jxl/count.jxl"); +} + +TEST(JXLTests, RandomDecodeAfterClearFrameBufferCache) { + TestRandomDecodeAfterClearFrameBufferCache(&CreateJXLDecoder, + "/images/resources/jxl/count.jxl"); +} + +} // namespace +} // namespace blink diff --git a/third_party/blink/tools/commit_stats/git-dirs.txt b/third_party/blink/tools/commit_stats/git-dirs.txt --- a/third_party/blink/tools/commit_stats/git-dirs.txt +++ b/third_party/blink/tools/commit_stats/git-dirs.txt @@ -75,6 +75,7 @@ ./third_party/angle/third_party/glmark2/src,ANGLE ./third_party/openh264/src,OpenH264 ./third_party/googletest/src,GoogleTest +./third_party/libjxl/src,libjxl ./third_party/highway/src,highway ./third_party/wuffs/src,wuffs ./third_party/catapult,catapult diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations --- a/third_party/blink/web_tests/TestExpectations +++ b/third_party/blink/web_tests/TestExpectations @@ -4781,6 +4781,12 @@ crbug.com/1199522 http/tests/devtools/layers/layers-3d-view-hit-testing.js [ Fai # Started failing after rolling new version of check-layout-th.js css3/flexbox/perpendicular-writing-modes-inside-flex-item.html [ Failure ] +# JXL tests fail due to rounding error differences. +# TODO(https://crbug.com/1274220): Rebaseline all images once new tone mapping +# lands. +crbug.com/1210658 virtual/jxl-enabled/images/jxl/jxl-images.html [ Crash Failure Pass Timeout ] +crbug.com/1358616 virtual/jxl-enabled/images/jxl/progressive.html [ Crash Failure Pass Timeout ] + # Temporarily disabled to unblock https://crrev.com/c/3099011 crbug.com/1199701 http/tests/devtools/console/console-big-array.js [ Crash Failure Pass Timeout ] diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites --- a/third_party/blink/web_tests/VirtualTestSuites +++ b/third_party/blink/web_tests/VirtualTestSuites @@ -447,6 +447,15 @@ "--disable-threaded-compositing", "--disable-threaded-animation"], "expires": "Jul 1, 2023" }, + { + "prefix": "jxl-enabled", + "platforms": ["Linux", "Mac", "Win"], + "bases": ["http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js", + "images/jxl"], + "exclusive_tests": ["http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js"], + "args": ["--enable-features=JXL"], + "expires": "Jul 1, 2023" + }, { "prefix": "scalefactor150", "platforms": ["Linux", "Win"], diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl-expected.txt new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl-expected.txt @@ -0,0 +1,13 @@ +Tests the Emulation.setDisabledImageTypes method for JPEG XL. +With emulation (jxl enabled): +Expected jxl image: http://127.0.0.1:8000/inspector-protocol/emulation/resources/test.jxl +Image request Accept header: image/jxl,image/avif,image/apng,image/svg+xml,image/*,*/*;q=0.8 +With emulation (jxl disabled): +Expected png image: http://127.0.0.1:8000/inspector-protocol/emulation/resources/test.png +Image request Accept header: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 +With emulation (jxl enabled): +Expected jxl image: http://127.0.0.1:8000/inspector-protocol/emulation/resources/test.jxl +Image request Accept header: image/jxl,image/avif,image/apng,image/svg+xml,image/*,*/*;q=0.8 +With emulation (jxl disabled): +Expected png image: http://127.0.0.1:8000/inspector-protocol/emulation/resources/test.png +Image request Accept header: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/emulation-set-disabled-image-types-jxl.js @@ -0,0 +1,50 @@ +(async function(testRunner) { + const {page, session, dp} = + await testRunner.startBlank('Tests the Emulation.setDisabledImageTypes method for JPEG XL.'); + + await dp.Page.enable(); + await dp.Network.enable(); + + let requestEvents = []; + dp.Network.onRequestWillBeSent(event => requestEvents.push(event)); + + await dp.Emulation.setDisabledImageTypes({ imageTypes: ['webp'] }); + + testRunner.log('With emulation (jxl enabled):'); + await page.navigate(testRunner.url('resources/image-jxl-fallback-img.html')); + testRunner.log('Expected jxl image: ' + await session.evaluate(() => document.querySelector('img').currentSrc)); + let jxlRequest = requestEvents.map(event => event.params.request).find(request => request.url.endsWith('test.jxl')); + testRunner.log('Image request Accept header: ' + jxlRequest.headers.Accept); + + requestEvents = []; + + testRunner.log('With emulation (jxl disabled):'); + await dp.Emulation.setDisabledImageTypes({ imageTypes: ['jxl'] }); + dp.Page.reload({ ignoreCache: true }); + await dp.Page.onceLoadEventFired(); + testRunner.log('Expected png image: ' + await session.evaluate(() => document.querySelector('img').currentSrc)); + let pngRequest = requestEvents.map(event => event.params.request).find(request => request.url.endsWith('test.png')); + testRunner.log('Image request Accept header: ' + pngRequest.headers.Accept); + + requestEvents = []; + + await dp.Emulation.setDisabledImageTypes({ imageTypes: ['webp'] }); + + testRunner.log('With emulation (jxl enabled):'); + await page.navigate(testRunner.url('resources/image-jxl-fallback-picture.html')); + testRunner.log('Expected jxl image: ' + await session.evaluate(() => document.querySelector('img').currentSrc)); + jxlRequest = requestEvents.map(event => event.params.request).find(request => request.url.endsWith('test.jxl')); + testRunner.log('Image request Accept header: ' + jxlRequest.headers.Accept); + + requestEvents = []; + + testRunner.log('With emulation (jxl disabled):'); + await dp.Emulation.setDisabledImageTypes({ imageTypes: ['jxl'] }); + dp.Page.reload({ ignoreCache: true }); + await dp.Page.onceLoadEventFired(); + testRunner.log('Expected png image: ' + await session.evaluate(() => document.querySelector('img').currentSrc)); + pngRequest = requestEvents.map(event => event.params.request).find(request => request.url.endsWith('test.png')); + testRunner.log('Image request Accept header: ' + pngRequest.headers.Accept); + + testRunner.completeTest(); +}) \ No newline at end of file diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-img.html b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-img.html new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-img.html @@ -0,0 +1 @@ + diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-picture.html b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-picture.html new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/emulation/resources/image-jxl-fallback-picture.html @@ -0,0 +1,4 @@ + + + + diff --git a/third_party/blink/web_tests/images/jxl/jxl-images.html b/third_party/blink/web_tests/images/jxl/jxl-images.html new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/images/jxl/jxl-images.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/blink/web_tests/images/jxl/progressive.html b/third_party/blink/web_tests/images/jxl/progressive.html new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/images/jxl/progressive.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party/blink/web_tests/images/resources/jxl/README.md b/third_party/blink/web_tests/images/resources/jxl/README.md new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/images/resources/jxl/README.md @@ -0,0 +1,79 @@ +# JPEG XL Test files + +## How to generate the test set + +We assume to have a the following images (from +`third_party/blink/web_tests/images/resources/`) available: +``` +red-10.png +green-10.png +blue-10.png +png_per_row_alpha.png +icc-v2-gbr.jpg +dice.png +animated.gif +jxl/3x3.png +jxl/3x3a.png +``` +Then we run: +``` +cjxl red-10.png red-10-default.jxl +cjxl --container red-10.png red-10-container.jxl +cjxl -d 0 red-10.png red-10-lossless.jxl +cjxl -d 0 green-10.png green-10-lossless.jxl +cjxl -d 0 blue-10.png blue-10-lossless.jxl +cjxl -d 0 png_per_row_alpha.png alpha-lossless.jxl +cjxl icc-v2-gbr.jpg icc-v2-gbr.jxl +cjxl -d 0 dice.png alpha-large-dice.jxl + +cjxl 3x3.png temp.jxl -d 0 +djxl temp.jxl 3x3_srgb.png +cjxl 3x3_srgb.png 3x3_srgb_lossy.jxl -d 0.1 -e 7 +cjxl 3x3_srgb.png 3x3_srgb_lossless.jxl -d 0 + +cjxl 3x3a.png temp.jxl -d 0 +djxl temp.jxl 3x3a_srgb.png +cjxl 3x3a_srgb.png 3x3a_srgb_lossy.jxl -d 0.1 -e 7 +cjxl 3x3a_srgb.png 3x3a_srgb_lossless.jxl -d 0 + +cjxl 3x3.png temp.jxl -x color_space=RGB_D65_202_Rel_PeQ -d 0 +djxl temp.jxl 3x3_pq.png +cjxl 3x3_pq.png 3x3_pq_lossy.jxl -d 0.1 -e 7 +cjxl 3x3_pq.png 3x3_pq_lossless.jxl -d 0 + +cjxl 3x3a.png temp.jxl -x color_space=RGB_D65_202_Rel_PeQ -d 0 +djxl temp.jxl 3x3a_pq.png +cjxl 3x3a_pq.png 3x3a_pq_lossy.jxl -d 0.1 -e 7 +cjxl 3x3a_pq.png 3x3a_pq_lossless.jxl -d 0 + +cjxl 3x3.png temp.jxl -x color_space=RGB_D65_202_Rel_HLG -d 0 +djxl temp.jxl 3x3_hlg.png +cjxl 3x3_hlg.png 3x3_hlg_lossy.jxl -d 0.1 -e 7 +cjxl 3x3_hlg.png 3x3_hlg_lossless.jxl -d 0 + +cjxl 3x3a.png temp.jxl -x color_space=RGB_D65_202_Rel_HLG -d 0 +djxl temp.jxl 3x3a_hlg.png +cjxl 3x3a_hlg.png 3x3a_hlg_lossy.jxl -d 0.1 -e 7 +cjxl 3x3a_hlg.png 3x3a_hlg_lossless.jxl -d 0 + +convert icc-v2-gbr.jpg icc-v2-gbr.icc +cjxl 3x3.png temp.jxl -x icc_pathname=icc-v2-gbr.icc -d 0 +djxl temp.jxl 3x3_gbr.png +cjxl 3x3_gbr.png 3x3_gbr_lossy.jxl -d 0.1 -e 7 +cjxl 3x3_gbr.png 3x3_gbr_lossless.jxl -d 0 + +cjxl 3x3a.png temp.jxl -x icc_pathname=icc-v2-gbr.icc -d 0 +djxl temp.jxl 3x3a_gbr.png +cjxl 3x3a_gbr.png 3x3a_gbr_lossy.jxl -d 0.1 -e 7 +cjxl 3x3a_gbr.png 3x3a_gbr_lossless.jxl -d 0 + +cjxl animated.gif animated.jxl + +for i in $(seq 0 9); do J=$(printf '%03d' $i); convert -fill black -size 500x500 -font 'Courier' -pointsize 72 -gravity center label:$J $J.png; done +convert -delay 20 *.png count.gif +cjxl count.gif count.jxl + +convert -size 680x420 xc:black black.png +cjxl --group_order 1 -d 0 black.png black.jxl +dd bs=1 count=46 if=black.jxl of=partial_black.jxl +``` diff --git a/third_party/blink/web_tests/virtual/jxl-enabled/README.md b/third_party/blink/web_tests/virtual/jxl-enabled/README.md new file mode 100644 --- /dev/null +++ b/third_party/blink/web_tests/virtual/jxl-enabled/README.md @@ -0,0 +1,5 @@ +This suite runs the tests with +--enable-features=JXL + +See the issue for more details: +https://crbug.com/1178058 diff --git a/third_party/libjxl/BUILD.gn b/third_party/libjxl/BUILD.gn new file mode 100644 --- /dev/null +++ b/third_party/libjxl/BUILD.gn @@ -0,0 +1,79 @@ +# Copyright 2020 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Import list of source files and flags from the jpeg-xl project. +import("src/lib/lib.gni") +import("//build/util/process_version.gni") + +# This config is applied to targets that depend on libjxl. +config("libjxl_external_config") { + include_dirs = [ + # Path to the public headers. + "src/lib/include", + + # Configuration headers normally generated by the cmake build system. + "gen_headers", + + "${target_gen_dir}/", + ] +} + +source_set("libjxl") { + sources = rebase_path(libjxl_dec_sources, ".", "src/lib") + + cflags_cc = [ + "-Wno-shadow", + "-Wno-unused-function", + ] + + defines = [ + "JPEGXL_ENABLE_SKCMS=1", + + # Disabling decode-to-JPEG bytes in the library removes about 20% + # of the binary size (as measured in android arm builds). + # Transcoding back to JPEG is not used in Chrome, only decoding to + # pixels is used even for files that were originally transcoded + # *from* JPEG. + "JPEGXL_ENABLE_TRANSCODE_JPEG=0", + ] + + if (is_official_build) { + # Disable assertion messages, saving about 6 kB in android. + defines += [ "JXL_DEBUG_ON_ABORT=0" ] + } + + include_dirs = [ + "src", + "src/lib/include/", + "${target_gen_dir}/", + "//third_party/skia/include/third_party/skcms", # for "skcms.h" + ] + + deps = [ + ":libjxml_version", + "//skia:skcms", + "//third_party/brotli:dec", + "//third_party/highway:libhwy", + ] + + public_configs = [ ":libjxl_external_config" ] +} + +process_version("libjxml_version") { + write_file("$target_gen_dir/jxl/version", libjxl_version_defines) + + template_file = "src/lib/jxl/version.h.in" + output = "$target_gen_dir/jxl/version.h" + + sources = [ "$target_gen_dir/jxl/version" ] + + extra_args = [ + "-e", + "JPEGXL_MAJOR_VERSION=\"%s\"%(JPEGXL_MAJOR_VERSION)", + "-e", + "JPEGXL_MINOR_VERSION=\"%s\"%(JPEGXL_MINOR_VERSION)", + "-e", + "JPEGXL_PATCH_VERSION=\"%s\"%(JPEGXL_PATCH_VERSION)", + ] +} diff --git a/third_party/libjxl/DIR_METADATA b/third_party/libjxl/DIR_METADATA new file mode 100644 --- /dev/null +++ b/third_party/libjxl/DIR_METADATA @@ -0,0 +1,4 @@ + +monorail: { + component: "Internals>Images>Codecs" +} diff --git a/third_party/libjxl/LICENSE b/third_party/libjxl/LICENSE new file mode 100644 --- /dev/null +++ b/third_party/libjxl/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) the JPEG XL Project Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/libjxl/OWNERS b/third_party/libjxl/OWNERS new file mode 100644 --- /dev/null +++ b/third_party/libjxl/OWNERS @@ -0,0 +1,9 @@ +# Owners: +noel@chromium.org +scroggo@google.com + +# Reviewers: +# eustas@chromium.org +# firsching@google.com +# sboukortt@google.com +# veluca@google.com diff --git a/third_party/libjxl/README.chromium b/third_party/libjxl/README.chromium new file mode 100644 --- /dev/null +++ b/third_party/libjxl/README.chromium @@ -0,0 +1,15 @@ +Name: JPEG XL image decoder library +Short Name: libjxl +URL: https://github.com/libjxl/libjxl +Version: 0.7rc +Date: 2022-08-24 +Revision: 3e246a860ea6d510d91ceca5413dcc50e8c41dd9 +License: BSD 3-Clause +Security Critical: yes +CPEPrefix: cpe:/a:libjxl_project:libjxl:0.7rc + +Description: +The reference implementation for the JPEG XL image encoder/decoder. + +Local Modifications: +None. Only decoder-side is compiled. diff --git a/third_party/libjxl/gen_headers/jxl/jxl_export.h b/third_party/libjxl/gen_headers/jxl/jxl_export.h new file mode 100644 --- /dev/null +++ b/third_party/libjxl/gen_headers/jxl/jxl_export.h @@ -0,0 +1,11 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_LIBJXL_GEN_HEADERS_JXL_JXL_EXPORT_H_ +#define THIRD_PARTY_LIBJXL_GEN_HEADERS_JXL_JXL_EXPORT_H_ + +#define JXL_EXPORT +#define JXL_DEPRECATED + +#endif // THIRD_PARTY_LIBJXL_GEN_HEADERS_JXL_JXL_EXPORT_H_ diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -26495,7 +26495,7 @@ Called by update_debug_scenarios.py.--> - + @@ -65579,6 +65579,7 @@ from previous Chrome versions. + -- 2.25.1