Bug 1767432 - Update libjxl to 192ddd r=tnikkel

Differential Revision: https://phabricator.services.mozilla.com/D145332
This commit is contained in:
Kagami Sascha Rosylight 2022-05-04 00:22:14 +00:00
parent 733cea14b9
commit 0d3b3ecabe
134 changed files with 2394 additions and 3059 deletions

View File

@ -8,6 +8,9 @@
#define JXL_EXPORT_H
#define JXL_EXPORT
#define JXL_DEPRECATED [[deprecated]]
// TODO: go back to [[deprecated]]
// https://github.com/libjxl/libjxl/issues/1388
#define JXL_DEPRECATED __attribute__((__deprecated__))
#endif /* JXL_EXPORT_H */

View File

@ -19,7 +19,6 @@ SOURCES += [
"/third_party/jpeg-xl/lib/jxl/base/data_parallel.cc",
"/third_party/jpeg-xl/lib/jxl/base/padded_bytes.cc",
"/third_party/jpeg-xl/lib/jxl/base/random.cc",
"/third_party/jpeg-xl/lib/jxl/base/status.cc",
"/third_party/jpeg-xl/lib/jxl/blending.cc",
"/third_party/jpeg-xl/lib/jxl/box_content_decoder.cc",
"/third_party/jpeg-xl/lib/jxl/chroma_from_luma.cc",
@ -46,6 +45,7 @@ SOURCES += [
"/third_party/jpeg-xl/lib/jxl/enc_bit_writer.cc",
"/third_party/jpeg-xl/lib/jxl/entropy_coder.cc",
"/third_party/jpeg-xl/lib/jxl/epf.cc",
"/third_party/jpeg-xl/lib/jxl/exif.cc",
"/third_party/jpeg-xl/lib/jxl/fast_dct.cc",
"/third_party/jpeg-xl/lib/jxl/fields.cc",
"/third_party/jpeg-xl/lib/jxl/frame_header.cc",

View File

@ -46,7 +46,7 @@ jobs:
# Build scalar-only hwy instructions.
- name: scalar
mode: release
cxxflags: -DHWY_DISABLED_TARGETS=~HWY_SCALAR
cxxflags: -DHWY_COMPILE_ONLY_SCALAR
# Disabling optional features to speed up msan build a little bit.
- name: msan
skip_install: true
@ -59,8 +59,6 @@ jobs:
apt_pkgs: gcovr
# Coverage builds require a bit more RAM.
env_test_stack_size: 2048
# Exclude roundtrip tests from the unittest coverage.
ctest_args: -E '^JxlTest'
# Build with support for decoding to JPEG bytes disabled. Produces a
# smaller build if only decoding to pixels is needed.
- name: release-nojpeg
@ -149,6 +147,7 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ steps.git-env.outputs.parent }}-${{ matrix.name }}
- name: Build
if: matrix.name != 'coverage' || env.WILL_RUN_TESTS == 'true'
run: |
mkdir -p ${CCACHE_DIR}
echo "max_size = 200M" > ${CCACHE_DIR}/ccache.conf
@ -216,10 +215,10 @@ jobs:
files: build/coverage.xml
- name: Fast benchmark ${{ matrix.mode }}
if: |
github.event_name == 'push' ||
matrix.name != 'coverage' && (github.event_name == 'push' ||
(github.event_name == 'pull_request' && (
matrix.test_in_pr ||
contains(github.event.pull_request.labels.*.names, 'CI:full')))
contains(github.event.pull_request.labels.*.names, 'CI:full'))))
run: |
STORE_IMAGES=0 ./ci.sh fast_benchmark
# Run gbench once, just to make sure it runs, not for actual benchmarking.
@ -310,7 +309,6 @@ jobs:
# Build dependencies
cmake
doxygen
libgtest-dev:${{ matrix.arch }}
ninja-build
pkg-config
qemu-user-static

View File

@ -98,8 +98,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# 'main_level10' currently fails
name: [main_level5]
name: [main_level5, main_level10]
steps:
- name: Install deps
run: |

View File

@ -146,6 +146,11 @@ jobs:
cd "git-${git_version}"
make prefix=/usr -j4 install
- name: Set git safe dir
run: |
export GIT_CEILING_DIRECTORIES=/__w # only work before git v2.35.2
git config --global --add safe.directory /__w/libjxl/libjxl
- name: Checkout the source
uses: actions/checkout@v2
with:

View File

@ -25,16 +25,20 @@ Daniel Novomeský <dnovomesky@gmail.com>
David Burnett <vargolsoft@gmail.com>
Dirk Lemstra <dirk@lemstra.org>
Don Olmstead <don.j.olmstead@gmail.com>
Heiko Becker <heirecka@exherbo.org>
Jon Sneyers <jon@cloudinary.com>
Kleis Auke Wolthuizen <github@kleisauke.nl>
L. E. Segovia
Leo Izen <leo.izen@gmail.com>
Lovell Fuller
Marcin Konicki <ahwayakchih@gmail.com>
Martin Strunz
Mathieu Malaterre <mathieu.malaterre@gmail.com>
Misaki Kasumi <misakikasumi@outlook.com>
Petr Diblík
Pieter Wuille
Samuel Leong <wvvwvvvvwvvw@gmail.com>
Stephan T. Lavavej <stl@nuwen.net>
Vincent Torri <vincent.torri@gmail.com>
xiota
Yonatan Nebenzhal <yonatan.nebenzhl@gmail.com>

View File

@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the non-coalesced case.
- decoder API: new function `JxlDecoderGetExtraChannelBlendInfo` to get
the blending information for extra channels in the non-coalesced case.
- decoder API: new function `JxlDecoderSetMultithreadedImageOutCallback`,
allowing output callbacks to receive more information about the number of
threads on which they are running.
- encoder API: added ability to set several encoder options to frames using
`JxlEncoderFrameSettingsSetOption`
- encoder API: new functions `JxlEncoderSetFrameHeader` and

View File

@ -191,6 +191,15 @@ endif() # JPEGXL_STATIC
set(THREADS_PREFER_PTHREAD_FLAG YES)
find_package(Threads REQUIRED)
# These settings are important to drive check_cxx_source_compiles
# See CMP0067 (min cmake version is 3.10 anyway)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
# Atomics
find_package(Atomics REQUIRED)
if(JPEGXL_STATIC)
if (MINGW)
# In MINGW libstdc++ uses pthreads directly. When building statically a
@ -298,10 +307,6 @@ endif () # !MSVC
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
add_subdirectory(third_party)
# Copy the JXL license file to the output build directory.
@ -311,6 +316,10 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/LICENSE"
# Enable tests regardless of where they are defined.
enable_testing()
include(CTest)
# Specify default location of `testdata`:
if(NOT DEFINED JPEGXL_TEST_DATA_PATH)
set(JPEGXL_TEST_DATA_PATH "${PROJECT_SOURCE_DIR}/third_party/testdata")
endif()
# Libraries.
add_subdirectory(lib)
@ -419,7 +428,7 @@ if (ASCIIDOC_PY_FOUND)
list(APPEND MANPAGES "${PAGE}.1")
endforeach()
add_custom_target(manpages ALL DEPENDS ${MANPAGES})
install(FILES ${MANPAGE_FILES} DESTINATION share/man/man1)
install(FILES ${MANPAGE_FILES} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
endif() # ASCIIDOC_PY_FOUND
else()
message(WARNING "asciidoc was not found, the man pages will not be installed.")

View File

@ -905,7 +905,7 @@ run_benchmark() {
local benchmark_args=(
--input "${src_img_dir}/*.png"
--codec=jpeg:yuv420:q85,webp:q80,jxl:fast:d1,jxl:fast:d1:downsampling=8,jxl:fast:d4,jxl:fast:d4:downsampling=8,jxl:cheetah:m,jxl:m:cheetah:P6,jxl:m:falcon:q80
--codec=jpeg:yuv420:q85,webp:q80,jxl:d1:6,jxl:d1:6:downsampling=8,jxl:d5:6,jxl:d5:6:downsampling=8,jxl:m:d0:2,jxl:m:d0:3,jxl:m:d2:2
--output_dir "${output_dir}"
--noprofiler --show_progress
--num_threads="${num_threads}"
@ -1020,11 +1020,11 @@ cmd_arm_benchmark() {
)
local images=(
"third_party/testdata/imagecompression.info/flower_foveon.png"
"third_party/testdata/third_party/imagecompression.info/flower_foveon.png"
)
local jpg_images=(
"third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_420.jpg"
"third_party/testdata/third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg"
)
if [[ "${SKIP_CPUSET:-}" == "1" ]]; then

View File

@ -0,0 +1,53 @@
# Original issue:
# * https://gitlab.kitware.com/cmake/cmake/-/issues/23021#note_1098733
#
# For reference:
# * https://gcc.gnu.org/wiki/Atomic/GCCMM
#
# riscv64 specific:
# * https://lists.debian.org/debian-riscv/2022/01/msg00009.html
#
# ATOMICS_FOUND - system has c++ atomics
# ATOMICS_LIBRARIES - libraries needed to use c++ atomics
include(CheckCXXSourceCompiles)
# RISC-V only has 32-bit and 64-bit atomic instructions. GCC is supposed
# to convert smaller atomics to those larger ones via masking and
# shifting like LLVM, but its a known bug that it does not. This means
# anything that wants to use atomics on 1-byte or 2-byte types needs
# -latomic, but not 4-byte or 8-byte (though it does no harm).
set(atomic_code
"
#include <atomic>
#include <cstdint>
std::atomic<uint8_t> n8 (0); // riscv64
std::atomic<uint64_t> n64 (0); // armel, mipsel, powerpc
int main() {
++n8;
++n64;
return 0;
}")
check_cxx_source_compiles("${atomic_code}" ATOMICS_LOCK_FREE_INSTRUCTIONS)
if(ATOMICS_LOCK_FREE_INSTRUCTIONS)
set(ATOMICS_FOUND TRUE)
set(ATOMICS_LIBRARIES)
else()
set(CMAKE_REQUIRED_LIBRARIES "-latomic")
check_cxx_source_compiles("${atomic_code}" ATOMICS_IN_LIBRARY)
set(CMAKE_REQUIRED_LIBRARIES)
if(ATOMICS_IN_LIBRARY)
set(ATOMICS_LIBRARY atomic)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Atomics DEFAULT_MSG ATOMICS_LIBRARY)
set(ATOMICS_LIBRARIES ${ATOMICS_LIBRARY})
unset(ATOMICS_LIBRARY)
else()
if(Atomics_FIND_REQUIRED)
message(FATAL_ERROR "Neither lock free instructions nor -latomic found.")
endif()
endif()
endif()
unset(atomic_code)

View File

@ -38,7 +38,7 @@ License: BSD-3-clause
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Files: third_party/testdata/imagecompression.info/*
Files: third_party/testdata/third_party/imagecompression.info/*
Copyright: their respective owners.
License: License without any prohibitive copyright restrictions.
See https://imagecompression.info/test_images/ for details.
@ -58,7 +58,7 @@ License: License without any prohibitive copyright restrictions.
sell the material.
4. This notice may not be removed or altered from any distribution.
Files: third_party/testdata/pngsuite/*
Files: third_party/testdata/third_party/pngsuite/*
Copyright: Willem van Schaik, 1996, 2011
License: PngSuite License
See http://www.schaik.com/pngsuite/ for details.
@ -74,7 +74,7 @@ Files: third_party/testdata/raw.pixls/*
Copyright: their respective owners listed in https://www.wesaturate.com/
License: CC0-1.0
Files: third_party/testdata/wide-gamut-tests/
Files: third_party/testdata/third_party/wide-gamut-tests/
Copyright: github.com/codelogic/wide-gamut-tests authors.
License: Apache-2.0

View File

@ -13,6 +13,7 @@ MYDIR=$(dirname $(realpath "$0"))
# Git revisions we use for the given submodules. Update these whenever you
# update a git submodule.
THIRD_PARTY_BROTLI="35ef5c554d888bef217d449346067de05e269b30"
THIRD_PARTY_GFLAGS="827c769e5fc98e0f2a34c47cef953cc6328abced"
THIRD_PARTY_HIGHWAY="f13e3b956eb226561ac79427893ec0afd66f91a8"
THIRD_PARTY_SKCMS="64374756e03700d649f897dbd98c95e78c30c7da"
@ -71,6 +72,7 @@ EOF
fi
# Sources downloaded from a tarball.
download_github third_party/brotli google/brotli
download_github third_party/gflags gflags/gflags
download_github third_party/highway google/highway
download_github third_party/sjpeg webmproject/sjpeg

View File

@ -25,9 +25,6 @@ target_link_libraries(decode_progressive PkgConfig::Jxl PkgConfig::JxlThreads)
add_executable(encode_oneshot encode_oneshot.cc)
target_link_libraries(encode_oneshot PkgConfig::Jxl PkgConfig::JxlThreads)
add_executable(jxlinfo jxlinfo.c)
target_link_libraries(jxlinfo PkgConfig::Jxl)
# Building a static binary with the static libjxl dependencies. How to load
# static library configs from pkg-config and how to build static binaries

View File

@ -9,13 +9,3 @@ add_executable(decode_progressive ${CMAKE_CURRENT_LIST_DIR}/decode_progressive.c
target_link_libraries(decode_progressive jxl_dec jxl_threads)
add_executable(encode_oneshot ${CMAKE_CURRENT_LIST_DIR}/encode_oneshot.cc)
target_link_libraries(encode_oneshot jxl jxl_threads)
add_executable(jxlinfo ${CMAKE_CURRENT_LIST_DIR}/jxlinfo.c)
target_link_libraries(jxlinfo jxl)
if(NOT ${SANITIZER} STREQUAL "none")
# Linking a C test binary with the C++ JPEG XL implementation when using
# address sanitizer is not well supported by clang 9, so force using clang++
# for linking this test if a sanitizer is used.
set_target_properties(jxlinfo PROPERTIES LINKER_LANGUAGE CXX)
endif() # SANITIZER != "none"

View File

@ -1,332 +0,0 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This example prints information from the main codestream header.
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jxl/decode.h"
int PrintBasicInfo(FILE* file) {
uint8_t* data = NULL;
size_t data_size = 0;
// In how large chunks to read from the file and try decoding the basic info.
const size_t chunk_size = 64;
JxlDecoder* dec = JxlDecoderCreate(NULL);
if (!dec) {
fprintf(stderr, "JxlDecoderCreate failed\n");
return 0;
}
JxlDecoderSetKeepOrientation(dec, 1);
if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(
dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
JXL_DEC_FRAME | JXL_DEC_BOX)) {
fprintf(stderr, "JxlDecoderSubscribeEvents failed\n");
JxlDecoderDestroy(dec);
return 0;
}
JxlBasicInfo info;
int seen_basic_info = 0;
JxlFrameHeader frame_header;
for (;;) {
// 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 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);
}
// 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);
if (feof(file)) JxlDecoderCloseInput(dec);
} else if (status == JXL_DEC_SUCCESS) {
// Finished all processing.
break;
} else if (status == JXL_DEC_BASIC_INFO) {
if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &info)) {
fprintf(stderr, "JxlDecoderGetBasicInfo failed\n");
break;
}
seen_basic_info = 1;
printf("dimensions: %ux%u\n", info.xsize, info.ysize);
printf("have_container: %d\n", info.have_container);
printf("uses_original_profile: %d\n", info.uses_original_profile);
printf("bits_per_sample: %d\n", info.bits_per_sample);
if (info.exponent_bits_per_sample)
printf("float, with exponent_bits_per_sample: %d\n",
info.exponent_bits_per_sample);
if (info.intensity_target != 255.f || info.min_nits != 0.f ||
info.relative_to_max_display != 0 ||
info.relative_to_max_display != 0.f) {
printf("intensity_target: %f\n", info.intensity_target);
printf("min_nits: %f\n", info.min_nits);
printf("relative_to_max_display: %d\n", info.relative_to_max_display);
printf("linear_below: %f\n", info.linear_below);
}
printf("have_preview: %d\n", info.have_preview);
if (info.have_preview) {
printf("preview xsize: %u\n", info.preview.xsize);
printf("preview ysize: %u\n", info.preview.ysize);
}
printf("have_animation: %d\n", info.have_animation);
if (info.have_animation) {
printf("ticks per second (numerator / denominator): %u / %u\n",
info.animation.tps_numerator, info.animation.tps_denominator);
printf("num_loops: %u\n", info.animation.num_loops);
printf("have_timecodes: %d\n", info.animation.have_timecodes);
}
printf("intrinsic xsize: %u\n", info.intrinsic_xsize);
printf("intrinsic ysize: %u\n", info.intrinsic_ysize);
const char* const orientation_string[8] = {
"Normal", "Flipped horizontally",
"Upside down", "Flipped vertically",
"Transposed", "90 degrees clockwise",
"Anti-Transposed", "90 degrees counter-clockwise"};
if (info.orientation > 0 && info.orientation < 9) {
printf("orientation: %d (%s)\n", info.orientation,
orientation_string[info.orientation - 1]);
} else {
fprintf(stderr, "Invalid orientation\n");
}
printf("num_color_channels: %d\n", info.num_color_channels);
printf("num_extra_channels: %d\n", info.num_extra_channels);
const char* const ec_type_names[7] = {"Alpha", "Depth",
"Spot color", "Selection mask",
"K (of CMYK)", "CFA (Bayer data)",
"Thermal"};
for (uint32_t i = 0; i < info.num_extra_channels; i++) {
JxlExtraChannelInfo extra;
if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelInfo(dec, i, &extra)) {
fprintf(stderr, "JxlDecoderGetExtraChannelInfo failed\n");
break;
}
printf("extra channel %u:\n", i);
printf(" type: %s\n",
(extra.type < 7 ? ec_type_names[extra.type]
: (extra.type == JXL_CHANNEL_OPTIONAL
? "Unknown but can be ignored"
: "Unknown, please update your libjxl")));
printf(" bits_per_sample: %u\n", extra.bits_per_sample);
if (extra.exponent_bits_per_sample > 0) {
printf(" float, with exponent_bits_per_sample: %u\n",
extra.exponent_bits_per_sample);
}
if (extra.dim_shift > 0) {
printf(" dim_shift: %u (upsampled %ux)\n", extra.dim_shift,
1 << extra.dim_shift);
}
if (extra.name_length) {
char* name = malloc(extra.name_length + 1);
if (JXL_DEC_SUCCESS != JxlDecoderGetExtraChannelName(
dec, i, name, extra.name_length + 1)) {
fprintf(stderr, "JxlDecoderGetExtraChannelName failed\n");
free(name);
break;
}
printf(" name: %s\n", name);
free(name);
}
if (extra.type == JXL_CHANNEL_ALPHA)
printf(" alpha_premultiplied: %d (%s)\n", extra.alpha_premultiplied,
extra.alpha_premultiplied ? "Premultiplied"
: "Non-premultiplied");
if (extra.type == JXL_CHANNEL_SPOT_COLOR) {
printf(" spot_color: (%f, %f, %f) with opacity %f\n",
extra.spot_color[0], extra.spot_color[1], extra.spot_color[2],
extra.spot_color[3]);
}
if (extra.type == JXL_CHANNEL_CFA)
printf(" cfa_channel: %u\n", extra.cfa_channel);
}
} else if (status == JXL_DEC_COLOR_ENCODING) {
JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0};
printf("color profile:\n");
JxlColorEncoding color_encoding;
if (JXL_DEC_SUCCESS ==
JxlDecoderGetColorAsEncodedProfile(dec, &format,
JXL_COLOR_PROFILE_TARGET_ORIGINAL,
&color_encoding)) {
printf(" format: JPEG XL encoded color profile\n");
const char* const cs_string[4] = {"RGB color", "Grayscale", "XYB",
"Unknown"};
const char* const wp_string[12] = {"", "D65", "Custom", "", "", "",
"", "", "", "", "E", "P3"};
const char* const pr_string[12] = {
"", "sRGB", "Custom", "", "", "", "", "", "", "Rec.2100", "", "P3"};
const char* const tf_string[19] = {
"", "709", "Unknown", "", "", "", "", "", "Linear", "",
"", "", "", "sRGB", "", "", "PQ", "DCI", "HLG"};
const char* const ri_string[4] = {"Perceptual", "Relative",
"Saturation", "Absolute"};
printf(" color_space: %d (%s)\n", color_encoding.color_space,
cs_string[color_encoding.color_space]);
printf(" white_point: %d (%s)\n", color_encoding.white_point,
wp_string[color_encoding.white_point]);
if (color_encoding.white_point == JXL_WHITE_POINT_CUSTOM) {
printf(" white_point XY: %f %f\n", color_encoding.white_point_xy[0],
color_encoding.white_point_xy[1]);
}
if (color_encoding.color_space == JXL_COLOR_SPACE_RGB ||
color_encoding.color_space == JXL_COLOR_SPACE_UNKNOWN) {
printf(" primaries: %d (%s)\n", color_encoding.primaries,
pr_string[color_encoding.primaries]);
if (color_encoding.primaries == JXL_PRIMARIES_CUSTOM) {
printf(" red primaries XY: %f %f\n",
color_encoding.primaries_red_xy[0],
color_encoding.primaries_red_xy[1]);
printf(" green primaries XY: %f %f\n",
color_encoding.primaries_green_xy[0],
color_encoding.primaries_green_xy[1]);
printf(" blue primaries XY: %f %f\n",
color_encoding.primaries_blue_xy[0],
color_encoding.primaries_blue_xy[1]);
}
}
if (color_encoding.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) {
printf(" transfer_function: gamma: %f\n", color_encoding.gamma);
} else {
printf(" transfer_function: %d (%s)\n",
color_encoding.transfer_function,
tf_string[color_encoding.transfer_function]);
}
printf(" rendering_intent: %d (%s)\n", color_encoding.rendering_intent,
ri_string[color_encoding.rendering_intent]);
} else {
// The profile is not in JPEG XL encoded form, get as ICC profile
// instead.
printf(" format: ICC profile\n");
size_t profile_size;
if (JXL_DEC_SUCCESS !=
JxlDecoderGetICCProfileSize(dec, &format,
JXL_COLOR_PROFILE_TARGET_ORIGINAL,
&profile_size)) {
fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n");
continue;
}
printf(" ICC profile size: %" PRIu64 "\n", (uint64_t)profile_size);
if (profile_size < 132) {
fprintf(stderr, "ICC profile too small\n");
continue;
}
uint8_t* profile = (uint8_t*)malloc(profile_size);
if (JXL_DEC_SUCCESS !=
JxlDecoderGetColorAsICCProfile(dec, &format,
JXL_COLOR_PROFILE_TARGET_ORIGINAL,
profile, profile_size)) {
fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n");
free(profile);
continue;
}
printf(" CMM type: \"%.4s\"\n", profile + 4);
printf(" color space: \"%.4s\"\n", profile + 16);
printf(" rendering intent: %d\n", (int)profile[67]);
free(profile);
}
} else if (status == JXL_DEC_FRAME) {
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &frame_header)) {
fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
break;
}
printf("frame:\n");
if (frame_header.name_length) {
char* name = malloc(frame_header.name_length + 1);
if (JXL_DEC_SUCCESS !=
JxlDecoderGetFrameName(dec, name, frame_header.name_length + 1)) {
fprintf(stderr, "JxlDecoderGetFrameName failed\n");
free(name);
break;
}
printf(" name: %s\n", name);
free(name);
}
float ms = frame_header.duration * 1000.f *
info.animation.tps_denominator / info.animation.tps_numerator;
if (info.have_animation) {
printf(" duration: %u ticks (%f ms)\n", frame_header.duration, ms);
if (info.animation.have_timecodes) {
printf(" time code: %X\n", frame_header.timecode);
}
}
if (!frame_header.name_length && !info.have_animation) {
printf(" still frame, unnamed\n");
}
} else if (status == JXL_DEC_BOX) {
JxlBoxType type;
uint64_t size;
JxlDecoderGetBoxType(dec, type, JXL_FALSE);
JxlDecoderGetBoxSizeRaw(dec, &size);
printf("box: type: \"%c%c%c%c\" size: %" PRIu64 "\n", type[0], type[1],
type[2], type[3], (uint64_t)size);
} else {
fprintf(stderr, "Unexpected decoder status\n");
break;
}
}
JxlDecoderDestroy(dec);
free(data);
return seen_basic_info;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr,
"Usage: %s <jxl>\n"
"Where:\n"
" jxl = input JPEG XL image filename\n",
argv[0]);
return 1;
}
const char* jxl_filename = argv[1];
FILE* file = fopen(jxl_filename, "rb");
if (!file) {
fprintf(stderr, "Failed to read file %s\n", jxl_filename);
return 1;
}
if (!PrintBasicInfo(file)) {
fclose(file);
fprintf(stderr, "Couldn't print basic info\n");
return 1;
}
fclose(file);
return 0;
}

View File

@ -126,7 +126,7 @@ endif() # WIN32
# Internal flags for coverage builds:
if(JPEGXL_ENABLE_COVERAGE)
set(JPEGXL_COVERAGE_FLAGS
-g -O0 -fprofile-arcs -ftest-coverage -DJXL_DISABLE_SLOW_TESTS
-g -O0 -fprofile-arcs -ftest-coverage
-DJXL_ENABLE_ASSERT=0 -DJXL_ENABLE_CHECK=0
)
endif() # JPEGXL_ENABLE_COVERAGE

View File

@ -21,7 +21,6 @@
#include "lib/extras/enc/exr.h"
#endif
#include "lib/extras/codec_psd.h"
#include "lib/extras/dec/decode.h"
#include "lib/extras/enc/pgx.h"
#include "lib/extras/enc/pnm.h"
@ -43,13 +42,9 @@ Status SetFromBytes(const Span<const uint8_t> bytes,
if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
extras::PackedPixelFile ppf;
if (extras::DecodeBytes(bytes, color_hints, io->constraints, &ppf, pool,
if (extras::DecodeBytes(bytes, color_hints, io->constraints, &ppf,
orig_codec)) {
return ConvertPackedPixelFileToCodecInOut(ppf, pool, io);
} else if (extras::DecodeImagePSD(bytes, color_hints, pool, io)) {
// TODO(deymo): Migrate PSD codec too.
if (orig_codec) *orig_codec = extras::Codec::kPSD;
return true;
}
return JXL_FAILURE("Codecs failed to decode");
}
@ -124,9 +119,6 @@ Status Encode(const CodecInOut& io, const extras::Codec codec,
bytes);
case extras::Codec::kGIF:
return JXL_FAILURE("Encoding to GIF is not implemented");
case extras::Codec::kPSD:
return extras::EncodeImagePSD(&io, c_desired, bits_per_sample, pool,
bytes);
case extras::Codec::kEXR:
#if JPEGXL_ENABLE_EXR
return extras::EncodeImageEXR(&io, c_desired, pool, bytes);
@ -147,10 +139,13 @@ Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired,
const extras::Codec codec =
extras::CodecFromExtension(extension, &bits_per_sample);
// Warn about incorrect usage of PBM/PGM/PGX/PPM - only the latter supports
// Warn about incorrect usage of PGM/PGX/PPM - only the latter supports
// color, but CodecFromExtension lumps them all together.
if (codec == extras::Codec::kPNM && extension != ".pfm") {
if (!io.Main().IsGray() && extension != ".ppm") {
if (io.Main().HasAlpha() && extension != ".pam") {
JXL_WARNING(
"For images with alpha, the filename should end with .pam.\n");
} else if (!io.Main().IsGray() && extension == ".pgm") {
JXL_WARNING("For color images, the filename should end with .ppm.\n");
} else if (io.Main().IsGray() && extension == ".ppm") {
JXL_WARNING(

View File

@ -1,621 +0,0 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/extras/codec_psd.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <numeric>
#include <string>
#include <utility>
#include <vector>
#include "lib/jxl/base/bits.h"
#include "lib/jxl/base/byte_order.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/file_io.h"
#include "lib/jxl/base/printf_macros.h"
#include "lib/jxl/color_management.h"
#include "lib/jxl/common.h"
#include "lib/jxl/fields.h" // AllDefault
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
#include "lib/jxl/luminance.h"
namespace jxl {
namespace extras {
namespace {
uint64_t get_be_int(int bytes, const uint8_t*& pos, const uint8_t* maxpos) {
uint64_t r = 0;
if (pos + bytes <= maxpos) {
if (bytes == 1) {
r = *pos;
} else if (bytes == 2) {
r = LoadBE16(pos);
} else if (bytes == 4) {
r = LoadBE32(pos);
} else if (bytes == 8) {
r = LoadBE64(pos);
}
}
pos += bytes;
return r;
}
// Copies up to n bytes, without reading from maxpos (the STL-style end).
void safe_copy(const uint8_t* JXL_RESTRICT pos,
const uint8_t* JXL_RESTRICT maxpos, char* JXL_RESTRICT out,
size_t n) {
for (size_t i = 0; i < n; ++i) {
if (pos + i >= maxpos) return;
out[i] = pos[i];
}
}
// maxpos is the STL-style end! The valid range is up to [pos, maxpos).
int safe_strncmp(const uint8_t* pos, const uint8_t* maxpos, const char* s2,
size_t n) {
if (pos + n > maxpos) return 1;
return strncmp((const char*)pos, s2, n);
}
constexpr int PSD_VERBOSITY = 1;
Status decode_layer(const uint8_t*& pos, const uint8_t* maxpos,
ImageBundle& layer, std::vector<int> chans,
std::vector<bool> invert, int w, int h, int version,
int colormodel, bool is_layer, int depth) {
int compression_method = 2;
int nb_channels = chans.size();
JXL_DEBUG_V(PSD_VERBOSITY,
"Trying to decode layer with dimensions %ix%i and %i channels", w,
h, nb_channels);
if (w <= 0 || h <= 0) return JXL_FAILURE("PSD: empty layer");
for (int c = 0; c < nb_channels; c++) {
// skip nop byte padding
while (pos < maxpos && *pos == 128) pos++;
JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i (pos %" PRIuS ")", c, (size_t)pos);
// Merged image stores all channels together (same compression method)
// Layers store channel per channel
if (is_layer || c == 0) {
compression_method = get_be_int(2, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, "compression method: %i", compression_method);
if (compression_method > 1 || compression_method < 0) {
return JXL_FAILURE("PSD: can't handle compression method %i",
compression_method);
}
}
if (!is_layer && c < colormodel) {
// skip to the extra channels
if (compression_method == 0) {
pos += w * h * (depth >> 3) * colormodel;
c = colormodel - 1;
continue;
}
size_t skip_amount = 0;
for (int i = 0; i < nb_channels; i++) {
if (i < colormodel) {
for (int y = 0; y < h; y++) {
skip_amount += get_be_int(2 * version, pos, maxpos);
}
} else {
pos += h * 2 * version;
}
}
pos += skip_amount;
c = colormodel - 1;
continue;
}
if (is_layer || c == 0) {
// skip the line-counts, we don't need them
if (compression_method == 1) {
pos += h * (is_layer ? 1 : nb_channels) * 2 *
version; // PSB uses 4 bytes per rowsize instead of 2
}
}
int c_id = chans[c];
if (c_id < 0) continue; // skip
if (static_cast<unsigned int>(c_id) >= 3 + layer.extra_channels().size())
return JXL_FAILURE("PSD: can't handle channel id %i", c_id);
ImageF& ch = (c_id < 3 ? layer.color()->Plane(c_id)
: layer.extra_channels()[c_id - 3]);
for (int y = 0; y < h; y++) {
if (pos > maxpos) return JXL_FAILURE("PSD: premature end of input");
float* const JXL_RESTRICT row = ch.Row(y);
if (compression_method == 0) {
// uncompressed is easy
if (depth == 8) {
for (int x = 0; x < w; x++) {
row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f);
}
} else if (depth == 16) {
for (int x = 0; x < w; x++) {
row[x] = get_be_int(2, pos, maxpos) * (1.f / 65535.f);
}
} else if (depth == 32) {
for (int x = 0; x < w; x++) {
uint32_t f = get_be_int(4, pos, maxpos);
memcpy(&row[x], &f, 4);
}
}
} else {
// RLE is not that hard
if (depth != 8)
return JXL_FAILURE("PSD: did not expect RLE with depth>1");
for (int x = 0; x < w;) {
if (pos >= maxpos) return JXL_FAILURE("PSD: out of bounds");
int8_t rle = *pos++;
if (rle <= 0) {
if (rle == -128) continue; // nop
int count = 1 - rle;
float v = get_be_int(1, pos, maxpos) * (1.f / 255.f);
while (count && x < w) {
row[x] = v;
count--;
x++;
}
if (count) return JXL_FAILURE("PSD: row overflow");
} else {
int count = 1 + rle;
while (count && x < w) {
row[x] = get_be_int(1, pos, maxpos) * (1.f / 255.f);
count--;
x++;
}
if (count) return JXL_FAILURE("PSD: row overflow");
}
}
}
if (invert[c]) {
// sometimes 0 means full ink
for (int x = 0; x < w; x++) {
row[x] = 1.f - row[x];
}
}
}
JXL_DEBUG_V(PSD_VERBOSITY, "Channel %i read.", c);
}
return true;
}
} // namespace
Status DecodeImagePSD(const Span<const uint8_t> bytes,
const ColorHints& /*color_hints*/, ThreadPool* pool,
CodecInOut* io) {
const uint8_t* pos = bytes.data();
const uint8_t* maxpos = bytes.data() + bytes.size();
if (safe_strncmp(pos, maxpos, "8BPS", 4)) return false; // not a PSD file
JXL_DEBUG_V(PSD_VERBOSITY, "trying psd decode");
pos += 4;
int version = get_be_int(2, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, "Version=%i", version);
if (version < 1 || version > 2)
return JXL_FAILURE("PSD: unknown format version");
// PSD = version 1, PSB = version 2
pos += 6;
int nb_channels = get_be_int(2, pos, maxpos);
size_t ysize = get_be_int(4, pos, maxpos);
size_t xsize = get_be_int(4, pos, maxpos);
const SizeConstraints* constraints = &io->constraints;
JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, xsize, ysize));
uint64_t total_pixel_count = static_cast<uint64_t>(xsize) * ysize;
int bitdepth = get_be_int(2, pos, maxpos);
if (bitdepth != 8 && bitdepth != 16 && bitdepth != 32) {
return JXL_FAILURE("PSD: bit depth %i invalid or not supported", bitdepth);
}
if (bitdepth == 32) {
io->metadata.m.SetFloat32Samples();
} else {
io->metadata.m.SetUintSamples(bitdepth);
}
int colormodel = get_be_int(2, pos, maxpos);
// 1 = Grayscale, 3 = RGB, 4 = CMYK
if (colormodel != 1 && colormodel != 3 && colormodel != 4)
return JXL_FAILURE("PSD: unsupported color model");
int real_nb_channels = colormodel;
std::vector<std::vector<float>> spotcolor;
if (get_be_int(4, pos, maxpos))
return JXL_FAILURE("PSD: Unsupported color mode section");
bool hasmergeddata = true;
bool have_alpha = false;
bool merged_has_alpha = false;
bool color_already_set = false;
size_t metalength = get_be_int(4, pos, maxpos);
const uint8_t* metaoffset = pos;
while (pos < metaoffset + metalength) {
char header[5] = "????";
safe_copy(pos, maxpos, header, 4);
if (memcmp(header, "8BIM", 4) != 0) {
return JXL_FAILURE("PSD: Unexpected image resource header: %s", header);
}
pos += 4;
int id = get_be_int(2, pos, maxpos);
int namelength = get_be_int(1, pos, maxpos);
pos += namelength;
if (!(namelength & 1)) pos++; // padding to even length
size_t blocklength = get_be_int(4, pos, maxpos);
// JXL_DEBUG_V(PSD_VERBOSITY, "block id: %i | block length: %" PRIuS,id,
// blocklength);
if (pos > maxpos) return JXL_FAILURE("PSD: Unexpected end of file");
if (id == 1039) { // ICC profile
size_t delta = maxpos - pos;
if (delta < blocklength) {
return JXL_FAILURE("PSD: Invalid block length");
}
PaddedBytes icc;
icc.resize(blocklength);
memcpy(icc.data(), pos, blocklength);
if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) {
return JXL_FAILURE("PSD: Invalid color profile");
}
color_already_set = true;
} else if (id == 1057) { // compatibility mode or not?
if (get_be_int(4, pos, maxpos) != 1) {
return JXL_FAILURE("PSD: expected version=1 in id=1057 resource block");
}
hasmergeddata = get_be_int(1, pos, maxpos);
pos++;
blocklength -= 6; // already skipped these bytes
} else if (id == 1077) { // spot colors
int version = get_be_int(4, pos, maxpos);
if (version != 1) {
return JXL_FAILURE(
"PSD: expected DisplayInfo version 1, got version %i", version);
}
int spotcolorcount = nb_channels - colormodel;
JXL_DEBUG_V(PSD_VERBOSITY, "Reading %i spot colors. %" PRIuS,
spotcolorcount, blocklength);
for (int k = 0; k < spotcolorcount; k++) {
int colorspace = get_be_int(2, pos, maxpos);
if ((colormodel == 3 && colorspace != 0) ||
(colormodel == 4 && colorspace != 2)) {
return JXL_FAILURE(
"PSD: cannot handle spot colors in different color spaces than "
"image itself");
}
if (colorspace == 2) JXL_WARNING("PSD: K ignored in CMYK spot color");
std::vector<float> color;
color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // R or C
color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // G or M
color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // B or Y
color.push_back(get_be_int(2, pos, maxpos) / 65535.f); // ignored or K
color.push_back(get_be_int(2, pos, maxpos) /
100.f); // solidity (alpha, basically)
int kind = get_be_int(1, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, "Kind=%i", kind);
color.push_back(kind);
spotcolor.push_back(color);
if (kind == 2) {
JXL_DEBUG_V(PSD_VERBOSITY, "Actual spot color");
} else if (kind == 1) {
JXL_DEBUG_V(PSD_VERBOSITY, "Mask (alpha) channel");
} else if (kind == 0) {
JXL_DEBUG_V(PSD_VERBOSITY, "Selection (alpha) channel");
} else {
return JXL_FAILURE("PSD: Unknown extra channel type");
}
}
if (blocklength & 1) pos++;
blocklength = 0;
}
pos += blocklength;
if (blocklength & 1) pos++; // padding again
}
// TODO(deymo): Apply color hints when PSD is converted to PackedPixelFile.
(void)color_already_set;
// JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, color_already_set,
// /*is_gray=*/false, io));
size_t layerlength = get_be_int(4 * version, pos, maxpos);
const uint8_t* after_layers_pos = pos + layerlength;
if (after_layers_pos < pos) return JXL_FAILURE("PSD: invalid layer length");
if (layerlength) {
pos += 4 * version; // don't care about layerinfolength
JXL_DEBUG_V(PSD_VERBOSITY, "Layer section length: %" PRIuS, layerlength);
int layercount = static_cast<int16_t>(get_be_int(2, pos, maxpos));
JXL_DEBUG_V(PSD_VERBOSITY, "Layer count: %i", layercount);
io->frames.clear();
if (layercount == 0) {
if (get_be_int(2, pos, maxpos) != 0) {
return JXL_FAILURE(
"PSD: Expected zero padding before additional layer info");
}
while (pos < after_layers_pos) {
if (safe_strncmp(pos, maxpos, "8BIM", 4) &&
safe_strncmp(pos, maxpos, "8B64", 4))
return JXL_FAILURE("PSD: Unexpected layer info signature");
pos += 4;
const uint8_t* tpos = pos;
pos += 4;
size_t blocklength = get_be_int(4 * version, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, "Length=%" PRIuS, blocklength);
if (blocklength > 0) {
if (pos >= maxpos) return JXL_FAILURE("PSD: Unexpected end of file");
size_t delta = maxpos - pos;
if (delta < blocklength) {
return JXL_FAILURE("PSD: Invalid block length");
}
}
if (!safe_strncmp(tpos, maxpos, "Layr", 4) ||
!safe_strncmp(tpos, maxpos, "Lr16", 4) ||
!safe_strncmp(tpos, maxpos, "Lr32", 4)) {
layercount = static_cast<int16_t>(get_be_int(2, pos, maxpos));
if (layercount < 0) {
return JXL_FAILURE("PSD: Invalid layer count");
}
JXL_DEBUG_V(PSD_VERBOSITY, "Real layer count: %i", layercount);
if (layercount > 1) have_alpha = true;
break;
}
if (!safe_strncmp(tpos, maxpos, "Mtrn", 4) ||
!safe_strncmp(tpos, maxpos, "Mt16", 4) ||
!safe_strncmp(tpos, maxpos, "Mt32", 4)) {
JXL_DEBUG_V(PSD_VERBOSITY, "Merged layer has transparency channel");
if (nb_channels > real_nb_channels) {
have_alpha = true;
merged_has_alpha = true;
}
}
pos += blocklength;
}
} else if (layercount < 0) {
// negative layer count indicates merged has alpha and it is to be shown
if (nb_channels > real_nb_channels) {
have_alpha = true;
merged_has_alpha = true;
}
layercount = -layercount;
} else {
// multiple layers implies there is alpha
have_alpha = true;
}
ExtraChannelInfo info;
info.bit_depth.bits_per_sample = bitdepth;
info.dim_shift = 0;
if (colormodel == 4) { // cmyk
info.type = ExtraChannel::kBlack;
io->metadata.m.extra_channel_info.push_back(info);
}
if (have_alpha) {
JXL_DEBUG_V(PSD_VERBOSITY, "Have alpha");
real_nb_channels++;
info.type = ExtraChannel::kAlpha;
info.alpha_associated =
false; // true? PSD is not consistent with this, need to check
io->metadata.m.extra_channel_info.push_back(info);
}
if (merged_has_alpha && !spotcolor.empty() && spotcolor[0][5] == 1) {
// first alpha channel
spotcolor.erase(spotcolor.begin());
}
for (size_t i = 0; i < spotcolor.size(); i++) {
real_nb_channels++;
if (spotcolor[i][5] == 2) {
info.type = ExtraChannel::kSpotColor;
info.spot_color[0] = spotcolor[i][0];
info.spot_color[1] = spotcolor[i][1];
info.spot_color[2] = spotcolor[i][2];
info.spot_color[3] = spotcolor[i][4];
} else if (spotcolor[i][5] == 1) {
info.type = ExtraChannel::kAlpha;
} else if (spotcolor[i][5] == 0) {
info.type = ExtraChannel::kSelectionMask;
} else
return JXL_FAILURE("PSD: unhandled extra channel");
io->metadata.m.extra_channel_info.push_back(info);
}
std::vector<std::vector<int>> layer_chan_id;
std::vector<size_t> layer_offsets(layercount + 1, 0);
std::vector<bool> is_real_layer(layercount, false);
for (int l = 0; l < layercount; l++) {
ImageBundle layer(&io->metadata.m);
layer.duration = 0;
layer.blend = (l > 0);
layer.use_for_next_frame = (l + 1 < layercount);
layer.origin.y0 = get_be_int(4, pos, maxpos);
layer.origin.x0 = get_be_int(4, pos, maxpos);
size_t height = get_be_int(4, pos, maxpos) - layer.origin.y0;
size_t width = get_be_int(4, pos, maxpos) - layer.origin.x0;
JXL_DEBUG_V(PSD_VERBOSITY,
"Layer %i: %" PRIuS " x %" PRIuS " at origin (%i, %i)", l,
width, height, layer.origin.x0, layer.origin.y0);
int nb_chs = get_be_int(2, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, " channels: %i", nb_chs);
std::vector<int> chan_ids;
layer_offsets[l + 1] = layer_offsets[l];
for (int lc = 0; lc < nb_chs; lc++) {
int id = get_be_int(2, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, " id=%i", id);
if (id == 65535) {
chan_ids.push_back(colormodel); // alpha
} else if (id == 65534) {
chan_ids.push_back(-1); // layer mask, ignored
} else {
chan_ids.push_back(id); // color channel
}
layer_offsets[l + 1] += get_be_int(4 * version, pos, maxpos);
}
layer_chan_id.push_back(chan_ids);
if (safe_strncmp(pos, maxpos, "8BIM", 4))
return JXL_FAILURE("PSD: Layer %i: Unexpected signature (not 8BIM)", l);
pos += 4;
if (safe_strncmp(pos, maxpos, "norm", 4)) {
return JXL_FAILURE(
"PSD: Layer %i: Cannot handle non-default blend mode", l);
}
pos += 4;
int opacity = get_be_int(1, pos, maxpos);
if (opacity < 100) {
JXL_WARNING(
"PSD: ignoring opacity of semi-transparent layer %i (opacity=%i)",
l, opacity);
}
pos++; // clipping
int flags = get_be_int(1, pos, maxpos);
pos++;
bool invisible = (flags & 2);
if (invisible) {
if (l + 1 < layercount) {
layer.blend = false;
layer.use_for_next_frame = false;
} else {
// TODO: instead add dummy last frame?
JXL_WARNING("PSD: invisible top layer was made visible");
}
}
size_t extradata = get_be_int(4, pos, maxpos);
JXL_DEBUG_V(PSD_VERBOSITY, " extradata: %" PRIuS " bytes", extradata);
const uint8_t* after_extra = pos + extradata;
// TODO: deal with non-empty layer masks
pos += get_be_int(4, pos, maxpos); // skip layer mask data
pos += get_be_int(4, pos, maxpos); // skip layer blend range data
size_t namelength = get_be_int(1, pos, maxpos);
size_t delta = maxpos - pos;
if (delta < namelength) return JXL_FAILURE("PSD: Invalid block length");
char lname[256] = {};
memcpy(lname, pos, namelength);
lname[namelength] = 0;
JXL_DEBUG_V(PSD_VERBOSITY, " name: %s", lname);
pos = after_extra;
if (width == 0 || height == 0) {
JXL_DEBUG_V(PSD_VERBOSITY,
" NOT A REAL LAYER"); // probably layer group
continue;
}
is_real_layer[l] = true;
JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, width, height));
uint64_t pixel_count = static_cast<uint64_t>(width) * height;
if (!SafeAdd(total_pixel_count, pixel_count, total_pixel_count)) {
return JXL_FAILURE("Image too big");
}
if (total_pixel_count > constraints->dec_max_pixels) {
return JXL_FAILURE("Image too big");
}
Image3F rgb(width, height);
layer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding);
std::vector<ImageF> ec;
for (const auto& ec_meta : layer.metadata()->extra_channel_info) {
ImageF extra(width, height);
if (ec_meta.type == ExtraChannel::kAlpha) {
FillPlane(1.0f, &extra, Rect(extra)); // opaque
} else {
ZeroFillPlane(&extra, Rect(extra)); // zeroes
}
ec.push_back(std::move(extra));
}
if (!ec.empty()) layer.SetExtraChannels(std::move(ec));
layer.name = lname;
io->dec_pixels += layer.xsize() * layer.ysize();
io->frames.push_back(std::move(layer));
}
std::vector<bool> invert(real_nb_channels, false);
int il = 0;
const uint8_t* bpos = pos;
for (int l = 0; l < layercount; l++) {
if (!is_real_layer[l]) continue;
pos = bpos + layer_offsets[l];
if (pos < bpos) return JXL_FAILURE("PSD: invalid layer offset");
JXL_DEBUG_V(PSD_VERBOSITY, "At position %i (%" PRIuS ")",
(int)(pos - bytes.data()), (size_t)pos);
ImageBundle& layer = io->frames[il++];
std::vector<int>& chan_id = layer_chan_id[l];
if (chan_id.size() > invert.size()) invert.resize(chan_id.size(), false);
JXL_RETURN_IF_ERROR(decode_layer(pos, maxpos, layer, chan_id, invert,
layer.xsize(), layer.ysize(), version,
colormodel, true, bitdepth));
}
} else
return JXL_FAILURE("PSD: no layer data found");
if (!hasmergeddata && !spotcolor.empty()) {
return JXL_FAILURE("PSD: extra channel data declared but not found");
}
if (!spotcolor.empty() || (hasmergeddata && io->frames.empty())) {
// PSD only has spot colors / extra alpha/mask data in the merged image
// We don't redundantly store the merged image, so we put it in the first
// layer (the next layers will kAdd zeroes to it)
pos = after_layers_pos;
bool have_only_merged = false;
if (io->frames.empty()) {
// There is only the merged image, no layers
ImageBundle nlayer(&io->metadata.m);
Image3F rgb(xsize, ysize);
nlayer.SetFromImage(std::move(rgb), io->metadata.m.color_encoding);
std::vector<ImageF> ec;
for (const auto& ec_meta : nlayer.metadata()->extra_channel_info) {
ImageF extra(xsize, ysize);
if (ec_meta.type == ExtraChannel::kAlpha) {
FillPlane(1.0f, &extra, Rect(extra)); // opaque
} else {
ZeroFillPlane(&extra, Rect(extra)); // zeroes
}
ec.push_back(std::move(extra));
}
if (!ec.empty()) nlayer.SetExtraChannels(std::move(ec));
io->dec_pixels += nlayer.xsize() * nlayer.ysize();
io->frames.push_back(std::move(nlayer));
have_only_merged = true;
}
ImageBundle& layer = io->frames[0];
std::vector<int> chan_id(real_nb_channels);
std::iota(chan_id.begin(), chan_id.end(), 0);
std::vector<bool> invert(real_nb_channels, false);
if (static_cast<int>(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 {
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));
}
if (io->frames.empty()) return JXL_FAILURE("PSD: no layers");
io->SetSize(xsize, ysize);
SetIntensityTarget(io);
return true;
}
Status EncodeImagePSD(const CodecInOut* io, const ColorEncoding& c_desired,
size_t bits_per_sample, ThreadPool* pool,
PaddedBytes* bytes) {
return JXL_FAILURE("PSD encoding not yet implemented");
}
} // namespace extras
} // namespace jxl

