diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -1,5 +1,5 @@ { - "phabricator.uri" : "https://phabricator.services.mozilla.com/", - "repository.callsign": "MOZILLACENTRAL", + "phabricator.uri" : "https://phabricator-dev.allizom.org/", +"repository.callsign": "MOZILLACENTRAL", "history.immutable": false } diff --git a/media/libjxl/moz.yaml b/media/libjxl/moz.yaml --- a/media/libjxl/moz.yaml +++ b/media/libjxl/moz.yaml @@ -18,16 +18,16 @@ # Usually different from repository url url: https://gitlab.com/wg1/jpeg-xl # Human-readable identifier for this version/release # Generally "version NNN", "tag SSS", "bookmark SSS" - release: commit 30ea86ab4c1f1b98c21967a2e3d72a51fe77e454 (2021-05-12T21:50:20.000+02:00). + release: commit 44778c6902084bd239c5fb8eaa53bfd90dd9face (2021-05-21T20:39:54.000+02:00). # Revision to pull in # Must be a long or short commit SHA (long preferred) # NOTE(krosylight): Update highway together when updating this! - revision: 30ea86ab4c1f1b98c21967a2e3d72a51fe77e454 + revision: 44778c6902084bd239c5fb8eaa53bfd90dd9face # The package's license, where possible using the mnemonic from # https://spdx.org/licenses/ # Multiple licenses can be specified (as a YAML list) # A "LICENSE" file must exist containing the full license text diff --git a/third_party/jpeg-xl/ci.sh b/third_party/jpeg-xl/ci.sh --- a/third_party/jpeg-xl/ci.sh +++ b/third_party/jpeg-xl/ci.sh @@ -231,33 +231,50 @@ MR_ANCESTOR_SHA="" # Populate MR_HEAD_SHA and MR_ANCESTOR_SHA. merge_request_commits() { { set +x; } 2>/dev/null - # CI_BUILD_REF is the reference currently being build in the CI workflow. - MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") - if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then - # We are in a local branch, not a merge request. - MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) + # GITHUB_SHA is the current reference being build in GitHub Actions. + if [[ -n "${GITHUB_SHA:-}" ]]; then + git -C "${MYDIR}" fetch -q origin "${GITHUB_SHA}" + MR_HEAD_SHA="${GITHUB_SHA}" else + # CI_BUILD_REF is the reference currently being build in the CI workflow. + MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") + fi + + if [[ -n "${CI_MERGE_REQUEST_IID:-}" ]]; then # Merge request pipeline in CI. In this case the upstream is called "origin" # but it refers to the forked project that's the source of the merge # request. We need to get the target of the merge request, for which we need # to query that repository using our CI_JOB_TOKEN. echo "machine gitlab.com login gitlab-ci-token password ${CI_JOB_TOKEN}" \ >> "${HOME}/.netrc" git -C "${MYDIR}" fetch "${CI_MERGE_REQUEST_PROJECT_URL}" \ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) + elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then + # Pull request workflow in GitHub Actions. GitHub checkout action uses + # "origin" as the remote for the git checkout. + git -C "${MYDIR}" fetch -q origin "${GITHUB_BASE_REF}" + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) + else + # We are in a local branch, not a merge request. + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) fi + if [[ -z "${MR_ANCESTOR_SHA}" ]]; then echo "Warning, not tracking any branch, using the last commit in HEAD.">&2 # This prints the return value with just HEAD. MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^") else - MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base --all \ - "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") + # GitHub runs the pipeline on a merge commit, no need to look for the common + # ancestor in that case. + if [[ -z "${GITHUB_BASE_REF:-}" ]]; then + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base \ + "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") + fi fi set -x } # Load the MR iid from the landed commit message when running not from a diff --git a/third_party/jpeg-xl/examples/jxlinfo.c b/third_party/jpeg-xl/examples/jxlinfo.c --- a/third_party/jpeg-xl/examples/jxlinfo.c +++ b/third_party/jpeg-xl/examples/jxlinfo.c @@ -44,20 +44,20 @@ JxlBasicInfo info; int seen_basic_info = 0; for (;;) { - // The firs time, this will output JXL_DEC_NEED_MORE_INPUT because no + // The first time, this will output JXL_DEC_NEED_MORE_INPUT because no // input is set yet, this is ok since the input is set when handling this // event. JxlDecoderStatus status = JxlDecoderProcessInput(dec); if (status == JXL_DEC_ERROR) { fprintf(stderr, "Decoder error\n"); break; } else if (status == JXL_DEC_NEED_MORE_INPUT) { - // The firstt time there is nothing to release and it returns 0, but that + // The first time there is nothing to release and it returns 0, but that // is ok. size_t remaining = JxlDecoderReleaseInput(dec); // move any remaining bytes to the front if necessary if (remaining != 0) { memmove(data, data + data_size - remaining, remaining); @@ -65,10 +65,14 @@ // resize the buffer to append one more chunk of data // TODO(lode): avoid unnecessary reallocations data = (uint8_t*)realloc(data, remaining + chunk_size); // append bytes read from the file behind the remaining bytes size_t read_size = fread(data + remaining, 1, chunk_size, file); + if (read_size == 0 && feof(file)) { + fprintf(stderr, "Unexpected EOF\n"); + break; + } data_size = remaining + read_size; JxlDecoderSetInput(dec, data, data_size); } else if (status == JXL_DEC_SUCCESS) { // Finished all processing. break; diff --git a/third_party/jpeg-xl/lib/extras/codec_apng.cc b/third_party/jpeg-xl/lib/extras/codec_apng.cc --- a/third_party/jpeg-xl/lib/extras/codec_apng.cc +++ b/third_party/jpeg-xl/lib/extras/codec_apng.cc @@ -220,10 +220,11 @@ CHUNK chunkIHDR; std::vector chunksInfo; bool isAnimated = false; bool skipFirst = false; bool hasInfo = false; + bool all_dispose_bg = true; APNGFrame frameRaw = {}; if (!(f = fmemopen((void*)bytes.data(), bytes.size(), "rb"))) { return JXL_FAILURE("Failed to fmemopen"); } @@ -290,23 +291,25 @@ ImageBundle bundle(&io->metadata.m); bundle.duration = delay_num * 1000 / delay_den; bundle.origin.x0 = x0; bundle.origin.y0 = y0; // TODO(veluca): this could in principle be implemented. - if (last_base_was_none && + if (last_base_was_none && !all_dispose_bg && (x0 != 0 || y0 != 0 || w0 != w || h0 != h || bop != 0)) { return JXL_FAILURE( "APNG with dispose-to-0 is not supported for non-full or " "blended frames"); } switch (dop) { case 0: bundle.use_for_next_frame = true; last_base_was_none = false; + all_dispose_bg = false; break; case 2: bundle.use_for_next_frame = false; + all_dispose_bg = false; break; default: bundle.use_for_next_frame = false; last_base_was_none = true; } diff --git a/third_party/jpeg-xl/lib/extras/codec_png.cc b/third_party/jpeg-xl/lib/extras/codec_png.cc --- a/third_party/jpeg-xl/lib/extras/codec_png.cc +++ b/third_party/jpeg-xl/lib/extras/codec_png.cc @@ -788,10 +788,11 @@ io->metadata.m.bit_depth.bits_per_sample, endianness, /*flipped_y=*/false, pool, &io->Main()); JXL_RETURN_IF_ERROR(ok); io->dec_pixels = w * h; io->metadata.m.bit_depth.bits_per_sample = io->Main().DetectRealBitdepth(); + io->metadata.m.xyb_encoded = false; SetIntensityTarget(io); if (!reader.HaveColorProfile()) { JXL_RETURN_IF_ERROR(ApplyHints(is_gray, io)); } else { (void)io->dec_hints.Foreach( diff --git a/third_party/jpeg-xl/lib/extras/codec_psd.cc b/third_party/jpeg-xl/lib/extras/codec_psd.cc --- a/third_party/jpeg-xl/lib/extras/codec_psd.cc +++ b/third_party/jpeg-xl/lib/extras/codec_psd.cc @@ -575,22 +575,30 @@ } ImageBundle& layer = io->frames[0]; std::vector chan_id(real_nb_channels); std::iota(chan_id.begin(), chan_id.end(), 0); std::vector invert(real_nb_channels, false); + if (static_cast(spotcolor.size()) + colormodel + 1 < + real_nb_channels) { + return JXL_FAILURE("Inconsistent layer configuration"); + } if (!merged_has_alpha) { + if (colormodel <= real_nb_channels) { + return JXL_FAILURE("Inconsistent layer configuration"); + } chan_id.erase(chan_id.begin() + colormodel); invert.erase(invert.begin() + colormodel); - } else + } else { colormodel++; + } for (size_t i = colormodel; i < invert.size(); i++) { if (spotcolor[i - colormodel][5] == 2) invert[i] = true; if (spotcolor[i - colormodel][5] == 0) invert[i] = true; } - JXL_RETURN_IF_ERROR(decode_layer(pos, maxpos, layer, chan_id, invert, - layer.xsize(), layer.ysize(), version, - (have_only_merged ? 0 : colormodel), false, bitdepth)); + JXL_RETURN_IF_ERROR(decode_layer( + pos, maxpos, layer, chan_id, invert, layer.xsize(), layer.ysize(), + version, (have_only_merged ? 0 : colormodel), false, bitdepth)); } if (io->frames.empty()) return JXL_FAILURE("PSD: no layers"); io->SetSize(xsize, ysize); diff --git a/third_party/jpeg-xl/lib/include/jxl/decode.h b/third_party/jpeg-xl/lib/include/jxl/decode.h --- a/third_party/jpeg-xl/lib/include/jxl/decode.h +++ b/third_party/jpeg-xl/lib/include/jxl/decode.h @@ -133,14 +133,18 @@ /** An error occured, for example invalid input file or out of memory. * TODO(lode): add function to get error information from decoder. */ JXL_DEC_ERROR = 1, - /** The decoder needs more input bytes to continue. In the next - * JxlDecoderProcessInput call, next_in and avail_in must point to more - * bytes to continue. If *avail_in is not 0, the new bytes must be appended to - * the *avail_in last previous bytes. + /** The decoder needs more input bytes to continue. Before the next + * JxlDecoderProcessInput call, more input data must be set, by calling + * JxlDecoderReleaseInput (if input was set previously) and then calling + * JxlDecoderSetInput. JxlDecoderReleaseInput returns how many bytes are + * not yet processed, before a next call to JxlDecoderProcessInput all + * unprocessed bytes must be provided again (the address need not match, but + * the contents must), and more bytes must be concatenated after the + * unprocessed bytes. */ JXL_DEC_NEED_MORE_INPUT = 2, /** The decoder is able to decode a preview image and requests setting a * preview output buffer using JxlDecoderSetPreviewOutBuffer. This occurs if @@ -275,12 +279,12 @@ /** * Returns a hint indicating how many more bytes the decoder is expected to * need to make JxlDecoderGetBasicInfo available after the next * JxlDecoderProcessInput call. This is a suggested large enough value for - * the *avail_in parameter, but it is not guaranteed to be an upper bound nor - * a lower bound. + * the amount of bytes to provide in the next JxlDecoderSetInput call, but it is + * not guaranteed to be an upper bound nor a lower bound. * Can be used before the first JxlDecoderProcessInput call, and is correct * the first time in most cases. If not, JxlDecoderSizeHintBasicInfo can be * called again to get an updated hint. * * @param dec decoder object diff --git a/third_party/jpeg-xl/lib/jxl/alpha.cc b/third_party/jpeg-xl/lib/jxl/alpha.cc --- a/third_party/jpeg-xl/lib/jxl/alpha.cc +++ b/third_party/jpeg-xl/lib/jxl/alpha.cc @@ -72,11 +72,11 @@ } void PerformAlphaWeightedAdd(const float* bg, const float* fg, const float* fga, float* out, size_t num_pixels, bool clamp) { if (fg == fga) { - memcpy(out, bg, num_pixels * sizeof(out)); + memcpy(out, bg, num_pixels * sizeof(*out)); } else { for (size_t x = 0; x < num_pixels; ++x) { out[x] = bg[x] + fg[x] * Clamp(fga[x]); } } diff --git a/third_party/jpeg-xl/lib/jxl/ans_test.cc b/third_party/jpeg-xl/lib/jxl/ans_test.cc --- a/third_party/jpeg-xl/lib/jxl/ans_test.cc +++ b/third_party/jpeg-xl/lib/jxl/ans_test.cc @@ -193,7 +193,97 @@ EXPECT_EQ(uint_config[i].lsb_in_token, uint_config_dec[i].lsb_in_token); } } } +void TestCheckpointing(bool ans, bool lz77) { + std::vector> input_values(1); + for (size_t i = 0; i < 1024; i++) { + input_values[0].push_back(Token(0, i % 4)); + } + // up to lz77 window size. + for (size_t i = 0; i < (1 << 20) - 1022; i++) { + input_values[0].push_back(Token(0, (i % 5) + 4)); + } + // Ensure that when the window wraps around, new values are different. + input_values[0].push_back(Token(0, 0)); + for (size_t i = 0; i < 1024; i++) { + input_values[0].push_back(Token(0, i % 4)); + } + + std::vector context_map; + EntropyEncodingData codes; + HistogramParams params; + params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77 + : HistogramParams::LZ77Method::kNone; + params.force_huffman = !ans; + + BitWriter writer; + { + auto input_values_copy = input_values; + BuildAndEncodeHistograms(params, 1, input_values_copy, &codes, &context_map, + &writer, 0, nullptr); + WriteTokens(input_values_copy[0], codes, context_map, &writer, 0, nullptr); + writer.ZeroPadToByte(); + } + + // We do not truncate the output. Reading past the end reads out zeroes + // anyway. + BitReader br(writer.GetSpan()); + Status status = true; + { + BitReaderScopedCloser bc(&br, &status); + + std::vector dec_context_map; + ANSCode decoded_codes; + ASSERT_TRUE(DecodeHistograms(&br, 1, &decoded_codes, &dec_context_map)); + ASSERT_EQ(dec_context_map, context_map); + ANSSymbolReader reader(&decoded_codes, &br); + + ANSSymbolReader::Checkpoint checkpoint; + size_t br_pos; + constexpr size_t kInterval = ANSSymbolReader::kMaxCheckpointInterval - 2; + for (size_t i = 0; i < input_values[0].size(); i++) { + if (i % kInterval == 0 && i > 0) { + reader.Restore(checkpoint); + ASSERT_TRUE(br.Close()); + br = BitReader(writer.GetSpan()); + br.SkipBits(br_pos); + for (size_t j = i - kInterval; j < i; j++) { + Token symbol = input_values[0][j]; + uint32_t read_symbol = + reader.ReadHybridUint(symbol.context, &br, dec_context_map); + ASSERT_EQ(read_symbol, symbol.value) << "j = " << j; + } + } + if (i % kInterval == 0) { + reader.Save(&checkpoint); + br_pos = br.TotalBitsConsumed(); + } + Token symbol = input_values[0][i]; + uint32_t read_symbol = + reader.ReadHybridUint(symbol.context, &br, dec_context_map); + ASSERT_EQ(read_symbol, symbol.value) << "i = " << i; + } + ASSERT_TRUE(reader.CheckANSFinalState()); + } + EXPECT_TRUE(status); +} + +TEST(ANSTest, TestCheckpointingANS) { + TestCheckpointing(/*ans=*/true, /*lz77=*/false); +} + +TEST(ANSTest, TestCheckpointingPrefix) { + TestCheckpointing(/*ans=*/false, /*lz77=*/false); +} + +TEST(ANSTest, TestCheckpointingANSLZ77) { + TestCheckpointing(/*ans=*/true, /*lz77=*/true); +} + +TEST(ANSTest, TestCheckpointingPrefixLZ77) { + TestCheckpointing(/*ans=*/false, /*lz77=*/true); +} + } // namespace } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/blending.h b/third_party/jpeg-xl/lib/jxl/blending.h --- a/third_party/jpeg-xl/lib/jxl/blending.h +++ b/third_party/jpeg-xl/lib/jxl/blending.h @@ -43,14 +43,17 @@ explicit RectBlender(bool done) : done_(done) {} bool done_; Rect current_overlap_; Rect current_cropbox_; - ImageBundle foreground_; ImageBundle* dest_; std::vector fg_ptrs_; + std::vector fg_strides_; std::vector bg_ptrs_; + std::vector bg_strides_; + std::vector fg_row_ptrs_; + std::vector bg_row_ptrs_; std::vector blending_info_; }; static bool NeedsBlending(PassesDecoderState* dec_state); diff --git a/third_party/jpeg-xl/lib/jxl/blending.cc b/third_party/jpeg-xl/lib/jxl/blending.cc --- a/third_party/jpeg-xl/lib/jxl/blending.cc +++ b/third_party/jpeg-xl/lib/jxl/blending.cc @@ -159,43 +159,28 @@ blender.current_overlap_.ysize() == 0) { blender.done_ = true; return blender; } - const Rect actual_input_rect( - input_rect.x0() + (blender.current_overlap_.x0() - rect.x0()), - input_rect.y0() + (blender.current_overlap_.y0() - rect.y0()), - blender.current_overlap_.xsize(), blender.current_overlap_.ysize()); - blender.current_cropbox_ = Rect(o_.x0 + blender.current_overlap_.x0(), o_.y0 + blender.current_overlap_.y0(), blender.current_overlap_.xsize(), blender.current_overlap_.ysize()); - Image3F cropped_foreground(actual_input_rect.xsize(), - actual_input_rect.ysize()); - CopyImageTo(actual_input_rect, foreground, &cropped_foreground); - blender.foreground_ = ImageBundle(dest_->metadata()); - blender.foreground_.SetFromImage( - std::move(cropped_foreground), - ColorEncoding::LinearSRGB() /* likely incorrect but unused anyway */); - if (!extra_channels.empty()) { - std::vector ec; - for (const auto& extra_channel : extra_channels) { - ImageF ec_image(actual_input_rect.xsize(), actual_input_rect.ysize()); - CopyImageTo(actual_input_rect, extra_channel, &ec_image); - ec.push_back(std::move(ec_image)); - } - blender.foreground_.SetExtraChannels(std::move(ec)); - } // Turn current_overlap_ from being relative to the full foreground to being - // relative to the rect. + // relative to the rect or input_rect. blender.current_overlap_ = Rect(blender.current_overlap_.x0() - rect.x0(), blender.current_overlap_.y0() - rect.y0(), blender.current_overlap_.xsize(), blender.current_overlap_.ysize()); + // And this one is relative to the `foreground` subimage. + const Rect input_overlap(blender.current_overlap_.x0() + input_rect.x0(), + blender.current_overlap_.y0() + input_rect.y0(), + blender.current_overlap_.xsize(), + blender.current_overlap_.ysize()); + blender.blending_info_.resize(extra_channels.size() + 1); auto make_blending = [&](const BlendingInfo& info, PatchBlending* pb) { pb->alpha_channel = info.alpha_channel; pb->clamp = info.clamp; switch (info.mode) { @@ -220,17 +205,37 @@ break; } default: { JXL_ABORT("Invalid blend mode"); // should have failed to decode } - }; + } }; make_blending(info_, &blender.blending_info_[0]); for (size_t i = 0; i < extra_channels.size(); i++) { make_blending((*ec_info_)[i], &blender.blending_info_[1 + i]); } + Rect cropbox_row = blender.current_cropbox_.Line(0); + Rect overlap_row = input_overlap.Line(0); + const auto num_ptrs = 3 + extra_channels.size(); + blender.fg_ptrs_.reserve(num_ptrs); + blender.fg_strides_.reserve(num_ptrs); + blender.bg_ptrs_.reserve(num_ptrs); + blender.bg_strides_.reserve(num_ptrs); + for (size_t c = 0; c < 3; c++) { + blender.fg_ptrs_.push_back(overlap_row.ConstPlaneRow(foreground, c, 0)); + blender.fg_strides_.push_back(foreground.PixelsPerRow()); + blender.bg_ptrs_.push_back(cropbox_row.PlaneRow(dest_->color(), c, 0)); + blender.bg_strides_.push_back(dest_->color()->PixelsPerRow()); + } + for (size_t c = 0; c < extra_channels.size(); c++) { + blender.fg_ptrs_.push_back(overlap_row.ConstRow(extra_channels[c], 0)); + blender.fg_strides_.push_back(extra_channels[c].PixelsPerRow()); + blender.bg_ptrs_.push_back(cropbox_row.Row(&dest_->extra_channels()[c], 0)); + blender.bg_strides_.push_back(dest_->extra_channels()[c].PixelsPerRow()); + } + return blender; } Status PerformBlending( const float* const* bg, const float* const* fg, float* const* out, @@ -353,24 +358,18 @@ if (done_ || y < current_overlap_.y0() || y >= current_overlap_.y0() + current_overlap_.ysize()) { return true; } y -= current_overlap_.y0(); - Rect cropbox_row = current_cropbox_.Line(y); - Rect overlap_row = Rect(0, y, current_overlap_.xsize(), 1); - fg_ptrs_.resize(3 + foreground_.extra_channels().size()); - bg_ptrs_.resize(3 + foreground_.extra_channels().size()); - for (size_t c = 0; c < 3; c++) { - fg_ptrs_[c] = overlap_row.ConstPlaneRow(*foreground_.color(), c, 0); - bg_ptrs_[c] = cropbox_row.PlaneRow(dest_->color(), c, 0); + fg_row_ptrs_.resize(fg_ptrs_.size()); + bg_row_ptrs_.resize(bg_ptrs_.size()); + for (size_t c = 0; c < fg_row_ptrs_.size(); c++) { + fg_row_ptrs_[c] = fg_ptrs_[c] + y * fg_strides_[c]; + bg_row_ptrs_[c] = bg_ptrs_[c] + y * bg_strides_[c]; } - for (size_t c = 0; c < foreground_.extra_channels().size(); c++) { - fg_ptrs_[c + 3] = overlap_row.ConstRow(foreground_.extra_channels()[c], 0); - bg_ptrs_[c + 3] = cropbox_row.Row(&dest_->extra_channels()[c], 0); - } - return PerformBlending(bg_ptrs_.data(), fg_ptrs_.data(), bg_ptrs_.data(), - current_overlap_.xsize(), blending_info_[0], - blending_info_.data() + 1, + return PerformBlending(bg_row_ptrs_.data(), fg_row_ptrs_.data(), + bg_row_ptrs_.data(), current_overlap_.xsize(), + blending_info_[0], blending_info_.data() + 1, dest_->metadata()->extra_channel_info); } } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/blending_test.cc b/third_party/jpeg-xl/lib/jxl/blending_test.cc --- a/third_party/jpeg-xl/lib/jxl/blending_test.cc +++ b/third_party/jpeg-xl/lib/jxl/blending_test.cc @@ -46,7 +46,58 @@ EXPECT_TRUE(SamePixels(ib.color(), *frame.Main().color())); ++i; } } +TEST(BlendingTest, Offset) { + const PaddedBytes background_bytes = ReadTestData("jxl/splines.png"); + CodecInOut background; + ASSERT_TRUE(SetFromBytes(Span(background_bytes), &background)); + const PaddedBytes foreground_bytes = + ReadTestData("jxl/grayscale_patches.png"); + CodecInOut foreground; + ASSERT_TRUE(SetFromBytes(Span(foreground_bytes), &foreground)); + + ImageBlender blender; + ImageBundle output; + CodecMetadata nonserialized_metadata; + ASSERT_TRUE( + nonserialized_metadata.size.Set(background.xsize(), background.ysize())); + PassesSharedState state; + state.frame_header.blending_info.mode = BlendMode::kReplace; + state.frame_header.blending_info.source = 0; + state.frame_header.nonserialized_metadata = &nonserialized_metadata; + state.metadata = &background.metadata; + state.reference_frames[0].frame = &background.Main(); + PassesDecoderState dec_state; + dec_state.shared = &state; + const FrameOrigin foreground_origin = {-50, -50}; + ASSERT_TRUE(blender.PrepareBlending(&dec_state, foreground_origin, + foreground.xsize(), foreground.ysize(), + background.Main().c_current(), &output)); + + static constexpr int kStep = 20; + for (size_t x0 = 0; x0 < foreground.xsize(); x0 += kStep) { + for (size_t y0 = 0; y0 < foreground.ysize(); y0 += kStep) { + const Rect rect = + Rect(x0, y0, kStep, kStep).Intersection(Rect(foreground.Main())); + Image3F foreground_crop(rect.xsize(), rect.ysize()); + CopyImageTo(rect, *foreground.Main().color(), Rect(foreground_crop), + &foreground_crop); + auto rect_blender = + blender.PrepareRect(rect, foreground_crop, {}, Rect(foreground_crop)); + for (size_t y = 0; y < rect.ysize(); ++y) { + ASSERT_TRUE(rect_blender.DoBlending(y)); + } + } + } + + const PaddedBytes expected_bytes = + ReadTestData("jxl/blending/grayscale_patches_on_splines.png"); + CodecInOut expected; + ASSERT_TRUE(SetFromBytes(Span(expected_bytes), &expected)); + VerifyRelativeError(*expected.Main().color(), *output.color(), 1. / (2 * 255), + 0); +} + } // namespace } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/chroma_from_luma.h b/third_party/jpeg-xl/lib/jxl/chroma_from_luma.h --- a/third_party/jpeg-xl/lib/jxl/chroma_from_luma.h +++ b/third_party/jpeg-xl/lib/jxl/chroma_from_luma.h @@ -59,11 +59,11 @@ // since the CfL denominator is 84, this leaves 12 bits of mantissa to be used. // For extra caution, we use 11. static constexpr uint8_t kCFLFixedPointPrecision = 11; static constexpr U32Enc kColorFactorDist(Val(kDefaultColorFactor), Val(256), - BitsOffset(2, 8), BitsOffset(258, 12)); + BitsOffset(8, 2), BitsOffset(16, 258)); struct ColorCorrelationMap { ColorCorrelationMap() = default; // xsize/ysize are in pixels // set XYB=false to do something close to no-op cmap (needed for now since diff --git a/third_party/jpeg-xl/lib/jxl/common.h b/third_party/jpeg-xl/lib/jxl/common.h --- a/third_party/jpeg-xl/lib/jxl/common.h +++ b/third_party/jpeg-xl/lib/jxl/common.h @@ -85,11 +85,11 @@ // Maximum number of passes in an image. constexpr size_t kMaxNumPasses = 11; // Maximum number of reference frames. -constexpr size_t kMaxNumReferenceFrames = 3; +constexpr size_t kMaxNumReferenceFrames = 4; // Dimensions of a frame, in pixels, and other derived dimensions. // Computed from FrameHeader. // TODO(veluca): add extra channels. struct FrameDimensions { diff --git a/third_party/jpeg-xl/lib/jxl/dec_ans.h b/third_party/jpeg-xl/lib/jxl/dec_ans.h --- a/third_party/jpeg-xl/lib/jxl/dec_ans.h +++ b/third_party/jpeg-xl/lib/jxl/dec_ans.h @@ -351,10 +351,59 @@ } } return true; } + static constexpr size_t kMaxCheckpointInterval = 512; + struct Checkpoint { + uint32_t state; + uint32_t num_to_copy; + uint32_t copy_pos; + uint32_t num_decoded; + uint32_t lz77_window[kMaxCheckpointInterval]; + }; + void Save(Checkpoint* checkpoint) { + checkpoint->state = state_; + checkpoint->num_decoded = num_decoded_; + checkpoint->num_to_copy = num_to_copy_; + checkpoint->copy_pos = copy_pos_; + if (lz77_window_) { + size_t win_start = num_decoded_ & kWindowMask; + size_t win_end = (num_decoded_ + kMaxCheckpointInterval) & kWindowMask; + if (win_end > win_start) { + memcpy(checkpoint->lz77_window, lz77_window_ + win_start, + (win_end - win_start) * sizeof(*lz77_window_)); + } else { + memcpy(checkpoint->lz77_window, lz77_window_ + win_start, + (kWindowSize - win_start) * sizeof(*lz77_window_)); + memcpy(checkpoint->lz77_window + (kWindowSize - win_start), + lz77_window_, win_end * sizeof(*lz77_window_)); + } + } + } + void Restore(const Checkpoint& checkpoint) { + state_ = checkpoint.state; + JXL_DASSERT(num_decoded_ <= + checkpoint.num_decoded + kMaxCheckpointInterval); + num_decoded_ = checkpoint.num_decoded; + num_to_copy_ = checkpoint.num_to_copy; + copy_pos_ = checkpoint.copy_pos; + if (lz77_window_) { + size_t win_start = num_decoded_ & kWindowMask; + size_t win_end = (num_decoded_ + kMaxCheckpointInterval) & kWindowMask; + if (win_end > win_start) { + memcpy(lz77_window_ + win_start, checkpoint.lz77_window, + (win_end - win_start) * sizeof(*lz77_window_)); + } else { + memcpy(lz77_window_ + win_start, checkpoint.lz77_window, + (kWindowSize - win_start) * sizeof(*lz77_window_)); + memcpy(lz77_window_, checkpoint.lz77_window + (kWindowSize - win_start), + win_end * sizeof(*lz77_window_)); + } + } + } + private: const AliasTable::Entry* JXL_RESTRICT alias_tables_; // not owned const HuffmanDecodingData* huffman_data_; bool use_prefix_code_; uint32_t state_ = ANS_SIGNATURE << 16u; diff --git a/third_party/jpeg-xl/lib/jxl/dec_external_image.cc b/third_party/jpeg-xl/lib/jxl/dec_external_image.cc --- a/third_party/jpeg-xl/lib/jxl/dec_external_image.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_external_image.cc @@ -57,20 +57,17 @@ const auto one = Set(d, 1.0f); const auto scale = Set(d, mul); for (size_t x = 0; x < vec_num; x += Lanes(d)) { auto v = Load(d, in + x); - // Check for NaNs. - JXL_DASSERT(AllTrue(v == v)); // Clamp turns NaN to 'min'. v = Clamp(v, Zero(d), one); auto i = NearestInt(v * scale); Store(BitCast(du, i), du, out + x); } for (size_t x = vec_num; x < num; x++) { float v = in[x]; - JXL_DASSERT(!std::isnan(v)); // Inverted condition grants that NaN is mapped to 0.0f. v = (v >= 0.0f) ? (v > 1.0f ? mul : (v * mul)) : 0.0f; out[x] = static_cast(v + 0.5f); } } diff --git a/third_party/jpeg-xl/lib/jxl/dec_modular.cc b/third_party/jpeg-xl/lib/jxl/dec_modular.cc --- a/third_party/jpeg-xl/lib/jxl/dec_modular.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_modular.cc @@ -475,41 +475,44 @@ } // TODO(eustas): could we detect it on earlier stage? if (gi.channel[c_in].w == 0 || gi.channel[c_in].h == 0) { return JXL_FAILURE("Empty image"); } + size_t xsize_shifted = DivCeil(xsize, 1 << gi.channel[c_in].hshift); + size_t ysize_shifted = DivCeil(ysize, 1 << gi.channel[c_in].vshift); + if (ysize_shifted != gi.channel[c_in].h || xsize_shifted != gi.channel[c_in].w) { + return JXL_FAILURE("Dimension mismatch"); + } if (frame_header.color_transform == ColorTransform::kXYB && c == 2) { JXL_ASSERT(!fp); RunOnPool( - pool, 0, ysize, jxl::ThreadPool::SkipInit(), + pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), [&](const int task, const int thread) { const size_t y = task; const pixel_type* const JXL_RESTRICT row_in = gi.channel[c_in].Row(y); const pixel_type* const JXL_RESTRICT row_in_Y = gi.channel[0].Row(y); float* const JXL_RESTRICT row_out = decoded.PlaneRow(c, y); HWY_DYNAMIC_DISPATCH(MultiplySum) - (xsize, row_in, row_in_Y, factor, row_out); + (xsize_shifted, row_in, row_in_Y, factor, row_out); }, "ModularIntToFloat"); } else if (fp) { int bits = metadata->m.bit_depth.bits_per_sample; int exp_bits = metadata->m.bit_depth.exponent_bits_per_sample; RunOnPool( - pool, 0, ysize, jxl::ThreadPool::SkipInit(), + pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), [&](const int task, const int thread) { const size_t y = task; const pixel_type* const JXL_RESTRICT row_in = gi.channel[c_in].Row(y); float* const JXL_RESTRICT row_out = decoded.PlaneRow(c, y); - int_to_float(row_in, row_out, xsize, bits, exp_bits); + int_to_float(row_in, row_out, xsize_shifted, bits, exp_bits); }, "ModularIntToFloat_losslessfloat"); } else { - size_t xsize_shifted = DivCeil(xsize, 1 << gi.channel[c_in].hshift); - size_t ysize_shifted = DivCeil(ysize, 1 << gi.channel[c_in].vshift); RunOnPool( pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), [&](const int task, const int thread) { const size_t y = task; const pixel_type* const JXL_RESTRICT row_in = diff --git a/third_party/jpeg-xl/lib/jxl/dec_patch_dictionary.cc b/third_party/jpeg-xl/lib/jxl/dec_patch_dictionary.cc --- a/third_party/jpeg-xl/lib/jxl/dec_patch_dictionary.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_patch_dictionary.cc @@ -191,12 +191,10 @@ const Rect& image_rect) const { JXL_CHECK(SameSize(opsin_rect, image_rect)); size_t num_ec = shared_->metadata->m.num_extra_channels; std::vector fg_ptrs(3 + num_ec); std::vector bg_ptrs(3 + num_ec); - BlendingInfo color_blending; - std::vector ec_blending(num_ec); for (size_t y = image_rect.y0(); y < image_rect.y0() + image_rect.ysize(); y++) { if (y + 1 >= patch_starts_.size()) continue; for (size_t id = patch_starts_[y]; id < patch_starts_[y + 1]; id++) { const PatchPosition& pos = positions_[sorted_patches_[id]]; diff --git a/third_party/jpeg-xl/lib/jxl/dec_reconstruct.cc b/third_party/jpeg-xl/lib/jxl/dec_reconstruct.cc --- a/third_party/jpeg-xl/lib/jxl/dec_reconstruct.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_reconstruct.cc @@ -152,13 +152,20 @@ return IfThenZeroElse( v <= Set(d, 1e-5f), FastPowf(d, v, Set(d, output_encoding_info.inverse_gamma))); }; for (size_t x = 0; x < rect.xsize(); x += Lanes(d)) { +#if MEMORY_SANITIZER + const auto mask = Iota(d, x) < Set(d, rect.xsize()); + const auto in_opsin_x = IfThenElseZero(mask, Load(d, row0 + x)); + const auto in_opsin_y = IfThenElseZero(mask, Load(d, row1 + x)); + const auto in_opsin_b = IfThenElseZero(mask, Load(d, row2 + x)); +#else const auto in_opsin_x = Load(d, row0 + x); const auto in_opsin_y = Load(d, row1 + x); const auto in_opsin_b = Load(d, row2 + x); +#endif JXL_COMPILER_FENCE; auto linear_r = Undefined(d); auto linear_g = Undefined(d); auto linear_b = Undefined(d); XybToRgb(d, in_opsin_x, in_opsin_y, in_opsin_b, @@ -971,11 +978,14 @@ 1 << frame_header.chroma_subsampling.HShift(c)), DivCeil(frame_dim.ysize_padded, 1 << frame_header.chroma_subsampling.VShift(c))); for (size_t i = 0; i < frame_header.chroma_subsampling.HShift(c); i++) { plane.InitializePaddingForUnalignedAccesses(); - plane = UpsampleH2(plane, pool); + const size_t output_xsize = + DivCeil(frame_dim.xsize_padded, + 1 << (frame_header.chroma_subsampling.HShift(c) - i - 1)); + plane = UpsampleH2(plane, output_xsize, pool); } for (size_t i = 0; i < frame_header.chroma_subsampling.VShift(c); i++) { plane.InitializePaddingForUnalignedAccesses(); plane = UpsampleV2(plane, pool); } diff --git a/third_party/jpeg-xl/lib/jxl/dec_xyb.h b/third_party/jpeg-xl/lib/jxl/dec_xyb.h --- a/third_party/jpeg-xl/lib/jxl/dec_xyb.h +++ b/third_party/jpeg-xl/lib/jxl/dec_xyb.h @@ -67,11 +67,11 @@ ImageF UpsampleV2(const ImageF& src, ThreadPool* pool); // WARNING: this uses unaligned accesses, so the caller must first call // src.InitializePaddingForUnalignedAccesses() to avoid msan crashes. -ImageF UpsampleH2(const ImageF& src, ThreadPool* pool); +ImageF UpsampleH2(const ImageF& src, size_t output_xsize, ThreadPool* pool); bool HasFastXYBTosRGB8(); void FastXYBTosRGB8(const Image3F& input, const Rect& input_rect, const Rect& output_buf_rect, const ImageF* alpha, const Rect& alpha_rect, bool is_rgba, diff --git a/third_party/jpeg-xl/lib/jxl/dec_xyb.cc b/third_party/jpeg-xl/lib/jxl/dec_xyb.cc --- a/third_party/jpeg-xl/lib/jxl/dec_xyb.cc +++ b/third_party/jpeg-xl/lib/jxl/dec_xyb.cc @@ -220,16 +220,21 @@ * o := 3 * b + l * e := 3 * b + r * output: * |o1 e1 o2 e2 o3 e3 o4 e4| =: (o, e) */ -ImageF UpsampleH2(const ImageF& src, ThreadPool* pool) { +ImageF UpsampleH2(const ImageF& src, size_t output_xsize, ThreadPool* pool) { const size_t xsize = src.xsize(); const size_t ysize = src.ysize(); JXL_ASSERT(xsize != 0); JXL_ASSERT(ysize != 0); - ImageF dst(xsize * 2, ysize); + JXL_ASSERT(DivCeil(output_xsize, 2) == xsize); + // Extra pixel in output might cause the whole extra vector overhead; thus + // we request specific output size. Should be safe, because the last 2 values + // are processed in non-vectorized form, and the "Plane" row padding concerns + // only about the case when unaligned vector store is applied at last pixel. + ImageF dst(output_xsize, ysize); constexpr size_t kGroupArea = kGroupDim * kGroupDim; const size_t lines_per_group = DivCeil(kGroupArea, xsize); const size_t num_stripes = DivCeil(ysize, lines_per_group); @@ -304,12 +309,12 @@ ImageF UpsampleV2(const ImageF& src, ThreadPool* pool) { return HWY_DYNAMIC_DISPATCH(UpsampleV2)(src, pool); } HWY_EXPORT(UpsampleH2); -ImageF UpsampleH2(const ImageF& src, ThreadPool* pool) { - return HWY_DYNAMIC_DISPATCH(UpsampleH2)(src, pool); +ImageF UpsampleH2(const ImageF& src, size_t output_xsize, ThreadPool* pool) { + return HWY_DYNAMIC_DISPATCH(UpsampleH2)(src, output_xsize, pool); } HWY_EXPORT(HasFastXYBTosRGB8); bool HasFastXYBTosRGB8() { return HWY_DYNAMIC_DISPATCH(HasFastXYBTosRGB8)(); } diff --git a/third_party/jpeg-xl/lib/jxl/decode.cc b/third_party/jpeg-xl/lib/jxl/decode.cc --- a/third_party/jpeg-xl/lib/jxl/decode.cc +++ b/third_party/jpeg-xl/lib/jxl/decode.cc @@ -293,10 +293,11 @@ std::vector section_info; std::vector section_status; std::vector section_received; }; +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) struct JxlDecoderStruct { JxlDecoderStruct() = default; JxlMemoryManager memory_manager; std::unique_ptr thread_pool; @@ -310,11 +311,13 @@ // guaranteed to be true for the last box because a jxl file may have multiple // "jxlp" boxes and it is possible (and permitted) that the last one is not a // final box that uses size 0 to indicate the end. bool last_codestream_seen; bool got_basic_info; - bool got_all_headers; // Codestream metadata headers + size_t header_except_icc_bits = 0; // To skip everything before ICC. + bool got_all_headers; // Codestream metadata headers + jxl::ICCReader icc_reader; // This means either we actually got the preview image, or determined we // cannot get it or there is none. bool got_preview_image; @@ -449,11 +452,13 @@ dec->stage = DecoderStage::kInited; dec->got_signature = false; dec->first_codestream_seen = false; dec->last_codestream_seen = false; dec->got_basic_info = false; + dec->header_except_icc_bits = 0; dec->got_all_headers = false; + dec->icc_reader.Reset(); dec->got_preview_image = false; dec->last_frame_reached = false; dec->file_pos = 0; dec->codestream_pos = 0; dec->codestream_begin = 0; @@ -654,24 +659,31 @@ return JXL_API_ERROR("invalid signature"); } Span span(in + pos, size - pos); auto reader = GetBitReader(span); - SizeHeader dummy_size_header; - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_size_header)); + + if (dec->header_except_icc_bits != 0) { + // Headers were decoded already. + reader->SkipBits(dec->header_except_icc_bits); + } else { + SizeHeader dummy_size_header; + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_size_header)); - // We already decoded the metadata to dec->metadata.m, no reason to - // overwrite it, use a dummy metadata instead. - ImageMetadata dummy_metadata; - JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_metadata)); + // We already decoded the metadata to dec->metadata.m, no reason to + // overwrite it, use a dummy metadata instead. + ImageMetadata dummy_metadata; + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_metadata)); - JXL_API_RETURN_IF_ERROR( - ReadBundle(span, reader.get(), &dec->metadata.transform_data)); + JXL_API_RETURN_IF_ERROR( + ReadBundle(span, reader.get(), &dec->metadata.transform_data)); + } + + dec->header_except_icc_bits = reader->TotalBitsConsumed(); if (dec->metadata.m.color_encoding.WantICC()) { - PaddedBytes icc; - jxl::Status status = ReadICC(reader.get(), &icc, memory_limit_base_); + jxl::Status status = dec->icc_reader.Init(reader.get(), memory_limit_base_); // Always check AllReadsWithinBounds, not all the C++ decoder implementation // handles reader out of bounds correctly yet (e.g. context map). Not // checking AllReadsWithinBounds can cause reader->Close() to trigger an // assert, but we don't want library to quit program for invalid codestream. if (!reader->AllReadsWithinBounds()) { @@ -682,10 +694,19 @@ return JXL_DEC_NEED_MORE_INPUT; } // Other non-successful status is an error return JXL_DEC_ERROR; } + PaddedBytes icc; + status = dec->icc_reader.Process(reader.get(), &icc); + if (!status) { + if (status.code() == StatusCode::kNotEnoughBytes) { + return JXL_DEC_NEED_MORE_INPUT; + } + // Other non-successful status is an error + return JXL_DEC_ERROR; + } if (!dec->metadata.m.color_encoding.SetICCRaw(std::move(icc))) { return JXL_DEC_ERROR; } } @@ -761,10 +782,13 @@ JxlDecoderStatus ParseFrameHeader(JxlDecoder* dec, jxl::FrameHeader* frame_header, const uint8_t* in, size_t size, size_t pos, bool is_preview, size_t* frame_size, size_t* dc_size) { + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } Span span(in + pos, size - pos); auto reader = GetBitReader(span); frame_header->nonserialized_is_preview = is_preview; jxl::Status status = DecodeFrameHeader(reader.get(), frame_header); @@ -981,10 +1005,13 @@ } } if (dec->frame_stage == FrameStage::kTOC) { size_t pos = dec->frame_start - dec->codestream_pos; + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } Span span(in + pos, size - pos); auto reader = GetBitReader(span); if (!dec->passes_state) { dec->passes_state.reset(new jxl::PassesDecoderState()); @@ -1018,12 +1045,18 @@ dec->sections.reset( new Sections(dec->frame_dec.get(), dec->frame_size, sections_begin)); JXL_API_RETURN_IF_ERROR(dec->sections->Init()); - dec->frame_dec_in_progress = true; - dec->frame_stage = FrameStage::kDC; + // If we don't need pixels, we can skip actually decoding the frames + // (kFull / kDC / kFullOut / kDCOut). By not updating frame_stage, none of + // these stages will execute, and the loop will continue from the next + // frame. + if (dec->events_wanted & (JXL_DEC_DC_IMAGE | JXL_DEC_FULL_IMAGE)) { + dec->frame_dec_in_progress = true; + dec->frame_stage = FrameStage::kDC; + } } if (dec->frame_stage == FrameStage::kDC) { if (!(dec->events_wanted & JXL_DEC_DC_IMAGE)) { dec->frame_stage = FrameStage::kFull; @@ -1075,10 +1108,13 @@ }, is_rgba); } size_t pos = dec->frame_start - dec->codestream_pos; + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } bool get_dc = dec->is_last_of_still && (dec->frame_stage == FrameStage::kDC) && dec->dc_size != 0; dec->sections->SetInput(in + pos, size - pos); jxl::Status status = diff --git a/third_party/jpeg-xl/lib/jxl/enc_file.cc b/third_party/jpeg-xl/lib/jxl/enc_file.cc --- a/third_party/jpeg-xl/lib/jxl/enc_file.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_file.cc @@ -122,12 +122,14 @@ Status PrepareCodecMetadataFromIO(const CompressParams& cparams, const CodecInOut* io, CodecMetadata* metadata) { *metadata = io->metadata; + size_t ups = 1; + if (cparams.already_downsampled) ups = cparams.resampling; - JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize(), io->ysize())); + JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize() * ups, io->ysize() * ups)); // Keep ICC profile in lossless modes because a reconstructed profile may be // slightly different (quantization). // Also keep ICC in JPEG reconstruction mode as we need byte-exact profiles. const bool lossless_modular = diff --git a/third_party/jpeg-xl/lib/jxl/enc_frame.cc b/third_party/jpeg-xl/lib/jxl/enc_frame.cc --- a/third_party/jpeg-xl/lib/jxl/enc_frame.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_frame.cc @@ -338,15 +338,17 @@ return JXL_FAILURE("Invalid ec_resampling factor"); } // Resized frames. if (frame_info.frame_type != FrameType::kDCFrame) { frame_header->frame_origin = ib.origin; - frame_header->frame_size.xsize = ib.xsize(); - frame_header->frame_size.ysize = ib.ysize(); + size_t ups = 1; + if (cparams.already_downsampled) ups = cparams.resampling; + frame_header->frame_size.xsize = ib.xsize() * ups; + frame_header->frame_size.ysize = ib.ysize() * ups; if (ib.origin.x0 != 0 || ib.origin.y0 != 0 || - ib.xsize() != frame_header->default_xsize() || - ib.ysize() != frame_header->default_ysize()) { + frame_header->frame_size.xsize != frame_header->default_xsize() || + frame_header->frame_size.ysize != frame_header->default_ysize()) { frame_header->custom_size_or_origin = true; } } // Upsampling. frame_header->upsampling = cparams.resampling; @@ -1053,13 +1055,11 @@ cparams.gaborish = Override::kOff; cparams.epf = 0; cparams.modular_mode = false; } - const size_t xsize = ib.xsize(); - const size_t ysize = ib.ysize(); - if (xsize == 0 || ysize == 0) return JXL_FAILURE("Empty image"); + if (ib.xsize() == 0 || ib.ysize() == 0) return JXL_FAILURE("Empty image"); // Assert that this metadata is correctly set up for the compression params, // this should have been done by enc_file.cc JXL_ASSERT(metadata->m.xyb_encoded == (cparams.color_transform == ColorTransform::kXYB)); @@ -1182,21 +1182,21 @@ if (frame_header->encoding == FrameEncoding::kVarDCT) { PadImageToBlockMultipleInPlace(&opsin); JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( ib_or_linear, &opsin, pool, modular_frame_encoder.get(), writer, frame_header.get())); - } else if (frame_header->upsampling != 1) { + } else if (frame_header->upsampling != 1 && !cparams.already_downsampled) { // In VarDCT mode, LossyFrameHeuristics takes care of running downsampling // after noise, if necessary. DownsampleImage(&opsin, frame_header->upsampling); } } else { JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( &ib, &opsin, pool, modular_frame_encoder.get(), writer, frame_header.get())); } - if (cparams.ec_resampling != 1) { + if (cparams.ec_resampling != 1 && !cparams.already_downsampled) { extra_channels = &extra_channels_storage; for (size_t i = 0; i < ib.extra_channels().size(); i++) { extra_channels_storage.emplace_back(CopyImage(ib.extra_channels()[i])); DownsampleImage(&extra_channels_storage.back(), cparams.ec_resampling); } diff --git a/third_party/jpeg-xl/lib/jxl/enc_heuristics.cc b/third_party/jpeg-xl/lib/jxl/enc_heuristics.cc --- a/third_party/jpeg-xl/lib/jxl/enc_heuristics.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_heuristics.cc @@ -250,11 +250,11 @@ if (!GetNoiseParameter(*opsin, &shared.image_features.noise_params, quality_coef)) { shared.frame_header.flags &= ~FrameHeader::kNoise; } } - if (enc_state->shared.frame_header.upsampling != 1) { + if (enc_state->shared.frame_header.upsampling != 1 && !cparams.already_downsampled) { // In VarDCT mode, LossyFrameHeuristics takes care of running downsampling // after noise, if necessary. DownsampleImage(opsin, cparams.resampling); PadImageToBlockMultipleInPlace(opsin); } diff --git a/third_party/jpeg-xl/lib/jxl/enc_icc_codec.cc b/third_party/jpeg-xl/lib/jxl/enc_icc_codec.cc --- a/third_party/jpeg-xl/lib/jxl/enc_icc_codec.cc +++ b/third_party/jpeg-xl/lib/jxl/enc_icc_codec.cc @@ -423,11 +423,12 @@ tokens[0].emplace_back( ICCANSContext(i, i > 0 ? enc[i - 1] : 0, i > 1 ? enc[i - 2] : 0), enc[i]); } HistogramParams params; - params.lz77_method = HistogramParams::LZ77Method::kOptimal; + params.lz77_method = enc.size() < 4096 ? HistogramParams::LZ77Method::kOptimal + : HistogramParams::LZ77Method::kLZ77; EntropyEncodingData code; std::vector context_map; params.force_huffman = true; BuildAndEncodeHistograms(params, kNumICCContexts, tokens, &code, &context_map, writer, layer, aux_out); diff --git a/third_party/jpeg-xl/lib/jxl/enc_params.h b/third_party/jpeg-xl/lib/jxl/enc_params.h --- a/third_party/jpeg-xl/lib/jxl/enc_params.h +++ b/third_party/jpeg-xl/lib/jxl/enc_params.h @@ -231,10 +231,12 @@ bool use_new_heuristics = false; // Down/upsample the image before encoding / after decoding by this factor. size_t resampling = 1; size_t ec_resampling = 1; + // Skip the downsampling before encoding if this is true. + bool already_downsampled = false; }; static constexpr float kMinButteraugliForDynamicAR = 0.5f; static constexpr float kMinButteraugliForDots = 3.0f; static constexpr float kMinButteraugliToSubtractOriginalPatches = 3.0f; diff --git a/third_party/jpeg-xl/lib/jxl/icc_codec.h b/third_party/jpeg-xl/lib/jxl/icc_codec.h --- a/third_party/jpeg-xl/lib/jxl/icc_codec.h +++ b/third_party/jpeg-xl/lib/jxl/icc_codec.h @@ -23,19 +23,40 @@ #include "lib/jxl/aux_out.h" #include "lib/jxl/aux_out_fwd.h" #include "lib/jxl/base/compiler_specific.h" #include "lib/jxl/base/padded_bytes.h" #include "lib/jxl/base/status.h" +#include "lib/jxl/dec_ans.h" #include "lib/jxl/dec_bit_reader.h" #include "lib/jxl/enc_bit_writer.h" namespace jxl { // Should still be called if `icc.empty()` - if so, writes only 1 bit. Status WriteICC(const PaddedBytes& icc, BitWriter* JXL_RESTRICT writer, size_t layer, AuxOut* JXL_RESTRICT aux_out); +struct ICCReader { + Status Init(BitReader* reader, size_t output_limit); + Status Process(BitReader* reader, PaddedBytes* icc); + void Reset() { + bits_to_skip_ = 0; + decompressed_.clear(); + } + + private: + Status CheckEOI(BitReader* reader); + size_t i_ = 0; + size_t bits_to_skip_ = 0; + size_t used_bits_base_ = 0; + uint64_t enc_size_ = 0; + std::vector context_map_; + ANSCode code_; + ANSSymbolReader ans_reader_; + PaddedBytes decompressed_; +}; + // `icc` may be empty afterwards - if so, call CreateProfile. Does not append, // clears any original data that was in icc. // If `output_limit` is not 0, then returns error if resulting profile would be // longer than `output_limit` Status ReadICC(BitReader* JXL_RESTRICT reader, PaddedBytes* JXL_RESTRICT icc, diff --git a/third_party/jpeg-xl/lib/jxl/icc_codec.cc b/third_party/jpeg-xl/lib/jxl/icc_codec.cc --- a/third_party/jpeg-xl/lib/jxl/icc_codec.cc +++ b/third_party/jpeg-xl/lib/jxl/icc_codec.cc @@ -312,72 +312,102 @@ if (result->size() != osize) return JXL_FAILURE("Invalid result size"); return true; } -Status ReadICC(BitReader* JXL_RESTRICT reader, PaddedBytes* JXL_RESTRICT icc, - size_t output_limit) { - icc->clear(); - const auto checkEndOfInput = [&]() -> Status { - if (reader->AllReadsWithinBounds()) return true; - return JXL_STATUS(StatusCode::kNotEnoughBytes, - "Not enough bytes for reading ICC profile"); - }; - JXL_RETURN_IF_ERROR(checkEndOfInput()); - uint64_t enc_size = U64Coder::Read(reader); - if (enc_size > 268435456) { - // Avoid too large memory allocation for invalid file. - // TODO(lode): a more accurate limit would be the filesize of the JXL file, - // if we can have it available here. - return JXL_FAILURE("Too large encoded profile"); +Status ICCReader::Init(BitReader* reader, size_t output_limit) { + JXL_RETURN_IF_ERROR(CheckEOI(reader)); + used_bits_base_ = reader->TotalBitsConsumed(); + if (bits_to_skip_ == 0) { + enc_size_ = U64Coder::Read(reader); + if (enc_size_ > 268435456) { + // Avoid too large memory allocation for invalid file. + return JXL_FAILURE("Too large encoded profile"); + } + JXL_RETURN_IF_ERROR( + DecodeHistograms(reader, kNumICCContexts, &code_, &context_map_)); + ans_reader_ = ANSSymbolReader(&code_, reader); + i_ = 0; + decompressed_.resize(std::min(i_ + 0x400, enc_size_)); + for (; i_ < std::min(2, enc_size_); i_++) { + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, i_ > 0 ? decompressed_[i_ - 1] : 0, + i_ > 1 ? decompressed_[i_ - 2] : 0), + reader, context_map_); + } + if (enc_size_ > kPreambleSize) { + for (; i_ < kPreambleSize; i_++) { + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, decompressed_[i_ - 1], decompressed_[i_ - 2]), + reader, context_map_); + } + JXL_RETURN_IF_ERROR(CheckEOI(reader)); + JXL_RETURN_IF_ERROR( + CheckPreamble(decompressed_, enc_size_, output_limit)); + } + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + } else { + reader->SkipBits(bits_to_skip_); } - PaddedBytes decompressed; - std::vector context_map; - ANSCode code; - JXL_RETURN_IF_ERROR( - DecodeHistograms(reader, kNumICCContexts, &code, &context_map)); - ANSSymbolReader ans_reader(&code, reader); - size_t used_bits_base = reader->TotalBitsConsumed(); - size_t i = 0; - decompressed.resize(std::min(i + 0x400, enc_size)); + return true; +} - for (; i < std::min(2, enc_size); i++) { - decompressed[i] = ans_reader.ReadHybridUint( - ICCANSContext(i, i > 0 ? decompressed[i - 1] : 0, - i > 1 ? decompressed[i - 2] : 0), - reader, context_map); - } - if (enc_size > kPreambleSize) { - for (; i < kPreambleSize; i++) { - decompressed[i] = ans_reader.ReadHybridUint( - ICCANSContext(i, decompressed[i - 1], decompressed[i - 2]), reader, - context_map); +Status ICCReader::Process(BitReader* reader, PaddedBytes* icc) { + ANSSymbolReader::Checkpoint checkpoint; + size_t saved_i = 0; + auto save = [&]() { + ans_reader_.Save(&checkpoint); + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + saved_i = i_; + }; + save(); + auto check_and_restore = [&]() { + Status status = CheckEOI(reader); + if (!status) { + // not enough bytes. + ans_reader_.Restore(checkpoint); + i_ = saved_i; + return status; } - JXL_RETURN_IF_ERROR(checkEndOfInput()); - JXL_RETURN_IF_ERROR(CheckPreamble(decompressed, enc_size, output_limit)); + return Status(true); + }; + for (; i_ < enc_size_; i_++) { + if (i_ % ANSSymbolReader::kMaxCheckpointInterval == 0 && i_ > 0) { + JXL_RETURN_IF_ERROR(check_and_restore()); + save(); + if ((i_ > 0) && (((i_ & 0xFFFF) == 0))) { + float used_bytes = + (reader->TotalBitsConsumed() - used_bits_base_) / 8.0f; + if (i_ > used_bytes * 256) return JXL_FAILURE("Corrupted stream"); + } + decompressed_.resize(std::min(i_ + 0x400, enc_size_)); + } + JXL_DASSERT(i_ >= 2); + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, decompressed_[i_ - 1], decompressed_[i_ - 2]), reader, + context_map_); } - for (; i < enc_size; i++) { - if ((i & 0x3FF) == 0) { - JXL_RETURN_IF_ERROR(checkEndOfInput()); - if ((i > 0) && (((i & 0xFFFF) == 0))) { - float used_bytes = - (reader->TotalBitsConsumed() - used_bits_base) / 8.0f; - if (i > used_bytes * 256) return JXL_FAILURE("Corrupted stream"); - } - decompressed.resize(std::min(i + 0x400, enc_size)); - } - JXL_DASSERT(i >= 2); - decompressed[i] = ans_reader.ReadHybridUint( - ICCANSContext(i, decompressed[i - 1], decompressed[i - 2]), reader, - context_map); - } - JXL_RETURN_IF_ERROR(checkEndOfInput()); - if (!ans_reader.CheckANSFinalState()) { + JXL_RETURN_IF_ERROR(check_and_restore()); + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + if (!ans_reader_.CheckANSFinalState()) { return JXL_FAILURE("Corrupted ICC profile"); } - JXL_RETURN_IF_ERROR( - UnpredictICC(decompressed.data(), decompressed.size(), icc)); + icc->clear(); + return UnpredictICC(decompressed_.data(), decompressed_.size(), icc); +} + +Status ICCReader::CheckEOI(BitReader* reader) { + if (reader->AllReadsWithinBounds()) return true; + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for reading ICC profile"); +} + +Status ReadICC(BitReader* JXL_RESTRICT reader, PaddedBytes* JXL_RESTRICT icc, + size_t output_limit) { + ICCReader icc_reader; + JXL_RETURN_IF_ERROR(icc_reader.Init(reader, output_limit)); + JXL_RETURN_IF_ERROR(icc_reader.Process(reader, icc)); return true; } } // namespace jxl diff --git a/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h b/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h --- a/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h +++ b/third_party/jpeg-xl/lib/jxl/modular/encoding/context_predict.h @@ -464,10 +464,52 @@ kUseWP = 2, kForceComputeProperties = 4, kAllPredictions = 8, }; +JXL_INLINE pixel_type_w PredictOne(Predictor p, pixel_type_w left, + pixel_type_w top, pixel_type_w toptop, + pixel_type_w topleft, pixel_type_w topright, + pixel_type_w leftleft, + pixel_type_w toprightright, + pixel_type_w wp_pred) { + switch (p) { + case Predictor::Zero: + return pixel_type_w{0}; + case Predictor::Left: + return left; + case Predictor::Top: + return top; + case Predictor::Select: + return Select(left, top, topleft); + case Predictor::Weighted: + return wp_pred; + case Predictor::Gradient: + return pixel_type_w{ClampedGradient(left, top, topleft)}; + case Predictor::TopLeft: + return topleft; + case Predictor::TopRight: + return topright; + case Predictor::LeftLeft: + return leftleft; + case Predictor::Average0: + return (left + top) / 2; + case Predictor::Average1: + return (left + topleft) / 2; + case Predictor::Average2: + return (topleft + top) / 2; + case Predictor::Average3: + return (top + topright) / 2; + case Predictor::Average4: + return (6 * top - 2 * toptop + 7 * left + 1 * leftleft + + 1 * toprightright + 3 * topright + 8) / + 16; + default: + return pixel_type_w{0}; + } +} + template inline PredictionResult Predict( Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp, const intptr_t onerow, const size_t x, const size_t y, Predictor predictor, const MATreeLookup *lookup, const Channel *references, @@ -526,32 +568,18 @@ result.context = lr.context; result.guess = lr.offset; result.multiplier = lr.multiplier; predictor = lr.predictor; } - pixel_type_w pred_storage[kNumModularPredictors]; - if (!(mode & kAllPredictions)) { - predictions = pred_storage; + if (mode & kAllPredictions) { + for (size_t i = 0; i < kNumModularPredictors; i++) { + predictions[i] = PredictOne((Predictor)i, left, top, toptop, topleft, + topright, leftleft, toprightright, wp_pred); + } } - predictions[(int)Predictor::Zero] = 0; - predictions[(int)Predictor::Left] = left; - predictions[(int)Predictor::Top] = top; - predictions[(int)Predictor::Select] = Select(left, top, topleft); - predictions[(int)Predictor::Weighted] = wp_pred; - predictions[(int)Predictor::Gradient] = ClampedGradient(left, top, topleft); - predictions[(int)Predictor::TopLeft] = topleft; - predictions[(int)Predictor::TopRight] = topright; - predictions[(int)Predictor::LeftLeft] = leftleft; - predictions[(int)Predictor::Average0] = (left + top) / 2; - predictions[(int)Predictor::Average1] = (left + topleft) / 2; - predictions[(int)Predictor::Average2] = (topleft + top) / 2; - predictions[(int)Predictor::Average3] = (top + topright) / 2; - predictions[(int)Predictor::Average4] = - (6 * top - 2 * toptop + 7 * left + 1 * leftleft + 1 * toprightright + - 3 * topright + 8) / - 16; - result.guess += predictions[(int)predictor]; + result.guess += PredictOne(predictor, left, top, toptop, topleft, topright, + leftleft, toprightright, wp_pred); result.predictor = predictor; return result; } } // namespace detail diff --git a/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h b/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h --- a/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h +++ b/third_party/jpeg-xl/lib/jxl/modular/transform/palette.h @@ -42,10 +42,14 @@ static constexpr int kLargeCubeOffset = kSmallCube * kSmallCube * kSmallCube; // Inclusive. static constexpr int kMinImplicitPaletteIndex = -(2 * 72 - 1); +static constexpr pixel_type Scale(int value, int bit_depth, int denom) { + return (static_cast(value) * ((1 << bit_depth) - 1)) / denom; +} + // The purpose of this function is solely to extend the interpretation of // palette indices to implicit values. If index < nb_deltas, indicating that the // result is a delta palette entry, it is the responsibility of the caller to // treat it as such. static pixel_type GetPaletteValue(const pixel_type *const palette, int index, @@ -94,12 +98,11 @@ for (size_t i = 1; i < c; ++i) { divisor *= kSmallCube; } index /= divisor; } - index %= kSmallCube; - return (index * ((1 << bit_depth) - 1)) / kSmallCube + + return Scale(index % kSmallCube, bit_depth, kSmallCube) + (1 << (std::max(0, bit_depth - 3))); } else if (palette_size + kLargeCubeOffset <= index) { if (c >= kCubePow) return 0; index -= palette_size + kLargeCubeOffset; // TODO(eustas): should we take care of ambiguity created by @@ -109,12 +112,11 @@ for (size_t i = 1; i < c; ++i) { divisor *= kLargeCube; } index /= divisor; } - index %= kLargeCube; - return (index * ((1 << bit_depth) - 1)) / (kLargeCube - 1); + return Scale(index % kLargeCube, bit_depth, kLargeCube - 1); } return palette[c * onerow + static_cast(index)]; } @@ -230,11 +232,11 @@ const Channel &palette = input.channel[0]; const pixel_type *JXL_RESTRICT p_palette = input.channel[0].Row(0); intptr_t onerow = input.channel[0].plane.PixelsPerRow(); intptr_t onerow_image = input.channel[c0].plane.PixelsPerRow(); const int bit_depth = - CeilLog2Nonzero(static_cast(input.maxval - input.minval + 1)); + CeilLog2Nonzero(static_cast(input.maxval) - input.minval + 1); if (w == 0) { // Nothing to do. // Avoid touching "empty" channels with non-zero height. } else if (nb_deltas == 0 && predictor == Predictor::Zero) { diff --git a/third_party/jpeg-xl/lib/jxl/passes_state.h b/third_party/jpeg-xl/lib/jxl/passes_state.h --- a/third_party/jpeg-xl/lib/jxl/passes_state.h +++ b/third_party/jpeg-xl/lib/jxl/passes_state.h @@ -88,11 +88,11 @@ // the CodecInOut, or can point to an existing ImageBundle. // TODO(veluca): pointing to ImageBundles in CodecInOut is not possible for // now, as they are stored in a vector and thus may be moved. Fix this. ImageBundle* JXL_RESTRICT frame = &storage; // ImageBundle doesn't yet have a simple way to state it is in XYB. - bool ib_is_in_xyb = true; + bool ib_is_in_xyb = false; } reference_frames[4] = {}; // Number of pre-clustered set of histograms (with the same ctx map), per // pass. Encoded as num_histograms_ - 1. size_t num_histograms = 0;