View File

@ -1,37 +0,0 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#ifndef LIB_EXTRAS_CODEC_PSD_H_
#define LIB_EXTRAS_CODEC_PSD_H_
// Decodes Photoshop PSD/PSB, preserving the layers
#include <stddef.h>
#include <stdint.h>
#include "lib/extras/dec/color_hints.h"
#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
namespace jxl {
namespace extras {
// Decodes `bytes` into `io`.
Status DecodeImagePSD(const Span<const uint8_t> bytes,
const extras::ColorHints& color_hints, ThreadPool* pool,
CodecInOut* io);
// Not implemented yet
Status EncodeImagePSD(const CodecInOut* io, const ColorEncoding& c_desired,
size_t bits_per_sample, ThreadPool* pool,
PaddedBytes* bytes);
} // namespace extras
} // namespace jxl
#endif // LIB_EXTRAS_CODEC_PSD_H_

View File

@ -29,6 +29,31 @@ namespace jxl {
namespace extras {
namespace {
std::string ExtensionFromCodec(Codec codec, const bool is_gray,
const bool has_alpha,
const size_t bits_per_sample) {
switch (codec) {
case Codec::kJPG:
return ".jpg";
case Codec::kPGX:
return ".pgx";
case Codec::kPNG:
return ".png";
case Codec::kPNM:
if (has_alpha) return ".pam";
if (is_gray) return ".pgm";
return (bits_per_sample == 32) ? ".pfm" : ".ppm";
case Codec::kGIF:
return ".gif";
case Codec::kEXR:
return ".exr";
case Codec::kUnknown:
return std::string();
}
JXL_UNREACHABLE;
return std::string();
}
CodecInOut CreateTestImage(const size_t xsize, const size_t ysize,
const bool is_gray, const bool add_alpha,
const size_t bits_per_sample,
@ -76,7 +101,7 @@ void TestRoundTrip(Codec codec, const size_t xsize, const size_t ysize,
// grayscale, and somehow does not have sufficient precision for this test.
if (codec == Codec::kEXR) return;
printf("Codec %s bps:%" PRIuS " gr:%d al:%d\n",
ExtensionFromCodec(codec, is_gray, bits_per_sample).c_str(),
ExtensionFromCodec(codec, is_gray, add_alpha, bits_per_sample).c_str(),
bits_per_sample, is_gray, add_alpha);
ColorEncoding c_native;
@ -231,11 +256,11 @@ CodecInOut DecodeRoundtrip(const std::string& pathname, ThreadPool* pool,
TEST(CodecTest, TestMetadataSRGB) {
ThreadPoolInternal pool(12);
const char* paths[] = {"raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png",
"raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png",
"raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png",
"raw.pixls/Nikon-D300-12bit_srgb8_dt.png",
"raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"};
const char* paths[] = {"third_party/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png",
"third_party/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png",
"third_party/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png",
"third_party/raw.pixls/Nikon-D300-12bit_srgb8_dt.png",
"third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"};
for (const char* relative_pathname : paths) {
const CodecInOut io =
DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool);
@ -260,9 +285,9 @@ TEST(CodecTest, TestMetadataLinear) {
ThreadPoolInternal pool(12);
const char* paths[3] = {
"raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png",
"raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png",
"raw.pixls/Nikon-D300-12bit_2020_g1_dt.png",
"third_party/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png",
"third_party/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png",
"third_party/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png",
};
const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65,
WhitePoint::kD65};
@ -292,8 +317,8 @@ TEST(CodecTest, TestMetadataICC) {
ThreadPoolInternal pool(12);
const char* paths[] = {
"raw.pixls/DJI-FC6310-16bit_709_v4_krita.png",
"raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png",
"third_party/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png",
"third_party/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png",
};
for (const char* relative_pathname : paths) {
const CodecInOut io =
@ -315,28 +340,28 @@ TEST(CodecTest, TestMetadataICC) {
}
}
TEST(CodecTest, TestPNGSuite) {
TEST(CodecTest, Testthird_party/pngsuite) {
ThreadPoolInternal pool(12);
// Ensure we can load PNG with text, japanese UTF-8, compressed text.
(void)DecodeRoundtrip("pngsuite/ct1n0g04.png", Codec::kPNG, &pool);
(void)DecodeRoundtrip("pngsuite/ctjn0g04.png", Codec::kPNG, &pool);
(void)DecodeRoundtrip("pngsuite/ctzn0g04.png", Codec::kPNG, &pool);
(void)DecodeRoundtrip("third_party/pngsuite/ct1n0g04.png", Codec::kPNG, &pool);
(void)DecodeRoundtrip("third_party/pngsuite/ctjn0g04.png", Codec::kPNG, &pool);
(void)DecodeRoundtrip("third_party/pngsuite/ctzn0g04.png", Codec::kPNG, &pool);
// Extract gAMA
const CodecInOut b1 =
DecodeRoundtrip("pngsuite/g10n3p04.png", Codec::kPNG, &pool);
DecodeRoundtrip("third_party/pngsuite/g10n3p04.png", Codec::kPNG, &pool);
EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear());
// Extract cHRM
const CodecInOut b_p =
DecodeRoundtrip("pngsuite/ccwn2c08.png", Codec::kPNG, &pool);
DecodeRoundtrip("third_party/pngsuite/ccwn2c08.png", Codec::kPNG, &pool);
EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries);
EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point);
// Extract EXIF from (new-style) dedicated chunk
const CodecInOut b_exif =
DecodeRoundtrip("pngsuite/exif2c08.png", Codec::kPNG, &pool);
DecodeRoundtrip("third_party/pngsuite/exif2c08.png", Codec::kPNG, &pool);
EXPECT_EQ(978, b_exif.blobs.exif.size());
}
#endif
@ -359,13 +384,13 @@ void VerifyWideGamutMetadata(const std::string& relative_pathname,
TEST(CodecTest, TestWideGamut) {
ThreadPoolInternal pool(12);
// VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-bars.png",
// VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-bars.png",
// Primaries::kP3, &pool);
VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-ring.png",
VerifyWideGamutMetadata("third_party/wide-gamut-tests/P3-sRGB-color-ring.png",
Primaries::kP3, &pool);
// VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-bars.png",
// VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-bars.png",
// Primaries::k2100, &pool);
// VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-ring.png",
// VerifyWideGamutMetadata("third_party/wide-gamut-tests/R2020-sRGB-color-ring.png",
// Primaries::k2100, &pool);
}

View File

@ -43,6 +43,7 @@
#include <utility>
#include <vector>
#include "jxl/codestream_header.h"
#include "jxl/encode.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/printf_macros.h"
@ -56,6 +57,12 @@ namespace extras {
namespace {
/* hIST chunk tail is not proccesed properly; skip this chunk completely;
see https://github.com/glennrp/libpng/pull/413 */
const png_byte kIgnoredPngChunks[] = {
104, 73, 83, 84, '\0' /* hIST */
};
// Returns floating-point value from the PNG encoding (times 10^5).
static double F64FromU32(const uint32_t x) {
return static_cast<int32_t>(x) * 1E-5;
@ -163,6 +170,31 @@ class BlobsReaderPNG {
return true;
}
// Returns false if invalid.
static JXL_INLINE Status DecodeDecimal(const char** pos, const char* end,
uint32_t* JXL_RESTRICT value) {
size_t len = 0;
*value = 0;
while (*pos < end) {
char next = **pos;
if (next >= '0' && next <= '9') {
*value = (*value * 10) + static_cast<uint32_t>(next - '0');
len++;
if (len > 8) {
break;
}
} else {
// Do not consume terminator (non-decimal digit).
break;
}
(*pos)++;
}
if (len == 0 || len > 8) {
return JXL_FAILURE("Failed to parse decimal");
}
return true;
}
// Parses a PNG text chunk with key of the form "Raw profile type ####", with
// #### a type.
// Returns whether it could successfully parse the content.
@ -196,17 +228,15 @@ class BlobsReaderPNG {
// We parsed so far a \n, some number of non \n characters and are now
// pointing at a \n.
if (*(pos++) != '\n') return false;
unsigned long bytes_to_decode;
const int fields = sscanf(pos, "%8lu", &bytes_to_decode);
if (fields != 1) return false; // Failed to decode metadata header
JXL_ASSERT(pos + 8 <= encoded_end);
pos += 8; // read %8lu
uint32_t bytes_to_decode = 0;
JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode));
// We need 2*bytes for the hex values plus 1 byte every 36 values.
// We need 2*bytes for the hex values plus 1 byte every 36 values,
// plus terminal \n for length.
const unsigned long needed_bytes =
bytes_to_decode * 2 + 1 + DivCeil(bytes_to_decode, 36);
if (needed_bytes != static_cast<size_t>(encoded_end - pos)) {
return JXL_FAILURE("Not enough bytes to parse %lu bytes in hex",
return JXL_FAILURE("Not enough bytes to parse %d bytes in hex",
bytes_to_decode);
}
JXL_ASSERT(bytes->empty());
@ -251,7 +281,7 @@ constexpr uint32_t kId_cHRM = 0x4D524863;
constexpr uint32_t kId_eXIf = 0x66495865;
struct APNGFrame {
PaddedBytes pixels;
std::vector<uint8_t> pixels;
std::vector<uint8_t*> rows;
unsigned int w, h, delay_num, delay_den;
};
@ -270,7 +300,7 @@ struct Reader {
};
const unsigned long cMaxPNGSize = 1000000UL;
const size_t kMaxPNGChunkSize = 100000000; // 100 MB
const size_t kMaxPNGChunkSize = 1lu << 30; // 1 GB
void info_fn(png_structp png_ptr, png_infop info_ptr) {
png_set_expand(png_ptr);
@ -284,11 +314,12 @@ void row_fn(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num,
int pass) {
APNGFrame* frame = (APNGFrame*)png_get_progressive_ptr(png_ptr);
JXL_CHECK(frame);
JXL_CHECK(row_num < frame->rows.size());
JXL_CHECK(frame->rows[row_num] < frame->pixels.data() + frame->pixels.size());
png_progressive_combine_row(png_ptr, frame->rows[row_num], new_row);
}
inline unsigned int read_chunk(Reader* r, PaddedBytes* pChunk) {
inline unsigned int read_chunk(Reader* r, std::vector<uint8_t>* pChunk) {
unsigned char len[4];
if (r->Read(&len, 4)) {
const auto size = png_get_uint_32(len);
@ -307,8 +338,8 @@ inline unsigned int read_chunk(Reader* r, PaddedBytes* pChunk) {
}
int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr,
bool hasInfo, PaddedBytes& chunkIHDR,
std::vector<PaddedBytes>& chunksInfo) {
bool hasInfo, std::vector<uint8_t>& chunkIHDR,
std::vector<std::vector<uint8_t>>& chunksInfo) {
unsigned char header[8] = {137, 80, 78, 71, 13, 10, 26, 10};
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
@ -319,6 +350,9 @@ int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr,
return 1;
}
png_set_keep_unknown_chunks(png_ptr, 1, kIgnoredPngChunks,
(int)sizeof(kIgnoredPngChunks) / 5);
png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE);
png_set_progressive_read_fn(png_ptr, frame_ptr, info_fn, row_fn, NULL);
@ -380,9 +414,9 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
unsigned char sig[8];
png_structp png_ptr = nullptr;
png_infop info_ptr = nullptr;
PaddedBytes chunk;
PaddedBytes chunkIHDR;
std::vector<PaddedBytes> chunksInfo;
std::vector<uint8_t> chunk;
std::vector<uint8_t> chunkIHDR;
std::vector<std::vector<uint8_t>> chunksInfo;
bool isAnimated = false;
bool hasInfo = false;
APNGFrame frameRaw = {};
@ -594,8 +628,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
auto ok = png_get_iCCP(png_ptr, info_ptr, &name, &compression_type,
&profile, &proflen);
if (ok && proflen) {
ppf->icc.resize(proflen);
memcpy(ppf->icc.data(), profile, proflen);
ppf->icc.assign(profile, profile + proflen);
have_color = true;
} else {
// TODO(eustas): JXL_WARNING?
@ -670,7 +703,8 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
has_nontrivial_background && frame.dispose_op != DISPOSE_OP_PREVIOUS;
size_t x0 = frame.x0;
size_t y0 = frame.y0;
size_t xsize = frame.data.xsize;
size_t ysize = frame.data.ysize;
if (previous_frame_should_be_cleared) {
size_t xs = frame.data.xsize;
size_t ys = frame.data.ysize;
@ -710,6 +744,8 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
x0 = px0;
y0 = py0;
xsize = pxs;
ysize = pys;
should_blend = false;
ppf->frames.emplace_back(std::move(new_data));
} else {
@ -718,12 +754,15 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
memset(blank.pixels(), 0, blank.pixels_size);
ppf->frames.emplace_back(std::move(blank));
auto& pframe = ppf->frames.back();
pframe.x0 = px0;
pframe.y0 = py0;
pframe.frame_info.layer_info.crop_x0 = px0;
pframe.frame_info.layer_info.crop_y0 = py0;
pframe.frame_info.layer_info.xsize = frame.xsize;
pframe.frame_info.layer_info.ysize = frame.ysize;
pframe.frame_info.duration = 0;
pframe.blend = false;
pframe.use_for_next_frame = true;
pframe.frame_info.layer_info.have_crop = 0;
pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE;
pframe.frame_info.layer_info.blend_info.source = 0;
pframe.frame_info.layer_info.save_as_reference = 1;
ppf->frames.emplace_back(std::move(frame.data));
}
} else {
@ -731,18 +770,22 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
}
auto& pframe = ppf->frames.back();
pframe.x0 = x0;
pframe.y0 = y0;
pframe.frame_info.layer_info.crop_x0 = x0;
pframe.frame_info.layer_info.crop_y0 = y0;
pframe.frame_info.layer_info.xsize = xsize;
pframe.frame_info.layer_info.ysize = ysize;
pframe.frame_info.duration = frame.duration;
pframe.blend = should_blend;
pframe.use_for_next_frame = use_for_next_frame;
pframe.frame_info.layer_info.blend_info.blendmode =
should_blend ? JXL_BLEND_BLEND : JXL_BLEND_REPLACE;
bool is_full_size = x0 == 0 && y0 == 0 && xsize == ppf->info.xsize &&
ysize == ppf->info.ysize;
pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1;
pframe.frame_info.layer_info.blend_info.source = should_blend ? 1 : 0;
pframe.frame_info.layer_info.blend_info.alpha = 0;
pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0;
if (has_nontrivial_background &&
frame.dispose_op == DISPOSE_OP_BACKGROUND) {
previous_frame_should_be_cleared = true;
} else {
previous_frame_should_be_cleared = false;
}
previous_frame_should_be_cleared =
has_nontrivial_background && frame.dispose_op == DISPOSE_OP_BACKGROUND;
}
if (ppf->frames.empty()) return JXL_FAILURE("No frames decoded");
ppf->frames.back().frame_info.is_last = true;
@ -750,60 +793,5 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
return true;
}
static void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) {
PaddedBytes* bytes = static_cast<PaddedBytes*>(png_get_io_ptr(png_ptr));
bytes->append(data, data + length);
}
// Stores XMP and EXIF/IPTC into key/value strings for PNG
class BlobsWriterPNG {
public:
static Status Encode(const Blobs& blobs, std::vector<std::string>* strings) {
if (!blobs.exif.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("exif", blobs.exif, strings));
}
if (!blobs.iptc.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings));
}
if (!blobs.xmp.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings));
}
return true;
}
private:
static JXL_INLINE char EncodeNibble(const uint8_t nibble) {
JXL_ASSERT(nibble < 16);
return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10;
}
static Status EncodeBase16(const std::string& type, const PaddedBytes& bytes,
std::vector<std::string>* strings) {
// Encoding: base16 with newline after 72 chars.
const size_t base16_size =
2 * bytes.size() + DivCeil(bytes.size(), size_t(36)) + 1;
std::string base16;
base16.reserve(base16_size);
for (size_t i = 0; i < bytes.size(); ++i) {
if (i % 36 == 0) base16.push_back('\n');
base16.push_back(EncodeNibble(bytes[i] >> 4));
base16.push_back(EncodeNibble(bytes[i] & 0x0F));
}
base16.push_back('\n');
JXL_ASSERT(base16.length() == base16_size);
char key[30];
snprintf(key, sizeof(key), "Raw profile type %s", type.c_str());
char header[30];
snprintf(header, sizeof(header), "\n%s\n%8" PRIuS, type.c_str(),
bytes.size());
strings->push_back(std::string(key));
strings->push_back(std::string(header) + base16);
return true;
}
};
} // namespace extras
} // namespace jxl

View File

@ -42,9 +42,7 @@ Status ApplyColorHints(const ColorHints& color_hints,
got_color_space = true;
} else if (key == "icc_pathname") {
PaddedBytes icc;
JXL_RETURN_IF_ERROR(ReadFile(value, &icc));
ppf->icc = std::vector<uint8_t>{icc.data(), icc.data() + icc.size()};
JXL_RETURN_IF_ERROR(ReadFile(value, &ppf->icc));
got_color_space = true;
} else {
JXL_WARNING("Ignoring %s hint", key.c_str());

View File

@ -31,31 +31,6 @@ constexpr size_t kMinBytes = 9;
} // namespace
std::string ExtensionFromCodec(Codec codec, const bool is_gray,
const size_t bits_per_sample) {
switch (codec) {
case Codec::kJPG:
return ".jpg";
case Codec::kPGX:
return ".pgx";
case Codec::kPNG:
return ".png";
case Codec::kPNM:
if (is_gray) return ".pgm";
return (bits_per_sample == 32) ? ".pfm" : ".ppm";
case Codec::kGIF:
return ".gif";
case Codec::kEXR:
return ".exr";
case Codec::kPSD:
return ".psd";
case Codec::kUnknown:
return std::string();
}
JXL_UNREACHABLE;
return std::string();
}
Codec CodecFromExtension(std::string extension,
size_t* JXL_RESTRICT bits_per_sample) {
std::transform(
@ -68,10 +43,8 @@ Codec CodecFromExtension(std::string extension,
if (extension == ".pgx") return Codec::kPGX;
if (extension == ".pbm") {
if (bits_per_sample != nullptr) *bits_per_sample = 1;
return Codec::kPNM;
}
if (extension == ".pam") return Codec::kPNM;
if (extension == ".pnm") return Codec::kPNM;
if (extension == ".pgm") return Codec::kPNM;
if (extension == ".ppm") return Codec::kPNM;
if (extension == ".pfm") {
@ -83,16 +56,13 @@ Codec CodecFromExtension(std::string extension,
if (extension == ".exr") return Codec::kEXR;
if (extension == ".psd") return Codec::kPSD;
return Codec::kUnknown;
}
Status DecodeBytes(const Span<const uint8_t> bytes,
const ColorHints& color_hints,
const SizeConstraints& constraints,
extras::PackedPixelFile* ppf, ThreadPool* pool,
Codec* orig_codec) {
extras::PackedPixelFile* ppf, Codec* orig_codec) {
if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes");
*ppf = extras::PackedPixelFile();
@ -123,7 +93,7 @@ Status DecodeBytes(const Span<const uint8_t> bytes,
}
#endif
#if JPEGXL_ENABLE_EXR
else if (DecodeImageEXR(bytes, color_hints, constraints, pool, ppf)) {
else if (DecodeImageEXR(bytes, color_hints, constraints, ppf)) {
codec = Codec::kEXR;
}
#endif

View File

@ -33,8 +33,7 @@ enum class Codec : uint32_t {
kPGX,
kJPG,
kGIF,
kEXR,
kPSD
kEXR
};
static inline constexpr uint64_t EnumBits(Codec /*unused*/) {
@ -49,13 +48,9 @@ static inline constexpr uint64_t EnumBits(Codec /*unused*/) {
#if JPEGXL_ENABLE_EXR
| MakeBit(Codec::kEXR)
#endif
| MakeBit(Codec::kPSD);
;
}
// Lower case ASCII including dot, e.g. ".png".
std::string ExtensionFromCodec(Codec codec, bool is_gray,
size_t bits_per_sample);
// If and only if extension is ".pfm", *bits_per_sample is updated to 32 so
// that Encode() would encode to PFM instead of PPM.
Codec CodecFromExtension(std::string extension,
@ -65,8 +60,7 @@ Codec CodecFromExtension(std::string extension,
// color_space_hint may specify the color space, otherwise, defaults to sRGB.
Status DecodeBytes(Span<const uint8_t> bytes, const ColorHints& color_hints,
const SizeConstraints& constraints,
extras::PackedPixelFile* ppf, ThreadPool* pool = nullptr,
Codec* orig_codec = nullptr);
extras::PackedPixelFile* ppf, Codec* orig_codec = nullptr);
} // namespace extras
} // namespace jxl

View File

@ -29,18 +29,6 @@ using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg());
constexpr int kExrBitsPerSample = 16;
constexpr int kExrAlphaBits = 16;
size_t GetNumThreads(ThreadPool* pool) {
size_t exr_num_threads = 1;
JXL_CHECK(RunOnPool(
pool, 0, 1,
[&](size_t num_threads) {
exr_num_threads = num_threads;
return true;
},
[&](uint32_t /* task */, size_t /*thread*/) {}, "DecodeImageEXRThreads"));
return exr_num_threads;
}
class InMemoryIStream : public OpenEXR::IStream {
public:
// The data pointed to by `bytes` must outlive the InMemoryIStream.
@ -74,15 +62,8 @@ class InMemoryIStream : public OpenEXR::IStream {
} // namespace
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
const SizeConstraints& constraints, ThreadPool* pool,
const SizeConstraints& constraints,
PackedPixelFile* ppf) {
// Get the number of threads we should be using for OpenEXR.
// OpenEXR creates its own set of threads, independent from ours. `pool` is
// only used for converting from a buffer of OpenEXR::Rgba to Image3F.
// TODO(sboukortt): look into changing that with OpenEXR 2.3 which allows
// custom thread pools according to its changelog.
OpenEXR::setGlobalThreadCount(GetNumThreads(pool));
InMemoryIStream is(bytes);
#ifdef __EXCEPTIONS
@ -149,27 +130,24 @@ Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
input_rows.data() - input.dataWindow().min.x - start_y * row_size,
/*xStride=*/1, /*yStride=*/row_size);
input.readPixels(start_y, end_y);
JXL_RETURN_IF_ERROR(RunOnPool(
pool, start_y, end_y + 1, ThreadPool::NoInit,
[&](const uint32_t exr_y, size_t /* thread */) {
const int image_y = exr_y - input.displayWindow().min.y;
const OpenEXR::Rgba* const JXL_RESTRICT input_row =
&input_rows[(exr_y - start_y) * row_size];
uint8_t* row = static_cast<uint8_t*>(frame.color.pixels()) +
frame.color.stride * image_y;
const uint32_t pixel_size =
(3 + (has_alpha ? 1 : 0)) * kExrBitsPerSample / 8;
for (int exr_x = std::max(input.dataWindow().min.x,
input.displayWindow().min.x);
exr_x <=
std::min(input.dataWindow().max.x, input.displayWindow().max.x);
++exr_x) {
const int image_x = exr_x - input.displayWindow().min.x;
memcpy(row + image_x * pixel_size,
input_row + (exr_x - input.dataWindow().min.x), pixel_size);
}
},
"DecodeImageEXR"));
for (int exr_y = start_y; exr_y <= end_y; ++exr_y) {
const int image_y = exr_y - input.displayWindow().min.y;
const OpenEXR::Rgba* const JXL_RESTRICT input_row =
&input_rows[(exr_y - start_y) * row_size];
uint8_t* row = static_cast<uint8_t*>(frame.color.pixels()) +
frame.color.stride * image_y;
const uint32_t pixel_size =
(3 + (has_alpha ? 1 : 0)) * kExrBitsPerSample / 8;
for (int exr_x =
std::max(input.dataWindow().min.x, input.displayWindow().min.x);
exr_x <=
std::min(input.dataWindow().max.x, input.displayWindow().max.x);
++exr_x) {
const int image_x = exr_x - input.displayWindow().min.x;
memcpy(row + image_x * pixel_size,
input_row + (exr_x - input.dataWindow().min.x), pixel_size);
}
}
}
ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;

View File

@ -21,8 +21,7 @@ namespace extras {
// Decodes `bytes` into `ppf`. color_hints are ignored.
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
const SizeConstraints& constraints, ThreadPool* pool,
PackedPixelFile* ppf);
const SizeConstraints& constraints, PackedPixelFile* ppf);
} // namespace extras
} // namespace jxl

View File

@ -12,6 +12,7 @@
#include <utility>
#include <vector>
#include "jxl/codestream_header.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/sanitizers.h"
@ -257,15 +258,26 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
GraphicsControlBlock gcb;
DGifSavedExtensionToGCB(gif.get(), i, &gcb);
msan::UnpoisonMemory(&gcb, sizeof(gcb));
bool is_full_size = total_rect.x0() == 0 && total_rect.y0() == 0 &&
total_rect.xsize() == canvas.color.xsize &&
total_rect.ysize() == canvas.color.ysize;
if (ppf->info.have_animation) {
frame->frame_info.duration = gcb.DelayTime;
frame->x0 = total_rect.x0();
frame->y0 = total_rect.y0();
frame->frame_info.layer_info.have_crop = static_cast<int>(!is_full_size);
frame->frame_info.layer_info.crop_x0 = total_rect.x0();
frame->frame_info.layer_info.crop_y0 = total_rect.y0();
frame->frame_info.layer_info.xsize = frame->color.xsize;
frame->frame_info.layer_info.ysize = frame->color.ysize;
if (last_base_was_none) {
replace = true;
}
frame->blend = !replace;
frame->frame_info.layer_info.blend_info.blendmode =
replace ? JXL_BLEND_REPLACE : JXL_BLEND_BLEND;
// We always only reference at most the last frame
frame->frame_info.layer_info.blend_info.source =
last_base_was_none ? 0u : 1u;
frame->frame_info.layer_info.blend_info.clamp = 1;
frame->frame_info.layer_info.blend_info.alpha = 0;
// TODO(veluca): this could in principle be implemented.
if (last_base_was_none &&
(total_rect.x0() != 0 || total_rect.y0() != 0 ||
@ -278,14 +290,14 @@ Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
switch (gcb.DisposalMode) {
case DISPOSE_DO_NOT:
case DISPOSE_BACKGROUND:
frame->use_for_next_frame = true;
frame->frame_info.layer_info.save_as_reference = 1u;
last_base_was_none = false;
break;
case DISPOSE_PREVIOUS:
frame->use_for_next_frame = false;
frame->frame_info.layer_info.save_as_reference = 0u;
break;
default:
frame->use_for_next_frame = false;
frame->frame_info.layer_info.save_as_reference = 0u;
last_base_was_none = true;
}
}

View File

@ -117,6 +117,8 @@ class Parser {
// 0xa, or 0xd 0xa.
JXL_RETURN_IF_ERROR(SkipLineBreak());
// TODO(jon): could do up to 24-bit by converting the values to
// JXL_TYPE_FLOAT.
if (header->bits_per_sample > 16) {
return JXL_FAILURE("PGX: >16 bits not yet supported");
}
@ -127,9 +129,7 @@ class Parser {
}
size_t numpixels = header->xsize * header->ysize;
size_t bytes_per_pixel = header->bits_per_sample <= 8 ? 1
: header->bits_per_sample <= 16 ? 2
: 4;
size_t bytes_per_pixel = header->bits_per_sample <= 8 ? 1 : 2;
if (pos_ + numpixels * bytes_per_pixel > end_) {
return JXL_FAILURE("PGX: data too small");
}
@ -174,9 +174,7 @@ Status DecodeImagePGX(const Span<const uint8_t> bytes,
ppf->info.orientation = JXL_ORIENT_IDENTITY;
JxlDataType data_type;
if (header.bits_per_sample > 16) {
data_type = JXL_TYPE_UINT32;
} else if (header.bits_per_sample > 8) {
if (header.bits_per_sample > 8) {
data_type = JXL_TYPE_UINT16;
} else {
data_type = JXL_TYPE_UINT8;

View File

@ -19,7 +19,6 @@ namespace {
struct HeaderPNM {
size_t xsize;
size_t ysize;
bool is_bit; // PBM
bool is_gray; // PGM
bool has_alpha; // PAM
size_t bits_per_sample;
@ -39,14 +38,9 @@ class Parser {
const uint8_t type = pos_[1];
pos_ += 2;
header->is_bit = false;
switch (type) {
case '4':
header->is_bit = true;
header->is_gray = true;
header->bits_per_sample = 1;
return ParseHeaderPNM(header, pos);
return JXL_FAILURE("pbm not supported");
case '5':
header->is_gray = true;
@ -169,7 +163,7 @@ class Parser {
return true;
}
Status MatchString(const char* keyword) {
Status MatchString(const char* keyword, bool skipws = true) {
const uint8_t* ppos = pos_;
while (*keyword) {
if (ppos >= end_) return JXL_FAILURE("PAM: unexpected end of input");
@ -178,14 +172,18 @@ class Parser {
keyword++;
}
pos_ = ppos;
JXL_RETURN_IF_ERROR(SkipWhitespace());
if (skipws) {
JXL_RETURN_IF_ERROR(SkipWhitespace());
} else {
JXL_RETURN_IF_ERROR(SkipSingleWhitespace());
}
return true;
}
Status ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) {
size_t depth = 3;
size_t max_val = 255;
while (!MatchString("ENDHDR")) {
while (!MatchString("ENDHDR", /*skipws=*/false)) {
JXL_RETURN_IF_ERROR(SkipWhitespace());
if (MatchString("WIDTH")) {
JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize));
@ -231,7 +229,11 @@ class Parser {
if (max_val == 0 || max_val >= 65536) {
return JXL_FAILURE("PAM: bad MAXVAL");
}
header->bits_per_sample = CeilLog2Nonzero(max_val);
// e.g When `max_val` is 1 , we want 1 bit:
header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
if ((1u << header->bits_per_sample) - 1 != max_val)
return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
// PAM does not pack bits as in PBM.
header->floating_point = false;
header->big_endian = true;
@ -246,15 +248,15 @@ class Parser {
JXL_RETURN_IF_ERROR(SkipWhitespace());
JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize));
if (!header->is_bit) {
JXL_RETURN_IF_ERROR(SkipWhitespace());
size_t max_val;
JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
if (max_val == 0 || max_val >= 65536) {
return JXL_FAILURE("PNM: bad MaxVal");
}
header->bits_per_sample = CeilLog2Nonzero(max_val);
JXL_RETURN_IF_ERROR(SkipWhitespace());
size_t max_val;
JXL_RETURN_IF_ERROR(ParseUnsigned(&max_val));
if (max_val == 0 || max_val >= 65536) {
return JXL_FAILURE("PNM: bad MaxVal");
}
header->bits_per_sample = FloorLog2Nonzero(max_val) + 1;
if ((1u << header->bits_per_sample) - 1 != max_val)
return JXL_FAILURE("PNM: unsupported MaxVal (expected 2^n - 1)");
header->floating_point = false;
header->big_endian = true;
@ -276,7 +278,12 @@ class Parser {
// indicate endianness. All software expects nominal range 0..1.
double scale;
JXL_RETURN_IF_ERROR(ParseSigned(&scale));
header->big_endian = scale >= 0.0;
if (scale == 0.0) {
return JXL_FAILURE("PFM: bad scale factor value.");
} else if (std::abs(scale) != 1.0) {
JXL_WARNING("PFM: Discarding non-unit scale factor");
}
header->big_endian = scale > 0.0;
header->bits_per_sample = 32;
header->floating_point = true;
@ -341,12 +348,8 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
// There's no float16 pnm version.
data_type = JXL_TYPE_FLOAT;
} else {
if (header.bits_per_sample > 16) {
data_type = JXL_TYPE_UINT32;
} else if (header.bits_per_sample > 8) {
if (header.bits_per_sample > 8) {
data_type = JXL_TYPE_UINT16;
} else if (header.is_bit) {
data_type = JXL_TYPE_BOOLEAN;
} else {
data_type = JXL_TYPE_UINT8;
}
@ -363,6 +366,7 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
ppf->frames.emplace_back(header.xsize, header.ysize, format);
auto* frame = &ppf->frames.back();
frame->color.bitdepth_from_format = false;
frame->color.flipped_y = header.bits_per_sample == 32; // PFMs are flipped
size_t pnm_remaining_size = bytes.data() + bytes.size() - pos;
if (pnm_remaining_size < frame->color.pixels_size) {

View File

@ -49,6 +49,7 @@
#include "lib/jxl/dec_external_image.h"
#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_image_bundle.h"
#include "lib/jxl/exif.h"
#include "lib/jxl/frame_header.h"
#include "lib/jxl/headers.h"
#include "lib/jxl/image.h"
@ -70,7 +71,12 @@ class BlobsWriterPNG {
public:
static Status Encode(const Blobs& blobs, std::vector<std::string>* strings) {
if (!blobs.exif.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("exif", blobs.exif, strings));
// PNG viewers typically ignore Exif orientation but not all of them do
// (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the
// identity to avoid repeated orientation.
std::vector<uint8_t> exif = blobs.exif;
ResetExifOrientation(exif);
JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings));
}
if (!blobs.iptc.empty()) {
JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings));
@ -87,7 +93,8 @@ class BlobsWriterPNG {
return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10;
}
static Status EncodeBase16(const std::string& type, const PaddedBytes& bytes,
static Status EncodeBase16(const std::string& type,
const std::vector<uint8_t>& bytes,
std::vector<std::string>* strings) {
// Encoding: base16 with newline after 72 chars.
const size_t base16_size =
@ -158,12 +165,12 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired,
size_t stride = ib.oriented_xsize() *
DivCeil(c_desired.Channels() * bits_per_sample + alpha_bits,
kBitsPerByte);
PaddedBytes raw_bytes(stride * ib.oriented_ysize());
std::vector<uint8_t> raw_bytes(stride * ib.oriented_ysize());
JXL_RETURN_IF_ERROR(ConvertToExternal(
*transformed, bits_per_sample, /*float_out=*/false,
c_desired.Channels() + (ib.HasAlpha() ? 1 : 0), JXL_BIG_ENDIAN, stride,
pool, raw_bytes.data(), raw_bytes.size(), /*out_callback=*/nullptr,
/*out_opaque=*/nullptr, metadata.GetOrientation()));
pool, raw_bytes.data(), raw_bytes.size(),
/*out_callback=*/{}, metadata.GetOrientation()));
int width = ib.oriented_xsize();
int height = ib.oriented_ysize();
@ -188,10 +195,10 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired,
std::vector<std::string> textstrings;
JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(io->blobs, &textstrings));
for (size_t i = 0; i + 1 < textstrings.size(); i += 2) {
for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) {
png_text text;
text.key = const_cast<png_charp>(textstrings[i].c_str());
text.text = const_cast<png_charp>(textstrings[i + 1].c_str());
text.key = const_cast<png_charp>(textstrings[kk].c_str());
text.text = const_cast<png_charp>(textstrings[kk + 1].c_str());
text.compression = PNG_TEXT_COMPRESSION_zTXt;
png_set_text(png_ptr, info_ptr, &text, 1);
}
@ -241,7 +248,7 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired,
png_write_image(png_ptr, &rows[0]);
png_write_flush(png_ptr);
if (count > 0) {
PaddedBytes fdata(4);
std::vector<uint8_t> fdata(4);
png_save_uint_32(fdata.data(), anim_chunks++);
size_t p = pos;
while (p + 8 < bytes->size()) {
@ -250,7 +257,8 @@ Status EncodeImageAPNG(const CodecInOut* io, const ColorEncoding& c_desired,
JXL_ASSERT(bytes->operator[](p + 5) == 'D');
JXL_ASSERT(bytes->operator[](p + 6) == 'A');
JXL_ASSERT(bytes->operator[](p + 7) == 'T');
fdata.append(bytes->data() + p + 8, bytes->data() + p + 8 + len);
fdata.insert(fdata.end(), bytes->data() + p + 8,
bytes->data() + p + 8 + len);
p += len + 12;
}
bytes->resize(pos);

View File

@ -19,8 +19,10 @@
#include "lib/jxl/base/status.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/common.h"
#include "lib/jxl/dec_external_image.h"
#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_image_bundle.h"
#include "lib/jxl/exif.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/sanitizers.h"
@ -33,7 +35,6 @@ namespace extras {
namespace {
constexpr float kJPEGSampleMultiplier = MAXJSAMPLE;
constexpr unsigned char kICCSignature[12] = {
0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
constexpr int kICCMarker = JPEG_APP0 + 2;
@ -43,13 +44,6 @@ constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
0x66, 0x00, 0x00};
constexpr int kExifMarker = JPEG_APP0 + 1;
constexpr float kJPEGSampleMin = 0;
constexpr float kJPEGSampleMax = MAXJSAMPLE;
// TODO (jon): take orientation into account when writing jpeg output
// TODO (jon): write Exif blob also in sjpeg encoding
// TODO (jon): overwrite orientation in Exif blob to avoid double orientation
void WriteICCProfile(jpeg_compress_struct* const cinfo,
const PaddedBytes& icc) {
constexpr size_t kMaxIccBytesInMarker =
@ -73,15 +67,15 @@ void WriteICCProfile(jpeg_compress_struct* const cinfo,
}
}
}
void WriteExif(jpeg_compress_struct* const cinfo, const PaddedBytes& exif) {
if (exif.size() < 4) return;
void WriteExif(jpeg_compress_struct* const cinfo,
const std::vector<uint8_t>& exif) {
jpeg_write_m_header(
cinfo, kExifMarker,
static_cast<unsigned int>(exif.size() - 4 + sizeof kExifSignature));
static_cast<unsigned int>(exif.size() + sizeof kExifSignature));
for (const unsigned char c : kExifSignature) {
jpeg_write_m_byte(cinfo, c);
}
for (size_t i = 4; i < exif.size(); ++i) {
for (size_t i = 0; i < exif.size(); ++i) {
jpeg_write_m_byte(cinfo, exif[i]);
}
}
@ -115,8 +109,8 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io,
unsigned char* buffer = nullptr;
unsigned long size = 0;
jpeg_mem_dest(&cinfo, &buffer, &size);
cinfo.image_width = ib->xsize();
cinfo.image_height = ib->ysize();
cinfo.image_width = ib->oriented_xsize();
cinfo.image_height = ib->oriented_ysize();
if (ib->IsGray()) {
cinfo.input_components = 1;
cinfo.in_color_space = JCS_GRAYSCALE;
@ -134,27 +128,26 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io,
if (!ib->IsSRGB()) {
WriteICCProfile(&cinfo, ib->c_current().ICC());
}
WriteExif(&cinfo, io->blobs.exif);
if (!io->blobs.exif.empty()) {
std::vector<uint8_t> exif = io->blobs.exif;
ResetExifOrientation(exif);
WriteExif(&cinfo, exif);
}
if (cinfo.input_components > 3 || cinfo.input_components < 0)
return JXL_FAILURE("invalid numbers of components");
std::unique_ptr<JSAMPLE[]> row(
new JSAMPLE[cinfo.input_components * cinfo.image_width]);
for (size_t y = 0; y < ib->ysize(); ++y) {
const float* const JXL_RESTRICT input_row[3] = {
ib->color().ConstPlaneRow(0, y), ib->color().ConstPlaneRow(1, y),
ib->color().ConstPlaneRow(2, y)};
for (size_t x = 0; x < ib->xsize(); ++x) {
for (size_t c = 0; c < static_cast<size_t>(cinfo.input_components); ++c) {
JXL_RETURN_IF_ERROR(c < 3);
row[cinfo.input_components * x + c] = static_cast<JSAMPLE>(
std::max(std::min(kJPEGSampleMultiplier * input_row[c][x] + .5f,
kJPEGSampleMax),
kJPEGSampleMin));
}
}
JSAMPROW rows[] = {row.get()};
jpeg_write_scanlines(&cinfo, rows, 1);
size_t stride =
ib->oriented_xsize() * cinfo.input_components * sizeof(JSAMPLE);
PaddedBytes raw_bytes(stride * ib->oriented_ysize());
JXL_RETURN_IF_ERROR(ConvertToExternal(
*ib, BITS_IN_JSAMPLE, /*float_out=*/false, cinfo.input_components,
JXL_BIG_ENDIAN, stride, nullptr, raw_bytes.data(), raw_bytes.size(),
/*out_callback=*/{}, ib->metadata()->GetOrientation()));
for (size_t y = 0; y < ib->oriented_ysize(); ++y) {
JSAMPROW row[] = {raw_bytes.data() + y * stride};
jpeg_write_scanlines(&cinfo, row, 1);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
@ -167,7 +160,8 @@ Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io,
return true;
}
Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality,
Status EncodeWithSJpeg(const ImageBundle* ib, const CodecInOut* io,
size_t quality,
const YCbCrChromaSubsampling& chroma_subsampling,
PaddedBytes* bytes) {
#if !JPEGXL_ENABLE_SJPEG
@ -178,6 +172,11 @@ Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality,
param.iccp.assign(ib->metadata()->color_encoding.ICC().begin(),
ib->metadata()->color_encoding.ICC().end());
}
std::vector<uint8_t> exif = io->blobs.exif;
if (!exif.empty()) {
ResetExifOrientation(exif);
param.exif.assign(exif.begin(), exif.end());
}
if (chroma_subsampling.Is444()) {
param.yuv_mode = SJPEG_YUV_444;
} else if (chroma_subsampling.Is420()) {
@ -185,24 +184,17 @@ Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality,
} else {
return JXL_FAILURE("sjpeg does not support this chroma subsampling mode");
}
std::vector<uint8_t> rgb;
rgb.reserve(ib->xsize() * ib->ysize() * 3);
for (size_t y = 0; y < ib->ysize(); ++y) {
const float* const rows[] = {
ib->color().ConstPlaneRow(0, y),
ib->color().ConstPlaneRow(1, y),
ib->color().ConstPlaneRow(2, y),
};
for (size_t x = 0; x < ib->xsize(); ++x) {
for (const float* const row : rows) {
rgb.push_back(static_cast<uint8_t>(
std::max(0.f, std::min(255.f, roundf(255.f * row[x])))));
}
}
}
size_t stride = ib->oriented_xsize() * 3;
PaddedBytes rgb(ib->xsize() * ib->ysize() * 3);
JXL_RETURN_IF_ERROR(
ConvertToExternal(*ib, 8, /*float_out=*/false, 3, JXL_BIG_ENDIAN, stride,
nullptr, rgb.data(), rgb.size(),
/*out_callback=*/{}, ib->metadata()->GetOrientation()));
std::string output;
JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->xsize(), ib->ysize(),
ib->xsize() * 3, param, &output));
JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->oriented_xsize(),
ib->oriented_ysize(), stride, param,
&output));
bytes->assign(
reinterpret_cast<const uint8_t*>(output.data()),
reinterpret_cast<const uint8_t*>(output.data() + output.size()));
@ -234,7 +226,7 @@ Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality,
break;
case JpegEncoder::kSJpeg:
JXL_RETURN_IF_ERROR(
EncodeWithSJpeg(ib, quality, chroma_subsampling, bytes));
EncodeWithSJpeg(ib, io, quality, chroma_subsampling, bytes));
break;
default:
return JXL_FAILURE("tried to use an unknown JPEG encoder");

View File

@ -75,8 +75,8 @@ Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired,
ConvertToExternal(*transformed, bits_per_sample,
/*float_out=*/false,
/*num_channels=*/1, JXL_BIG_ENDIAN, stride, pool,
pixels.data(), pixels.size(), /*out_callback=*/nullptr,
/*out_opaque=*/nullptr, metadata.GetOrientation()));
pixels.data(), pixels.size(),
/*out_callback=*/{}, metadata.GetOrientation()));
char header[kMaxHeaderSize];
int header_size = 0;

View File

@ -35,14 +35,23 @@ constexpr size_t kMaxHeaderSize = 200;
Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample,
const bool little_endian, char* header,
int* JXL_RESTRICT chars_written) {
if (ppf.info.alpha_bits > 0) return JXL_FAILURE("PNM: can't store alpha");
bool is_gray = ppf.info.num_color_channels <= 2;
size_t oriented_xsize =
ppf.info.orientation <= 4 ? ppf.info.xsize : ppf.info.ysize;
size_t oriented_ysize =
ppf.info.orientation <= 4 ? ppf.info.ysize : ppf.info.xsize;
if (bits_per_sample == 32) { // PFM
if (ppf.info.alpha_bits > 0) { // PAM
if (bits_per_sample > 16) return JXL_FAILURE("PNM cannot have > 16 bits");
const uint32_t max_val = (1U << bits_per_sample) - 1;
*chars_written =
snprintf(header, kMaxHeaderSize,
"P7\nWIDTH %" PRIuS "\nHEIGHT %" PRIuS
"\nDEPTH %u\nMAXVAL %u\nTUPLTYPE %s\nENDHDR\n",
oriented_xsize, oriented_ysize, is_gray ? 2 : 4, max_val,
is_gray ? "GRAYSCALE_ALPHA" : "RGB_ALPHA");
JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
kMaxHeaderSize);
} else if (bits_per_sample == 32) { // PFM
const char type = is_gray ? 'f' : 'F';
const double scale = little_endian ? -1.0 : 1.0;
*chars_written =
@ -50,15 +59,6 @@ Status EncodeHeader(const PackedPixelFile& ppf, const size_t bits_per_sample,
type, oriented_xsize, oriented_ysize, scale);
JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
kMaxHeaderSize);
} else if (bits_per_sample == 1) { // PBM
if (is_gray) {
return JXL_FAILURE("Cannot encode color as PBM");
}
*chars_written =
snprintf(header, kMaxHeaderSize, "P4\n%" PRIuS " %" PRIuS "\n",
oriented_xsize, oriented_ysize);
JXL_RETURN_IF_ERROR(static_cast<unsigned int>(*chars_written) <
kMaxHeaderSize);
} else { // PGM/PPM
const uint32_t max_val = (1U << bits_per_sample) - 1;
if (max_val >= 65536) return JXL_FAILURE("PNM cannot have > 16 bits");
@ -100,7 +100,7 @@ Status EncodeImagePNM(const PackedPixelFile& ppf, size_t bits_per_sample,
ThreadPool* pool, size_t frame_index,
std::vector<uint8_t>* bytes) {
const bool floating_point = bits_per_sample > 16;
// Choose native for PFM; PGM/PPM require big-endian (N/A for PBM)
// Choose native for PFM; PGM/PPM require big-endian
const JxlEndianness endianness =
floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN;
if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() ||

View File

@ -64,6 +64,12 @@ class PackedImage {
// Whether the y coordinate is flipped (y=0 is the last row).
bool flipped_y = false;
// Whether the range is determined by format or by JxlBasicInfo
// e.g. if format is UINT16 and JxlBasicInfo bits_per_sample is 10,
// then if bitdepth_from_format == true, the range is 0..65535
// while if bitdepth_from_format == false, the range is 0..1023.
bool bitdepth_from_format = true;
// The number of bytes per row.
size_t stride;
@ -73,21 +79,17 @@ class PackedImage {
static size_t BitsPerChannel(JxlDataType data_type) {
switch (data_type) {
case JXL_TYPE_BOOLEAN:
return 1;
case JXL_TYPE_UINT8:
return 8;
case JXL_TYPE_UINT16:
return 16;
case JXL_TYPE_UINT32:
return 32;
case JXL_TYPE_FLOAT:
return 32;
case JXL_TYPE_FLOAT16:
return 16;
// No default, give compiler error if new type not handled.
default:
JXL_ABORT("Unhandled JxlDataType");
}
return 0; // Indicate invalid data type.
}
private:
@ -116,16 +118,6 @@ class PackedFrame {
JxlFrameHeader frame_info = {};
std::string name;
// Offset of the frame in the image.
// TODO(deymo): Add support in the API for this.
size_t x0 = 0;
size_t y0 = 0;
// Whether this frame should be blended with the previous one.
// TODO(deymo): Maybe add support for this in the API.
bool blend = false;
bool use_for_next_frame = false;
// The pixel data for the color (or grayscale) channels.
PackedImage color;
// Extra channel image data.

View File

@ -65,21 +65,25 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
if (!io->metadata.m.color_encoding.SetICC(std::move(icc))) {
fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB");
io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
} else {
if (io->metadata.m.color_encoding.IsGray() != is_gray) {
// E.g. JPG image has 3 channels, but gray ICC.
return JXL_FAILURE("Embedded ICC does not match image color type");
}
}
} else {
JXL_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding(
ppf.color_encoding, &io->metadata.m.color_encoding));
if (io->metadata.m.color_encoding.ICC().empty()) {
return JXL_FAILURE("Failed to serialize ICC");
}
}
// Convert the extra blobs
io->blobs.exif.clear();
io->blobs.exif.append(ppf.metadata.exif);
io->blobs.iptc.clear();
io->blobs.iptc.append(ppf.metadata.iptc);
io->blobs.jumbf.clear();
io->blobs.jumbf.append(ppf.metadata.jumbf);
io->blobs.xmp.clear();
io->blobs.xmp.append(ppf.metadata.xmp);
io->blobs.exif = ppf.metadata.exif;
io->blobs.iptc = ppf.metadata.iptc;
io->blobs.jumbf = ppf.metadata.jumbf;
io->blobs.xmp = ppf.metadata.xmp;
// Append all other extra channels.
for (const PackedPixelFile::PackedExtraChannel& info :
@ -107,7 +111,9 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
for (const auto& frame : ppf.frames) {
JXL_ASSERT(frame.color.pixels() != nullptr);
size_t frame_bits_per_sample =
frame.color.BitsPerChannel(frame.color.format.data_type);
(frame.color.bitdepth_from_format
? frame.color.BitsPerChannel(frame.color.format.data_type)
: ppf.info.bits_per_sample);
JXL_ASSERT(frame_bits_per_sample != 0);
// It is ok for the frame.color.format.num_channels to not match the
// number of channels on the image.
@ -117,20 +123,21 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
const Span<const uint8_t> span(
static_cast<const uint8_t*>(frame.color.pixels()),
frame.color.pixels_size);
Rect frame_rect =
Rect(frame.x0, frame.y0, frame.color.xsize, frame.color.ysize);
Rect frame_rect = Rect(frame.frame_info.layer_info.crop_x0,
frame.frame_info.layer_info.crop_y0,
frame.frame_info.layer_info.xsize,
frame.frame_info.layer_info.ysize);
JXL_ASSERT(frame_rect.IsInside(Rect(0, 0, ppf.info.xsize, ppf.info.ysize)));
ImageBundle bundle(&io->metadata.m);
if (ppf.info.have_animation) {
bundle.duration = frame.frame_info.duration;
bundle.blend = frame.blend;
bundle.use_for_next_frame = frame.use_for_next_frame;
bundle.blend = frame.frame_info.layer_info.blend_info.blendmode > 0;
bundle.use_for_next_frame =
frame.frame_info.layer_info.save_as_reference > 0;
bundle.origin.x0 = frame.frame_info.layer_info.crop_x0;
bundle.origin.y0 = frame.frame_info.layer_info.crop_y0;
}
bundle.name = frame.name; // frame.frame_info.name_length is ignored here.
bundle.origin.x0 = frame.x0;
bundle.origin.y0 = frame.y0;
JXL_ASSERT(io->metadata.m.color_encoding.IsGray() ==
(frame.color.format.num_channels <= 2));
@ -144,11 +151,13 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
/*flipped_y=*/frame.color.flipped_y, pool, &bundle,
/*float_in=*/float_in, /*align=*/0));
for (const auto& ppf_ec : frame.extra_channels) {
bundle.extra_channels().emplace_back(ppf_ec.xsize, ppf_ec.ysize);
bundle.extra_channels().resize(io->metadata.m.extra_channel_info.size());
for (size_t i = 0; i < frame.extra_channels.size(); i++) {
const auto& ppf_ec = frame.extra_channels[i];
bundle.extra_channels()[i] = ImageF(ppf_ec.xsize, ppf_ec.ysize);
JXL_CHECK(BufferToImageF(ppf_ec.format, ppf_ec.xsize, ppf_ec.ysize,
ppf_ec.pixels(), ppf_ec.pixels_size, pool,
&bundle.extra_channels().back()));
&bundle.extra_channels()[i]));
}
io->frames.push_back(std::move(bundle));
@ -218,10 +227,10 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
}
// Convert the extra blobs
ppf->metadata.exif.assign(io.blobs.exif.begin(), io.blobs.exif.end());
ppf->metadata.iptc.assign(io.blobs.iptc.begin(), io.blobs.iptc.end());
ppf->metadata.jumbf.assign(io.blobs.jumbf.begin(), io.blobs.jumbf.end());
ppf->metadata.xmp.assign(io.blobs.xmp.begin(), io.blobs.xmp.end());
ppf->metadata.exif = io.blobs.exif;
ppf->metadata.iptc = io.blobs.iptc;
ppf->metadata.jumbf = io.blobs.jumbf;
ppf->metadata.xmp = io.blobs.xmp;
const bool float_out = pixel_format.data_type == JXL_TYPE_FLOAT ||
pixel_format.data_type == JXL_TYPE_FLOAT16;
// Convert the pixels
@ -240,8 +249,11 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
PackedFrame packed_frame(frame.oriented_xsize(), frame.oriented_ysize(),
format);
packed_frame.color.bitdepth_from_format = float_out;
const size_t bits_per_sample =
packed_frame.color.BitsPerChannel(pixel_format.data_type);
packed_frame.color.bitdepth_from_format
? packed_frame.color.BitsPerChannel(pixel_format.data_type)
: ppf->info.bits_per_sample;
packed_frame.name = frame.name;
packed_frame.frame_info.name_length = frame.name.size();
// Color transform
@ -264,8 +276,7 @@ Status ConvertCodecInOutToPackedPixelFile(const CodecInOut& io,
format.endianness,
/* stride_out=*/packed_frame.color.stride, pool,
packed_frame.color.pixels(), packed_frame.color.pixels_size,
/*out_callback=*/nullptr, /*out_opaque=*/nullptr,
frame.metadata()->GetOrientation()));
/*out_callback=*/{}, frame.metadata()->GetOrientation()));
// TODO(firsching): Convert the extra channels, beside one potential alpha
// channel. FIXME!

View File

@ -14,7 +14,7 @@ namespace jxl {
static void BM_ToneMapping(benchmark::State& state) {
CodecInOut image;
const PaddedBytes image_bytes =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
JXL_CHECK(SetFromBytes(Span<const uint8_t>(image_bytes), &image));
// Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we

View File

@ -917,6 +917,56 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetImageOutBuffer(
typedef void (*JxlImageOutCallback)(void* opaque, size_t x, size_t y,
size_t num_pixels, const void* pixels);
/**
* Initialization callback for @c JxlDecoderSetMultithreadedImageOutCallback.
*
* @see JxlDecoderSetMultithreadedImageOutCallback
*
* @param init_opaque optional user data, as given to
* @c JxlDecoderSetMultithreadedImageOutCallback.
* @param num_threads maximum number of threads that will call the @c run
* callback concurrently.
* @param num_pixels_per_thread maximum number of pixels that will be passed in
* one call to @c run.
* @return a pointer to data that will be passed to the @c run callback, or
* @c NULL if initialization failed.
*/
typedef void* (*JxlImageOutInitCallback)(void* init_opaque, size_t num_threads,
size_t num_pixels_per_thread);
/**
* Worker callback for @c JxlDecoderSetMultithreadedImageOutCallback.
*
* @see JxlDecoderSetMultithreadedImageOutCallback
*
* @param run_opaque user data returned by the @c init callback.
* @param thread_id number in `[0, num_threads)` identifying the thread of the
* current invocation of the callback.
* @param x horizontal position of the first (leftmost) pixel of the pixel data.
* @param y vertical position of the pixel data.
* @param num_pixels number of pixels in the pixel data. May be less than the
* full @c xsize of the image, and will be at most equal to the @c
* num_pixels_per_thread that was passed to @c init.
* @param pixels pixel data as a horizontal stripe, in the format passed to @c
* JxlDecoderSetMultithreadedImageOutCallback. The data pointed to remains owned
* by the caller and is only guaranteed to outlive the current callback
* invocation.
*/
typedef void (*JxlImageOutRunCallback)(void* run_opaque, size_t thread_id,
size_t x, size_t y, size_t num_pixels,
const void* pixels);
/**
* Destruction callback for @c JxlDecoderSetMultithreadedImageOutCallback,
* called after all invocations of the @c run callback to perform any
* appropriate clean-up of the @c run_opaque data returned by @c init.
*
* @see JxlDecoderSetMultithreadedImageOutCallback
*
* @param run_opaque user data returned by the @c init callback.
*/
typedef void (*JxlImageOutDestroyCallback)(void* run_opaque);
/**
* Sets pixel output callback. This is an alternative to
* JxlDecoderSetImageOutBuffer. This can be set when the JXL_DEC_FRAME event
@ -950,8 +1000,8 @@ typedef void (*JxlImageOutCallback)(void* opaque, size_t x, size_t y,
* guaranteed).
*
* @param dec decoder object
* @param format format of the pixels. Object owned by user and its contents
* are copied internally.
* @param format format of the pixels. Object owned by user; its contents are
* copied internally.
* @param callback the callback function receiving partial scanlines of pixel
* data.
* @param opaque optional user data, which will be passed on to the callback,
@ -963,6 +1013,30 @@ JXL_EXPORT JxlDecoderStatus
JxlDecoderSetImageOutCallback(JxlDecoder* dec, const JxlPixelFormat* format,
JxlImageOutCallback callback, void* opaque);
/** Similar to @c JxlDecoderSetImageOutCallback except that the callback is
* allowed an initialization phase during which it is informed of how many
* threads will call it concurrently, and those calls are further informed of
* which thread they are occurring in.
*
* @param dec decoder object
* @param format format of the pixels. Object owned by user; its contents are
* copied internally.
* @param init_callback initialization callback.
* @param run_callback the callback function receiving partial scanlines of
* pixel data.
* @param destroy_callback clean-up callback invoked after all calls to @c
* run_callback. May be NULL if no clean-up is necessary.
* @param init_opaque optional user data passed to @c init_callback, may be NULL
* (unlike the return value from @c init_callback which may only be NULL if
* initialization failed).
* @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as @c
* JxlDecoderSetImageOutBuffer having already been called.
*/
JXL_EXPORT JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback(
JxlDecoder* dec, const JxlPixelFormat* format,
JxlImageOutInitCallback init_callback, JxlImageOutRunCallback run_callback,
JxlImageOutDestroyCallback destroy_callback, void* init_opaque);
/**
* Returns the minimum size in bytes of an extra channel pixel buffer for the
* given format. This is the buffer for JxlDecoderSetExtraChannelBuffer.
@ -1123,10 +1197,60 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetDecompressBoxes(JxlDecoder* dec,
* box, this will return "brob" if the decompressed argument is JXL_FALSE, or
* the underlying box type if the decompressed argument is JXL_TRUE.
*
* The following box types are currently described in ISO/IEC 18181-2:
* - "Exif": a box with EXIF metadata. Starts with a 4-byte tiff header
* offset (big-endian uint32) that indicates the start of the actual EXIF data
* (which starts with a tiff header). Usually the offset will be zero and the
* EXIF data starts immediately after the offset field. The Exif orientation
* should be ignored by applications; the JPEG XL codestream orientation takes
* precedence and libjxl will by default apply the correct orientation
* automatically (see JxlDecoderSetKeepOrientation).
* - "xml ": a box with XML data, in particular XMP metadata.
* - "jumb": a JUMBF superbox (JPEG Universal Metadata Box Format, ISO/IEC
* 19566-5).
* - "JXL ": mandatory signature box, must come first, 12 bytes long including
* the box header
* - "ftyp": a second mandatory signature box, must come second, 20 bytes long
* including the box header
* - "jxll": a JXL level box. This indicates if the codestream is level 5 or
* level 10 compatible. If not present, it is level 5. Level 10 allows more
* features such as very high image resolution and bit-depths above 16 bits
* per channel. Added automatically by the encoder when
* JxlEncoderSetCodestreamLevel is used
* - "jxlc": a box with the image codestream, in case the codestream is not
* split across multiple boxes. The codestream contains the JPEG XL image
* itself, including the basic info such as image dimensions, ICC color
* profile, and all the pixel data of all the image frames.
* - "jxlp": a codestream box in case it is split across multiple boxes.
* The contents are the same as in case of a jxlc box, when concatenated.
* - "brob": a Brotli-compressed box, which otherwise represents an existing
* type of box such as Exif or "xml ". When JxlDecoderSetDecompressBoxes is
* set to JXL_TRUE, these boxes will be transparently decompressed by the
* decoder.
* - "jxli": frame index box, can list the keyframes in case of a JXL animation,
* allowing the decoder to jump to individual frames more efficiently.
* - "jbrd": JPEG reconstruction box, contains the information required to
* byte-for-byte losslessly recontruct a JPEG-1 image. The JPEG DCT
* coefficients (pixel content) themselves as well as the ICC profile are
* encoded in the JXL codestream (jxlc or jxlp) itself. EXIF, XMP and JUMBF
* metadata is encoded in the corresponding boxes. The jbrd box itself
* contains information such as the remaining app markers of the JPEG-1 file
* and everything else required to fit the information together into the
* exact original JPEG file.
*
* Other application-specific boxes can exist. Their typename should not begin
* with "jxl" or "JXL" or conflict with other existing typenames.
*
* The signature, jxl* and jbrd boxes are processed by the decoder and would
* typically be ignored by applications. The typical way to use this function is
* to check if an encountered box contains metadata that the application is
* interested in (e.g. EXIF or XMP metadata), in order to conditionally set a
* box buffer.
*
* @param dec decoder object
* @param type buffer to copy the type into
* @param decompressed which box type to get: JXL_TRUE to get the raw box type,
* which can be "brob", JXL_FALSE, get the underlying box type.
* @param decompressed which box type to get: JXL_FALSE to get the raw box type,
* which can be "brob", JXL_TRUE, get the underlying box type.
* @return JXL_DEC_SUCCESS if the value is available, JXL_DEC_ERROR if not, for
* example the JXL file does not use the container format.
*/

View File

@ -283,6 +283,13 @@ typedef enum {
*/
JXL_ENC_FRAME_INDEX_BOX = 31,
/** Sets brotli encode effort for use in JPEG recompression and compressed
* metadata boxes (brob). Can be -1 (default) or 0 (fastest) to 11 (slowest).
* Default is based on the general encode effort in case of JPEG
* recompression, and 4 for brob boxes.
*/
JXL_ENC_FRAME_SETTING_BROTLI_EFFORT = 32,
/** Enum value not to be used as an option. This value is added to force the
* C compiler to have the enum to take a known size.
*/
@ -509,6 +516,8 @@ JxlEncoderAddJPEGFrame(const JxlEncoderFrameSettings* frame_settings,
*
* Extra channels not handled here need to be set by @ref
* JxlEncoderSetExtraChannelBuffer.
* If the image has alpha, and alpha is not passed here, it will implicitly be
* set to all-opaque (an alpha value of 1.0 everywhere).
*
* The color profile of the pixels depends on the value of uses_original_profile
* in the JxlBasicInfo. If true, the pixels are assumed to be encoded in the
@ -571,10 +580,8 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelBuffer(
* to effectively write the box to the output. @ref JxlEncoderUseBoxes must
* be enabled before using this function.
*
* Background information about the container format and boxes follows here:
*
* For users of libjxl, boxes allow inserting application-specific data and
* metadata (Exif, XML, JUMBF and user defined boxes).
* Boxes allow inserting application-specific data and metadata (Exif, XML/XMP,
* JUMBF and user defined boxes).
*
* The box format follows ISO BMFF and shares features and box types with other
* image and video formats, including the Exif, XML and JUMBF boxes. The box
@ -582,7 +589,7 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelBuffer(
*
* Boxes in general don't contain other boxes inside, except a JUMBF superbox.
* Boxes follow each other sequentially and are byte-aligned. If the container
* format is used, the JXL stream exists out of 3 or more concatenated boxes.
* format is used, the JXL stream consists of concatenated boxes.
* It is also possible to use a direct codestream without boxes, but in that
* case metadata cannot be added.
*
@ -594,80 +601,42 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelBuffer(
* - N bytes: box contents.
*
* Only the box contents are provided to the contents argument of this function,
* the encoder encodes the size header itself.
* the encoder encodes the size header itself. Most boxes are written
* automatically by the encoder as needed ("JXL ", "ftyp", "jxll", "jxlc",
* "jxlp", "jxli", "jbrd"), and this function only needs to be called to add
* optional metadata when encoding from pixels (using JxlEncoderAddImageFrame).
* When recompressing JPEG files (using JxlEncoderAddJPEGFrame), if the input
* JPEG contains EXIF, XMP or JUMBF metadata, the corresponding boxes are
* already added automatically.
*
* Box types are given by 4 characters. A list of known types follows:
* - "JXL ": mandatory signature box, must come first, 12 bytes long including
* the box header
* - "ftyp": a second mandatory signature box, must come second, 20 bytes long
* including the box header
* - "jxll": A JXL level box. This indicates if the codestream is level 5 or
* level 10 compatible. If not present, it is level 5. Level 10 allows more
* features such as very high image resolution and bit-depths above 16 bits
* per channel. Added automatically by the encoder when
* JxlEncoderSetCodestreamLevel is used
* - "jxlc": a box with the image codestream, in case the codestream is not
* split across multiple boxes. The codestream contains the JPEG XL image
* itself, including the basic info such as image dimensions, ICC color
* profile, and all the pixel data of all the image frames.
* - "jxlp": a codestream box in case it is split across multiple boxes. The
* encoder will automatically do this if necessary. The contents are the same
* as in case of a jxlc box, when concatenated.
* Box types are given by 4 characters. The following boxes can be added with
* this function:
* - "Exif": a box with EXIF metadata, can be added by libjxl users, or is
* automatically added when needed for JPEG reconstruction. The contents of
* this box must be prepended by a 4-byte tiff header offset, which may
* be 4 zero bytes.
* - "XML ": a box with XMP or IPTC metadata, can be added by libjxl users, or
* is automatically added when needed for JPEG reconstruction
* be 4 zero bytes in case the tiff header follows immediately.
* The EXIF metadata must be in sync with what is encoded in the JPEG XL
* codestream, specifically the image orientation. While this is not
* recommended in practice, in case of conflicting metadata, the JPEG XL
* codestream takes precedence.
* - "xml ": a box with XML data, in particular XMP metadata, can be added by
* libjxl users, or is automatically added when needed for JPEG reconstruction
* - "jumb": a JUMBF superbox, which can contain boxes with different types of
* metadata inside. This box type can be added by the encoder transparently,
* and other libraries to create and handle JUMBF content exist.
* - "brob": a Brotli-compressed box, which otherwise represents an existing
* type of box such as Exif or XML. The encoder creates these when enabled and
* users of libjxl don't need to create them directly. Some box types are not
* allowed to be compressed: any of the signature, jxl* and jbrd boxes.
* - "jxli": frame index box, can list the keyframes in case of a JXL animation,
* allowing the decoder to jump to individual frames more efficiently. This
* box type is specified, but not currently supported by the encoder or
* decoder.
* - "jbrd": JPEG reconstruction box, contains the information required to
* byte-for-byte losslessly recontruct a JPEG-1 image. The JPEG coefficients
* (pixel content) themselves are encoded in the JXL codestream (jxlc or jxlp)
* itself. Exif and XMP metadata will be encoded in Exif and XMP boxes. The
* jbrd box itself contains information such as the app markers of the JPEG-1
* file and everything else required to fit the information together into the
* exact original JPEG file. This box is added automatically by the encoder
* when needed, and only when JPEG reconstruction is used.
* - other: other application-specific boxes can be added. Their typename should
* not begin with "jxl" or "JXL" or conflict with other existing typenames.
* - Application-specific boxes. Their typename should not begin with "jxl" or
* "JXL" or conflict with other existing typenames, and they should be
* registered with MP4RA (mp4ra.org).
*
* Most boxes are automatically added by the encoder and should not be added
* with JxlEncoderAddBox. Boxes that one may wish to add with JxlEncoderAddBox
* are: Exif and XML (but not when using JPEG reconstruction since if the
* JPEG has those, these boxes are already added automatically), jumb, and
* application-specific boxes.
*
* Adding metadata boxes increases the filesize. When adding Exif metadata, the
* data must be in sync with what is encoded in the JPEG XL codestream,
* specifically the image orientation. While this is not recommended in
* practice, in case of conflicting metadata, the JPEG XL codestream takes
* precedence.
*
* It is possible to create a codestream without boxes, then what would be in
* the jxlc box is written directly to the output
*
* It is possible to split the codestream across multiple boxes, in that case
* multiple boxes of type jxlp are used. This is handled by the encoder when
* needed.
* These boxes can be stored uncompressed or Brotli-compressed (using a "brob"
* box), depending on the compress_box parameter.
*
* @param enc encoder object.
* @param type the box type, e.g. "Exif" for EXIF metadata, "XML " for XMP or
* @param type the box type, e.g. "Exif" for EXIF metadata, "xml " for XMP or
* IPTC metadata, "jumb" for JUMBF metadata.
* @param contents the full contents of the box, for example EXIF
* data. For an "Exif" box, the EXIF data must be prepended by a 4-byte tiff
* header offset, which may be 4 zero-bytes. The ISO BMFF box header must not
* be included, only the contents. Owned by the caller and its contents are
* copied internally.
* data. ISO BMFF box header must not be included, only the contents. Owned by
* the caller and its contents are copied internally.
* @param size size of the box contents.
* @param compress_box Whether to compress this box as a "brob" box. Requires
* Brotli support.
@ -1049,7 +1018,7 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetFrameDistance(
/** DEPRECATED: use JxlEncoderSetFrameDistance instead.
*/
JXL_EXPORT JxlEncoderStatus
JXL_EXPORT JXL_DEPRECATED JxlEncoderStatus
JxlEncoderOptionsSetDistance(JxlEncoderFrameSettings*, float);
/**
@ -1072,7 +1041,7 @@ JXL_EXPORT JxlEncoderFrameSettings* JxlEncoderFrameSettingsCreate(
/** DEPRECATED: use JxlEncoderFrameSettingsCreate instead.
*/
JXL_EXPORT JxlEncoderFrameSettings* JxlEncoderOptionsCreate(
JXL_EXPORT JXL_DEPRECATED JxlEncoderFrameSettings* JxlEncoderOptionsCreate(
JxlEncoder*, const JxlEncoderFrameSettings*);
/**

View File

@ -16,6 +16,8 @@
#include <stddef.h>
#include <stdint.h>
#include "jxl/jxl_export.h"
#if defined(__cplusplus) || defined(c_plusplus)
extern "C" {
#endif
@ -41,28 +43,26 @@ typedef enum {
* for HDR and wide gamut images when color profile conversion is required. */
JXL_TYPE_FLOAT = 0,
/** Use 1-bit packed in uint8_t, first pixel in LSB, padded to uint8_t per
* row.
* TODO(lode): support first in MSB, other padding.
*/
JXL_TYPE_BOOLEAN,
/** Use type uint8_t. May clip wide color gamut data.
*/
JXL_TYPE_UINT8,
JXL_TYPE_UINT8 = 2,
/** Use type uint16_t. May clip wide color gamut data.
*/
JXL_TYPE_UINT16,
/** Use type uint32_t. May clip wide color gamut data.
*/
JXL_TYPE_UINT32,
JXL_TYPE_UINT16 = 3,
/** Use 16-bit IEEE 754 half-precision floating point values */
JXL_TYPE_FLOAT16,
JXL_TYPE_FLOAT16 = 5,
} JxlDataType;
/* DEPRECATED: bit-packed 1-bit data type. Use JXL_TYPE_UINT8 instead.
*/
static const int JXL_DEPRECATED JXL_TYPE_BOOLEAN = 1;
/* DEPRECATED: uint32_t data type. Use JXL_TYPE_FLOAT instead.
*/
static const int JXL_DEPRECATED JXL_TYPE_UINT32 = 4;
/** Ordering of multi-byte data.
*/
typedef enum {

View File

@ -39,9 +39,9 @@ set(JPEGXL_INTERNAL_SOURCES_DEC
jxl/base/profiler.h
jxl/base/random.cc
jxl/base/random.h
jxl/base/sanitizer_definitions.h
jxl/base/scope_guard.h
jxl/base/span.h
jxl/base/status.cc
jxl/base/status.h
jxl/base/thread_pool_internal.h
jxl/blending.cc
@ -106,6 +106,8 @@ set(JPEGXL_INTERNAL_SOURCES_DEC
jxl/entropy_coder.h
jxl/epf.cc
jxl/epf.h
jxl/exif.cc
jxl/exif.h
jxl/fast_dct-inl.h
jxl/fast_dct.cc
jxl/fast_dct.h
@ -344,6 +346,8 @@ set(JPEGXL_DEC_INTERNAL_LIBS
brotlidec-static
brotlicommon-static
hwy
Threads::Threads
${ATOMICS_LIBRARIES}
)
if(JPEGXL_ENABLE_PROFILER)
@ -353,7 +357,6 @@ endif()
set(JPEGXL_INTERNAL_LIBS
${JPEGXL_DEC_INTERNAL_LIBS}
brotlienc-static
Threads::Threads
)
# strips the -static suffix from all the elements in LIST
@ -465,7 +468,7 @@ add_library(jxl_dec-static STATIC
$<TARGET_OBJECTS:jxl_dec-obj>
)
target_link_libraries(jxl_dec-static
PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_DEC_INTERNAL_LIBS} hwy)
PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_DEC_INTERNAL_LIBS})
target_include_directories(jxl_dec-static PUBLIC
"${PROJECT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/include"
@ -486,7 +489,7 @@ endif()
# to do, remove $<TARGET_OBJECTS:jxl_dec-obj> here and depend on jxl_dec-static
add_library(jxl-static STATIC ${JPEGXL_INTERNAL_OBJECTS})
target_link_libraries(jxl-static
PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_INTERNAL_LIBS} hwy)
PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_INTERNAL_LIBS})
target_include_directories(jxl-static PUBLIC
"${PROJECT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/include"

View File

@ -220,7 +220,7 @@ struct AuxOut {
Image3MinMax(image, &min, &max);
Image3B normalized(image.xsize(), image.ysize());
for (size_t c = 0; c < 3; ++c) {
float mul = min[c] == max[c] ? 0 : (1.0f / (max[c] - min[c]));
float mul = min[c] == max[c] ? 0 : (255.0f / (max[c] - min[c]));
for (size_t y = 0; y < image.ysize(); ++y) {
const T* JXL_RESTRICT row_in = image.ConstPlaneRow(c, y);
uint8_t* JXL_RESTRICT row_out = normalized.PlaneRow(c, y);

View File

@ -0,0 +1,44 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#ifndef LIB_JXL_BASE_SANITIZER_DEFINITIONS_H_
#define LIB_JXL_BASE_SANITIZER_DEFINITIONS_H_
#ifdef MEMORY_SANITIZER
#define JXL_MEMORY_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(memory_sanitizer)
#define JXL_MEMORY_SANITIZER 1
#else
#define JXL_MEMORY_SANITIZER 0
#endif
#else
#define JXL_MEMORY_SANITIZER 0
#endif
#ifdef ADDRESS_SANITIZER
#define JXL_ADDRESS_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#define JXL_ADDRESS_SANITIZER 1
#else
#define JXL_ADDRESS_SANITIZER 0
#endif
#else
#define JXL_ADDRESS_SANITIZER 0
#endif
#ifdef THREAD_SANITIZER
#define JXL_THREAD_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(thread_sanitizer)
#define JXL_THREAD_SANITIZER 1
#else
#define JXL_THREAD_SANITIZER 0
#endif
#else
#define JXL_THREAD_SANITIZER 0
#endif
#endif // LIB_JXL_BASE_SANITIZER_DEFINITIONS_H

View File

@ -1,46 +0,0 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/jxl/base/status.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include "lib/jxl/sanitizers.h"
#if JXL_ADDRESS_SANITIZER || JXL_MEMORY_SANITIZER || JXL_THREAD_SANITIZER
#include "sanitizer/common_interface_defs.h" // __sanitizer_print_stack_trace
#endif // defined(*_SANITIZER)
namespace jxl {
bool Debug(const char* format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
return false;
}
bool Abort() {
#if JXL_ADDRESS_SANITIZER || JXL_MEMORY_SANITIZER || JXL_THREAD_SANITIZER
// If compiled with any sanitizer print a stack trace. This call doesn't crash
// the program, instead the trap below will crash it also allowing gdb to
// break there.
__sanitizer_print_stack_trace();
#endif // *_SANITIZER)
#if JXL_COMPILER_MSVC
__debugbreak();
abort();
#else
__builtin_trap();
#endif
}
} // namespace jxl

View File

@ -14,6 +14,11 @@
#include <stdlib.h>
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/sanitizer_definitions.h"
#if JXL_ADDRESS_SANITIZER || JXL_MEMORY_SANITIZER || JXL_THREAD_SANITIZER
#include "sanitizer/common_interface_defs.h" // __sanitizer_print_stack_trace
#endif // defined(*_SANITIZER)
namespace jxl {
@ -70,7 +75,13 @@ namespace jxl {
// instead of calling Debug directly. This function returns false, so it can be
// used as a return value in JXL_FAILURE.
JXL_FORMAT(1, 2)
bool Debug(const char* format, ...);
inline JXL_NOINLINE bool Debug(const char* format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
return false;
}
// Print a debug message on standard error if "enabled" is true. "enabled" is
// normally a macro that evaluates to 0 or 1 at compile time, so the Debug
@ -113,7 +124,21 @@ bool Debug(const char* format, ...);
JXL_DEBUG(JXL_DEBUG_WARNING, format, ##__VA_ARGS__)
// Exits the program after printing a stack trace when possible.
JXL_NORETURN bool Abort();
JXL_NORETURN inline JXL_NOINLINE bool Abort() {
#if JXL_ADDRESS_SANITIZER || JXL_MEMORY_SANITIZER || JXL_THREAD_SANITIZER
// If compiled with any sanitizer print a stack trace. This call doesn't crash
// the program, instead the trap below will crash it also allowing gdb to
// break there.
__sanitizer_print_stack_trace();
#endif // *_SANITIZER)
#if JXL_COMPILER_MSVC
__debugbreak();
abort();
#else
__builtin_trap();
#endif
}
// Exits the program after printing file/line plus a formatted string.
#define JXL_ABORT(format, ...) \

View File

@ -41,6 +41,10 @@ TEST(BitsTest, TestFloorLog2) {
EXPECT_EQ(expected[i - 1], FloorLog2Nonzero(uint64_t(i))) << " " << i;
}
EXPECT_EQ(11u, FloorLog2Nonzero(0x00000fffu)); // 4095
EXPECT_EQ(12u, FloorLog2Nonzero(0x00001000u)); // 4096
EXPECT_EQ(12u, FloorLog2Nonzero(0x00001001u)); // 4097
EXPECT_EQ(31u, FloorLog2Nonzero(0x80000000u));
EXPECT_EQ(31u, FloorLog2Nonzero(0x80000001u));
EXPECT_EQ(31u, FloorLog2Nonzero(0xFFFFFFFFu));
@ -62,6 +66,10 @@ TEST(BitsTest, TestCeilLog2) {
EXPECT_EQ(expected[i - 1], CeilLog2Nonzero(uint64_t(i))) << " " << i;
}
EXPECT_EQ(12u, CeilLog2Nonzero(0x00000fffu)); // 4095
EXPECT_EQ(12u, CeilLog2Nonzero(0x00001000u)); // 4096
EXPECT_EQ(13u, CeilLog2Nonzero(0x00001001u)); // 4097
EXPECT_EQ(31u, CeilLog2Nonzero(0x80000000u));
EXPECT_EQ(32u, CeilLog2Nonzero(0x80000001u));
EXPECT_EQ(32u, CeilLog2Nonzero(0xFFFFFFFFu));

View File

@ -78,7 +78,7 @@ void PerformBlending(const float* const* bg, const float* const* fg,
} else if (ec_blending[i].mode == PatchBlendMode::kReplace) {
memcpy(tmp.Row(3 + i), fg[3 + i] + x0, xsize * sizeof(**fg));
} else if (ec_blending[i].mode == PatchBlendMode::kNone) {
memcpy(tmp.Row(3 + i), bg[3 + i] + x0, xsize * sizeof(**fg));
if (xsize) memcpy(tmp.Row(3 + i), bg[3 + i] + x0, xsize * sizeof(**fg));
} else {
JXL_ABORT("Unreachable");
}

View File

@ -36,10 +36,6 @@ void SetMetadataFromPixelFormat(const JxlPixelFormat* pixel_format,
metadata->SetFloat16Samples();
potential_alpha_bits = 16;
break;
case JXL_TYPE_UINT32:
metadata->SetUintSamples(32);
potential_alpha_bits = 16;
break;
case JXL_TYPE_UINT16:
metadata->SetUintSamples(16);
potential_alpha_bits = 16;
@ -48,10 +44,8 @@ void SetMetadataFromPixelFormat(const JxlPixelFormat* pixel_format,
metadata->SetUintSamples(8);
potential_alpha_bits = 8;
break;
case JXL_TYPE_BOOLEAN:
metadata->SetUintSamples(2);
potential_alpha_bits = 2;
break;
default:
JXL_ABORT("Unhandled JxlDataType");
}
if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) {
metadata->SetAlphaBits(potential_alpha_bits);
@ -116,9 +110,10 @@ void JxlButteraugliApiSetIntensityTarget(JxlButteraugliApi* api, float v) {
void JxlButteraugliApiDestroy(JxlButteraugliApi* api) {
if (api) {
JxlMemoryManager local_memory_manager = api->memory_manager;
// Call destructor directly since custom free function is used.
api->~JxlButteraugliApi();
jxl::MemoryManagerFree(&api->memory_manager, api);
jxl::MemoryManagerFree(&local_memory_manager, api);
}
}
@ -200,8 +195,9 @@ float JxlButteraugliResultGetMaxDistance(const JxlButteraugliResult* result) {
void JxlButteraugliResultDestroy(JxlButteraugliResult* result) {
if (result) {
JxlMemoryManager local_memory_manager = result->memory_manager;
// Call destructor directly since custom free function is used.
result->~JxlButteraugliResult();
jxl::MemoryManagerFree(&result->memory_manager, result);
jxl::MemoryManagerFree(&local_memory_manager, result);
}
}

View File

@ -56,10 +56,10 @@ using CodecIntervals = std::array<CodecInterval, 4>; // RGB[A] or Y[A]
// Optional text/EXIF metadata.
struct Blobs {
PaddedBytes exif;
PaddedBytes iptc;
PaddedBytes jumbf;
PaddedBytes xmp;
std::vector<uint8_t> exif;
std::vector<uint8_t> iptc;
std::vector<uint8_t> jumbf;
std::vector<uint8_t> xmp;
};
// Holds a preview, a main image or one or more frames, plus the inputs/outputs

View File

@ -17,6 +17,7 @@
#include <string>
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/padded_bytes.h"
#ifndef JXL_HIGH_PRECISION
#define JXL_HIGH_PRECISION 1
@ -192,6 +193,46 @@ std::string ToString(T n) {
}
return data;
}
namespace {
static inline uint64_t DecodeVarInt(const uint8_t* input, size_t inputSize,
size_t* pos) {
size_t i;
uint64_t ret = 0;
for (i = 0; *pos + i < inputSize && i < 10; ++i) {
ret |= uint64_t(input[*pos + i] & 127) << uint64_t(7 * i);
// If the next-byte flag is not set, stop
if ((input[*pos + i] & 128) == 0) break;
}
// TODO: Return a decoding error if i == 10.
*pos += i + 1;
return ret;
}
static inline bool EncodeVarInt(uint64_t value, size_t output_size,
size_t* output_pos, uint8_t* output) {
// While more than 7 bits of data are left,
// store 7 bits and set the next byte flag
while (value > 127) {
if (*output_pos > output_size) return false;
// |128: Set the next byte flag
output[(*output_pos)++] = ((uint8_t)(value & 127)) | 128;
// Remove the seven bits we just wrote
value >>= 7;
}
if (*output_pos > output_size) return false;
output[(*output_pos)++] = ((uint8_t)value) & 127;
return true;
}
static inline void EncodeVarInt(uint64_t value, PaddedBytes* data) {
size_t pos = data->size();
data->resize(data->size() + 9);
JXL_CHECK(EncodeVarInt(value, data->size(), &pos, data->data()));
data->resize(pos);
}
} // namespace
} // namespace jxl
#endif // LIB_JXL_COMMON_H_

View File

@ -44,7 +44,7 @@ class Neighbors {
return c; // Same (the first mirrored value is the last valid one)
#else // 128 bit
// c = LKJI
#if HWY_ARCH_X86
#if HWY_TARGET <= (1 << HWY_HIGHEST_TARGET_BIT_X86)
return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(2, 1, 0, 0))}; // KJII
#else
const D d;
@ -72,7 +72,7 @@ class Neighbors {
return Zero(d);
#else // 128 bit
// c = LKJI
#if HWY_ARCH_X86
#if HWY_TARGET <= (1 << HWY_HIGHEST_TARGET_BIT_X86)
return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(1, 0, 0, 1))}; // JIIJ
#else
const D d;
@ -98,7 +98,7 @@ class Neighbors {
return Zero(d);
#else // 128 bit
// c = LKJI
#if HWY_ARCH_X86
#if HWY_TARGET <= (1 << HWY_HIGHEST_TARGET_BIT_X86)
return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(0, 0, 1, 2))}; // IIJK
#else
const D d;

View File

@ -74,9 +74,6 @@ Status ReadHistogram(int precision_bits, std::vector<int>* counts,
int is_flat = input->ReadBits(1);
if (is_flat == 1) {
int alphabet_size = DecodeVarLenUint8(input) + 1;
if (alphabet_size == 0) {
return JXL_FAILURE("Invalid alphabet size for flat histogram.");
}
*counts = CreateFlatHistogram(alphabet_size, 1 << precision_bits);
return true;
}

View File

@ -180,12 +180,12 @@ Status PassesDecoderState::PreparePipeline(ImageBundle* decoded,
}
}
if (pixel_callback) {
if (pixel_callback.IsPresent()) {
builder.AddStage(GetWriteToPixelCallbackStage(pixel_callback, width,
height, rgb_output_is_rgba,
has_alpha, alpha_c));
} else if (rgb_output) {
builder.AddStage(GetWriteToU8Stage(rgb_output, rgb_stride, width, height,
builder.AddStage(GetWriteToU8Stage(rgb_output, rgb_stride, height,
rgb_output_is_rgba, has_alpha,
alpha_c));
} else {

View File

@ -10,6 +10,7 @@
#include <hwy/base.h> // HWY_ALIGN_MAX
#include "jxl/decode.h"
#include "lib/jxl/ac_strategy.h"
#include "lib/jxl/base/profiler.h"
#include "lib/jxl/coeff_order.h"
@ -29,6 +30,31 @@ namespace jxl {
constexpr size_t kSigmaBorder = 1;
constexpr size_t kSigmaPadding = 2;
struct PixelCallback {
PixelCallback() = default;
PixelCallback(JxlImageOutInitCallback init, JxlImageOutRunCallback run,
JxlImageOutDestroyCallback destroy, void* init_opaque)
: init(init), run(run), destroy(destroy), init_opaque(init_opaque) {
#if JXL_ENABLE_ASSERT
const bool has_init = init != nullptr;
const bool has_run = run != nullptr;
const bool has_destroy = destroy != nullptr;
JXL_ASSERT(has_init == has_run && has_run == has_destroy);
#endif
}
bool IsPresent() const { return run != nullptr; }
void* Init(size_t num_threads, size_t num_pixels) const {
return init(init_opaque, num_threads, num_pixels);
}
JxlImageOutInitCallback init = nullptr;
JxlImageOutRunCallback run = nullptr;
JxlImageOutDestroyCallback destroy = nullptr;
void* init_opaque = nullptr;
};
// Per-frame decoder state. All the images here should be accessed through a
// group rect (either with block units or pixel units).
struct PassesDecoderState {
@ -65,7 +91,8 @@ struct PassesDecoderState {
bool rgb_output_is_rgba;
// Callback for line-by-line output.
std::function<void(const float*, size_t, size_t, size_t)> pixel_callback;
PixelCallback pixel_callback;
// Buffer of upsampling * kApplyImageFeaturesTileDim ones.
std::vector<float> opaque_alpha;
// One row per thread
@ -106,7 +133,6 @@ struct PassesDecoderState {
std::pow(1 / (1.25f), shared->frame_header.b_qm_scale - 2.0f);
rgb_output = nullptr;
pixel_callback = nullptr;
rgb_output_is_rgba = false;
fast_xyb_srgb8_conversion = false;
used_acs = 0;

View File

@ -32,28 +32,9 @@ HWY_BEFORE_NAMESPACE();
namespace jxl {
namespace HWY_NAMESPACE {
// TODO(jon): check if this can be replaced by a FloatToU16 function
void FloatToU32(const float* in, uint32_t* out, size_t num, float mul,
size_t bits_per_sample) {
// TODO(eustas): investigate 24..31 bpp cases.
if (bits_per_sample == 32) {
// Conversion to real 32-bit *unsigned* integers requires more intermediate
// precision that what is given by the usual f32 -> i32 conversion
// instructions, so we run the non-SIMD path for those.
const uint32_t cap = (1ull << bits_per_sample) - 1;
for (size_t x = 0; x < num; x++) {
float v = in[x];
if (v >= 1.0f) {
out[x] = cap;
} else if (v >= 0.0f) { // Inverted condition => NaN -> 0.
out[x] = static_cast<uint32_t>(v * mul + 0.5f);
} else {
out[x] = 0;
}
}
return;
}
// General SIMD case for less than 32 bits output.
const HWY_FULL(float) d;
const hwy::HWY_NAMESPACE::Rebind<uint32_t, decltype(d)> du;
@ -267,39 +248,36 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
JxlEndianness endianness, size_t stride,
jxl::ThreadPool* pool, void* out_image,
size_t out_size,
JxlImageOutCallback out_callback,
void* out_opaque,
const PixelCallback& out_callback,
jxl::Orientation undo_orientation) {
JXL_DASSERT(num_channels != 0 && num_channels <= kConvertMaxChannels);
JXL_DASSERT(channels[0] != nullptr);
if (bits_per_sample < 1 || bits_per_sample > 32) {
return JXL_FAILURE("Invalid bits_per_sample value.");
}
if (!!out_image == !!out_callback) {
JXL_CHECK(float_out ? bits_per_sample == 16 || bits_per_sample == 32
: bits_per_sample > 0 && bits_per_sample <= 16);
if (!!out_image == out_callback.IsPresent()) {
return JXL_FAILURE(
"Must provide either an out_image or an out_callback, but not both.");
}
// TODO(deymo): Implement 1-bit per pixel packed in 8 samples per byte.
if (bits_per_sample == 1) {
return JXL_FAILURE("packed 1-bit per sample is not yet supported");
}
if (bits_per_sample > 16 && bits_per_sample < 32) {
return JXL_FAILURE("not supported, try bits_per_sample=32");
}
// bytes_per_channel and is only valid for bits_per_sample > 1.
const size_t bytes_per_channel = DivCeil(bits_per_sample, jxl::kBitsPerByte);
const size_t bytes_per_pixel = num_channels * bytes_per_channel;
std::vector<std::vector<uint8_t>> row_out_callback;
auto InitOutCallback = [&](size_t num_threads) {
if (out_callback) {
const auto FreeCallbackOpaque = [&out_callback](void* p) {
out_callback.destroy(p);
};
std::unique_ptr<void, decltype(FreeCallbackOpaque)> out_run_opaque(
nullptr, FreeCallbackOpaque);
auto InitOutCallback = [&](size_t num_threads) -> Status {
if (out_callback.IsPresent()) {
out_run_opaque.reset(out_callback.Init(num_threads, stride));
JXL_RETURN_IF_ERROR(out_run_opaque != nullptr);
row_out_callback.resize(num_threads);
for (size_t i = 0; i < num_threads; ++i) {
row_out_callback[i].resize(stride);
}
}
return true;
};
// Channels used to store the transformed original channels if needed.
@ -322,7 +300,7 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
" vs %" PRIuS,
stride, bytes_per_pixel * xsize);
}
if (!out_callback &&
if (!out_callback.IsPresent() &&
out_size < (ysize - 1) * stride + bytes_per_pixel * xsize) {
return JXL_FAILURE("out_size is too small to store image");
}
@ -351,8 +329,7 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
[&](size_t num_threads) {
f16_cache =
Plane<hwy::float16_t>(xsize, num_channels * num_threads);
InitOutCallback(num_threads);
return true;
return InitOutCallback(num_threads);
},
[&](const uint32_t task, const size_t thread) {
const int64_t y = task;
@ -367,7 +344,7 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
(row_in[c], row_f16[c], xsize);
}
uint8_t* row_out =
out_callback
out_callback.IsPresent()
? row_out_callback[thread].data()
: &(reinterpret_cast<uint8_t*>(out_image))[stride * y];
// interleave the one scanline
@ -384,22 +361,20 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
std::swap(row_out[i + 0], row_out[i + 1]);
}
}
if (out_callback) {
(*out_callback)(out_opaque, 0, y, xsize, row_out);
if (out_callback.IsPresent()) {
out_callback.run(out_run_opaque.get(), thread, 0, y, xsize,
row_out);
}
},
"ConvertF16"));
} else if (bits_per_sample == 32) {
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, static_cast<uint32_t>(ysize),
[&](size_t num_threads) {
InitOutCallback(num_threads);
return true;
},
[&](size_t num_threads) { return InitOutCallback(num_threads); },
[&](const uint32_t task, const size_t thread) {
const int64_t y = task;
uint8_t* row_out =
out_callback
out_callback.IsPresent()
? row_out_callback[thread].data()
: &(reinterpret_cast<uint8_t*>(out_image))[stride * y];
const float* JXL_RESTRICT row_in[kConvertMaxChannels];
@ -411,8 +386,9 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
} else {
StoreFloatRow<StoreBEFloat>(row_in, num_channels, xsize, row_out);
}
if (out_callback) {
(*out_callback)(out_opaque, 0, y, xsize, row_out);
if (out_callback.IsPresent()) {
out_callback.run(out_run_opaque.get(), thread, 0, y, xsize,
row_out);
}
},
"ConvertFloat"));
@ -428,13 +404,12 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
pool, 0, static_cast<uint32_t>(ysize),
[&](size_t num_threads) {
u32_cache = Plane<uint32_t>(xsize, num_channels * num_threads);
InitOutCallback(num_threads);
return true;
return InitOutCallback(num_threads);
},
[&](const uint32_t task, const size_t thread) {
const int64_t y = task;
uint8_t* row_out =
out_callback
out_callback.IsPresent()
? row_out_callback[thread].data()
: &(reinterpret_cast<uint8_t*>(out_image))[stride * y];
const float* JXL_RESTRICT row_in[kConvertMaxChannels];
@ -450,24 +425,18 @@ Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels,
HWY_DYNAMIC_DISPATCH(FloatToU32)
(row_in[c], row_u32[c], xsize, mul, bits_per_sample);
}
// TODO(deymo): add bits_per_sample == 1 case here.
if (bits_per_sample <= 8) {
StoreUintRow<Store8>(row_u32, num_channels, xsize, 1, row_out);
} else if (bits_per_sample <= 16) {
} else {
if (little_endian) {
StoreUintRow<StoreLE16>(row_u32, num_channels, xsize, 2, row_out);
} else {
StoreUintRow<StoreBE16>(row_u32, num_channels, xsize, 2, row_out);
}
} else {
if (little_endian) {
StoreUintRow<StoreLE32>(row_u32, num_channels, xsize, 4, row_out);
} else {
StoreUintRow<StoreBE32>(row_u32, num_channels, xsize, 4, row_out);
}
}
if (out_callback) {
(*out_callback)(out_opaque, 0, y, xsize, row_out);
if (out_callback.IsPresent()) {
out_callback.run(out_run_opaque.get(), thread, 0, y, xsize,
row_out);
}
},
"ConvertUint"));
@ -481,8 +450,8 @@ Status ConvertToExternal(const jxl::ImageBundle& ib, size_t bits_per_sample,
bool float_out, size_t num_channels,
JxlEndianness endianness, size_t stride,
jxl::ThreadPool* pool, void* out_image,
size_t out_size, JxlImageOutCallback out_callback,
void* out_opaque, jxl::Orientation undo_orientation) {
size_t out_size, const PixelCallback& out_callback,
jxl::Orientation undo_orientation) {
bool want_alpha = num_channels == 2 || num_channels == 4;
size_t color_channels = num_channels <= 2 ? 1 : 3;
@ -512,19 +481,19 @@ Status ConvertToExternal(const jxl::ImageBundle& ib, size_t bits_per_sample,
return ConvertChannelsToExternal(
channels, num_channels, bits_per_sample, float_out, endianness, stride,
pool, out_image, out_size, out_callback, out_opaque, undo_orientation);
pool, out_image, out_size, out_callback, undo_orientation);
}
Status ConvertToExternal(const jxl::ImageF& channel, size_t bits_per_sample,
bool float_out, JxlEndianness endianness,
size_t stride, jxl::ThreadPool* pool, void* out_image,
size_t out_size, JxlImageOutCallback out_callback,
void* out_opaque, jxl::Orientation undo_orientation) {
size_t out_size, const PixelCallback& out_callback,
jxl::Orientation undo_orientation) {
const ImageF* channels[1];
channels[0] = &channel;
return ConvertChannelsToExternal(
channels, 1, bits_per_sample, float_out, endianness, stride, pool,
out_image, out_size, out_callback, out_opaque, undo_orientation);
return ConvertChannelsToExternal(channels, 1, bits_per_sample, float_out,
endianness, stride, pool, out_image,
out_size, out_callback, undo_orientation);
}
} // namespace jxl

View File

@ -15,18 +15,18 @@
#include "jxl/types.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/dec_cache.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
namespace jxl {
// Converts ib to interleaved void* pixel buffer with the given format.
// bits_per_sample: must be 8, 16 or 32, and must be 32 if float_out
// is true. 1 and 32 int are not yet implemented.
// bits_per_sample: must be 16 or 32 if float_out is true, and at most 16
// if it is false. No bit packing is done.
// num_channels: must be 1, 2, 3 or 4 for gray, gray+alpha, RGB, RGB+alpha.
// This supports the features needed for the C API and does not perform
// color space conversion.
// TODO(lode): support 1-bit output (bits_per_sample == 1)
// TODO(lode): support rectangle crop.
// stride_out is output scanline size in bytes, must be >=
// output_xsize * output_bytes_per_pixel.
@ -37,24 +37,18 @@ Status ConvertToExternal(const jxl::ImageBundle& ib, size_t bits_per_sample,
bool float_out, size_t num_channels,
JxlEndianness endianness, size_t stride_out,
jxl::ThreadPool* thread_pool, void* out_image,
size_t out_size, JxlImageOutCallback out_callback,
void* out_opaque, jxl::Orientation undo_orientation);
size_t out_size, const PixelCallback& out_callback,
jxl::Orientation undo_orientation);
// Converts single-channel image to interleaved void* pixel buffer with the
// given format, with a single channel.
// bits_per_sample: must be 8, 16 or 32, and must be 32 if float_out
// is true. 1 and 32 int are not yet implemented.
// This supports the features needed for the C API to get extra channels.
// stride_out is output scanline size in bytes, must be >=
// output_xsize * output_bytes_per_pixel.
// undo_orientation is an EXIF orientation to undo. Depending on the
// orientation, the output xsize and ysize are swapped compared to input
// xsize and ysize.
// Arguments are similar to the multi-channel function above.
Status ConvertToExternal(const jxl::ImageF& channel, size_t bits_per_sample,
bool float_out, JxlEndianness endianness,
size_t stride_out, jxl::ThreadPool* thread_pool,
void* out_image, size_t out_size,
JxlImageOutCallback out_callback, void* out_opaque,
const PixelCallback& out_callback,
jxl::Orientation undo_orientation);
} // namespace jxl

View File

@ -38,7 +38,7 @@ void BM_DecExternalImage_ConvertImageRGBA(benchmark::State& state) {
/*float_out=*/false, num_channels, JXL_NATIVE_ENDIAN,
/*stride*/ bytes_per_row,
/*thread_pool=*/nullptr, interleaved.data(), interleaved.size(),
/*out_callback=*/nullptr, /*out_opaque=*/nullptr,
/*out_callback=*/{},
/*undo_orientation=*/jxl::Orientation::kIdentity));
}
}

View File

@ -66,7 +66,23 @@ Status DecodeFile(const DecompressParams& dparams,
{
BitReader reader(file);
BitReaderScopedCloser reader_closer(&reader, &ret);
(void)reader.ReadFixedBits<16>(); // skip marker
if (reader.ReadFixedBits<16>() != 0x0AFF) {
// We don't have a naked codestream. Make a quick & dirty attempt to find
// the codestream.
// TODO(jon): get rid of this whole function
const unsigned char* begin = file.data();
const unsigned char* end = file.data() + file.size() - 4;
while (begin < end) {
if (!memcmp(begin, "jxlc", 4)) break;
begin++;
}
if (begin >= end) return JXL_FAILURE("Couldn't find jxl codestream");
reader.SkipBits(8 * (begin - file.data() + 2));
unsigned int firstbytes = reader.ReadFixedBits<16>();
if (firstbytes != 0x0AFF)
return JXL_FAILURE("Codestream didn't start with FF0A but with %X",
firstbytes);
}
{
JXL_RETURN_IF_ERROR(DecodeHeaders(&reader, io));

View File

@ -265,6 +265,7 @@ Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded,
decoded->duration = frame_header_.animation_frame.duration;
if (!frame_header_.nonserialized_is_preview &&
(frame_header_.is_last || frame_header_.animation_frame.duration > 0) &&
(frame_header_.frame_type == kRegularFrame ||
frame_header_.frame_type == kSkipProgressive)) {
++dec_state_->visible_frame_index;
@ -556,10 +557,11 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id,
size_t num_passes, size_t thread,
bool force_draw, bool dc_only) {
PROFILER_ZONE("process_group");
size_t group_dim = frame_dim_.group_dim;
const size_t gx = ac_group_id % frame_dim_.xsize_groups;
const size_t gy = ac_group_id / frame_dim_.xsize_groups;
const size_t x = gx * frame_dim_.group_dim;
const size_t y = gy * frame_dim_.group_dim;
const size_t x = gx * group_dim;
const size_t y = gy * group_dim;
RenderPipelineInput render_pipeline_input =
dec_state_->render_pipeline->GetInputBuffers(ac_group_id, thread);
@ -577,7 +579,7 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id,
}
// don't limit to image dimensions here (is done in DecodeGroup)
const Rect mrect(x, y, frame_dim_.group_dim, frame_dim_.group_dim);
const Rect mrect(x, y, group_dim, group_dim);
for (size_t i = 0; i < frame_header_.passes.num_passes; i++) {
int minShift, maxShift;
frame_header_.passes.GetDownsamplingBracket(i, minShift, maxShift);
@ -612,14 +614,14 @@ Status FrameDecoder::ProcessACGroup(size_t ac_group_id,
rects[c].first = r.first;
size_t x1 = r.second.x0() + r.second.xsize();
size_t y1 = r.second.y0() + r.second.ysize();
rects[c].second = Rect(r.second.x0() + ix * kGroupDim,
r.second.y0() + iy * kGroupDim, kGroupDim,
kGroupDim, x1, y1);
rects[c].second = Rect(r.second.x0() + ix * group_dim,
r.second.y0() + iy * group_dim, group_dim,
group_dim, x1, y1);
}
Random3Planes(dec_state_->visible_frame_index,
dec_state_->nonvisible_frame_index,
(gx * frame_header_.upsampling + ix) * kGroupDim,
(gy * frame_header_.upsampling + iy) * kGroupDim,
(gx * frame_header_.upsampling + ix) * group_dim,
(gy * frame_header_.upsampling + iy) * group_dim,
rects[0], rects[1], rects[2]);
}
}
@ -792,8 +794,8 @@ Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num,
JXL_RETURN_IF_ERROR(RunOnPool(
pool_, 0, ac_group_sec.size(),
[this](size_t num_threads) {
PrepareStorage(num_threads, decoded_passes_per_ac_group_.size());
return true;
return PrepareStorage(num_threads,
decoded_passes_per_ac_group_.size());
},
[this, &ac_group_sec, &num_ac_passes, &num, &sections, &section_status,
&has_error](size_t g, size_t thread) {
@ -863,8 +865,8 @@ Status FrameDecoder::Flush() {
JXL_RETURN_IF_ERROR(RunOnPool(
pool_, 0, decoded_passes_per_ac_group_.size(),
[this](const size_t num_threads) {
PrepareStorage(num_threads, decoded_passes_per_ac_group_.size());
return true;
return PrepareStorage(num_threads,
decoded_passes_per_ac_group_.size());
},
[this, &has_error](const uint32_t g, size_t thread) {
if (decoded_passes_per_ac_group_[g] ==

View File

@ -8,6 +8,7 @@
#include <stdint.h>
#include "jxl/decode.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/span.h"
@ -159,7 +160,7 @@ class FrameDecoder {
dec_state_->rgb_output = rgb_output;
dec_state_->rgb_output_is_rgba = is_rgba;
dec_state_->rgb_stride = stride;
JXL_ASSERT(dec_state_->pixel_callback == nullptr);
JXL_ASSERT(!dec_state_->pixel_callback.IsPresent());
#if !JXL_HIGH_PRECISION
if (decoded_->metadata()->xyb_encoded &&
dec_state_->output_encoding_info.color_encoding.IsSRGB() &&
@ -181,12 +182,10 @@ class FrameDecoder {
// orientation. Performing this operation is not yet supported, so this
// results in not setting the buffer if the image has a non-identity EXIF
// orientation. When outputting to the ImageBundle, no orientation is undone.
void MaybeSetFloatCallback(
const std::function<void(const float* pixels, size_t x, size_t y,
size_t num_pixels)>& cb,
bool is_rgba, bool undo_orientation) const {
void MaybeSetFloatCallback(const PixelCallback& pixel_callback, bool is_rgba,
bool undo_orientation) const {
if (!CanDoLowMemoryPath(undo_orientation)) return;
dec_state_->pixel_callback = cb;
dec_state_->pixel_callback = pixel_callback;
dec_state_->rgb_output_is_rgba = is_rgba;
JXL_ASSERT(dec_state_->rgb_output == nullptr);
}
@ -196,7 +195,7 @@ class FrameDecoder {
// callback has been used.
bool HasRGBBuffer() const {
return dec_state_->rgb_output != nullptr ||
dec_state_->pixel_callback != nullptr;
dec_state_->pixel_callback.IsPresent();
}
private:
@ -216,18 +215,19 @@ class FrameDecoder {
// `GetStorageLocation` must be smaller than the `num_threads` value passed
// here. The value of `task` passed to `GetStorageLocation` must be smaller
// than the value of `num_tasks` passed here.
void PrepareStorage(size_t num_threads, size_t num_tasks) {
Status PrepareStorage(size_t num_threads, size_t num_tasks) {
size_t storage_size = std::min(num_threads, num_tasks);
if (storage_size > group_dec_caches_.size()) {
group_dec_caches_.resize(storage_size);
}
use_task_id_ = num_threads > num_tasks;
if (dec_state_->render_pipeline) {
dec_state_->render_pipeline->PrepareForThreads(
JXL_RETURN_IF_ERROR(dec_state_->render_pipeline->PrepareForThreads(
storage_size,
/*use_group_ids=*/modular_frame_decoder_.UsesFullImage() &&
frame_header_.encoding == FrameEncoding::kVarDCT);
frame_header_.encoding == FrameEncoding::kVarDCT));
}
return true;
}
size_t GetStorageLocation(size_t thread, size_t task) {
@ -242,18 +242,8 @@ class FrameDecoder {
// (uint8 output buffer or float pixel callback).
// TODO(veluca): reduce this set of restrictions.
bool CanDoLowMemoryPath(bool undo_orientation) const {
if (undo_orientation &&
decoded_->metadata()->GetOrientation() != Orientation::kIdentity) {
return false;
}
if (NeedsBlending(dec_state_)) return false;
if (frame_header_.CanBeReferenced()) return false;
if (render_spotcolors_ &&
decoded_->metadata()->Find(ExtraChannel::kSpotColor)) {
return false;
}
if (decoded_->AlphaIsPremultiplied()) return false;
return true;
return !(undo_orientation &&
decoded_->metadata()->GetOrientation() != Orientation::kIdentity);
}
PassesDecoderState* dec_state_;

View File

@ -740,7 +740,7 @@ Status DecodeGroup(BitReader* JXL_RESTRICT* JXL_RESTRICT readers,
// Arguments set to 0/nullptr are not used.
dec_state->upsampler8x->ProcessRow(input_rows, output_rows,
/*xextra=*/0, src_rect.xsize(), 0, 0,
nullptr);
thread);
}
}
return true;

View File

@ -89,6 +89,16 @@ HWY_EXPORT(MultiplySum); // Local function
HWY_EXPORT(RgbFromSingle); // Local function
HWY_EXPORT(SingleFromSingle); // Local function
// Slow conversion using double precision multiplication, only
// needed when the bit depth is too high for single precision
void SingleFromSingleAccurate(const size_t xsize,
const pixel_type* const JXL_RESTRICT row_in,
const double factor, float* row_out) {
for (size_t x = 0; x < xsize; x++) {
row_out[x] = row_in[x] * factor;
}
}
// convert custom [bits]-bit float (with [exp_bits] exponent bits) stored as int
// back to binary32 float
void int_to_float(const pixel_type* const JXL_RESTRICT row_in,
@ -457,9 +467,9 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
const auto* metadata = frame_header.nonserialized_metadata;
JXL_CHECK(gi.transform.empty());
auto get_row = [&](Rect r, size_t c, size_t y) {
return render_pipeline_input.GetBuffer(c).second.Row(
render_pipeline_input.GetBuffer(c).first, y);
auto get_row = [&](size_t c, size_t y) {
const auto& buffer = render_pipeline_input.GetBuffer(c);
return buffer.second.Row(buffer.first, y);
};
size_t c = 0;
@ -470,9 +480,9 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
const bool fp = metadata->m.bit_depth.floating_point_sample &&
frame_header.color_transform != ColorTransform::kXYB;
for (; c < 3; c++) {
float factor = full_image.bitdepth < 32
? 1.f / ((1u << full_image.bitdepth) - 1)
: 0;
double factor = full_image.bitdepth < 32
? 1.0 / ((1u << full_image.bitdepth) - 1)
: 0;
size_t c_in = c;
if (frame_header.color_transform == ColorTransform::kXYB) {
factor = dec_state->shared->matrices.DCQuants()[c];
@ -513,7 +523,7 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
mr.Row(&ch_in.plane, y);
const pixel_type* const JXL_RESTRICT row_in_Y =
mr.Row(&gi.channel[0].plane, y);
float* const JXL_RESTRICT row_out = get_row(r, c, y);
float* const JXL_RESTRICT row_out = get_row(c, y);
HWY_DYNAMIC_DISPATCH(MultiplySum)
(xsize_shifted, row_in, row_in_Y, factor, row_out);
},
@ -529,11 +539,11 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
mr.Row(&ch_in.plane, y);
if (rgb_from_gray) {
for (size_t cc = 0; cc < 3; cc++) {
float* const JXL_RESTRICT row_out = get_row(r, cc, y);
float* const JXL_RESTRICT row_out = get_row(cc, y);
int_to_float(row_in, row_out, xsize_shifted, bits, exp_bits);
}
} else {
float* const JXL_RESTRICT row_out = get_row(r, c, y);
float* const JXL_RESTRICT row_out = get_row(c, y);
int_to_float(row_in, row_out, xsize_shifted, bits, exp_bits);
}
},
@ -546,13 +556,27 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
const pixel_type* const JXL_RESTRICT row_in =
mr.Row(&ch_in.plane, y);
if (rgb_from_gray) {
HWY_DYNAMIC_DISPATCH(RgbFromSingle)
(xsize_shifted, row_in, factor, get_row(r, 0, y),
get_row(r, 1, y), get_row(r, 2, y));
if (full_image.bitdepth < 23) {
HWY_DYNAMIC_DISPATCH(RgbFromSingle)
(xsize_shifted, row_in, factor, get_row(0, y), get_row(1, y),
get_row(2, y));
} else {
SingleFromSingleAccurate(xsize_shifted, row_in, factor,
get_row(0, y));
SingleFromSingleAccurate(xsize_shifted, row_in, factor,
get_row(1, y));
SingleFromSingleAccurate(xsize_shifted, row_in, factor,
get_row(2, y));
}
} else {
float* const JXL_RESTRICT row_out = get_row(r, c, y);
HWY_DYNAMIC_DISPATCH(SingleFromSingle)
(xsize_shifted, row_in, factor, row_out);
float* const JXL_RESTRICT row_out = get_row(c, y);
if (full_image.bitdepth < 23) {
HWY_DYNAMIC_DISPATCH(SingleFromSingle)
(xsize_shifted, row_in, factor, row_out);
} else {
SingleFromSingleAccurate(xsize_shifted, row_in, factor,
row_out);
}
}
},
"ModularIntToFloat"));
@ -572,7 +596,7 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
int exp_bits = eci.bit_depth.exponent_bits_per_sample;
bool fp = eci.bit_depth.floating_point_sample;
JXL_ASSERT(fp || bits < 32);
const float mul = fp ? 0 : (1.0f / ((1u << bits) - 1));
const double factor = fp ? 0 : (1.0 / ((1u << bits) - 1));
JXL_ASSERT(c < gi.channel.size());
Channel& ch_in = gi.channel[c];
Rect r = render_pipeline_input.GetBuffer(3 + ec).second;
@ -589,8 +613,11 @@ Status ModularFrameDecoder::ModularImageToDecodedRect(
if (fp) {
int_to_float(row_in, row_out, r.xsize(), bits, exp_bits);
} else {
for (size_t x = 0; x < r.xsize(); ++x) {
row_out[x] = row_in[x] * mul;
if (full_image.bitdepth < 23) {
HWY_DYNAMIC_DISPATCH(SingleFromSingle)
(r.xsize(), row_in, factor, row_out);
} else {
SingleFromSingleAccurate(r.xsize(), row_in, factor, row_out);
}
}
}
@ -621,11 +648,10 @@ Status ModularFrameDecoder::FinalizeDecoding(PassesDecoderState* dec_state,
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, dec_state->shared->frame_dim.num_groups,
[&](size_t num_threads) {
dec_state->render_pipeline->PrepareForThreads(
return dec_state->render_pipeline->PrepareForThreads(
num_threads,
/*use_group_ids=*/dec_state->shared->frame_header.encoding ==
FrameEncoding::kVarDCT);
return true;
},
[&](const uint32_t group, size_t thread_id) {
RenderPipelineInput input =

View File

@ -85,182 +85,6 @@ void RandomImage(Xorshift128Plus* rng, const Rect& rect,
}
}
}
// [0, max_value]
template <class D, class V>
static HWY_INLINE V Clamp0ToMax(D d, const V x, const V max_value) {
const auto clamped = Min(x, max_value);
return ZeroIfNegative(clamped);
}
// x is in [0+delta, 1+delta], delta ~= 0.06
template <class StrengthEval>
typename StrengthEval::V NoiseStrength(const StrengthEval& eval,
const typename StrengthEval::V x) {
return Clamp0ToMax(D(), eval(x), Set(D(), 1.0f));
}
// TODO(veluca): SIMD-fy.
class StrengthEvalLut {
public:
using V = Vec<D>;
explicit StrengthEvalLut(const NoiseParams& noise_params)
#if HWY_TARGET == HWY_SCALAR
: noise_params_(noise_params)
#endif
{
#if HWY_TARGET != HWY_SCALAR
uint32_t lut[8];
memcpy(lut, noise_params.lut, sizeof(lut));
for (size_t i = 0; i < 8; i++) {
low16_lut[2 * i] = (lut[i] >> 0) & 0xFF;
low16_lut[2 * i + 1] = (lut[i] >> 8) & 0xFF;
high16_lut[2 * i] = (lut[i] >> 16) & 0xFF;
high16_lut[2 * i + 1] = (lut[i] >> 24) & 0xFF;
}
#endif
}
V operator()(const V vx) const {
constexpr size_t kScale = NoiseParams::kNumNoisePoints - 2;
auto scaled_vx = Max(Zero(D()), vx * Set(D(), kScale));
auto floor_x = Floor(scaled_vx);
auto frac_x = scaled_vx - floor_x;
floor_x = IfThenElse(scaled_vx >= Set(D(), kScale + 1), Set(D(), kScale),
floor_x);
frac_x = IfThenElse(scaled_vx >= Set(D(), kScale + 1), Set(D(), 1), frac_x);
auto floor_x_int = ConvertTo(DI(), floor_x);
#if HWY_TARGET == HWY_SCALAR
auto low = Set(D(), noise_params_.lut[floor_x_int.raw]);
auto hi = Set(D(), noise_params_.lut[floor_x_int.raw + 1]);
#else
// Set each lane's bytes to {0, 0, 2x+1, 2x}.
auto floorx_indices_low =
floor_x_int * Set(DI(), 0x0202) + Set(DI(), 0x0100);
// Set each lane's bytes to {2x+1, 2x, 0, 0}.
auto floorx_indices_hi =
floor_x_int * Set(DI(), 0x02020000) + Set(DI(), 0x01000000);
// load LUT
auto low16 = BitCast(DI(), LoadDup128(DI8(), low16_lut));
auto lowm = Set(DI(), 0xFFFF);
auto hi16 = BitCast(DI(), LoadDup128(DI8(), high16_lut));
auto him = Set(DI(), 0xFFFF0000);
// low = noise_params.lut[floor_x]
auto low =
BitCast(D(), (TableLookupBytes(low16, floorx_indices_low) & lowm) |
(TableLookupBytes(hi16, floorx_indices_hi) & him));
// hi = noise_params.lut[floor_x+1]
floorx_indices_low += Set(DI(), 0x0202);
floorx_indices_hi += Set(DI(), 0x02020000);
auto hi =
BitCast(D(), (TableLookupBytes(low16, floorx_indices_low) & lowm) |
(TableLookupBytes(hi16, floorx_indices_hi) & him));
#endif
return MulAdd(hi - low, frac_x, low);
}
private:
#if HWY_TARGET != HWY_SCALAR
// noise_params.lut transformed into two 16-bit lookup tables.
HWY_ALIGN uint8_t high16_lut[16];
HWY_ALIGN uint8_t low16_lut[16];
#else
const NoiseParams& noise_params_;
#endif
};
template <class D>
void AddNoiseToRGB(const D d, const Vec<D> rnd_noise_r,
const Vec<D> rnd_noise_g, const Vec<D> rnd_noise_cor,
const Vec<D> noise_strength_g, const Vec<D> noise_strength_r,
float ytox, float ytob, float* JXL_RESTRICT out_x,
float* JXL_RESTRICT out_y, float* JXL_RESTRICT out_b) {
const auto kRGCorr = Set(d, 0.9921875f); // 127/128
const auto kRGNCorr = Set(d, 0.0078125f); // 1/128
const auto red_noise = kRGNCorr * rnd_noise_r * noise_strength_r +
kRGCorr * rnd_noise_cor * noise_strength_r;
const auto green_noise = kRGNCorr * rnd_noise_g * noise_strength_g +
kRGCorr * rnd_noise_cor * noise_strength_g;
auto vx = Load(d, out_x);
auto vy = Load(d, out_y);
auto vb = Load(d, out_b);
vx += red_noise - green_noise + Set(d, ytox) * (red_noise + green_noise);
vy += red_noise + green_noise;
vb += Set(d, ytob) * (red_noise + green_noise);
Store(vx, d, out_x);
Store(vy, d, out_y);
Store(vb, d, out_b);
}
void AddNoise(const NoiseParams& noise_params, const Rect& noise_rect,
const Image3F& noise, const Rect& opsin_rect,
const ColorCorrelationMap& cmap, Image3F* opsin) {
if (!noise_params.HasAny()) return;
const StrengthEvalLut noise_model(noise_params);
D d;
const auto half = Set(d, 0.5f);
const size_t xsize = opsin_rect.xsize();
const size_t ysize = opsin_rect.ysize();
// With the prior subtract-random Laplacian approximation, rnd_* ranges were
// about [-1.5, 1.6]; Laplacian3 about doubles this to [-3.6, 3.6], so the
// normalizer is half of what it was before (0.5).
const auto norm_const = Set(d, 0.22f);
float ytox = cmap.YtoXRatio(0);
float ytob = cmap.YtoBRatio(0);
const size_t xsize_v = RoundUpTo(xsize, Lanes(d));
for (size_t y = 0; y < ysize; ++y) {
float* JXL_RESTRICT row_x = opsin_rect.PlaneRow(opsin, 0, y);
float* JXL_RESTRICT row_y = opsin_rect.PlaneRow(opsin, 1, y);
float* JXL_RESTRICT row_b = opsin_rect.PlaneRow(opsin, 2, y);
const float* JXL_RESTRICT row_rnd_r = noise_rect.ConstPlaneRow(noise, 0, y);
const float* JXL_RESTRICT row_rnd_g = noise_rect.ConstPlaneRow(noise, 1, y);
const float* JXL_RESTRICT row_rnd_c = noise_rect.ConstPlaneRow(noise, 2, y);
// Needed by the calls to Floor() in StrengthEvalLut. Only arithmetic and
// shuffles are otherwise done on the data, so this is safe.
msan::UnpoisonMemory(row_x + xsize, (xsize_v - xsize) * sizeof(float));
msan::UnpoisonMemory(row_y + xsize, (xsize_v - xsize) * sizeof(float));
for (size_t x = 0; x < xsize; x += Lanes(d)) {
const auto vx = Load(d, row_x + x);
const auto vy = Load(d, row_y + x);
const auto in_g = vy - vx;
const auto in_r = vy + vx;
const auto noise_strength_g = NoiseStrength(noise_model, in_g * half);
const auto noise_strength_r = NoiseStrength(noise_model, in_r * half);
const auto addit_rnd_noise_red = Load(d, row_rnd_r + x) * norm_const;
const auto addit_rnd_noise_green = Load(d, row_rnd_g + x) * norm_const;
const auto addit_rnd_noise_correlated =
Load(d, row_rnd_c + x) * norm_const;
AddNoiseToRGB(D(), addit_rnd_noise_red, addit_rnd_noise_green,
addit_rnd_noise_correlated, noise_strength_g,
noise_strength_r, ytox, ytob, row_x + x, row_y + x,
row_b + x);
}
msan::PoisonMemory(row_x + xsize, (xsize_v - xsize) * sizeof(float));
msan::PoisonMemory(row_y + xsize, (xsize_v - xsize) * sizeof(float));
msan::PoisonMemory(row_b + xsize, (xsize_v - xsize) * sizeof(float));
}
}
void RandomImage3(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const Rect& rect,
Image3F* JXL_RESTRICT noise) {
HWY_ALIGN Xorshift128Plus rng(visible_frame_index, nonvisible_frame_index, x0,
y0);
RandomImage(&rng, rect, &noise->Plane(0));
RandomImage(&rng, rect, &noise->Plane(1));
RandomImage(&rng, rect, &noise->Plane(2));
}
void Random3Planes(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const std::pair<ImageF*, Rect>& plane0,
const std::pair<ImageF*, Rect>& plane1,
@ -280,22 +104,6 @@ HWY_AFTER_NAMESPACE();
#if HWY_ONCE
namespace jxl {
HWY_EXPORT(AddNoise);
void AddNoise(const NoiseParams& noise_params, const Rect& noise_rect,
const Image3F& noise, const Rect& opsin_rect,
const ColorCorrelationMap& cmap, Image3F* opsin) {
return HWY_DYNAMIC_DISPATCH(AddNoise)(noise_params, noise_rect, noise,
opsin_rect, cmap, opsin);
}
HWY_EXPORT(RandomImage3);
void RandomImage3(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const Rect& rect,
Image3F* JXL_RESTRICT noise) {
return HWY_DYNAMIC_DISPATCH(RandomImage3)(
visible_frame_index, nonvisible_frame_index, x0, y0, rect, noise);
}
HWY_EXPORT(Random3Planes);
void Random3Planes(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const std::pair<ImageF*, Rect>& plane0,

View File

@ -20,16 +20,6 @@
namespace jxl {
// Add a noise to Opsin image, loading generated random noise from `noise_rect`
// in `noise`.
void AddNoise(const NoiseParams& noise_params, const Rect& noise_rect,
const Image3F& noise, const Rect& opsin_rect,
const ColorCorrelationMap& cmap, Image3F* opsin);
void RandomImage3(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const Rect& rect,
Image3F* JXL_RESTRICT noise);
void Random3Planes(size_t visible_frame_index, size_t nonvisible_frame_index,
size_t x0, size_t y0, const std::pair<ImageF*, Rect>& plane0,
const std::pair<ImageF*, Rect>& plane1,

View File

@ -156,9 +156,6 @@ Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize,
if (!decoder.CheckANSFinalState()) {
return JXL_FAILURE("ANS checksum failure.");
}
if (!HasAny()) {
return JXL_FAILURE("Decoded patch dictionary but got none");
}
ComputePatchCache();
return true;
@ -237,51 +234,4 @@ void PatchDictionary::AddOneRow(float* const* inout, size_t y, size_t x0,
shared_->metadata->m.extra_channel_info);
}
}
void PatchDictionary::AddTo(Image3F* opsin, const Rect& opsin_rect,
float* const* extra_channels,
const Rect& image_rect) const {
JXL_CHECK(SameSize(opsin_rect, image_rect));
if (patch_starts_.empty()) return;
size_t num_ec = shared_->metadata->m.num_extra_channels;
std::vector<const float*> fg_ptrs(3 + num_ec);
std::vector<float*> bg_ptrs(3 + 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]];
size_t by = pos.y;
size_t bx = pos.x;
size_t xsize = pos.ref_pos.xsize;
JXL_DASSERT(y >= by);
JXL_DASSERT(y < by + pos.ref_pos.ysize);
size_t iy = y - by;
size_t ref = pos.ref_pos.ref;
if (bx >= image_rect.x0() + image_rect.xsize()) continue;
if (bx + xsize < image_rect.x0()) continue;
size_t x0 = std::max(bx, image_rect.x0());
size_t x1 = std::min(bx + xsize, image_rect.x0() + image_rect.xsize());
for (size_t c = 0; c < 3; c++) {
fg_ptrs[c] =
shared_->reference_frames[ref].frame->color()->ConstPlaneRow(
c, pos.ref_pos.y0 + iy) +
pos.ref_pos.x0 + x0 - bx;
bg_ptrs[c] = opsin_rect.PlaneRow(opsin, c, y - image_rect.y0()) + x0 -
image_rect.x0();
}
for (size_t i = 0; i < num_ec; i++) {
fg_ptrs[3 + i] =
shared_->reference_frames[ref].frame->extra_channels()[i].ConstRow(
pos.ref_pos.y0 + iy) +
pos.ref_pos.x0 + x0 - bx;
bg_ptrs[3 + i] = extra_channels[i] + x0 - image_rect.x0();
}
PerformBlending(bg_ptrs.data(), fg_ptrs.data(), bg_ptrs.data(), 0,
x1 - x0, pos.blending[0], pos.blending.data() + 1,
shared_->metadata->m.extra_channel_info);
}
}
}
} // namespace jxl

View File

@ -126,11 +126,6 @@ class PatchDictionary {
// to be located at position (x0, y) in the frame.
void AddOneRow(float* const* inout, size_t y, size_t x0, size_t xsize) const;
// Only adds patches that belong to the `image_rect` area of the decoded
// image, writing them to the `opsin_rect` area of `opsin`.
void AddTo(Image3F* opsin, const Rect& opsin_rect,
float* const* extra_channels, const Rect& image_rect) const;
// Returns dependencies of this patch dictionary on reference frame ids as a
// bit mask: bits 0-3 indicate reference frame 0-3.
int GetReferences() const;

View File

@ -139,21 +139,17 @@ namespace {
size_t BitsPerChannel(JxlDataType data_type) {
switch (data_type) {
case JXL_TYPE_BOOLEAN:
return 1;
case JXL_TYPE_UINT8:
return 8;
case JXL_TYPE_UINT16:
return 16;
case JXL_TYPE_UINT32:
return 32;
case JXL_TYPE_FLOAT:
return 32;
case JXL_TYPE_FLOAT16:
return 16;
// No default, give compiler error if new type not handled.
default:
return 0; // signals unhandled JxlDataType
}
return 0; // Indicate invalid data type.
}
enum class DecoderStage : uint32_t {
@ -465,8 +461,15 @@ struct JxlDecoderStruct {
// Owned by the caller, buffers for DC image and full resolution images
void* preview_out_buffer;
void* image_out_buffer;
JxlImageOutCallback image_out_callback;
void* image_out_opaque;
JxlImageOutInitCallback image_out_init_callback;
JxlImageOutRunCallback image_out_run_callback;
JxlImageOutDestroyCallback image_out_destroy_callback;
void* image_out_init_opaque;
struct SimpleImageOutCallback {
JxlImageOutCallback callback;
void* opaque;
};
SimpleImageOutCallback simple_image_out_callback;
size_t preview_out_size;
size_t image_out_size;
@ -609,6 +612,11 @@ namespace {
bool CheckSizeLimit(JxlDecoder* dec, size_t xsize, size_t ysize) {
if (!dec->memory_limit_base) return true;
if (xsize == 0 || ysize == 0) return true;
if (xsize >= dec->memory_limit_base || ysize >= dec->memory_limit_base) {
return false;
}
// Rough estimate of real row length.
xsize = jxl::DivCeil(xsize, 32) * 32;
size_t num_pixels = xsize * ysize;
if (num_pixels / xsize != ysize) return false; // overflow
if (num_pixels > dec->memory_limit_base) return false;
@ -670,8 +678,10 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->image_out_buffer_set = false;
dec->preview_out_buffer = nullptr;
dec->image_out_buffer = nullptr;
dec->image_out_callback = nullptr;
dec->image_out_opaque = nullptr;
dec->image_out_init_callback = nullptr;
dec->image_out_run_callback = nullptr;
dec->image_out_destroy_callback = nullptr;
dec->image_out_init_opaque = nullptr;
dec->preview_out_size = 0;
dec->image_out_size = 0;
dec->extra_channel_output.clear();
@ -731,7 +741,7 @@ JxlDecoder* JxlDecoderCreate(const JxlMemoryManager* memory_manager) {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!memory_manager) {
dec->memory_limit_base = 1 << 21;
dec->memory_limit_base = 53 << 16;
// Allow 5 x max_image_size processing units; every frame is accounted
// as W x H CPU processing units, so there could be numerous small frames
// or few larger ones.
@ -746,9 +756,10 @@ JxlDecoder* JxlDecoderCreate(const JxlMemoryManager* memory_manager) {
void JxlDecoderDestroy(JxlDecoder* dec) {
if (dec) {
JxlMemoryManager local_memory_manager = dec->memory_manager;
// Call destructor directly since custom free function is used.
dec->~JxlDecoder();
jxl::MemoryManagerFree(&dec->memory_manager, dec);
jxl::MemoryManagerFree(&local_memory_manager, dec);
}
}
@ -1051,7 +1062,7 @@ static JxlDecoderStatus ConvertImageInternal(
const JxlDecoder* dec, const jxl::ImageBundle& frame,
const JxlPixelFormat& format, bool want_extra_channel,
size_t extra_channel_index, void* out_image, size_t out_size,
JxlImageOutCallback out_callback, void* out_opaque) {
const PixelCallback& out_callback) {
// TODO(lode): handle mismatch of RGB/grayscale color profiles and pixel data
// color/grayscale format
const size_t stride = GetStride(dec, format);
@ -1065,19 +1076,17 @@ static JxlDecoderStatus ConvertImageInternal(
jxl::Status status(true);
if (want_extra_channel) {
status = jxl::ConvertToExternal(
frame.extra_channels()[extra_channel_index],
BitsPerChannel(format.data_type), float_format, format.endianness,
stride, dec->thread_pool.get(), out_image, out_size,
/*out_callback=*/out_callback,
/*out_opaque=*/out_opaque, undo_orientation);
JXL_ASSERT(extra_channel_index < frame.extra_channels().size());
status = jxl::ConvertToExternal(frame.extra_channels()[extra_channel_index],
BitsPerChannel(format.data_type),
float_format, format.endianness, stride,
dec->thread_pool.get(), out_image, out_size,
out_callback, undo_orientation);
} else {
status = jxl::ConvertToExternal(
frame, BitsPerChannel(format.data_type), float_format,
format.num_channels, format.endianness, stride, dec->thread_pool.get(),
out_image, out_size,
/*out_callback=*/out_callback,
/*out_opaque=*/out_opaque, undo_orientation);
out_image, out_size, out_callback, undo_orientation);
}
return status ? JXL_DEC_SUCCESS : JXL_DEC_ERROR;
@ -1252,8 +1261,8 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in,
JxlDecoderStatus status = ConvertImageInternal(
dec, ib, dec->preview_out_format, /*want_extra_channel=*/false,
/*extra_channel_index=*/0, dec->preview_out_buffer,
dec->preview_out_size, /*out_callback=*/nullptr,
/*out_opaque=*/nullptr);
dec->preview_out_size,
/*out_callback=*/{});
if (status != JXL_DEC_SUCCESS) return status;
}
return JXL_DEC_PREVIEW_IMAGE;
@ -1453,17 +1462,16 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in,
// TODO(lode): Support more formats than just native endian float32 for
// the low-memory callback path
if (dec->image_out_buffer_set && !!dec->image_out_callback &&
if (dec->image_out_buffer_set && !!dec->image_out_init_callback &&
!!dec->image_out_run_callback &&
dec->image_out_format.data_type == JXL_TYPE_FLOAT &&
dec->image_out_format.num_channels >= 3 && !swap_endianness &&
dec->frame_dec_in_progress) {
bool is_rgba = dec->image_out_format.num_channels == 4;
dec->frame_dec->MaybeSetFloatCallback(
[dec](const float* pixels, size_t x, size_t y, size_t num_pixels) {
JXL_DASSERT(num_pixels > 0);
dec->image_out_callback(dec->image_out_opaque, x, y, num_pixels,
pixels);
},
PixelCallback{
dec->image_out_init_callback, dec->image_out_run_callback,
dec->image_out_destroy_callback, dec->image_out_init_opaque},
is_rgba, !dec->keep_orientation);
}
@ -1557,21 +1565,30 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec, const uint8_t* in,
dec, *dec->ib, dec->image_out_format,
/*want_extra_channel=*/false,
/*extra_channel_index=*/0, dec->image_out_buffer,
dec->image_out_size, dec->image_out_callback,
dec->image_out_opaque);
dec->image_out_size,
PixelCallback{dec->image_out_init_callback,
dec->image_out_run_callback,
dec->image_out_destroy_callback,
dec->image_out_init_opaque});
if (status != JXL_DEC_SUCCESS) return status;
}
dec->image_out_buffer_set = false;
bool has_ec = !dec->ib->extra_channels().empty();
for (size_t i = 0; i < dec->extra_channel_output.size(); ++i) {
void* buffer = dec->extra_channel_output[i].buffer;
// buffer nullptr indicates this extra channel is not requested
if (!buffer) continue;
if (!has_ec) {
JXL_WARNING(
"Extra channels are not supported when callback is used");
return JXL_DEC_ERROR;
}
const JxlPixelFormat* format = &dec->extra_channel_output[i].format;
JxlDecoderStatus status = ConvertImageInternal(
dec, *dec->ib, *format,
/*want_extra_channel=*/true, i, buffer,
dec->extra_channel_output[i].buffer_size, nullptr, nullptr);
/*want_extra_channel=*/true, /*extra_channel_index=*/i, buffer,
dec->extra_channel_output[i].buffer_size, /*out_callback=*/{});
if (status != JXL_DEC_SUCCESS) return status;
}
@ -2385,17 +2402,11 @@ JxlDecoderStatus PrepareSizeCheck(const JxlDecoder* dec,
if (format->num_channels > 4) {
return JXL_API_ERROR("More than 4 channels not supported");
}
if (format->data_type == JXL_TYPE_BOOLEAN) {
return JXL_API_ERROR("Boolean data type not yet supported");
}
if (format->data_type == JXL_TYPE_UINT32) {
return JXL_API_ERROR("uint32 data type not yet supported");
}
*bits = BitsPerChannel(format->data_type);
if (*bits == 0) {
return JXL_API_ERROR("Invalid data type");
return JXL_API_ERROR("Invalid/unsupported data type");
}
return JXL_DEC_SUCCESS;
@ -2440,7 +2451,7 @@ JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) {
dec, *dec->ib, dec->image_out_format,
/*want_extra_channel=*/false,
/*extra_channel_index=*/0, dec->image_out_buffer, dec->image_out_size,
/*out_callback=*/nullptr, /*out_opaque=*/nullptr);
/*out_callback=*/{});
dec->ib->ShrinkTo(xsize, ysize);
if (status != JXL_DEC_SUCCESS) return status;
return JXL_DEC_SUCCESS;
@ -2548,7 +2559,7 @@ JxlDecoderStatus JxlDecoderSetImageOutBuffer(JxlDecoder* dec,
if (!dec->got_basic_info || !(dec->orig_events_wanted & JXL_DEC_FULL_IMAGE)) {
return JXL_API_ERROR("No image out buffer needed at this time");
}
if (dec->image_out_buffer_set && !!dec->image_out_callback) {
if (dec->image_out_buffer_set && !!dec->image_out_run_callback) {
return JXL_API_ERROR(
"Cannot change from image out callback to image out buffer");
}
@ -2634,19 +2645,51 @@ JxlDecoderStatus JxlDecoderSetImageOutCallback(JxlDecoder* dec,
const JxlPixelFormat* format,
JxlImageOutCallback callback,
void* opaque) {
dec->simple_image_out_callback.callback = callback;
dec->simple_image_out_callback.opaque = opaque;
const auto init_callback =
+[](void* init_opaque, size_t num_threads, size_t num_pixels_per_thread) {
// No initialization to do, just reuse init_opaque as run_opaque.
return init_opaque;
};
const auto run_callback =
+[](void* run_opaque, size_t thread_id, size_t x, size_t y,
size_t num_pixels, const void* pixels) {
const auto* const simple_callback =
static_cast<const JxlDecoder::SimpleImageOutCallback*>(run_opaque);
simple_callback->callback(simple_callback->opaque, x, y, num_pixels,
pixels);
};
const auto destroy_callback = +[](void* run_opaque) {};
return JxlDecoderSetMultithreadedImageOutCallback(
dec, format, init_callback, run_callback,
/*destroy_callback=*/destroy_callback, &dec->simple_image_out_callback);
}
JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback(
JxlDecoder* dec, const JxlPixelFormat* format,
JxlImageOutInitCallback init_callback, JxlImageOutRunCallback run_callback,
JxlImageOutDestroyCallback destroy_callback, void* init_opaque) {
if (dec->image_out_buffer_set && !!dec->image_out_buffer) {
return JXL_API_ERROR(
"Cannot change from image out buffer to image out callback");
}
if (init_callback == nullptr || run_callback == nullptr ||
destroy_callback == nullptr) {
return JXL_API_ERROR("All callbacks are required");
}
// Perform error checking for invalid format.
size_t bits_dummy;
JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits_dummy);
if (status != JXL_DEC_SUCCESS) return status;
dec->image_out_buffer_set = true;
dec->image_out_callback = callback;
dec->image_out_opaque = opaque;
dec->image_out_init_callback = init_callback;
dec->image_out_run_callback = run_callback;
dec->image_out_destroy_callback = destroy_callback;
dec->image_out_init_opaque = init_opaque;
dec->image_out_format = *format;
return JXL_DEC_SUCCESS;

View File

@ -211,7 +211,7 @@ PaddedBytes CreateTestJXLCodestream(
jpeg_bytes.data() + jpeg_bytes.size());
EXPECT_TRUE(jxl::jpeg::DecodeImageJPG(
jxl::Span<const uint8_t>(jpeg_bytes.data(), jpeg_bytes.size()), &io));
EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data));
EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data, cparams));
io.metadata.m.xyb_encoded = false;
#else // JPEGXL_ENABLE_JPEG
JXL_ABORT(
@ -1234,7 +1234,8 @@ TEST_P(DecodeTestParam, PixelTest) {
io.Main(), 16,
/*float_out=*/false, orig_channels, JXL_BIG_ENDIAN,
xsize * 2 * orig_channels, nullptr, pixels.data(), pixels.size(),
nullptr, nullptr, static_cast<jxl::Orientation>(config.orientation)));
/*out_callback=*/{},
static_cast<jxl::Orientation>(config.orientation)));
}
if (config.upsampling == 1) {
EXPECT_EQ(0u, jxl::test::ComparePixels(pixels.data(), pixels2.data(), xsize,
@ -1406,12 +1407,8 @@ std::ostream& operator<<(std::ostream& os, const PixelTestConfig& c) {
case JXL_TYPE_FLOAT16:
os << "f16";
break;
case JXL_TYPE_UINT32:
os << "u32";
break;
case JXL_TYPE_BOOLEAN:
os << "b";
break;
default:
JXL_ASSERT(false);
};
if (jxl::test::GetDataBits(c.data_type) > jxl::kBitsPerByte) {
if (c.endianness == JXL_NATIVE_ENDIAN) {
@ -3568,7 +3565,7 @@ TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructTestCodestream)) {
TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) {
const std::string jpeg_path =
"imagecompression.info/flower_foveon.png.im_q85_420.jpg";
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg";
const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path);
jxl::CodecInOut orig_io;
ASSERT_TRUE(
@ -3586,7 +3583,8 @@ TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) {
/*aux_out=*/nullptr));
jxl::PaddedBytes jpeg_data;
ASSERT_TRUE(EncodeJPEGData(*orig_io.Main().jpeg_data.get(), &jpeg_data));
ASSERT_TRUE(
EncodeJPEGData(*orig_io.Main().jpeg_data.get(), &jpeg_data, cparams));
jxl::PaddedBytes container;
container.append(jxl::kContainerHeader,
jxl::kContainerHeader + sizeof(jxl::kContainerHeader));
@ -4100,7 +4098,7 @@ TEST(DecodeTest, SpotColorTest) {
cparams.speed_tier = jxl::SpeedTier::kLightning;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.quality_pair = {100, 100};
cparams.butteraugli_distance = 0.f;
jxl::PaddedBytes compressed;
std::unique_ptr<jxl::PassesEncoderState> enc_state =

View File

@ -1102,9 +1102,10 @@ ImageBundle RoundtripImage(const Image3F& opsin, PassesEncoderState* enc_state,
JXL_CHECK(dec_state->PreparePipeline(&decoded, options));
hwy::AlignedUniquePtr<GroupDecCache[]> group_dec_caches;
const auto allocate_storage = [&](const size_t num_threads) {
dec_state->render_pipeline->PrepareForThreads(num_threads,
/*use_group_ids=*/false);
const auto allocate_storage = [&](const size_t num_threads) -> Status {
JXL_RETURN_IF_ERROR(
dec_state->render_pipeline->PrepareForThreads(num_threads,
/*use_group_ids=*/false));
group_dec_caches = hwy::MakeUniqueAlignedArray<GroupDecCache>(num_threads);
return true;
};

View File

@ -98,8 +98,10 @@ Status InitializePassesEncoder(const Image3F& opsin, const JxlCmsInterface& cms,
// and kModular for the smallest DC (first in the bitstream)
if (cparams.progressive_dc == 0) {
cparams.modular_mode = true;
cparams.quality_pair.first = cparams.quality_pair.second =
99.f - enc_state->cparams.butteraugli_distance * 0.2f;
// TODO(jon): tweak mapping from image dist to dist for modular DC
cparams.butteraugli_distance =
std::max(kMinButteraugliDistance,
enc_state->cparams.butteraugli_distance * 0.03f);
}
ImageBundle ib(&shared.metadata->m);
// This is a lie - dc is in XYB

View File

@ -86,7 +86,6 @@ uint32_t JXL_INLINE Load8(const uint8_t* p) { return *p; }
Status PixelFormatToExternal(const JxlPixelFormat& pixel_format,
size_t* bitdepth, bool* float_in) {
// TODO(zond): Make this accept uint32.
if (pixel_format.data_type == JXL_TYPE_FLOAT) {
*bitdepth = 32;
*float_in = true;
@ -100,7 +99,7 @@ Status PixelFormatToExternal(const JxlPixelFormat& pixel_format,
*bitdepth = 16;
*float_in = false;
} else {
return JXL_FAILURE("unsupported bitdepth");
return JXL_FAILURE("unsupported pixel format data type");
}
return true;
}
@ -111,20 +110,9 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
JxlEndianness endianness, ThreadPool* pool,
ImageF* channel, bool float_in, size_t align) {
// TODO(firsching): Avoid code duplication with the function below.
if (bits_per_sample < 1 || bits_per_sample > 32) {
return JXL_FAILURE("Invalid bits_per_sample value.");
}
// TODO(deymo): Implement 1-bit per sample as 8 samples per byte. In
// any other case we use DivCeil(bits_per_sample, 8) bytes per pixel per
// channel.
if (bits_per_sample == 1) {
return JXL_FAILURE("packed 1-bit per sample is not yet supported");
}
// bytes_per_pixel are only valid for
// bits_per_sample > 1.
JXL_CHECK(float_in ? bits_per_sample == 16 || bits_per_sample == 32
: bits_per_sample > 0 && bits_per_sample <= 16);
const size_t bytes_per_pixel = DivCeil(bits_per_sample, jxl::kBitsPerByte);
const size_t last_row_size = xsize * bytes_per_pixel;
const size_t row_size =
(align > 1 ? jxl::DivCeil(last_row_size, align) * align : last_row_size);
@ -153,7 +141,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
const size_t y = task;
size_t i = row_size * task;
float* JXL_RESTRICT row_out = channel->Row(y);
if (bits_per_sample <= 16) {
if (bits_per_sample == 16) {
if (little_endian) {
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = LoadLEFloat16(in + i);
@ -188,11 +176,9 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
const size_t y = task;
size_t i = row_size * task;
float* JXL_RESTRICT row_out = channel->Row(y);
// TODO(deymo): add bits_per_sample == 1 case here. Also maybe
// implement masking if bits_per_sample is not a multiple of 8.
if (bits_per_sample <= 8) {
LoadFloatRow<Load8>(row_out, in + i, mul, xsize, bytes_per_pixel);
} else if (bits_per_sample <= 16) {
} else {
if (little_endian) {
LoadFloatRow<LoadLE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
@ -200,14 +186,6 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
LoadFloatRow<LoadBE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
} else {
if (little_endian) {
LoadFloatRow<LoadLE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
} else {
LoadFloatRow<LoadBE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
}
},
"ConvertExtraChannelUint"));
@ -221,15 +199,8 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
size_t bits_per_sample, JxlEndianness endianness,
bool flipped_y, ThreadPool* pool, ImageBundle* ib,
bool float_in, size_t align) {
if (bits_per_sample < 1 || bits_per_sample > 32) {
return JXL_FAILURE("Invalid bits_per_sample value.");
}
// TODO(deymo): Implement 1-bit per sample as 8 samples per byte. In
// any other case we use DivCeil(bits_per_sample, 8) bytes per pixel per
// channel.
if (bits_per_sample == 1) {
return JXL_FAILURE("packed 1-bit per sample is not yet supported");
}
JXL_CHECK(float_in ? bits_per_sample == 16 || bits_per_sample == 32
: bits_per_sample > 0 && bits_per_sample <= 16);
const size_t color_channels = c_current.Channels();
bool has_alpha = channels == 2 || channels == 4;
@ -239,8 +210,6 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
color_channels, channels);
}
// bytes_per_channel and bytes_per_pixel are only valid for
// bits_per_sample > 1.
const size_t bytes_per_channel = DivCeil(bits_per_sample, jxl::kBitsPerByte);
const size_t bytes_per_pixel = channels * bytes_per_channel;
if (bits_per_sample > 16 && bits_per_sample < 32) {
@ -287,7 +256,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
size_t i =
row_size * task + (c * bits_per_sample / jxl::kBitsPerByte);
float* JXL_RESTRICT row_out = color.PlaneRow(c, y);
if (bits_per_sample <= 16) {
if (bits_per_sample == 16) {
if (little_endian) {
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = LoadLEFloat16(in + i);
@ -325,11 +294,9 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
const size_t y = get_y(task);
size_t i = row_size * task + c * bytes_per_channel;
float* JXL_RESTRICT row_out = color.PlaneRow(c, y);
// TODO(deymo): add bits_per_sample == 1 case here. Also maybe
// implement masking if bits_per_sample is not a multiple of 8.
if (bits_per_sample <= 8) {
LoadFloatRow<Load8>(row_out, in + i, mul, xsize, bytes_per_pixel);
} else if (bits_per_sample <= 16) {
} else {
if (little_endian) {
LoadFloatRow<LoadLE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
@ -337,14 +304,6 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
LoadFloatRow<LoadBE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
} else {
if (little_endian) {
LoadFloatRow<LoadLE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
} else {
LoadFloatRow<LoadBE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
}
},
"ConvertRGBUint"));
@ -371,7 +330,7 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
size_t i = row_size * task +
((channels - 1) * bits_per_sample / jxl::kBitsPerByte);
float* JXL_RESTRICT row_out = alpha.Row(y);
if (bits_per_sample <= 16) {
if (bits_per_sample == 16) {
if (little_endian) {
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = LoadLEFloat16(in + i);
@ -406,11 +365,9 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
const size_t y = get_y(task);
size_t i = row_size * task + (channels - 1) * bytes_per_channel;
float* JXL_RESTRICT row_out = alpha.Row(y);
// TODO(deymo): add bits_per_sample == 1 case here. Also maybe
// implement masking if bits_per_sample is not a multiple of 8.
if (bits_per_sample <= 8) {
LoadFloatRow<Load8>(row_out, in + i, mul, xsize, bytes_per_pixel);
} else if (bits_per_sample <= 16) {
} else {
if (little_endian) {
LoadFloatRow<LoadLE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
@ -418,19 +375,17 @@ Status ConvertFromExternal(Span<const uint8_t> bytes, size_t xsize,
LoadFloatRow<LoadBE16>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
} else {
if (little_endian) {
LoadFloatRow<LoadLE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
} else {
LoadFloatRow<LoadBE32>(row_out, in + i, mul, xsize,
bytes_per_pixel);
}
}
},
"ConvertAlphaUint"));
}
ib->SetAlpha(std::move(alpha), alpha_is_premultiplied);
} else if (!has_alpha && ib->HasAlpha()) {
// if alpha is not passed, but it is expected, then assume
// it is all-opaque
ImageF alpha(xsize, ysize);
FillImage(1.0f, &alpha);
ib->SetAlpha(std::move(alpha), alpha_is_premultiplied);
}

View File

@ -20,6 +20,7 @@
#include "lib/jxl/enc_cache.h"
#include "lib/jxl/enc_frame.h"
#include "lib/jxl/enc_icc_codec.h"
#include "lib/jxl/exif.h"
#include "lib/jxl/frame_header.h"
#include "lib/jxl/headers.h"
#include "lib/jxl/image_bundle.h"
@ -64,51 +65,6 @@ PassDefinition progressive_passes_dc_quant_ac_full_ac[] = {
/*suitable_for_downsampling_of_at_least=*/0},
};
constexpr uint16_t kExifOrientationTag = 274;
// Parses the Exif data just enough to extract any render-impacting info.
// If the Exif data is invalid or could not be parsed, then it is treated
// as a no-op.
// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value
// "R03"
// TODO (jon): set intrinsic dimensions according to
// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24
void InterpretExif(const PaddedBytes& exif, CodecMetadata* metadata) {
if (exif.size() < 12) return; // not enough bytes for a valid exif blob
const uint8_t* t = exif.data();
bool bigendian = false;
if (LoadLE32(t) == 0x2A004D4D) {
bigendian = true;
} else if (LoadLE32(t) != 0x002A4949) {
return; // not a valid tiff header
}
t += 4;
uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
if (exif.size() < 12 + offset + 2 || offset < 8) return;
t += offset - 4;
uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
while (nb_tags > 0) {
if (t + 12 >= exif.data() + exif.size()) return;
uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t));
t += 4;
uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 4;
if (tag == kExifOrientationTag) {
if (type == 3 && count == 1) {
if (value >= 1 && value <= 8) {
metadata->m.orientation = value;
}
}
}
nb_tags--;
}
}
Status PrepareCodecMetadataFromIO(const CompressParams& cparams,
const CodecInOut* io,
CodecMetadata* metadata) {
@ -121,9 +77,7 @@ Status PrepareCodecMetadataFromIO(const CompressParams& cparams,
// 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 =
cparams.modular_mode && cparams.quality_pair.first == 100.0f;
if (!lossless_modular && !io->Main().IsJPEG()) {
if (!cparams.IsLossless() && !io->Main().IsJPEG()) {
metadata->m.color_encoding.DecideIfWantICC();
}

View File

@ -254,10 +254,9 @@ Status LoopFilterFromParams(const CompressParams& cparams,
}
// Strength of EPF in modular mode.
if (frame_header->encoding == FrameEncoding::kModular &&
cparams.quality_pair.first < 100) {
!cparams.IsLossless()) {
// TODO(veluca): this formula is nonsense.
loop_filter->epf_sigma_for_modular =
20.0f * (1.0f - cparams.quality_pair.first / 100);
loop_filter->epf_sigma_for_modular = cparams.butteraugli_distance;
}
if (frame_header->encoding == FrameEncoding::kModular &&
cparams.lossy_palette) {
@ -1105,12 +1104,6 @@ Status EncodeFrame(const CompressParams& cparams_orig,
return JXL_FAILURE("Butteraugli distance is too low (%f)",
cparams.butteraugli_distance);
}
if (cparams.butteraugli_distance > 0.9f && cparams.modular_mode == false &&
cparams.quality_pair.first == 100) {
// in case the color image is lossy, make the alpha slightly lossy too
cparams.quality_pair.first =
std::max(90.f, 99.f - 0.3f * cparams.butteraugli_distance);
}
if (ib.IsJPEG()) {
cparams.gaborish = Override::kOff;
@ -1228,8 +1221,7 @@ Status EncodeFrame(const CompressParams& cparams_orig,
// input is already in XYB.
CopyImageTo(ib.color(), &opsin);
}
bool lossless = (frame_header->encoding == FrameEncoding::kModular &&
cparams.quality_pair.first == 100);
bool lossless = cparams.IsLossless();
if (ib.HasAlpha() && !ib.AlphaIsPremultiplied() &&
frame_header->frame_type == FrameType::kRegularFrame &&
!ApplyOverride(cparams.keep_invisible, lossless) &&

View File

@ -22,29 +22,6 @@
namespace jxl {
namespace {
bool EncodeVarInt(uint64_t value, size_t output_size, size_t* output_pos,
uint8_t* output) {
// While more than 7 bits of data are left,
// store 7 bits and set the next byte flag
while (value > 127) {
if (*output_pos > output_size) return false;
// |128: Set the next byte flag
output[(*output_pos)++] = ((uint8_t)(value & 127)) | 128;
// Remove the seven bits we just wrote
value >>= 7;
}
if (*output_pos > output_size) return false;
output[(*output_pos)++] = ((uint8_t)value) & 127;
return true;
}
void EncodeVarInt(uint64_t value, PaddedBytes* data) {
size_t pos = data->size();
data->resize(data->size() + 9);
JXL_CHECK(EncodeVarInt(value, data->size(), &pos, data->data()));
data->resize(pos);
}
// Unshuffles or de-interleaves bytes, for example with width 2, turns
// "AaBbCcDc" into "ABCDabcd", this for example de-interleaves UTF-16 bytes into
// first all the high order bytes, then all the low order bytes.
@ -159,9 +136,9 @@ Status PredictICC(const uint8_t* icc, size_t size, PaddedBytes* result) {
ok &= DecodeKeyword(icc, size, pos + 0) == kGtrcTag;
ok &= DecodeKeyword(icc, size, pos + 12) == kBtrcTag;
if (ok) {
for (size_t i = 0; i < 8; i++) {
if (icc[pos - 8 + i] != icc[pos + 4 + i]) ok = false;
if (icc[pos - 8 + i] != icc[pos + 16 + i]) ok = false;
for (size_t kk = 0; kk < 8; kk++) {
if (icc[pos - 8 + kk] != icc[pos + 4 + kk]) ok = false;
if (icc[pos - 8 + kk] != icc[pos + 16 + kk]) ok = false;
}
}
if (ok) {

View File

@ -228,11 +228,18 @@ void QuantizeChannel(Channel& ch, const int q) {
// fit in pixel_type
Status float_to_int(const float* const row_in, pixel_type* const row_out,
size_t xsize, unsigned int bits, unsigned int exp_bits,
bool fp, float factor) {
bool fp, double dfactor) {
JXL_ASSERT(sizeof(pixel_type) * 8 >= bits);
if (!fp) {
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = row_in[x] * factor + (row_in[x] < 0 ? -0.5f : 0.5f);
if (bits > 22) {
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = row_in[x] * dfactor + (row_in[x] < 0 ? -0.5 : 0.5);
}
} else {
float factor = dfactor;
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = row_in[x] * factor + (row_in[x] < 0 ? -0.5f : 0.5f);
}
}
return true;
}
@ -296,8 +303,7 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header,
: frame_dim(frame_header.ToFrameDimensions()), cparams(cparams_orig) {
size_t num_streams =
ModularStreamId::Num(frame_dim, frame_header.passes.num_passes);
if (cparams.modular_mode &&
cparams.quality_pair == std::pair<float, float>{100.0, 100.0}) {
if (cparams.IsLossless()) {
switch (cparams.decoding_speed_tier) {
case 0:
break;
@ -322,18 +328,17 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header,
}
}
if (cparams.decoding_speed_tier >= 1 && cparams.responsive &&
cparams.quality_pair == std::make_pair(100.f, 100.f)) {
cparams.IsLossless()) {
cparams.options.tree_kind =
ModularOptions::TreeKind::kTrivialTreeNoPredictor;
cparams.options.nb_repeats = 0;
}
stream_images.resize(num_streams);
if (cquality > 100) cquality = quality;
// use a sensible default if nothing explicit is specified:
// Squeeze for lossy, no squeeze for lossless
if (cparams.responsive < 0) {
if (quality == 100) {
if (cparams.IsLossless()) {
cparams.responsive = 0;
} else {
cparams.responsive = 1;
@ -396,14 +401,14 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header,
// no explicit predictor(s) given, set a good default
if ((cparams.speed_tier <= SpeedTier::kTortoise ||
cparams.modular_mode == false) &&
quality == 100 && cparams.responsive == false) {
cparams.IsLossless() && cparams.responsive == false) {
// TODO(veluca): allow all predictors that don't break residual
// multipliers in lossy mode.
cparams.options.predictor = Predictor::Variable;
} else if (cparams.responsive || cparams.lossy_palette) {
// zero predictor for Squeeze residues and lossy palette
cparams.options.predictor = Predictor::Zero;
} else if (quality < 100) {
} else if (!cparams.IsLossless()) {
// If not responsive and lossy. TODO(veluca): use near_lossless instead?
cparams.options.predictor = Predictor::Gradient;
} else if (cparams.speed_tier < SpeedTier::kFalcon) {
@ -420,6 +425,12 @@ ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header,
delta_pred = cparams.options.predictor;
if (cparams.lossy_palette) cparams.options.predictor = Predictor::Zero;
}
if (!cparams.IsLossless()) {
if (cparams.options.predictor == Predictor::Weighted ||
cparams.options.predictor == Predictor::Variable ||
cparams.options.predictor == Predictor::Best)
cparams.options.predictor = Predictor::Zero;
}
tree_splits.push_back(0);
if (cparams.modular_mode == false) {
cparams.options.fast_decode_multiplier = 1.0f;
@ -499,17 +510,29 @@ Status ModularFrameEncoder::ComputeEncodingData(
}
}
// in the non-float case, there is an implicit 0 sign bit
int max_bitdepth =
do_color ? metadata.bit_depth.bits_per_sample + (fp ? 0 : 1) : 0;
Image& gi = stream_images[0];
gi = Image(xsize, ysize, metadata.bit_depth.bits_per_sample, nb_chans);
int c = 0;
if (cparams.color_transform == ColorTransform::kXYB &&
cparams.modular_mode == true) {
static const float enc_factors[3] = {32768.0f, 2048.0f, 2048.0f};
float enc_factors[3] = {32768.0f, 2048.0f, 2048.0f};
if (cparams.butteraugli_distance > 0 && !cparams.responsive) {
// quantize XYB here and then treat it as a lossless image
enc_factors[0] *= 1.f / (1.f + 23.f * cparams.butteraugli_distance);
enc_factors[1] *= 1.f / (1.f + 14.f * cparams.butteraugli_distance);
enc_factors[2] *= 1.f / (1.f + 14.f * cparams.butteraugli_distance);
cparams.butteraugli_distance = 0;
}
if (cparams.manual_xyb_factors.size() == 3) {
DequantMatricesSetCustomDC(&enc_state->shared.matrices,
cparams.manual_xyb_factors.data());
// TODO(jon): update max_bitdepth in this case
} else {
DequantMatricesSetCustomDC(&enc_state->shared.matrices, enc_factors);
max_bitdepth = 12;
}
}
pixel_type maxval = gi.bitdepth < 32 ? (1u << gi.bitdepth) - 1 : 0;
@ -523,7 +546,7 @@ Status ModularFrameEncoder::ComputeEncodingData(
// XYB is encoded as YX(B-Y)
if (cparams.color_transform == ColorTransform::kXYB && c < 2)
c_out = 1 - c_out;
float factor = maxval;
double factor = maxval;
if (cparams.color_transform == ColorTransform::kXYB)
factor = enc_state->shared.matrices.InvDCQuant(c);
if (c == 2 && cparams.color_transform == ColorTransform::kXYB) {
@ -535,6 +558,8 @@ Status ModularFrameEncoder::ComputeEncodingData(
for (size_t x = 0; x < xsize; ++x) {
row_out[x] = row_in[x] * factor + 0.5f;
row_out[x] -= row_Y[x];
// zero the lsb of B
row_out[x] = row_out[x] / 2 * 2;
}
}
} else {
@ -581,7 +606,8 @@ Status ModularFrameEncoder::ComputeEncodingData(
int bits = eci.bit_depth.bits_per_sample;
int exp_bits = eci.bit_depth.exponent_bits_per_sample;
bool fp = eci.bit_depth.floating_point_sample;
float factor = (fp ? 1 : ((1u << eci.bit_depth.bits_per_sample) - 1));
double factor = (fp ? 1 : ((1u << eci.bit_depth.bits_per_sample) - 1));
if (bits + (fp ? 0 : 1) > max_bitdepth) max_bitdepth = bits + (fp ? 0 : 1);
std::atomic<bool> has_error{false};
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, gi.channel[c].plane.ysize(), ThreadPool::NoInit,
@ -599,9 +625,16 @@ Status ModularFrameEncoder::ComputeEncodingData(
}
JXL_ASSERT(c == nb_chans);
int level_max_bitdepth = (cparams.level == 5 ? 16 : 32);
if (max_bitdepth > level_max_bitdepth)
return JXL_FAILURE(
"Bitdepth too high for level %i (need %i bits, have only %i in this "
"level)",
cparams.level, max_bitdepth, level_max_bitdepth);
// Set options and apply transformations
if (quality < 100) {
if (cparams.butteraugli_distance > 0) {
if (cparams.palette_colors != 0) {
JXL_DEBUG_V(3, "Lossy encode, not doing palette transforms");
}
@ -704,65 +737,59 @@ Status ModularFrameEncoder::ComputeEncodingData(
}
}
// don't do an RCT if we're short on bits
if (cparams.color_transform == ColorTransform::kNone && do_color && !fp &&
gi.channel.size() - gi.nb_meta_channels >= 3) {
gi.channel.size() - gi.nb_meta_channels >= 3 &&
max_bitdepth + 1 < level_max_bitdepth) {
if (cparams.colorspace == 1 ||
(cparams.colorspace < 0 &&
(quality < 100 || cparams.speed_tier > SpeedTier::kHare))) {
(!cparams.IsLossless() || cparams.speed_tier > SpeedTier::kHare))) {
Transform ycocg{TransformId::kRCT};
ycocg.rct_type = 6;
ycocg.begin_c = gi.nb_meta_channels;
do_transform(gi, ycocg, weighted::Header(), pool);
max_bitdepth++;
} else if (cparams.colorspace >= 2) {
Transform sg(TransformId::kRCT);
sg.begin_c = gi.nb_meta_channels;
sg.rct_type = cparams.colorspace - 2;
do_transform(gi, sg, weighted::Header(), pool);
max_bitdepth++;
}
}
if (cparams.responsive && !gi.channel.empty()) {
// don't do squeeze if we don't have some spare bits
if (cparams.responsive && !gi.channel.empty() &&
max_bitdepth + 2 < level_max_bitdepth) {
Transform t(TransformId::kSqueeze);
t.squeezes = cparams.squeezes;
do_transform(gi, t, weighted::Header(), pool);
max_bitdepth += 2;
}
if (max_bitdepth + 1 > level_max_bitdepth) {
// force no group RCTs if we don't have a spare bit
cparams.colorspace = 0;
}
JXL_ASSERT(max_bitdepth <= level_max_bitdepth);
std::vector<uint32_t> quants;
if (quality < 100 || cquality < 100) {
if (cparams.butteraugli_distance > 0) {
quants.resize(gi.channel.size(), 1);
JXL_DEBUG_V(
2,
"Adding quantization constants corresponding to luma quality %.2f "
"and chroma quality %.2f",
quality, cquality);
float quality = 0.25f * cparams.butteraugli_distance;
JXL_DEBUG_V(2,
"Adding quantization constants corresponding to distance %.3f ",
quality);
if (!cparams.responsive) {
JXL_DEBUG_V(1,
"Warning: lossy compression without Squeeze "
"transform is just color quantization.");
quality = (400 + quality) / 5;
cquality = (400 + cquality) / 5;
}
// convert 'quality' to quantization scaling factor
if (quality > 50) {
quality = 200.0 - quality * 2.0;
} else {
quality = 900.0 - quality * 16.0;
}
if (cquality > 50) {
cquality = 200.0 - cquality * 2.0;
} else {
cquality = 900.0 - cquality * 16.0;
quality *= 0.1f;
}
if (cparams.color_transform != ColorTransform::kXYB) {
quality *= 0.01f * maxval / 255.f;
cquality *= 0.01f * maxval / 255.f;
} else {
quality *= 0.01f;
cquality *= 0.01f;
quality *= maxval / 255.f;
}
if (cparams.options.nb_repeats == 0) {
return JXL_FAILURE("nb_repeats = 0 not supported with modular lossy!");
}
@ -773,17 +800,18 @@ Status ModularFrameEncoder::ComputeEncodingData(
if (shift > 0) shift--;
int q;
// assuming default Squeeze here
int component = ((i - gi.nb_meta_channels) % nb_chans);
int component =
(do_color ? 0 : 3) + ((i - gi.nb_meta_channels) % nb_chans);
// last 4 channels are final chroma residuals
if (nb_chans > 2 && i >= gi.channel.size() - 4 && cparams.responsive) {
component = 1;
}
if (cparams.color_transform == ColorTransform::kXYB && component < 3) {
q = (component == 0 ? quality : cquality) * squeeze_quality_factor_xyb *
q = quality * squeeze_quality_factor_xyb *
squeeze_xyb_qtable[component][shift];
} else {
if (cparams.colorspace != 0 && component > 0 && component < 3) {
q = cquality * squeeze_quality_factor * squeeze_chroma_qtable[shift];
q = quality * squeeze_quality_factor * squeeze_chroma_qtable[shift];
} else {
q = quality * squeeze_quality_factor * squeeze_luma_factor *
squeeze_luma_qtable[shift];
@ -1288,12 +1316,10 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect,
if (gi.channel.empty()) return true;
// Do some per-group transforms
float quality = cparams.quality_pair.first;
// Local palette
// TODO(veluca): make this work with quantize-after-prediction in lossy
// mode.
if (quality == 100 && cparams.palette_colors != 0 &&
if (cparams.butteraugli_distance == 0.f && cparams.palette_colors != 0 &&
cparams.speed_tier < SpeedTier::kCheetah) {
// all-channel palette (e.g. RGBA)
if (gi.channel.size() - gi.nb_meta_channels > 1) {
@ -1321,8 +1347,9 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect,
}
// Local channel palette
if (cparams.channel_colors_percent > 0 && quality == 100 &&
!cparams.lossy_palette && cparams.speed_tier < SpeedTier::kCheetah &&
if (cparams.channel_colors_percent > 0 &&
cparams.butteraugli_distance == 0.f && !cparams.lossy_palette &&
cparams.speed_tier < SpeedTier::kCheetah &&
!(cparams.responsive && cparams.decoding_speed_tier >= 1)) {
// single channel palette (like FLIF's ChannelCompact)
size_t nb_channels = gi.channel.size() - gi.nb_meta_channels;
@ -1348,8 +1375,9 @@ Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect,
// lossless and no specific color transform specified: try Nothing, YCoCg,
// and 17 RCTs
if (cparams.color_transform == ColorTransform::kNone && quality == 100 &&
cparams.colorspace < 0 && gi.channel.size() - gi.nb_meta_channels >= 3 &&
if (cparams.color_transform == ColorTransform::kNone &&
cparams.IsLossless() && cparams.colorspace < 0 &&
gi.channel.size() - gi.nb_meta_channels >= 3 &&
cparams.responsive == false && do_color &&
cparams.speed_tier <= SpeedTier::kHare) {
Transform sg(TransformId::kRCT);

View File

@ -80,8 +80,7 @@ class ModularFrameEncoder {
std::vector<uint8_t> context_map;
FrameDimensions frame_dim;
CompressParams cparams;
float quality = cparams.quality_pair.first;
float cquality = cparams.quality_pair.second;
float quality = 100.f;
std::vector<size_t> tree_splits;
std::vector<ModularMultiplierInfo> multiplier_info;
std::vector<std::vector<uint32_t>> gi_channel;

View File

@ -127,6 +127,7 @@ struct CompressParams {
float max_error[3] = {0.0, 0.0, 0.0};
SpeedTier speed_tier = SpeedTier::kSquirrel;
int brotli_effort = -1;
// 0 = default.
// 1 = slightly worse quality.
@ -218,8 +219,6 @@ struct CompressParams {
int responsive = -1;
// empty for default squeeze
std::vector<SqueezeParams> squeezes;
// A pair of <quality, cquality>.
std::pair<float, float> quality_pair{100.f, 100.f};
int colorspace = -1;
// Use Global channel palette if #colors < this percentage of range
float channel_colors_pre_transform_percent = 95.f;
@ -230,16 +229,16 @@ struct CompressParams {
// Returns whether these params are lossless as defined by SetLossless();
bool IsLossless() const {
return modular_mode && quality_pair.first == 100 &&
quality_pair.second == 100 &&
color_transform == jxl::ColorTransform::kNone;
// YCbCr is also considered lossless here since it's intended for
// source material that is already YCbCr (we don't do the fwd transform)
return modular_mode && butteraugli_distance == 0.0f &&
color_transform != jxl::ColorTransform::kXYB;
}
// Sets the parameters required to make the codec lossless.
void SetLossless() {
modular_mode = true;
quality_pair.first = 100;
quality_pair.second = 100;
butteraugli_distance = 0.0f;
color_transform = jxl::ColorTransform::kNone;
}
@ -255,6 +254,10 @@ struct CompressParams {
// Skip the downsampling before encoding if this is true.
bool already_downsampled = false;
// Codestream level to conform to.
// -1: don't care
int level = -1;
std::vector<float> manual_noise;
std::vector<float> manual_xyb_factors;
};

View File

@ -710,11 +710,6 @@ void FindBestPatchDictionary(const Image3F& opsin,
CompressParams cparams = state->cparams;
// Recursive application of patches could create very weird issues.
cparams.patches = Override::kOff;
// TODO(veluca): possibly change heuristics here.
if (!cparams.modular_mode) {
cparams.quality_pair.first = cparams.quality_pair.second =
90.f - cparams.butteraugli_distance * 5.f;
}
RoundtripPatchFrame(&reference_frame, state, 0, cparams, cms, pool, true);

View File

@ -146,6 +146,10 @@ int VerifyLevelSettings(const JxlEncoder* enc, std::string* debug_string) {
// Level 5 checks
if (!m.modular_16_bit_buffer_sufficient) {
if (debug_string) *debug_string = "Too high modular bit depth";
return 10;
}
if (xsize > (1ull << 18ull) || ysize > (1ull << 18ull) ||
xsize * ysize > (1ull << 28ull)) {
if (debug_string) *debug_string = "Too large image dimensions";
@ -248,6 +252,12 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() {
// TODO(lode): preview should be added here if a preview image is added
writer.ZeroPadToByte();
// Not actually the end of frame, but the end of metadata/ICC, but helps
// the next frame to start here for indexing purposes.
codestream_bytes_written_end_of_frame +=
jxl::DivCeil(writer.BitsWritten(), 8);
bytes = std::move(writer).TakeBytes();
if (MustUseContainer()) {
@ -363,9 +373,10 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() {
input_frame->option_values.header.layer_info.blend_info.alpha;
frame_info.extra_channel_blending_info.resize(
metadata.m.num_extra_channels);
// If extra channel blend info has not been set, use the default values.
JxlBlendInfo default_blend_info;
JxlEncoderInitBlendInfo(&default_blend_info);
// If extra channel blend info has not been set, use the blend mode from the
// layer_info.
JxlBlendInfo default_blend_info =
input_frame->option_values.header.layer_info.blend_info;
for (size_t i = 0; i < metadata.m.num_extra_channels; ++i) {
auto& to = frame_info.extra_channel_blending_info[i];
const auto& from =
@ -375,23 +386,28 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() {
to.mode = static_cast<jxl::BlendMode>(from.blendmode);
to.source = from.source;
to.alpha_channel = from.alpha;
to.clamp = (from.clamp != 0);
}
if (input_frame->option_values.header.layer_info.have_crop) {
ib.origin.x0 = input_frame->option_values.header.layer_info.crop_x0;
ib.origin.y0 = input_frame->option_values.header.layer_info.crop_y0;
}
JXL_ASSERT(writer.BitsWritten() == 0);
if (!jxl::EncodeFrame(input_frame->option_values.cparams, frame_info,
&metadata, input_frame->frame, &enc_state, cms,
thread_pool.get(), &writer,
/*aux_out=*/nullptr)) {
return JXL_API_ERROR("Failed to encode frame");
}
codestream_bytes_written_beginning_of_frame =
codestream_bytes_written_end_of_frame;
codestream_bytes_written_end_of_frame +=
jxl::DivCeil(writer.BitsWritten(), 8);
// Possibly bytes already contains the codestream header: in case this is
// the first frame, and the codestream header was not encoded as jxlp above.
bytes.append(std::move(writer).TakeBytes());
if (MustUseContainer()) {
if (last_frame && jxlp_counter == 0) {
// If this is the last frame and no jxlp boxes were used yet, it's
@ -423,9 +439,10 @@ JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() {
for (size_t i = 0; i < 4; i++) {
compressed[i] = static_cast<uint8_t>(box->type[i]);
}
if (JXL_ENC_SUCCESS != BrotliCompress(9, box->contents.data(),
box->contents.size(),
&compressed)) {
if (JXL_ENC_SUCCESS !=
BrotliCompress((brotli_effort >= 0 ? brotli_effort : 4),
box->contents.data(), box->contents.size(),
&compressed)) {
return JXL_API_ERROR("Brotli compression for brob box failed");
}
jxl::AppendBoxHeader(jxl::MakeBoxType("brob"), compressed.size(), false,
@ -561,7 +578,8 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc,
enc->metadata.m.bit_depth.floating_point_sample =
(info->exponent_bits_per_sample != 0u);
enc->metadata.m.modular_16_bit_buffer_sufficient =
(info->exponent_bits_per_sample == 0u) && info->bits_per_sample <= 12;
(!info->uses_original_profile || info->bits_per_sample <= 12) &&
info->alpha_bits <= 12;
// The number of extra channels includes the alpha channel, so for example and
// RGBA with no other extra channels, has exactly num_extra_channels == 1
@ -620,7 +638,15 @@ JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc,
enc->metadata.m.animation.num_loops = info->animation.num_loops;
enc->metadata.m.animation.have_timecodes = info->animation.have_timecodes;
}
std::string level_message;
int required_level = VerifyLevelSettings(enc, &level_message);
if (required_level == -1 ||
static_cast<int>(enc->codestream_level) < required_level) {
return JXL_API_ERROR("%s", ("Codestream level verification for level " +
std::to_string(enc->codestream_level) +
" failed: " + level_message)
.c_str());
}
return JXL_ENC_SUCCESS;
}
@ -652,6 +678,8 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelInfo(
jxl::ExtraChannelInfo& channel = enc->metadata.m.extra_channel_info[index];
channel.type = static_cast<jxl::ExtraChannel>(info->type);
channel.bit_depth.bits_per_sample = info->bits_per_sample;
enc->metadata.m.modular_16_bit_buffer_sufficient &=
info->bits_per_sample <= 12;
channel.bit_depth.exponent_bits_per_sample = info->exponent_bits_per_sample;
channel.bit_depth.floating_point_sample = info->exponent_bits_per_sample != 0;
channel.dim_shift = info->dim_shift;
@ -662,6 +690,15 @@ JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelInfo(
channel.spot_color[1] = info->spot_color[1];
channel.spot_color[2] = info->spot_color[2];
channel.spot_color[3] = info->spot_color[3];
std::string level_message;
int required_level = VerifyLevelSettings(enc, &level_message);
if (required_level == -1 ||
static_cast<int>(enc->codestream_level) < required_level) {
return JXL_API_ERROR("%s", ("Codestream level verification for level " +
std::to_string(enc->codestream_level) +
" failed: " + level_message)
.c_str());
}
return JXL_ENC_SUCCESS;
}
@ -688,6 +725,7 @@ JxlEncoderFrameSettings* JxlEncoderFrameSettingsCreate(
} else {
opts->values.lossless = false;
}
opts->values.cparams.level = enc->codestream_level;
JxlEncoderFrameSettings* ret = opts.get();
enc->encoder_options.emplace_back(std::move(opts));
return ret;
@ -701,6 +739,10 @@ JxlEncoderFrameSettings* JxlEncoderOptionsCreate(
JxlEncoderStatus JxlEncoderSetFrameLossless(
JxlEncoderFrameSettings* frame_settings, const JXL_BOOL lossless) {
if (lossless && frame_settings->enc->basic_info_set &&
frame_settings->enc->metadata.m.xyb_encoded) {
return JXL_API_ERROR("Set use_original_profile=true for lossless encoding");
}
frame_settings->values.lossless = lossless;
return JXL_ENC_SUCCESS;
}
@ -726,29 +768,6 @@ JxlEncoderStatus JxlEncoderSetFrameDistance(
distance = 0.01f;
}
frame_settings->values.cparams.butteraugli_distance = distance;
float jpeg_quality;
// Formula to translate butteraugli distance roughly into JPEG 0-100 quality.
// This is the inverse of the formula in cjxl.cc to translate JPEG quality
// into butteraugli distance.
if (distance > 6.56f) {
jpeg_quality = -5.456783f * std::log(0.0256f * distance - 0.16384f);
} else {
jpeg_quality = -11.11111f * distance + 101.11111f;
}
// Translate JPEG quality into the quality_pair setting for modular encoding.
// This is the formula also used in cjxl.cc to convert the command line JPEG
// quality parameter to the quality_pair setting.
// TODO(lode): combine the distance -> quality_pair conversion into a single
// formula, possibly altering it to a more suitable heuristic.
float quality;
if (jpeg_quality < 7.f) {
quality = std::min<float>(35.f + (jpeg_quality - 7.f) * 3.0f, 100.0f);
} else {
quality =
std::min<float>(35.f + (jpeg_quality - 7.f) * 65.f / 93.f, 100.0f);
}
frame_settings->values.cparams.quality_pair.first =
frame_settings->values.cparams.quality_pair.second = quality;
return JXL_ENC_SUCCESS;
}
@ -775,6 +794,15 @@ JxlEncoderStatus JxlEncoderFrameSettingsSetOption(
frame_settings->values.cparams.speed_tier =
static_cast<jxl::SpeedTier>(10 - value);
return JXL_ENC_SUCCESS;
case JXL_ENC_FRAME_SETTING_BROTLI_EFFORT:
if (value < -1 || value > 11) {
return JXL_ENC_ERROR;
}
// set cparams for brotli use in JPEG frames
frame_settings->values.cparams.brotli_effort = value;
// set enc option for brotli use in brob boxes
frame_settings->enc->brotli_effort = value;
return JXL_ENC_SUCCESS;
case JXL_ENC_FRAME_SETTING_DECODING_SPEED:
if (value < 0 || value > 4) {
return JXL_ENC_ERROR;
@ -1008,7 +1036,8 @@ void JxlEncoderReset(JxlEncoder* enc) {
enc->num_queued_boxes = 0;
enc->encoder_options.clear();
enc->output_byte_queue.clear();
enc->output_bytes_flushed = 0;
enc->codestream_bytes_written_beginning_of_frame = 0;
enc->codestream_bytes_written_end_of_frame = 0;
enc->wrote_bytes = false;
enc->jxlp_counter = 0;
enc->metadata = jxl::CodecMetadata();
@ -1025,9 +1054,10 @@ void JxlEncoderReset(JxlEncoder* enc) {
void JxlEncoderDestroy(JxlEncoder* enc) {
if (enc) {
JxlMemoryManager local_memory_manager = enc->memory_manager;
// Call destructor directly since custom free function is used.
enc->~JxlEncoder();
jxl::MemoryManagerFree(&enc->memory_manager, enc);
jxl::MemoryManagerFree(&local_memory_manager, enc);
}
}
@ -1062,7 +1092,10 @@ int JxlEncoderGetRequiredCodestreamLevel(const JxlEncoder* enc) {
return VerifyLevelSettings(enc, nullptr);
}
void JxlEncoderSetCms(JxlEncoder* enc, JxlCmsInterface cms) { enc->cms = cms; }
void JxlEncoderSetCms(JxlEncoder* enc, JxlCmsInterface cms) {
jxl::msan::MemoryIsInitialized(&cms, sizeof(cms));
enc->cms = cms;
}
JxlEncoderStatus JxlEncoderSetParallelRunner(JxlEncoder* enc,
JxlParallelRunner parallel_runner,
@ -1138,7 +1171,8 @@ JxlEncoderStatus JxlEncoderAddJPEGFrame(
if (frame_settings->enc->store_jpeg_metadata) {
jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data;
jxl::PaddedBytes jpeg_data;
if (!jxl::jpeg::EncodeJPEGData(data_in, &jpeg_data)) {
if (!jxl::jpeg::EncodeJPEGData(data_in, &jpeg_data,
frame_settings->values.cparams)) {
return JXL_ENC_ERROR;
}
frame_settings->enc->jpeg_metadata = std::vector<uint8_t>(
@ -1247,15 +1281,31 @@ JxlEncoderStatus JxlEncoderAddImageFrame(
queued_frame->ec_initialized.push_back(0);
}
}
queued_frame->frame.origin.x0 =
frame_settings->values.header.layer_info.crop_x0;
queued_frame->frame.origin.y0 =
frame_settings->values.header.layer_info.crop_y0;
queued_frame->frame.use_for_next_frame =
(frame_settings->values.header.layer_info.save_as_reference != 0u);
queued_frame->frame.blendmode =
frame_settings->values.header.layer_info.blend_info.blendmode ==
JXL_BLEND_REPLACE
? jxl::BlendMode::kReplace
: jxl::BlendMode::kBlend;
queued_frame->frame.blend =
frame_settings->values.header.layer_info.blend_info.source > 0;
if (!jxl::BufferToImageBundle(*pixel_format, xsize, ysize, buffer, size,
frame_settings->enc->thread_pool.get(),
c_current, &(queued_frame->frame))) {
return JXL_ENC_ERROR;
}
if (frame_settings->values.lossless) {
queued_frame->option_values.cparams.SetLossless();
if (frame_settings->values.lossless &&
frame_settings->enc->metadata.m.xyb_encoded) {
return JXL_API_ERROR("Set use_original_profile=true for lossless encoding");
}
queued_frame->option_values.cparams.level =
frame_settings->enc->codestream_level;
QueueFrame(frame_settings, queued_frame);
return JXL_ENC_SUCCESS;
@ -1350,7 +1400,6 @@ JxlEncoderStatus JxlEncoderProcessOutput(JxlEncoder* enc, uint8_t** next_out,
std::copy_n(enc->output_byte_queue.begin(), to_copy, *next_out);
*next_out += to_copy;
*avail_out -= to_copy;
enc->output_bytes_flushed += to_copy;
enc->output_byte_queue.erase(enc->output_byte_queue.begin(),
enc->output_byte_queue.begin() + to_copy);
} else if (!enc->input_queue.empty()) {
@ -1406,7 +1455,7 @@ JxlEncoderStatus JxlEncoderSetExtraChannelBlendInfo(
JxlEncoderStatus JxlEncoderSetFrameName(JxlEncoderFrameSettings* frame_settings,
const char* frame_name) {
std::string str = frame_name;
std::string str = frame_name ? frame_name : "";
if (str.size() > 1071) {
return JXL_API_ERROR("frame name can be max 1071 bytes long");
}

View File

@ -117,12 +117,15 @@ struct JxlEncoderStruct {
size_t num_queued_boxes;
std::vector<jxl::JxlEncoderQueuedInput> input_queue;
std::deque<uint8_t> output_byte_queue;
size_t output_bytes_flushed;
// Get the current write position in the stream (for indexing use).
size_t BytePosition() const {
return output_bytes_flushed + output_byte_queue.size();
}
// How many codestream bytes have been written, i.e.,
// content of jxlc and jxlp boxes. Frame index box jxli
// requires position indices to point to codestream bytes,
// so we need to keep track of the total of flushed or queue
// codestream bytes. These bytes may be in a single jxlc box
// or accross multiple jxlp boxes.
size_t codestream_bytes_written_beginning_of_frame;
size_t codestream_bytes_written_end_of_frame;
// Force using the container even if not needed
bool use_container;
@ -153,6 +156,7 @@ struct JxlEncoderStruct {
bool basic_info_set;
bool color_encoding_set;
bool intensity_target_set;
int brotli_effort = -1;
// Takes the first frame in the input_queue, encodes it, and appends
// the bytes to the output_byte_queue.

View File

@ -38,6 +38,7 @@ TEST(EncodeTest, AddFrameAfterCloseInputTest) {
basic_info.xsize = xsize;
basic_info.ysize = ysize;
basic_info.uses_original_profile = false;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding,
@ -58,7 +59,7 @@ TEST(EncodeTest, AddJPEGAfterCloseTest) {
JxlEncoderCloseInput(enc.get());
const std::string jpeg_path =
"imagecompression.info/flower_foveon.png.im_q85_420.jpg";
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg";
const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path);
JxlEncoderFrameSettings* frame_settings =
@ -85,6 +86,7 @@ TEST(EncodeTest, AddFrameBeforeColorEncodingTest) {
basic_info.xsize = xsize;
basic_info.ysize = ysize;
basic_info.uses_original_profile = true;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlEncoderFrameSettings* frame_settings =
JxlEncoderFrameSettingsCreate(enc.get(), NULL);
@ -173,6 +175,8 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc,
} else {
basic_info.uses_original_profile = false;
}
// 16-bit alpha means this requires level 10
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc, 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding,
@ -198,7 +202,6 @@ void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc,
}
compressed.resize(next_out - compressed.data());
EXPECT_EQ(JXL_ENC_SUCCESS, process_result);
jxl::DecompressParams dparams;
jxl::CodecInOut decoded_io;
EXPECT_TRUE(jxl::DecodeFile(
@ -630,6 +633,7 @@ TEST(EncodeTest, SingleFrameBoundedJXLCTest) {
basic_info.xsize = xsize;
basic_info.ysize = ysize;
basic_info.uses_original_profile = false;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding,
@ -739,7 +743,7 @@ TEST(EncodeTest, CodestreamLevelTest) {
}
TEST(EncodeTest, CodestreamLevelVerificationTest) {
JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0};
JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0};
JxlBasicInfo basic_info;
jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format);
@ -755,21 +759,20 @@ TEST(EncodeTest, CodestreamLevelVerificationTest) {
// Set an image dimension that is too large for level 5, but fits in level 10
basic_info.xsize = 1ull << 30ull;
EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
EXPECT_EQ(10, JxlEncoderGetRequiredCodestreamLevel(enc.get()));
// Set an image dimension that is too large even for level 10
basic_info.xsize = 1ull << 31ull;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
EXPECT_EQ(-1, JxlEncoderGetRequiredCodestreamLevel(enc.get()));
EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
}
TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) {
const std::string jpeg_path =
"imagecompression.info/flower_foveon.png.im_q85_420.jpg";
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg";
const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path);
JxlEncoderPtr enc = JxlEncoderMake(nullptr);
@ -900,6 +903,7 @@ TEST(EncodeTest, BasicInfoTest) {
basic_info.animation.tps_denominator = 77;
basic_info.animation.num_loops = 10;
basic_info.animation.have_timecodes = JXL_TRUE;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false);
@ -1001,6 +1005,7 @@ TEST(EncodeTest, AnimationHeaderTest) {
basic_info.animation.tps_numerator = 1000;
basic_info.animation.tps_denominator = 1;
basic_info.animation.have_timecodes = JXL_TRUE;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false);
@ -1102,6 +1107,7 @@ TEST(EncodeTest, CroppedFrameTest) {
basic_info.xsize = 100;
basic_info.ysize = 100;
basic_info.uses_original_profile = JXL_TRUE;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false);
@ -1194,6 +1200,7 @@ TEST(EncodeTest, BoxTest) {
basic_info.xsize = xsize;
basic_info.ysize = ysize;
basic_info.uses_original_profile = false;
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10));
EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info));
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding,
@ -1300,7 +1307,7 @@ TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGFrameTest)) {
for (int skip_color_encoding = 0; skip_color_encoding < 2;
skip_color_encoding++) {
const std::string jpeg_path =
"imagecompression.info/flower_foveon_cropped.jpg";
"third_party/imagecompression.info/flower_foveon_cropped.jpg";
const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path);
jxl::CodecInOut orig_io;
ASSERT_TRUE(SetFromBytes(jxl::Span<const uint8_t>(orig), &orig_io,

89
third_party/jpeg-xl/lib/jxl/exif.cc vendored Normal file
View File

@ -0,0 +1,89 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/jxl/exif.h"
namespace jxl {
constexpr uint16_t kExifOrientationTag = 274;
// Checks if a blob looks like Exif, and if so, sets bigendian
// according to the tiff endianness
bool IsExif(const std::vector<uint8_t>& exif, bool* bigendian) {
if (exif.size() < 12) return false; // not enough bytes for a valid exif blob
const uint8_t* t = exif.data();
if (LoadLE32(t) == 0x2A004D4D) {
*bigendian = true;
return true;
} else if (LoadLE32(t) == 0x002A4949) {
*bigendian = false;
return true;
}
return false; // not a valid tiff header
}
// Finds the position of an Exif tag, or 0 if it is not found
size_t FindExifTagPosition(const std::vector<uint8_t>& exif, uint16_t tagname) {
bool bigendian;
if (!IsExif(exif, &bigendian)) return 0;
const uint8_t* t = exif.data() + 4;
uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
if (exif.size() < 12 + offset + 2 || offset < 8) return 0;
t += offset - 4;
uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
while (nb_tags > 0) {
if (t + 12 >= exif.data() + exif.size()) return 0;
uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
if (tag == tagname) return static_cast<size_t>(t - exif.data());
t += 10;
nb_tags--;
}
return 0;
}
// TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value
// "R03"
// TODO (jon): set intrinsic dimensions according to
// https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24
void InterpretExif(const std::vector<uint8_t>& exif, CodecMetadata* metadata) {
bool bigendian;
if (!IsExif(exif, &bigendian)) return;
size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag);
if (o_pos) {
const uint8_t* t = exif.data() + o_pos;
uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t));
t += 4;
uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 4;
if (type == 3 && count == 1 && value >= 1 && value <= 8) {
metadata->m.orientation = value;
}
}
}
void ResetExifOrientation(std::vector<uint8_t>& exif) {
bool bigendian;
if (!IsExif(exif, &bigendian)) return;
size_t o_pos = FindExifTagPosition(exif, kExifOrientationTag);
if (o_pos) {
uint8_t* t = exif.data() + o_pos;
uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t));
t += 2;
uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t));
t += 4;
if (type == 3 && count == 1) {
if (bigendian)
StoreBE16(1, t);
else
StoreLE16(1, t);
}
}
}
} // namespace jxl

27
third_party/jpeg-xl/lib/jxl/exif.h vendored Normal file
View File

@ -0,0 +1,27 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#ifndef LIB_JXL_EXIF_H_
#define LIB_JXL_EXIF_H_
// Basic parsing of Exif (just enough for the render-impacting things
// like orientation)
#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/image_metadata.h"
namespace jxl {
// Parses the Exif data just enough to extract any render-impacting info.
// If the Exif data is invalid or could not be parsed, then it is treated
// as a no-op.
void InterpretExif(const std::vector<uint8_t>& exif, CodecMetadata* metadata);
// Sets the Exif orientation to the identity, to avoid repeated orientation
void ResetExifOrientation(std::vector<uint8_t>& exif);
} // namespace jxl
#endif // LIB_JXL_EXIF_H_

View File

@ -40,7 +40,7 @@ static inline Status VisitNameString(Visitor* JXL_RESTRICT visitor,
name->resize(name_length);
}
for (size_t i = 0; i < name_length; i++) {
uint32_t c = (*name)[i];
uint32_t c = static_cast<uint8_t>((*name)[i]);
JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(8, 0, &c));
(*name)[i] = static_cast<char>(c);
}

View File

@ -22,19 +22,6 @@
namespace jxl {
namespace {
uint64_t DecodeVarInt(const uint8_t* input, size_t inputSize, size_t* pos) {
size_t i;
uint64_t ret = 0;
for (i = 0; *pos + i < inputSize && i < 10; ++i) {
ret |= uint64_t(input[*pos + i] & 127) << uint64_t(7 * i);
// If the next-byte flag is not set, stop
if ((input[*pos + i] & 128) == 0) break;
}
// TODO: Return a decoding error if i == 10.
*pos += i + 1;
return ret;
}
// Shuffles or interleaves bytes, for example with width 2, turns "ABCDabcd"
// into "AaBbCcDc". Transposes a matrix of ceil(size / width) columns and
// width rows. There are size elements, size may be < width * height, if so the

View File

@ -133,47 +133,6 @@ void PlaneBase::Swap(PlaneBase& other) {
std::swap(bytes_, other.bytes_);
}
ImageB ImageFromPacked(const uint8_t* packed, const size_t xsize,
const size_t ysize, const size_t bytes_per_row) {
JXL_ASSERT(bytes_per_row >= xsize);
ImageB image(xsize, ysize);
PROFILER_FUNC;
for (size_t y = 0; y < ysize; ++y) {
uint8_t* const JXL_RESTRICT row = image.Row(y);
const uint8_t* const JXL_RESTRICT packed_row = packed + y * bytes_per_row;
memcpy(row, packed_row, xsize);
}
return image;
}
// Note that using mirroring here gives slightly worse results.
ImageF PadImage(const ImageF& in, const size_t xsize, const size_t ysize) {
JXL_ASSERT(xsize >= in.xsize());
JXL_ASSERT(ysize >= in.ysize());
ImageF out(xsize, ysize);
size_t y = 0;
for (; y < in.ysize(); ++y) {
const float* JXL_RESTRICT row_in = in.ConstRow(y);
float* JXL_RESTRICT row_out = out.Row(y);
memcpy(row_out, row_in, in.xsize() * sizeof(row_in[0]));
const int lastcol = in.xsize() - 1;
const float lastval = row_out[lastcol];
for (size_t x = in.xsize(); x < xsize; ++x) {
row_out[x] = lastval;
}
}
// TODO(janwas): no need to copy if we can 'extend' image: if rows are
// pointers to any memory? Or allocate larger image before IO?
const int lastrow = in.ysize() - 1;
for (; y < ysize; ++y) {
const float* JXL_RESTRICT row_in = out.ConstRow(lastrow);
float* JXL_RESTRICT row_out = out.Row(y);
memcpy(row_out, row_in, xsize * sizeof(row_out[0]));
}
return out;
}
Image3F PadImageMirror(const Image3F& in, const size_t xborder,
const size_t yborder) {
size_t xsize = in.xsize();
@ -216,19 +175,6 @@ Image3F PadImageMirror(const Image3F& in, const size_t xborder,
return out;
}
Image3F PadImageToMultiple(const Image3F& in, const size_t N) {
PROFILER_FUNC;
const size_t xsize_blocks = DivCeil(in.xsize(), N);
const size_t ysize_blocks = DivCeil(in.ysize(), N);
const size_t xsize = N * xsize_blocks;
const size_t ysize = N * ysize_blocks;
ImageF out[3];
for (size_t c = 0; c < 3; ++c) {
out[c] = PadImage(in.Plane(c), xsize, ysize);
}
return Image3F(std::move(out[0]), std::move(out[1]), std::move(out[2]));
}
void PadImageToBlockMultipleInPlace(Image3F* JXL_RESTRICT in) {
PROFILER_FUNC;
const size_t xsize_orig = in->xsize();

View File

@ -194,87 +194,94 @@ class Image3;
// shifting the pointer by x0/y0 allows this to apply to multiple images with
// different resolutions (e.g. color transform and quantization field).
// Can compare using SameSize(rect1, rect2).
class Rect {
template <typename T>
class RectT {
public:
// Most windows are xsize_max * ysize_max, except those on the borders where
// begin + size_max > end.
constexpr Rect(size_t xbegin, size_t ybegin, size_t xsize_max,
size_t ysize_max, size_t xend, size_t yend)
constexpr RectT(T xbegin, T ybegin, size_t xsize_max, size_t ysize_max,
T xend, T yend)
: x0_(xbegin),
y0_(ybegin),
xsize_(ClampedSize(xbegin, xsize_max, xend)),
ysize_(ClampedSize(ybegin, ysize_max, yend)) {}
// Construct with origin and known size (typically from another Rect).
constexpr Rect(size_t xbegin, size_t ybegin, size_t xsize, size_t ysize)
constexpr RectT(T xbegin, T ybegin, size_t xsize, size_t ysize)
: x0_(xbegin), y0_(ybegin), xsize_(xsize), ysize_(ysize) {}
// Construct a rect that covers a whole image/plane/ImageBundle etc.
template <typename Image>
explicit Rect(const Image& image)
: Rect(0, 0, image.xsize(), image.ysize()) {}
template <typename ImageT>
explicit RectT(const ImageT& image)
: RectT(0, 0, image.xsize(), image.ysize()) {}
Rect() : Rect(0, 0, 0, 0) {}
RectT() : RectT(0, 0, 0, 0) {}
Rect(const Rect&) = default;
Rect& operator=(const Rect&) = default;
RectT(const RectT&) = default;
RectT& operator=(const RectT&) = default;
// Construct a subrect that resides in an image/plane/ImageBundle etc.
template <typename Image>
Rect Crop(const Image& image) const {
return Rect(x0_, y0_, xsize_, ysize_, image.xsize(), image.ysize());
template <typename ImageT>
RectT Crop(const ImageT& image) const {
return Intersection(RectT(image));
}
// Construct a subrect that resides in the [0, ysize) x [0, xsize) region of
// the current rect.
Rect Crop(size_t area_xsize, size_t area_ysize) const {
return Rect(x0_, y0_, xsize_, ysize_, area_xsize, area_ysize);
RectT Crop(size_t area_xsize, size_t area_ysize) const {
return Intersection(RectT(0, 0, area_xsize, area_ysize));
}
// Returns a rect that only contains `num` lines with offset `y` from `y0()`.
Rect Lines(size_t y, size_t num) const {
RectT Lines(size_t y, size_t num) const {
JXL_DASSERT(y + num <= ysize_);
return Rect(x0_, y0_ + y, xsize_, num);
return RectT(x0_, y0_ + y, xsize_, num);
}
Rect Line(size_t y) const { return Lines(y, 1); }
RectT Line(size_t y) const { return Lines(y, 1); }
JXL_MUST_USE_RESULT Rect Intersection(const Rect& other) const {
return Rect(std::max(x0_, other.x0_), std::max(y0_, other.y0_), xsize_,
ysize_, std::min(x0_ + xsize_, other.x0_ + other.xsize_),
std::min(y0_ + ysize_, other.y0_ + other.ysize_));
JXL_MUST_USE_RESULT RectT Intersection(const RectT& other) const {
return RectT(std::max(x0_, other.x0_), std::max(y0_, other.y0_), xsize_,
ysize_, std::min(x0_ + xsize_, other.x0_ + other.xsize_),
std::min(y0_ + ysize_, other.y0_ + other.ysize_));
}
JXL_MUST_USE_RESULT Rect Translate(int64_t x_offset, int64_t y_offset) const {
return Rect(x0_ + x_offset, y0_ + y_offset, xsize_, ysize_);
JXL_MUST_USE_RESULT RectT Translate(int64_t x_offset,
int64_t y_offset) const {
return RectT(x0_ + x_offset, y0_ + y_offset, xsize_, ysize_);
}
template <typename T>
T* Row(Plane<T>* image, size_t y) const {
template <typename V>
V* Row(Plane<V>* image, size_t y) const {
JXL_DASSERT(y + y0_ >= 0);
return image->Row(y + y0_) + x0_;
}
template <typename T>
const T* Row(const Plane<T>* image, size_t y) const {
template <typename V>
const V* Row(const Plane<V>* image, size_t y) const {
JXL_DASSERT(y + y0_ >= 0);
return image->Row(y + y0_) + x0_;
}
template <typename T>
T* PlaneRow(Image3<T>* image, const size_t c, size_t y) const {
template <typename V>
V* PlaneRow(Image3<V>* image, const size_t c, size_t y) const {
JXL_DASSERT(y + y0_ >= 0);
return image->PlaneRow(c, y + y0_) + x0_;
}
template <typename T>
const T* ConstRow(const Plane<T>& image, size_t y) const {
template <typename V>
const V* ConstRow(const Plane<V>& image, size_t y) const {
JXL_DASSERT(y + y0_ >= 0);
return image.ConstRow(y + y0_) + x0_;
}
template <typename T>
const T* ConstPlaneRow(const Image3<T>& image, size_t c, size_t y) const {
template <typename V>
const V* ConstPlaneRow(const Image3<V>& image, size_t c, size_t y) const {
JXL_DASSERT(y + y0_ >= 0);
return image.ConstPlaneRow(c, y + y0_) + x0_;
}
bool IsInside(const Rect& other) const {
bool IsInside(const RectT& other) const {
return x0_ >= other.x0() && x0_ + xsize_ <= other.x0() + other.xsize_ &&
y0_ >= other.y0() && y0_ + ysize_ <= other.y0() + other.ysize();
}
@ -283,29 +290,33 @@ class Rect {
// Plane<T> or Image3<T>; however if ImageT is Rect, results are nonsensical.
template <class ImageT>
bool IsInside(const ImageT& image) const {
return (x0_ + xsize_ <= image.xsize()) && (y0_ + ysize_ <= image.ysize());
return IsInside(RectT(image));
}
size_t x0() const { return x0_; }
size_t y0() const { return y0_; }
T x0() const { return x0_; }
T y0() const { return y0_; }
size_t xsize() const { return xsize_; }
size_t ysize() const { return ysize_; }
T x1() const { return x0_ + xsize_; }
T y1() const { return y0_ + ysize_; }
private:
// Returns size_max, or whatever is left in [begin, end).
static constexpr size_t ClampedSize(size_t begin, size_t size_max,
size_t end) {
return (begin + size_max <= end) ? size_max
: (end > begin ? end - begin : 0);
static constexpr size_t ClampedSize(T begin, size_t size_max, T end) {
return (static_cast<T>(begin + size_max) <= end)
? size_max
: (end > begin ? end - begin : 0);
}
size_t x0_;
size_t y0_;
T x0_;
T y0_;
size_t xsize_;
size_t ysize_;
};
using Rect = RectT<size_t>;
// Currently, we abuse Image to either refer to an image that owns its storage
// or one that doesn't. In similar vein, we abuse Image* function parameters to
// either mean "assign to me" or "fill the provided image with data".

View File

@ -789,19 +789,10 @@ void ZeroFillPlane(Plane<T>* image, Rect rect) {
}
}
// First, image is padded horizontally, with the rightmost value.
// Next, image is padded vertically, by repeating the last line.
ImageF PadImage(const ImageF& in, size_t xsize, size_t ysize);
// Pad an image with xborder columns on each vertical side and yboder rows
// above and below, mirroring the image.
Image3F PadImageMirror(const Image3F& in, size_t xborder, size_t yborder);
// First, image is padded horizontally, with the rightmost value.
// Next, image is padded vertically, by repeating the last line.
// Prefer PadImageToBlockMultipleInPlace if padding to kBlockDim.
Image3F PadImageToMultiple(const Image3F& in, size_t N);
// Same as above, but operates in-place. Assumes that the `in` image was
// allocated large enough.
void PadImageToBlockMultipleInPlace(Image3F* JXL_RESTRICT in);

View File

@ -230,7 +230,8 @@ Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
return color_encoding->SetICC(std::move(icc_profile));
}
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes) {
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes,
const CompressParams& cparams) {
jpeg_data.app_marker_type.resize(jpeg_data.app_data.size(),
AppMarkerType::kUnknown);
JXL_RETURN_IF_ERROR(DetectIccProfile(jpeg_data));
@ -241,7 +242,9 @@ Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes) {
*bytes = std::move(writer).TakeBytes();
BrotliEncoderState* brotli_enc =
BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, 11);
int effort = cparams.brotli_effort;
if (effort < 0) effort = 11 - static_cast<int>(cparams.speed_tier);
BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, effort);
size_t total_data = 0;
for (size_t i = 0; i < jpeg_data.app_data.size(); i++) {
if (jpeg_data.app_marker_type[i] != AppMarkerType::kUnknown) {

View File

@ -8,11 +8,13 @@
#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/enc_params.h"
#include "lib/jxl/jpeg/jpeg_data.h"
namespace jxl {
namespace jpeg {
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes);
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes,
const CompressParams& cparams);
Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
ColorEncoding* color_encoding);

View File

@ -95,6 +95,7 @@ bool ProcessSOF(const uint8_t* data, const size_t len, JpegReadMode mode,
int height = ReadUint16(data, pos);
int width = ReadUint16(data, pos);
int num_components = ReadUint8(data, pos);
// 'jbrd' is hardcoded for 8bits:
JXL_JPEG_VERIFY_INPUT(precision, 8, 8, PRECISION);
JXL_JPEG_VERIFY_INPUT(height, 1, kMaxDimPixels, HEIGHT);
JXL_JPEG_VERIFY_INPUT(width, 1, kMaxDimPixels, WIDTH);

View File

@ -124,7 +124,7 @@ TEST(JxlTest, RoundtripMarker) {
TEST(JxlTest, RoundtripTinyFast) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(32, 32);
@ -142,7 +142,7 @@ TEST(JxlTest, RoundtripTinyFast) {
TEST(JxlTest, RoundtripSmallD1) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CompressParams cparams;
cparams.butteraugli_distance = 1.0;
DecompressParams dparams;
@ -183,7 +183,7 @@ TEST(JxlTest, RoundtripSmallD1) {
TEST(JxlTest, RoundtripOtherTransforms) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/64px/a2d1un_nkitzmiller_srgb8.png");
ReadTestData("third_party/wesaturate/64px/a2d1un_nkitzmiller_srgb8.png");
std::unique_ptr<CodecInOut> io = jxl::make_unique<CodecInOut>();
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), io.get(), pool));
@ -215,7 +215,7 @@ TEST(JxlTest, RoundtripOtherTransforms) {
TEST(JxlTest, RoundtripResample2) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize(), io.ysize());
@ -232,7 +232,7 @@ TEST(JxlTest, RoundtripResample2) {
TEST(JxlTest, RoundtripResample2MT) {
ThreadPoolInternal pool(4);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
// image has to be large enough to have multiple groups after downsampling
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -254,7 +254,7 @@ TEST(JxlTest, RoundtripOutOfOrderProcessing) {
FakeParallelRunner fake_pool(/*order_seed=*/123, /*num_threads=*/8);
ThreadPool pool(&JxlFakeParallelRunner, &fake_pool);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
// Image size is selected so that the block border needed is larger than the
@ -277,7 +277,7 @@ TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) {
FakeParallelRunner fake_pool(/*order_seed=*/47, /*num_threads=*/8);
ThreadPool pool(&JxlFakeParallelRunner, &fake_pool);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
// Image size is selected so that the block border needed is larger than the
@ -300,7 +300,7 @@ TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) {
TEST(JxlTest, RoundtripResample4) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize(), io.ysize());
@ -317,7 +317,7 @@ TEST(JxlTest, RoundtripResample4) {
TEST(JxlTest, RoundtripResample8) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize(), io.ysize());
@ -334,7 +334,7 @@ TEST(JxlTest, RoundtripResample8) {
TEST(JxlTest, RoundtripUnalignedD2) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 12, io.ysize() / 7);
@ -355,7 +355,7 @@ TEST(JxlTest, RoundtripUnalignedD2) {
TEST(JxlTest, RoundtripMultiGroupNL) {
ThreadPoolInternal pool(4);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
io.ShrinkTo(600, 1024); // partial X, full Y group
@ -384,7 +384,7 @@ TEST(JxlTest, RoundtripMultiGroupNL) {
TEST(JxlTest, RoundtripMultiGroup) {
ThreadPoolInternal pool(4);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
io.ShrinkTo(600, 1024);
@ -410,7 +410,7 @@ TEST(JxlTest, RoundtripMultiGroup) {
TEST(JxlTest, RoundtripLargeFast) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -425,7 +425,7 @@ TEST(JxlTest, RoundtripLargeFast) {
TEST(JxlTest, RoundtripDotsForceEpf) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -445,7 +445,7 @@ TEST(JxlTest, RoundtripDotsForceEpf) {
TEST(JxlTest, RoundtripD2Consistent) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -478,7 +478,7 @@ TEST(JxlTest, RoundtripD2Consistent) {
TEST(JxlTest, RoundtripLargeConsistent) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -508,7 +508,7 @@ TEST(JxlTest, RoundtripLargeConsistent) {
TEST(JxlTest, RoundtripSmallNL) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
@ -529,7 +529,7 @@ TEST(JxlTest, RoundtripSmallNL) {
TEST(JxlTest, RoundtripNoGaborishNoAR) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -549,7 +549,7 @@ TEST(JxlTest, RoundtripNoGaborishNoAR) {
TEST(JxlTest, RoundtripSmallNoGaborish) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
@ -595,7 +595,7 @@ TEST(JxlTest, RoundtripSmallPatchesAlpha) {
EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u);
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(0.2f));
IsSlightlyBelow(0.04f));
}
TEST(JxlTest, RoundtripSmallPatches) {
@ -623,7 +623,7 @@ TEST(JxlTest, RoundtripSmallPatches) {
EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u);
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(0.2f));
IsSlightlyBelow(0.04f));
}
// Test header encoding of original bits per sample
@ -700,8 +700,8 @@ TEST(JxlTest, RoundtripImageBundleOriginalBits) {
TEST(JxlTest, RoundtripGrayscale) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
ASSERT_NE(io.xsize(), 0u);
@ -756,8 +756,8 @@ TEST(JxlTest, RoundtripGrayscale) {
TEST(JxlTest, RoundtripAlpha) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -786,13 +786,13 @@ TEST(JxlTest, RoundtripAlpha) {
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(1.4));
IsSlightlyBelow(1.2));
}
TEST(JxlTest, RoundtripAlphaPremultiplied) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io, io_nopremul;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_nopremul, pool));
@ -821,18 +821,18 @@ TEST(JxlTest, RoundtripAlphaPremultiplied) {
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(1.4));
IsSlightlyBelow(1.2));
io2.Main().UnpremultiplyAlpha();
EXPECT_THAT(
ButteraugliDistance(io_nopremul, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(1.8));
IsSlightlyBelow(1.35));
}
TEST(JxlTest, RoundtripAlphaResampling) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -864,8 +864,8 @@ TEST(JxlTest, RoundtripAlphaResampling) {
TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -896,8 +896,8 @@ TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) {
TEST(JxlTest, RoundtripAlphaNonMultipleOf8) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -922,7 +922,7 @@ TEST(JxlTest, RoundtripAlphaNonMultipleOf8) {
CodecInOut io2;
EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
EXPECT_LE(compressed.size(), 200u);
EXPECT_LE(compressed.size(), 180u);
// TODO(robryk): Fix the following line in presence of different alpha_bits in
// the two contexts.
@ -930,7 +930,7 @@ TEST(JxlTest, RoundtripAlphaNonMultipleOf8) {
// TODO(robryk): Fix the distance estimate used in the encoder.
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(0.8));
IsSlightlyBelow(0.9));
}
TEST(JxlTest, RoundtripAlpha16) {
@ -965,8 +965,7 @@ TEST(JxlTest, RoundtripAlpha16) {
CompressParams cparams;
cparams.butteraugli_distance = 0.5;
// Prevent the test to be too slow, does not affect alpha
cparams.speed_tier = SpeedTier::kSquirrel;
cparams.speed_tier = SpeedTier::kWombat;
DecompressParams dparams;
io.metadata.m.SetUintSamples(16);
@ -978,8 +977,9 @@ TEST(JxlTest, RoundtripAlpha16) {
aux_out, &pool));
CodecInOut io2;
EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, &pool));
EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha()));
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, &pool),
IsSlightlyBelow(0.8));
}
namespace {
@ -987,7 +987,7 @@ CompressParams CParamsForLossless() {
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.quality_pair = {100, 100};
cparams.butteraugli_distance = 0.f;
cparams.options.predictor = {Predictor::Weighted};
return cparams;
}
@ -995,8 +995,8 @@ CompressParams CParamsForLossless() {
TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -1023,8 +1023,8 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) {
TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathWP)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -1040,8 +1040,8 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathWP)) {
TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -1058,8 +1058,8 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) {
TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -1079,8 +1079,8 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) {
TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8Falcon)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -1096,8 +1096,8 @@ TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8Falcon)) {
TEST(JxlTest, RoundtripLossless8Alpha) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
EXPECT_EQ(8u, io.metadata.m.GetAlphaBits());
@ -1221,11 +1221,11 @@ TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) {
TEST(JxlTest, RoundtripYCbCr420) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
const PaddedBytes yuv420 =
ReadTestData("imagecompression.info/flower_foveon.png.ffmpeg.y4m");
const PaddedBytes yuv420 = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.ffmpeg.y4m");
CodecInOut io2;
ASSERT_TRUE(test::DecodeImageY4M(Span<const uint8_t>(yuv420), &io2));
@ -1251,7 +1251,7 @@ TEST(JxlTest, RoundtripYCbCr420) {
TEST(JxlTest, RoundtripDots) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -1284,7 +1284,7 @@ TEST(JxlTest, RoundtripDots) {
TEST(JxlTest, RoundtripNoise) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -1315,8 +1315,8 @@ TEST(JxlTest, RoundtripNoise) {
TEST(JxlTest, RoundtripLossless8Gray) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
@ -1448,7 +1448,7 @@ size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) {
enc_container.codestream = std::move(codestream);
jpeg::JPEGData data_in = *io.Main().jpeg_data;
jxl::PaddedBytes jpeg_data;
EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data));
EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data, cparams));
enc_container.jpeg_reconstruction = jpeg_data.data();
enc_container.jpeg_reconstruction_size = jpeg_data.size();
EXPECT_TRUE(EncodeJpegXlContainerOneShot(enc_container, &compressed));
@ -1474,8 +1474,8 @@ size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) {
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_444.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg");
// JPEG size is 326'916 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u);
}
@ -1484,8 +1484,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444)) {
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_444.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg");
CodecInOut io;
ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
@ -1508,8 +1508,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) {
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_420.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
CodecInOut io;
ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
@ -1531,8 +1531,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) {
TEST(JxlTest,
JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420EarlyFlush)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_420.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
CodecInOut io;
ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
@ -1555,8 +1555,8 @@ TEST(JxlTest,
TEST(JxlTest,
JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420Mul16)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon_cropped.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon_cropped.jpg");
CodecInOut io;
ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
@ -1579,7 +1579,8 @@ TEST(JxlTest,
JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels_asymmetric)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig = ReadTestData(
"imagecompression.info/flower_foveon.png.im_q85_asymmetric.jpg");
"third_party/imagecompression.info/"
"flower_foveon.png.im_q85_asymmetric.jpg");
CodecInOut io;
ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
@ -1602,16 +1603,16 @@ TEST(JxlTest,
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionGray)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_gray.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_gray.jpg");
// JPEG size is 167'025 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 140000u);
}
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_420.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
// JPEG size is 226'018 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 181050u);
}
@ -1620,7 +1621,8 @@ TEST(JxlTest,
JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_luma_subsample)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig = ReadTestData(
"imagecompression.info/flower_foveon.png.im_q85_luma_subsample.jpg");
"third_party/imagecompression.info/"
"flower_foveon.png.im_q85_luma_subsample.jpg");
// JPEG size is 216'069 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u);
}
@ -1629,23 +1631,23 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444_12)) {
// 444 JPEG that has an interesting sampling-factor (1x2, 1x2, 1x2).
ThreadPoolInternal pool(8);
const PaddedBytes orig = ReadTestData(
"imagecompression.info/flower_foveon.png.im_q85_444_1x2.jpg");
"third_party/imagecompression.info/flower_foveon.png.im_q85_444_1x2.jpg");
// JPEG size is 329'942 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u);
}
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression422)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_422.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_422.jpg");
// JPEG size is 265'590 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
}
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression440)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png.im_q85_440.jpg");
const PaddedBytes orig = ReadTestData(
"third_party/imagecompression.info/flower_foveon.png.im_q85_440.jpg");
// JPEG size is 262'249 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
}
@ -1655,7 +1657,8 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_asymmetric)) {
// the other.
ThreadPoolInternal pool(8);
const PaddedBytes orig = ReadTestData(
"imagecompression.info/flower_foveon.png.im_q85_asymmetric.jpg");
"third_party/imagecompression.info/"
"flower_foveon.png.im_q85_asymmetric.jpg");
// JPEG size is 262'249 bytes.
EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
}
@ -1663,14 +1666,15 @@ TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_asymmetric)) {
TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420Progr)) {
ThreadPoolInternal pool(8);
const PaddedBytes orig = ReadTestData(
"imagecompression.info/flower_foveon.png.im_q85_420_progr.jpg");
"third_party/imagecompression.info/"
"flower_foveon.png.im_q85_420_progr.jpg");
EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u);
}
TEST(JxlTest, RoundtripProgressive) {
ThreadPoolInternal pool(4);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
io.ShrinkTo(600, 1024);
@ -1686,7 +1690,7 @@ TEST(JxlTest, RoundtripProgressive) {
EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u);
EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, &pool),
IsSlightlyBelow(2.5f));
IsSlightlyBelow(1.1f));
}
} // namespace

View File

@ -304,7 +304,7 @@ class MATreeLookup {
int64_t offset;
int32_t multiplier;
};
LookupResult Lookup(const Properties &properties) const {
JXL_INLINE LookupResult Lookup(const Properties &properties) const {
uint32_t pos = 0;
while (true) {
const FlatDecisionNode &node = nodes_[pos];
@ -416,6 +416,7 @@ enum PredictorMode {
kUseWP = 2,
kForceComputeProperties = 4,
kAllPredictions = 8,
kNoEdgeCases = 16
};
JXL_INLINE pixel_type_w PredictOne(Predictor p, pixel_type_w left,
@ -461,7 +462,7 @@ JXL_INLINE pixel_type_w PredictOne(Predictor p, pixel_type_w left,
}
template <int mode>
inline PredictionResult Predict(
JXL_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,
@ -470,13 +471,15 @@ inline PredictionResult Predict(
size_t offset = 3;
constexpr bool compute_properties =
mode & kUseTree || mode & kForceComputeProperties;
pixel_type_w left = (x ? pp[-1] : (y ? pp[-onerow] : 0));
pixel_type_w top = (y ? pp[-onerow] : left);
pixel_type_w topleft = (x && y ? pp[-1 - onerow] : left);
pixel_type_w topright = (x + 1 < w && y ? pp[1 - onerow] : top);
pixel_type_w leftleft = (x > 1 ? pp[-2] : left);
pixel_type_w toptop = (y > 1 ? pp[-onerow - onerow] : top);
pixel_type_w toprightright = (x + 2 < w && y ? pp[2 - onerow] : topright);
constexpr bool nec = mode & kNoEdgeCases;
pixel_type_w left = (nec || x ? pp[-1] : (y ? pp[-onerow] : 0));
pixel_type_w top = (nec || y ? pp[-onerow] : left);
pixel_type_w topleft = (nec || (x && y) ? pp[-1 - onerow] : left);
pixel_type_w topright = (nec || (x + 1 < w && y) ? pp[1 - onerow] : top);
pixel_type_w leftleft = (nec || x > 1 ? pp[-2] : left);
pixel_type_w toptop = (nec || y > 1 ? pp[-onerow - onerow] : top);
pixel_type_w toprightright =
(nec || (x + 2 < w && y) ? pp[2 - onerow] : topright);
if (compute_properties) {
// location
@ -506,7 +509,7 @@ inline PredictionResult Predict(
wp_pred = wp_state->Predict<compute_properties>(
x, y, w, top, left, topright, topleft, toptop, p, offset);
}
if (compute_properties) {
if (!nec && compute_properties) {
offset += weighted::kNumProperties;
// Extra properties.
const pixel_type *JXL_RESTRICT rp = references->Row(x);
@ -565,6 +568,15 @@ inline PredictionResult PredictTreeNoWP(Properties *p, size_t w,
p, w, pp, onerow, x, y, Predictor::Zero, &tree_lookup, &references,
/*wp_state=*/nullptr, /*predictions=*/nullptr);
}
// Only use for y > 1, x > 1, x < w-2, and empty references
JXL_INLINE PredictionResult
PredictTreeNoWPNEC(Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp,
const intptr_t onerow, const int x, const int y,
const MATreeLookup &tree_lookup, const Channel &references) {
return detail::Predict<detail::kUseTree | detail::kNoEdgeCases>(
p, w, pp, onerow, x, y, Predictor::Zero, &tree_lookup, &references,
/*wp_state=*/nullptr, /*predictions=*/nullptr);
}
inline PredictionResult PredictTreeWP(Properties *p, size_t w,
const pixel_type *JXL_RESTRICT pp,

View File

@ -186,14 +186,23 @@ Status DecodeModularChannelMAANS(BitReader *br, ANSSymbolReader *reader,
pixel_type *JXL_RESTRICT r = channel.Row(y);
std::fill(r, r + channel.w, v);
}
} else {
JXL_DEBUG_V(8, "Fast track.");
for (size_t y = 0; y < channel.h; y++) {
pixel_type *JXL_RESTRICT r = channel.Row(y);
for (size_t x = 0; x < channel.w; x++) {
uint32_t v = reader->ReadHybridUintClustered(ctx_id, br);
r[x] = make_pixel(v, multiplier, offset);
if (multiplier == 1 && offset == 0) {
for (size_t y = 0; y < channel.h; y++) {
pixel_type *JXL_RESTRICT r = channel.Row(y);
for (size_t x = 0; x < channel.w; x++) {
uint32_t v = reader->ReadHybridUintClustered(ctx_id, br);
r[x] = UnpackSigned(v);
}
}
} else {
for (size_t y = 0; y < channel.h; y++) {
pixel_type *JXL_RESTRICT r = channel.Row(y);
for (size_t x = 0; x < channel.w; x++) {
uint32_t v = reader->ReadHybridUintClustered(ctx_id, br);
r[x] = make_pixel(v, multiplier, offset);
}
}
}
}
@ -355,12 +364,36 @@ Status DecodeModularChannelMAANS(BitReader *br, ANSSymbolReader *reader,
pixel_type *JXL_RESTRICT p = channel.Row(y);
PrecomputeReferences(channel, y, *image, chan, &references);
InitPropsRow(&properties, static_props, y);
for (size_t x = 0; x < channel.w; x++) {
PredictionResult res =
PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y,
tree_lookup, references);
uint64_t v = reader->ReadHybridUintClustered(res.context, br);
p[x] = make_pixel(v, res.multiplier, res.guess);
if (y > 1 && channel.w > 8 && references.w == 0) {
for (size_t x = 0; x < 2; x++) {
PredictionResult res =
PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y,
tree_lookup, references);
uint64_t v = reader->ReadHybridUintClustered(res.context, br);
p[x] = make_pixel(v, res.multiplier, res.guess);
}
for (size_t x = 2; x < channel.w - 2; x++) {
PredictionResult res =
PredictTreeNoWPNEC(&properties, channel.w, p + x, onerow, x, y,
tree_lookup, references);
uint64_t v = reader->ReadHybridUintClustered(res.context, br);
p[x] = make_pixel(v, res.multiplier, res.guess);
}
for (size_t x = channel.w - 2; x < channel.w; x++) {
PredictionResult res =
PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y,
tree_lookup, references);
uint64_t v = reader->ReadHybridUintClustered(res.context, br);
p[x] = make_pixel(v, res.multiplier, res.guess);
}
} else {
for (size_t x = 0; x < channel.w; x++) {
PredictionResult res =
PredictTreeNoWP(&properties, channel.w, p + x, onerow, x, y,
tree_lookup, references);
uint64_t v = reader->ReadHybridUintClustered(res.context, br);
p[x] = make_pixel(v, res.multiplier, res.guess);
}
}
}
} else {

View File

@ -550,18 +550,15 @@ Status FwdPalette(Image &input, uint32_t begin_c, uint32_t end_c,
bool lossy, Predictor &predictor,
const weighted::Header &wp_header) {
PaletteIterationData palette_iteration_data;
uint32_t nb = end_c - begin_c + 1;
uint32_t nb_colors_orig = nb_colors;
uint32_t nb_deltas_orig = nb_deltas;
// if no channel palette special case
if ((lossy || nb != 1) && input.bitdepth >= 8) {
// preprocessing pass in case of lossy palette
if (lossy && input.bitdepth >= 8) {
JXL_RETURN_IF_ERROR(FwdPaletteIteration(
input, begin_c, end_c, nb_colors, nb_deltas, ordered, lossy, predictor,
wp_header, palette_iteration_data));
input, begin_c, end_c, nb_colors_orig, nb_deltas_orig, ordered, lossy,
predictor, wp_header, palette_iteration_data));
}
palette_iteration_data.final_run = true;
nb_colors = nb_colors_orig;
nb_deltas = nb_deltas_orig;
return FwdPaletteIteration(input, begin_c, end_c, nb_colors, nb_deltas,
ordered, lossy, predictor, wp_header,
palette_iteration_data);

View File

@ -12,13 +12,80 @@
#include "lib/jxl/common.h"
#include "lib/jxl/modular/modular_image.h"
#include "lib/jxl/modular/transform/transform.h"
#undef HWY_TARGET_INCLUDE
#define HWY_TARGET_INCLUDE "lib/jxl/modular/transform/squeeze.cc"
#include <hwy/foreach_target.h>
#include <hwy/highway.h>
#include "lib/jxl/simd_util-inl.h"
HWY_BEFORE_NAMESPACE();
namespace jxl {
namespace HWY_NAMESPACE {
using hwy::HWY_NAMESPACE::RebindToUnsigned;
using hwy::HWY_NAMESPACE::ShiftLeft;
using hwy::HWY_NAMESPACE::ShiftRight;
#if HWY_TARGET != HWY_SCALAR
JXL_INLINE void FastUnsqueeze(const pixel_type *JXL_RESTRICT p_residual,
const pixel_type *JXL_RESTRICT p_avg,
const pixel_type *JXL_RESTRICT p_navg,
const pixel_type *p_pout,
pixel_type *JXL_RESTRICT p_out,
pixel_type *p_nout) {
const HWY_CAPPED(pixel_type, 8) d;
const RebindToUnsigned<decltype(d)> du;
const size_t N = Lanes(d);
auto onethird = Set(d, 0x55555556);
for (size_t x = 0; x < 8; x += N) {
auto avg = Load(d, p_avg + x);
auto next_avg = Load(d, p_navg + x);
auto top = Load(d, p_pout + x);
// Equivalent to SmoothTendency(top,avg,next_avg), but without branches
auto Ba = top - avg;
auto an = avg - next_avg;
auto nonmono = Ba ^ an;
auto absBa = Abs(Ba);
auto absan = Abs(an);
auto absBn = Abs(top - next_avg);
// Compute a3 = absBa / 3
auto a3e = BitCast(d, ShiftRight<32>(MulEven(absBa, onethird)));
auto a3oi = MulEven(Reverse(d, absBa), onethird);
auto a3o = BitCast(
d, Reverse(hwy::HWY_NAMESPACE::Repartition<pixel_type_w, decltype(d)>(),
a3oi));
auto a3 = OddEven(a3o, a3e);
a3 += absBn + Set(d, 2);
auto absdiff = ShiftRight<2>(a3);
auto skipdiff = Ba != Zero(d);
skipdiff = And(skipdiff, an != Zero(d));
skipdiff = And(skipdiff, nonmono < Zero(d));
auto absBa2 = ShiftLeft<1>(absBa) + (absdiff & Set(d, 1));
absdiff =
IfThenElse(absdiff > absBa2, ShiftLeft<1>(absBa) + Set(d, 1), absdiff);
auto absan2 = ShiftLeft<1>(absan);
absdiff =
IfThenElse(absdiff + (absdiff & Set(d, 1)) > absan2, absan2, absdiff);
auto diff1 = IfThenElse(top < next_avg, Neg(absdiff), absdiff);
auto tendency = IfThenZeroElse(skipdiff, diff1);
auto diff_minus_tendency = Load(d, p_residual + x);
auto diff = diff_minus_tendency + tendency;
auto out = avg + ShiftRight<1>(
diff + BitCast(d, ShiftRight<31>(BitCast(du, diff))));
Store(out, d, p_out + x);
Store(out - diff, d, p_nout + x);
}
}
#endif
Status InvHSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) {
JXL_ASSERT(c < input.channel.size());
JXL_ASSERT(rc < input.channel.size());
const Channel &chin = input.channel[c];
Channel &chin = input.channel[c];
const Channel &chin_residual = input.channel[rc];
// These must be valid since we ran MetaApply already.
JXL_ASSERT(chin.w == DivCeil(chin.w + chin_residual.w, 2));
@ -42,42 +109,84 @@ Status InvHSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) {
input.channel[c] = std::move(chout);
return true;
}
auto unsqueeze_row = [&](size_t y, size_t x0) {
const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y);
const pixel_type *JXL_RESTRICT p_avg = chin.Row(y);
pixel_type *JXL_RESTRICT p_out = chout.Row(y);
for (size_t x = x0; x < chin_residual.w; x++) {
pixel_type diff_minus_tendency = p_residual[x];
pixel_type avg = p_avg[x];
pixel_type next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg);
pixel_type left = (x ? p_out[(x << 1) - 1] : avg);
pixel_type tendency = SmoothTendency(left, avg, next_avg);
pixel_type diff = diff_minus_tendency + tendency;
pixel_type A = avg + (diff / 2);
p_out[(x << 1)] = A;
pixel_type B = A - diff;
p_out[(x << 1) + 1] = B;
}
if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1];
};
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, chin.h, ThreadPool::NoInit,
[&](const uint32_t task, size_t /* thread */) {
const size_t y = task;
const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y);
const pixel_type *JXL_RESTRICT p_avg = chin.Row(y);
pixel_type *JXL_RESTRICT p_out = chout.Row(y);
// somewhat complicated trickery just to be able to SIMD this.
// Horizontal unsqueeze has horizontal data dependencies, so we do
// 8 rows at a time and treat it as a vertical unsqueeze of a
// transposed 8x8 block (or 9x8 for one input).
static constexpr const size_t kRowsPerThread = 8;
const auto unsqueeze_span = [&](const uint32_t task, size_t /* thread */) {
const size_t y0 = task * kRowsPerThread;
const size_t rows = std::min(kRowsPerThread, chin.h - y0);
size_t x = 0;
// special case for x=0 so we don't have to check x>0
pixel_type_w avg = p_avg[0];
pixel_type_w next_avg = (1 < chin.w ? p_avg[1] : avg);
pixel_type_w tendency = SmoothTendency(avg, avg, next_avg);
pixel_type_w diff = p_residual[0] + tendency;
pixel_type_w A =
((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
pixel_type_w B = A - diff;
p_out[0] = A;
p_out[1] = B;
for (size_t x = 1; x < chin_residual.w; x++) {
pixel_type_w diff_minus_tendency = p_residual[x];
pixel_type_w avg = p_avg[x];
pixel_type_w next_avg = (x + 1 < chin.w ? p_avg[x + 1] : avg);
pixel_type_w left = p_out[(x << 1) - 1];
pixel_type_w tendency = SmoothTendency(left, avg, next_avg);
pixel_type_w diff = diff_minus_tendency + tendency;
pixel_type_w A =
((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
p_out[x << 1] = A;
pixel_type_w B = A - diff;
p_out[(x << 1) + 1] = B;
#if HWY_TARGET != HWY_SCALAR
intptr_t onerow_in = chin.plane.PixelsPerRow();
intptr_t onerow_inr = chin_residual.plane.PixelsPerRow();
intptr_t onerow_out = chout.plane.PixelsPerRow();
const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y0);
const pixel_type *JXL_RESTRICT p_avg = chin.Row(y0);
pixel_type *JXL_RESTRICT p_out = chout.Row(y0);
HWY_ALIGN pixel_type b_p_avg[9 * kRowsPerThread];
HWY_ALIGN pixel_type b_p_residual[8 * kRowsPerThread];
HWY_ALIGN pixel_type b_p_out_even[8 * kRowsPerThread];
HWY_ALIGN pixel_type b_p_out_odd[8 * kRowsPerThread];
HWY_ALIGN pixel_type b_p_out_evenT[8 * kRowsPerThread];
HWY_ALIGN pixel_type b_p_out_oddT[8 * kRowsPerThread];
const HWY_CAPPED(pixel_type, 8) d;
const size_t N = Lanes(d);
if (chin_residual.w > 16 && rows == kRowsPerThread) {
for (; x < chin_residual.w - 9; x += 8) {
Transpose8x8Block(p_residual + x, b_p_residual, onerow_inr);
Transpose8x8Block(p_avg + x, b_p_avg, onerow_in);
for (size_t y = 0; y < kRowsPerThread; y++) {
b_p_avg[8 * 8 + y] = p_avg[x + 8 + onerow_in * y];
}
if (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1];
},
"InvHorizontalSqueeze"));
for (size_t i = 0; i < 8; i++) {
FastUnsqueeze(
b_p_residual + 8 * i, b_p_avg + 8 * i, b_p_avg + 8 * (i + 1),
(x + i ? b_p_out_odd + 8 * ((x + i - 1) & 7) : b_p_avg + 8 * i),
b_p_out_even + 8 * i, b_p_out_odd + 8 * i);
}
Transpose8x8Block(b_p_out_even, b_p_out_evenT, 8);
Transpose8x8Block(b_p_out_odd, b_p_out_oddT, 8);
for (size_t y = 0; y < kRowsPerThread; y++) {
for (size_t i = 0; i < kRowsPerThread; i += N) {
auto even = Load(d, b_p_out_evenT + 8 * y + i);
auto odd = Load(d, b_p_out_oddT + 8 * y + i);
StoreInterleaved(d, even, odd,
p_out + ((x + i) << 1) + onerow_out * y);
}
}
}
}
#endif
for (size_t y = 0; y < rows; y++) {
unsqueeze_row(y0 + y, x);
}
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 0, DivCeil(chin.h, kRowsPerThread),
ThreadPool::NoInit, unsqueeze_span,
"InvHorizontalSqueeze"));
input.channel[c] = std::move(chout);
return true;
}
@ -111,42 +220,47 @@ Status InvVSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) {
return true;
}
intptr_t onerow_in = chin.plane.PixelsPerRow();
intptr_t onerow_out = chout.plane.PixelsPerRow();
constexpr int kColsPerThread = 64;
JXL_RETURN_IF_ERROR(RunOnPool(
pool, 0, DivCeil(chin.w, kColsPerThread), ThreadPool::NoInit,
[&](const uint32_t task, size_t /* thread */) {
const size_t x0 = task * kColsPerThread;
const size_t x1 = std::min((size_t)(task + 1) * kColsPerThread, chin.w);
// We only iterate up to std::min(chin_residual.h, chin.h) which is
// always chin_residual.h.
for (size_t y = 0; y < chin_residual.h; y++) {
const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y);
const pixel_type *JXL_RESTRICT p_avg = chin.Row(y);
pixel_type *JXL_RESTRICT p_out = chout.Row(y << 1);
for (size_t x = x0; x < x1; x++) {
pixel_type_w diff_minus_tendency = p_residual[x];
pixel_type_w avg = p_avg[x];
pixel_type_w next_avg = avg;
if (y + 1 < chin.h) next_avg = p_avg[x + onerow_in];
pixel_type_w top =
(y > 0 ? p_out[static_cast<ssize_t>(x) - onerow_out] : avg);
pixel_type_w tendency = SmoothTendency(top, avg, next_avg);
pixel_type_w diff = diff_minus_tendency + tendency;
pixel_type_w out =
((avg * 2) + diff + (diff > 0 ? -(diff & 1) : (diff & 1))) >> 1;
p_out[x] = out;
// If the chin_residual.h == chin.h, the output has an even number
// of rows so the next line is fine. Otherwise, this loop won't
// write to the last output row which is handled separately.
p_out[x + onerow_out] = p_out[x] - diff;
}
}
},
"InvVertSqueeze"));
static constexpr const int kColsPerThread = 64;
const auto unsqueeze_slice = [&](const uint32_t task, size_t /* thread */) {
const size_t x0 = task * kColsPerThread;
const size_t x1 = std::min((size_t)(task + 1) * kColsPerThread, chin.w);
const size_t w = x1 - x0;
// We only iterate up to std::min(chin_residual.h, chin.h) which is
// always chin_residual.h.
for (size_t y = 0; y < chin_residual.h; y++) {
const pixel_type *JXL_RESTRICT p_residual = chin_residual.Row(y) + x0;
const pixel_type *JXL_RESTRICT p_avg = chin.Row(y) + x0;
const pixel_type *JXL_RESTRICT p_navg =
chin.Row(y + 1 < chin.h ? y + 1 : y) + x0;
pixel_type *JXL_RESTRICT p_out = chout.Row(y << 1) + x0;
pixel_type *JXL_RESTRICT p_nout = chout.Row((y << 1) + 1) + x0;
const pixel_type *p_pout = y > 0 ? chout.Row((y << 1) - 1) + x0 : p_avg;
size_t x = 0;
#if HWY_TARGET != HWY_SCALAR
for (; x + 7 < w; x += 8) {
FastUnsqueeze(p_residual + x, p_avg + x, p_navg + x, p_pout + x,
p_out + x, p_nout + x);
}
#endif
for (; x < w; x++) {
pixel_type avg = p_avg[x];
pixel_type next_avg = p_navg[x];
pixel_type top = p_pout[x];
pixel_type tendency = SmoothTendency(top, avg, next_avg);
pixel_type diff_minus_tendency = p_residual[x];
pixel_type diff = diff_minus_tendency + tendency;
pixel_type out = avg + (diff / 2);
p_out[x] = out;
// If the chin_residual.h == chin.h, the output has an even number
// of rows so the next line is fine. Otherwise, this loop won't
// write to the last output row which is handled separately.
p_nout[x] = out - diff;
}
}
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 0, DivCeil(chin.w, kColsPerThread),
ThreadPool::NoInit, unsqueeze_slice,
"InvVertSqueeze"));
if (chout.h & 1) {
size_t y = chin.h - 1;
@ -160,6 +274,62 @@ Status InvVSqueeze(Image &input, uint32_t c, uint32_t rc, ThreadPool *pool) {
return true;
}
Status InvSqueeze(Image &input, std::vector<SqueezeParams> parameters,
ThreadPool *pool) {
for (int i = parameters.size() - 1; i >= 0; i--) {
JXL_RETURN_IF_ERROR(
CheckMetaSqueezeParams(parameters[i], input.channel.size()));
bool horizontal = parameters[i].horizontal;
bool in_place = parameters[i].in_place;
uint32_t beginc = parameters[i].begin_c;
uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1;
uint32_t offset;
if (in_place) {
offset = endc + 1;
} else {
offset = input.channel.size() + beginc - endc - 1;
}
if (beginc < input.nb_meta_channels) {
// This is checked in MetaSqueeze.
JXL_ASSERT(input.nb_meta_channels > parameters[i].num_c);
input.nb_meta_channels -= parameters[i].num_c;
}
for (uint32_t c = beginc; c <= endc; c++) {
uint32_t rc = offset + c - beginc;
// MetaApply should imply that `rc` is within range, otherwise there's a
// programming bug.
JXL_ASSERT(rc < input.channel.size());
if ((input.channel[c].w < input.channel[rc].w) ||
(input.channel[c].h < input.channel[rc].h)) {
return JXL_FAILURE("Corrupted squeeze transform");
}
if (horizontal) {
JXL_RETURN_IF_ERROR(InvHSqueeze(input, c, rc, pool));
} else {
JXL_RETURN_IF_ERROR(InvVSqueeze(input, c, rc, pool));
}
}
input.channel.erase(input.channel.begin() + offset,
input.channel.begin() + offset + (endc - beginc + 1));
}
return true;
}
} // namespace HWY_NAMESPACE
} // namespace jxl
HWY_AFTER_NAMESPACE();
#if HWY_ONCE
namespace jxl {
HWY_EXPORT(InvSqueeze);
Status InvSqueeze(Image &input, std::vector<SqueezeParams> parameters,
ThreadPool *pool) {
return HWY_DYNAMIC_DISPATCH(InvSqueeze)(input, parameters, pool);
}
void DefaultSqueezeParameters(std::vector<SqueezeParams> *parameters,
const Image &image) {
int nb_channels = image.channel.size() - image.nb_meta_channels;
@ -285,46 +455,6 @@ Status MetaSqueeze(Image &image, std::vector<SqueezeParams> *parameters) {
return true;
}
Status InvSqueeze(Image &input, std::vector<SqueezeParams> parameters,
ThreadPool *pool) {
for (int i = parameters.size() - 1; i >= 0; i--) {
JXL_RETURN_IF_ERROR(
CheckMetaSqueezeParams(parameters[i], input.channel.size()));
bool horizontal = parameters[i].horizontal;
bool in_place = parameters[i].in_place;
uint32_t beginc = parameters[i].begin_c;
uint32_t endc = parameters[i].begin_c + parameters[i].num_c - 1;
uint32_t offset;
if (in_place) {
offset = endc + 1;
} else {
offset = input.channel.size() + beginc - endc - 1;
}
if (beginc < input.nb_meta_channels) {
// This is checked in MetaSqueeze.
JXL_ASSERT(input.nb_meta_channels > parameters[i].num_c);
input.nb_meta_channels -= parameters[i].num_c;
}
for (uint32_t c = beginc; c <= endc; c++) {
uint32_t rc = offset + c - beginc;
// MetaApply should imply that `rc` is within range, otherwise there's a
// programming bug.
JXL_ASSERT(rc < input.channel.size());
if ((input.channel[c].w < input.channel[rc].w) ||
(input.channel[c].h < input.channel[rc].h)) {
return JXL_FAILURE("Corrupted squeeze transform");
}
if (horizontal) {
JXL_RETURN_IF_ERROR(InvHSqueeze(input, c, rc, pool));
} else {
JXL_RETURN_IF_ERROR(InvVSqueeze(input, c, rc, pool));
}
}
input.channel.erase(input.channel.begin() + offset,
input.channel.begin() + offset + (endc - beginc + 1));
}
return true;
}
} // namespace jxl
#endif

View File

@ -75,10 +75,6 @@ inline pixel_type_w SmoothTendency(pixel_type_w B, pixel_type_w a,
return diff;
}
void InvHSqueeze(Image &input, int c, int rc, ThreadPool *pool);
void InvVSqueeze(Image &input, int c, int rc, ThreadPool *pool);
void DefaultSqueezeParameters(std::vector<SqueezeParams> *parameters,
const Image &image);

View File

@ -25,6 +25,7 @@
#include "lib/jxl/dec_file.h"
#include "lib/jxl/dec_params.h"
#include "lib/jxl/enc_butteraugli_comparator.h"
#include "lib/jxl/enc_butteraugli_pnorm.h"
#include "lib/jxl/enc_cache.h"
#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_file.h"
@ -45,11 +46,10 @@ using test::Roundtrip;
void TestLosslessGroups(size_t group_size_shift) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.SetLossless();
cparams.modular_group_size_shift = group_size_shift;
cparams.color_transform = jxl::ColorTransform::kNone;
DecompressParams dparams;
CodecInOut io_out;
@ -79,15 +79,14 @@ TEST(ModularTest, JXL_TSAN_SLOW_TEST(RoundtripLosslessGroups1024)) {
TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.SetLossless();
// 9 = permute to GBR, to test the special case of permutation-only
cparams.colorspace = 9;
// slowest speed so different WP modes are tried
cparams.speed_tier = SpeedTier::kTortoise;
cparams.options.predictor = {Predictor::Weighted};
cparams.color_transform = jxl::ColorTransform::kNone;
DecompressParams dparams;
CodecInOut io_out;
@ -107,7 +106,7 @@ TEST(ModularTest, RoundtripLosslessCustomWP_PermuteRCT) {
TEST(ModularTest, RoundtripLossyDeltaPalette) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
@ -133,10 +132,9 @@ TEST(ModularTest, RoundtripLossyDeltaPalette) {
TEST(ModularTest, RoundtripLossyDeltaPaletteWP) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.SetLossless();
cparams.lossy_palette = true;
cparams.palette_colors = 0;
cparams.options.predictor = jxl::Predictor::Weighted;
@ -161,10 +159,10 @@ TEST(ModularTest, RoundtripLossyDeltaPaletteWP) {
TEST(ModularTest, RoundtripLossy) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.quality_pair = {80.0f, 80.0f};
cparams.butteraugli_distance = 2.f;
DecompressParams dparams;
CodecInOut io_out;
@ -174,20 +172,20 @@ TEST(ModularTest, RoundtripLossy) {
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out);
EXPECT_LE(compressed_size, 40000u);
EXPECT_LE(compressed_size, 30000u);
cparams.ba_params.intensity_target = 80.0f;
EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(2.0));
IsSlightlyBelow(2.3));
}
TEST(ModularTest, RoundtripLossy16) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("raw.pixls/DJI-FC6310-16bit_709_v4_krita.png");
ReadTestData("third_party/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png");
CompressParams cparams;
cparams.modular_mode = true;
cparams.quality_pair = {80.0f, 80.0f};
cparams.butteraugli_distance = 2.f;
DecompressParams dparams;
CodecInOut io_out;
@ -199,11 +197,11 @@ TEST(ModularTest, RoundtripLossy16) {
io.metadata.m.color_encoding = ColorEncoding::SRGB();
compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out);
EXPECT_LE(compressed_size, 400u);
EXPECT_LE(compressed_size, 300u);
cparams.ba_params.intensity_target = 80.0f;
EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(),
/*distmap=*/nullptr, pool),
IsSlightlyBelow(1.5));
IsSlightlyBelow(1.6));
}
TEST(ModularTest, RoundtripExtraProperties) {
@ -250,15 +248,15 @@ TEST(ModularTest, RoundtripExtraProperties) {
TEST(ModularTest, RoundtripLosslessCustomSqueeze) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.quality_pair = {100, 100};
cparams.butteraugli_distance = 0.f;
cparams.options.predictor = {Predictor::Zero};
cparams.speed_tier = SpeedTier::kThunder;
cparams.responsive = 1;
@ -281,6 +279,105 @@ TEST(ModularTest, RoundtripLosslessCustomSqueeze) {
/*distmap=*/nullptr, pool));
}
struct RoundtripLosslessConfig {
int bitdepth;
int responsive;
};
class ModularTestParam
: public ::testing::TestWithParam<RoundtripLosslessConfig> {};
std::vector<RoundtripLosslessConfig> GenerateLosslessTests() {
std::vector<RoundtripLosslessConfig> all;
for (int responsive = 0; responsive <= 1; responsive++) {
for (int bitdepth = 1; bitdepth < 32; bitdepth++) {
if (responsive && bitdepth > 30) continue;
all.push_back({bitdepth, responsive});
}
}
return all;
}
std::string LosslessTestDescription(
const testing::TestParamInfo<ModularTestParam::ParamType>& info) {
std::stringstream name;
name << info.param.bitdepth << "bit";
if (info.param.responsive) name << "Squeeze";
return name.str();
}
JXL_GTEST_INSTANTIATE_TEST_SUITE_P(RoundtripLossless, ModularTestParam,
testing::ValuesIn(GenerateLosslessTests()),
LosslessTestDescription);
TEST_P(ModularTestParam, RoundtripLossless) {
RoundtripLosslessConfig config = GetParam();
int bitdepth = config.bitdepth;
int responsive = config.responsive;
ThreadPool* pool = nullptr;
Rng generator(123);
const PaddedBytes orig =
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io1;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io1, pool));
// vary the dimensions a bit, in case of bugs related to
// even vs odd width or height.
size_t xsize = 423 + bitdepth;
size_t ysize = 467 + bitdepth;
CodecInOut io;
io.SetSize(xsize, ysize);
io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false);
io.metadata.m.SetUintSamples(bitdepth);
double factor = ((1lu << bitdepth) - 1lu);
double ifactor = 1.0 / factor;
Image3F noise_added(xsize, ysize);
for (size_t c = 0; c < 3; c++) {
for (size_t y = 0; y < ysize; y++) {
const float* in = io1.Main().color()->PlaneRow(c, y);
float* out = noise_added.PlaneRow(c, y);
for (size_t x = 0; x < xsize; x++) {
// make the least significant bits random
float f = in[x] + generator.UniformF(0.0f, 1.f / 255.f);
if (f > 1.f) f = 1.f;
// quantize to the bitdepth we're testing
unsigned int u = f * factor + 0.5;
out[x] = u * ifactor;
}
}
}
io.SetFromImage(std::move(noise_added), jxl::ColorEncoding::SRGB(false));
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.butteraugli_distance = 0.f;
cparams.options.predictor = {Predictor::Zero};
cparams.speed_tier = SpeedTier::kThunder;
cparams.responsive = responsive;
DecompressParams dparams;
CodecInOut io2;
EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2),
bitdepth * xsize * ysize / 3);
EXPECT_LE(0, ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()));
size_t different = 0;
for (size_t c = 0; c < 3; c++) {
for (size_t y = 0; y < ysize; y++) {
const float* in = io.Main().color()->PlaneRow(c, y);
const float* out = io2.Main().color()->PlaneRow(c, y);
for (size_t x = 0; x < xsize; x++) {
uint32_t uin = in[x] * factor + 0.5;
uint32_t uout = out[x] * factor + 0.5;
// check that the integer values are identical
if (uin != uout) different++;
}
}
}
EXPECT_EQ(different, 0);
}
TEST(ModularTest, RoundtripLosslessCustomFloat) {
ThreadPool* pool = nullptr;
CodecInOut io;
@ -310,7 +407,7 @@ TEST(ModularTest, RoundtripLosslessCustomFloat) {
CompressParams cparams;
cparams.modular_mode = true;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.quality_pair = {100, 100};
cparams.butteraugli_distance = 0.f;
cparams.options.predictor = {Predictor::Zero};
cparams.speed_tier = SpeedTier::kThunder;
cparams.decoding_speed_tier = 2;

View File

@ -37,7 +37,7 @@ using test::Roundtrip;
TEST(PassesTest, RoundtripSmallPasses) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
@ -57,7 +57,7 @@ TEST(PassesTest, RoundtripSmallPasses) {
TEST(PassesTest, RoundtripUnalignedPasses) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 12, io.ysize() / 7);
@ -77,7 +77,7 @@ TEST(PassesTest, RoundtripUnalignedPasses) {
TEST(PassesTest, RoundtripMultiGroupPasses) {
ThreadPoolInternal pool(4);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
io.ShrinkTo(600, 1024); // partial X, full Y group
@ -104,7 +104,7 @@ TEST(PassesTest, RoundtripMultiGroupPasses) {
TEST(PassesTest, RoundtripLargeFastPasses) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -123,7 +123,7 @@ TEST(PassesTest, RoundtripLargeFastPasses) {
TEST(PassesTest, RoundtripProgressiveConsistent) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -160,7 +160,7 @@ TEST(PassesTest, RoundtripProgressiveConsistent) {
TEST(PassesTest, AllDownsampleFeasible) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -207,7 +207,7 @@ TEST(PassesTest, AllDownsampleFeasible) {
TEST(PassesTest, AllDownsampleFeasibleQProgressive) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -253,8 +253,8 @@ TEST(PassesTest, AllDownsampleFeasibleQProgressive) {
TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
const PaddedBytes orig = ReadTestData(
"third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
CodecInOut io_orig;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_orig, &pool));
Rect rect(0, 0, io_orig.xsize(), 128);
@ -302,7 +302,7 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) {
TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io_orig;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_orig, &pool));
Rect rect(0, 0, io_orig.xsize(), 128);
@ -349,7 +349,7 @@ TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) {
TEST(PassesTest, NonProgressiveDCImage) {
ThreadPoolInternal pool(8);
const PaddedBytes orig =
ReadTestData("imagecompression.info/flower_foveon.png");
ReadTestData("third_party/imagecompression.info/flower_foveon.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
@ -377,7 +377,7 @@ TEST(PassesTest, NonProgressiveDCImage) {
TEST(PassesTest, RoundtripSmallNoGaborishPasses) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);

View File

@ -24,8 +24,7 @@ TEST(PatchDictionaryTest, GrayscaleModular) {
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
CompressParams cparams;
cparams.color_transform = jxl::ColorTransform::kNone;
cparams.modular_mode = true;
cparams.SetLossless();
cparams.patches = jxl::Override::kOn;
DecompressParams dparams;

View File

@ -34,7 +34,7 @@ using test::Roundtrip;
TEST(PreviewTest, RoundtripGivenPreview) {
ThreadPool* pool = nullptr;
const PaddedBytes orig =
ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png");
ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
CodecInOut io;
ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);

Some files were not shown because too many files have changed in this diff Show More