From b8aee4491f6498d0cc17d18c80f6b3505e2a62ef Mon Sep 17 00:00:00 2001 From: "jiyong.min" Date: Tue, 5 Apr 2022 17:38:29 +0900 Subject: [PATCH] Imported Upstream version 0.6.1 Change-Id: I1d7cf6d8e9b1a01ffb0971218121010d59418875 --- .clang-format | 4 + .clang-tidy | 70 + .github/ISSUE_TEMPLATE/bug_report.md | 29 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/build_test.yml | 324 +++ .github/workflows/debug_ci.yml | 59 + .github/workflows/fuzz.yml | 52 + .github/workflows/pull_request.yml | 42 + .github/workflows/release.yaml | 352 +++ .gitignore | 16 + .gitlab-ci.yml | 434 +++ .gitmodules | 21 + AUTHORS | 28 + CHANGELOG.md | 166 ++ CMakeLists.txt | 385 +++ CODE_OF_CONDUCT.md | 93 + CONTRIBUTING.md | 132 + CONTRIBUTORS | 23 + LICENSE | 27 + PATENTS | 22 + README.Haiku.md | 20 + README.OSX.md | 41 + README.md | 167 ++ SECURITY.md | 37 + bash_test.sh | 252 ++ ci.sh | 1516 ++++++++++ debian/changelog | 83 + debian/compat | 1 + debian/control | 88 + debian/copyright | 236 ++ debian/jxl.install | 3 + debian/libjxl-dev.install | 4 + debian/libjxl-gdk-pixbuf.install | 3 + debian/libjxl-gimp-plugin.install | 1 + debian/libjxl.install | 1 + debian/rules | 17 + debian/source/format | 1 + deps.sh | 80 + doc/api.txt | 14 + doc/benchmarking.md | 82 + doc/building_and_testing.md | 171 ++ doc/building_wasm.md | 79 + doc/developing_in_debian.md | 57 + doc/developing_in_docker.md | 114 + doc/developing_in_github.md | 357 +++ doc/developing_in_windows_msys.md | 168 ++ doc/developing_in_windows_vcpkg.md | 91 + doc/developing_with_crossroad.md | 115 + doc/fuzzing.md | 184 ++ doc/jxl.svg | 1 + doc/man/cjxl.txt | 102 + doc/man/djxl.txt | 61 + doc/release.md | 262 ++ doc/software_support.md | 55 + doc/tables/adobe.md | 6 + doc/tables/all_tables.pdf | Bin 0 -> 164368 bytes doc/tables/all_tables.sh | 11 + doc/tables/app0.md | 6 + doc/tables/brn_proto.md | 23 + doc/tables/context_modes.md | 13 + doc/tables/dct_gen.md | 241 ++ doc/tables/ducky.md | 6 + doc/tables/freq_context.md | 54 + doc/tables/icc.md | 6 + doc/tables/is_zero_base.md | 9 + doc/tables/markdown-pdf.css | 36 + doc/tables/nonzero_buckets.md | 9 + doc/tables/num_nonzero_context.md | 60 + doc/tables/num_nonzeros_base.md | 258 ++ doc/tables/quant.md | 19 + doc/tables/stock_counts.md | 22 + doc/tables/stock_quant.md | 130 + doc/tables/stock_values.md | 44 + doc/tables/symbol_order.md | 30 + doc/xl_overview.md | 181 ++ docker/Dockerfile.jpegxl-builder | 21 + docker/Dockerfile.jpegxl-builder-run-aarch64 | 37 + docker/README.md | 7 + docker/build.sh | 83 + docker/scripts/99_norecommends | 1 + docker/scripts/binutils_align_fix.patch | 28 + docker/scripts/emsdk_install.sh | 37 + docker/scripts/jpegxl_builder.sh | 518 ++++ docker/scripts/msan_install.sh | 131 + docker/scripts/qemu_install.sh | 83 + examples/CMakeLists.txt | 56 + examples/decode_oneshot.cc | 245 ++ examples/encode_oneshot.cc | 272 ++ examples/examples.cmake | 19 + examples/jxlinfo.c | 317 ++ js-wasm-wrapper.sh | 19 + lib/CMakeLists.txt | 160 ++ lib/extras/README.md | 5 + lib/extras/codec.cc | 240 ++ lib/extras/codec.h | 94 + lib/extras/codec_apng.cc | 409 +++ lib/extras/codec_apng.h | 31 + lib/extras/codec_exr.cc | 352 +++ lib/extras/codec_exr.h | 34 + lib/extras/codec_gif.cc | 341 +++ lib/extras/codec_gif.h | 31 + lib/extras/codec_jpg.cc | 520 ++++ lib/extras/codec_jpg.h | 60 + lib/extras/codec_pgx.cc | 273 ++ lib/extras/codec_pgx.h | 38 + lib/extras/codec_pgx_test.cc | 74 + lib/extras/codec_png.cc | 852 ++++++ lib/extras/codec_png.h | 41 + lib/extras/codec_pnm.cc | 578 ++++ lib/extras/codec_pnm.h | 44 + lib/extras/codec_psd.cc | 617 ++++ lib/extras/codec_psd.h | 37 + lib/extras/codec_test.cc | 378 +++ lib/extras/color_description.cc | 218 ++ lib/extras/color_description.h | 22 + lib/extras/color_description_test.cc | 38 + lib/extras/color_hints.cc | 67 + lib/extras/color_hints.h | 73 + lib/extras/time.cc | 60 + lib/extras/time.h | 19 + lib/extras/tone_mapping.cc | 160 ++ lib/extras/tone_mapping.h | 18 + lib/extras/tone_mapping_gbench.cc | 45 + lib/include/jxl/butteraugli.h | 156 + lib/include/jxl/butteraugli_cxx.h | 55 + lib/include/jxl/codestream_header.h | 321 +++ lib/include/jxl/color_encoding.h | 145 + lib/include/jxl/decode.h | 938 ++++++ lib/include/jxl/decode_cxx.h | 52 + lib/include/jxl/encode.h | 392 +++ lib/include/jxl/encode_cxx.h | 52 + lib/include/jxl/memory_manager.h | 67 + lib/include/jxl/parallel_runner.h | 151 + lib/include/jxl/resizable_parallel_runner.h | 75 + lib/include/jxl/resizable_parallel_runner_cxx.h | 59 + lib/include/jxl/thread_parallel_runner.h | 69 + lib/include/jxl/thread_parallel_runner_cxx.h | 59 + lib/include/jxl/types.h | 116 + lib/jxl.cmake | 567 ++++ lib/jxl/ac_context.h | 149 + lib/jxl/ac_strategy.cc | 110 + lib/jxl/ac_strategy.h | 287 ++ lib/jxl/ac_strategy_test.cc | 225 ++ lib/jxl/adaptive_reconstruction_test.cc | 184 ++ lib/jxl/alpha.cc | 111 + lib/jxl/alpha.h | 66 + lib/jxl/alpha_test.cc | 134 + lib/jxl/ans_common.cc | 148 + lib/jxl/ans_common.h | 143 + lib/jxl/ans_common_test.cc | 43 + lib/jxl/ans_params.h | 36 + lib/jxl/ans_test.cc | 280 ++ lib/jxl/aux_out.cc | 96 + lib/jxl/aux_out.h | 313 ++ lib/jxl/aux_out_fwd.h | 28 + lib/jxl/base/arch_macros.h | 33 + lib/jxl/base/bits.h | 147 + lib/jxl/base/byte_order.h | 283 ++ lib/jxl/base/cache_aligned.cc | 154 + lib/jxl/base/cache_aligned.h | 74 + lib/jxl/base/compiler_specific.h | 153 + lib/jxl/base/data_parallel.cc | 23 + lib/jxl/base/data_parallel.h | 155 + lib/jxl/base/descriptive_statistics.cc | 102 + lib/jxl/base/descriptive_statistics.h | 126 + lib/jxl/base/file_io.h | 124 + lib/jxl/base/iaca.h | 65 + lib/jxl/base/os_macros.h | 50 + lib/jxl/base/override.h | 29 + lib/jxl/base/padded_bytes.cc | 63 + lib/jxl/base/padded_bytes.h | 195 ++ lib/jxl/base/profiler.h | 32 + lib/jxl/base/robust_statistics.h | 357 +++ lib/jxl/base/span.h | 58 + lib/jxl/base/status.cc | 46 + lib/jxl/base/status.h | 299 ++ lib/jxl/base/thread_pool_internal.h | 52 + lib/jxl/bit_reader_test.cc | 261 ++ lib/jxl/bits_test.cc | 79 + lib/jxl/blending.cc | 472 +++ lib/jxl/blending.h | 96 + lib/jxl/blending_test.cc | 98 + lib/jxl/butteraugli/butteraugli.cc | 2139 ++++++++++++++ lib/jxl/butteraugli/butteraugli.h | 220 ++ lib/jxl/butteraugli_test.cc | 102 + lib/jxl/butteraugli_wrapper.cc | 207 ++ lib/jxl/byte_order_test.cc | 53 + lib/jxl/chroma_from_luma.cc | 21 + lib/jxl/chroma_from_luma.h | 151 + lib/jxl/codec_in_out.h | 213 ++ lib/jxl/coeff_order.cc | 154 + lib/jxl/coeff_order.h | 66 + lib/jxl/coeff_order_fwd.h | 47 + lib/jxl/coeff_order_test.cc | 101 + lib/jxl/color_encoding_internal.cc | 731 +++++ lib/jxl/color_encoding_internal.h | 459 +++ lib/jxl/color_encoding_internal_test.cc | 157 + lib/jxl/color_management.cc | 519 ++++ lib/jxl/color_management.h | 38 + lib/jxl/color_management_test.cc | 237 ++ lib/jxl/common.h | 197 ++ lib/jxl/compressed_dc.cc | 312 ++ lib/jxl/compressed_dc.h | 34 + lib/jxl/compressed_image_test.cc | 102 + lib/jxl/convolve-inl.h | 119 + lib/jxl/convolve.cc | 1332 +++++++++ lib/jxl/convolve.h | 131 + lib/jxl/convolve_test.cc | 250 ++ lib/jxl/data_parallel_test.cc | 111 + lib/jxl/dct-inl.h | 361 +++ lib/jxl/dct_block-inl.h | 108 + lib/jxl/dct_for_test.h | 99 + lib/jxl/dct_scales.cc | 31 + lib/jxl/dct_scales.h | 390 +++ lib/jxl/dct_test.cc | 390 +++ lib/jxl/dct_util.h | 86 + lib/jxl/dec_ans.cc | 375 +++ lib/jxl/dec_ans.h | 432 +++ lib/jxl/dec_bit_reader.h | 354 +++ lib/jxl/dec_cache.cc | 177 ++ lib/jxl/dec_cache.h | 411 +++ lib/jxl/dec_context_map.cc | 105 + lib/jxl/dec_context_map.h | 30 + lib/jxl/dec_external_image.cc | 528 ++++ lib/jxl/dec_external_image.h | 61 + lib/jxl/dec_external_image_gbench.cc | 56 + lib/jxl/dec_file.cc | 193 ++ lib/jxl/dec_file.h | 48 + lib/jxl/dec_frame.cc | 1027 +++++++ lib/jxl/dec_frame.h | 282 ++ lib/jxl/dec_group.cc | 786 +++++ lib/jxl/dec_group.h | 47 + lib/jxl/dec_group_border.cc | 183 ++ lib/jxl/dec_group_border.h | 60 + lib/jxl/dec_huffman.cc | 255 ++ lib/jxl/dec_huffman.h | 32 + lib/jxl/dec_modular.cc | 663 +++++ lib/jxl/dec_modular.h | 133 + lib/jxl/dec_noise.cc | 295 ++ lib/jxl/dec_noise.h | 36 + lib/jxl/dec_params.h | 62 + lib/jxl/dec_patch_dictionary.cc | 248 ++ lib/jxl/dec_patch_dictionary.h | 200 ++ lib/jxl/dec_reconstruct.cc | 1269 ++++++++ lib/jxl/dec_reconstruct.h | 72 + lib/jxl/dec_render_pipeline.h | 91 + lib/jxl/dec_transforms-inl.h | 867 ++++++ lib/jxl/dec_transforms_testonly.cc | 41 + lib/jxl/dec_transforms_testonly.h | 32 + lib/jxl/dec_upsample.cc | 381 +++ lib/jxl/dec_upsample.h | 57 + lib/jxl/dec_xyb-inl.h | 351 +++ lib/jxl/dec_xyb.cc | 290 ++ lib/jxl/dec_xyb.h | 71 + lib/jxl/decode.cc | 2325 +++++++++++++++ lib/jxl/decode_test.cc | 3017 ++++++++++++++++++++ lib/jxl/decode_to_jpeg.cc | 77 + lib/jxl/decode_to_jpeg.h | 173 ++ lib/jxl/descriptive_statistics_test.cc | 152 + lib/jxl/docs/color_management.md | 68 + lib/jxl/docs/dc_predictor.md | 129 + lib/jxl/docs/entropy_coding_basic.md | 111 + lib/jxl/docs/file_format.md | 129 + lib/jxl/docs/upsample.md | 151 + lib/jxl/enc_ac_strategy.cc | 1104 +++++++ lib/jxl/enc_ac_strategy.h | 79 + lib/jxl/enc_adaptive_quantization.cc | 1118 ++++++++ lib/jxl/enc_adaptive_quantization.h | 65 + lib/jxl/enc_ans.cc | 1622 +++++++++++ lib/jxl/enc_ans.h | 142 + lib/jxl/enc_ans_params.h | 75 + lib/jxl/enc_ar_control_field.cc | 318 +++ lib/jxl/enc_ar_control_field.h | 49 + lib/jxl/enc_bit_writer.cc | 250 ++ lib/jxl/enc_bit_writer.h | 144 + lib/jxl/enc_butteraugli_comparator.cc | 93 + lib/jxl/enc_butteraugli_comparator.h | 56 + lib/jxl/enc_butteraugli_pnorm.cc | 212 ++ lib/jxl/enc_butteraugli_pnorm.h | 24 + lib/jxl/enc_cache.cc | 198 ++ lib/jxl/enc_cache.h | 92 + lib/jxl/enc_chroma_from_luma.cc | 375 +++ lib/jxl/enc_chroma_from_luma.h | 67 + lib/jxl/enc_cluster.cc | 310 ++ lib/jxl/enc_cluster.h | 61 + lib/jxl/enc_coeff_order.cc | 274 ++ lib/jxl/enc_coeff_order.h | 52 + lib/jxl/enc_color_management.cc | 891 ++++++ lib/jxl/enc_color_management.h | 70 + lib/jxl/enc_comparator.cc | 140 + lib/jxl/enc_comparator.h | 52 + lib/jxl/enc_context_map.cc | 139 + lib/jxl/enc_context_map.h | 33 + lib/jxl/enc_detect_dots.cc | 620 ++++ lib/jxl/enc_detect_dots.h | 66 + lib/jxl/enc_dot_dictionary.cc | 72 + lib/jxl/enc_dot_dictionary.h | 35 + lib/jxl/enc_entropy_coder.cc | 268 ++ lib/jxl/enc_entropy_coder.h | 46 + lib/jxl/enc_external_image.cc | 340 +++ lib/jxl/enc_external_image.h | 51 + lib/jxl/enc_external_image_gbench.cc | 49 + lib/jxl/enc_external_image_test.cc | 49 + lib/jxl/enc_fast_heuristics.cc | 361 +++ lib/jxl/enc_file.cc | 295 ++ lib/jxl/enc_file.h | 51 + lib/jxl/enc_frame.cc | 1423 +++++++++ lib/jxl/enc_frame.h | 51 + lib/jxl/enc_gamma_correct.h | 36 + lib/jxl/enc_group.cc | 342 +++ lib/jxl/enc_group.h | 30 + lib/jxl/enc_heuristics.cc | 946 ++++++ lib/jxl/enc_heuristics.h | 87 + lib/jxl/enc_huffman.cc | 214 ++ lib/jxl/enc_huffman.h | 22 + lib/jxl/enc_icc_codec.cc | 430 +++ lib/jxl/enc_icc_codec.h | 33 + lib/jxl/enc_image_bundle.cc | 170 ++ lib/jxl/enc_image_bundle.h | 25 + lib/jxl/enc_jxl_skcms.h | 54 + lib/jxl/enc_modular.cc | 1633 +++++++++++ lib/jxl/enc_modular.h | 92 + lib/jxl/enc_noise.cc | 378 +++ lib/jxl/enc_noise.h | 33 + lib/jxl/enc_params.h | 274 ++ lib/jxl/enc_patch_dictionary.cc | 842 ++++++ lib/jxl/enc_patch_dictionary.h | 66 + lib/jxl/enc_photon_noise.cc | 89 + lib/jxl/enc_photon_noise.h | 22 + lib/jxl/enc_photon_noise_test.cc | 50 + lib/jxl/enc_quant_weights.cc | 203 ++ lib/jxl/enc_quant_weights.h | 29 + lib/jxl/enc_splines.cc | 96 + lib/jxl/enc_splines.h | 39 + lib/jxl/enc_toc.cc | 46 + lib/jxl/enc_toc.h | 29 + lib/jxl/enc_transforms-inl.h | 844 ++++++ lib/jxl/enc_transforms.cc | 41 + lib/jxl/enc_transforms.h | 32 + lib/jxl/enc_xyb.cc | 437 +++ lib/jxl/enc_xyb.h | 45 + lib/jxl/encode.cc | 489 ++++ lib/jxl/encode_internal.h | 120 + lib/jxl/encode_test.cc | 594 ++++ lib/jxl/entropy_coder.cc | 70 + lib/jxl/entropy_coder.h | 45 + lib/jxl/entropy_coder_test.cc | 70 + lib/jxl/epf.cc | 684 +++++ lib/jxl/epf.h | 55 + lib/jxl/fake_parallel_runner_testonly.h | 76 + lib/jxl/fast_math-inl.h | 175 ++ lib/jxl/fast_math_test.cc | 280 ++ lib/jxl/field_encodings.h | 123 + lib/jxl/fields.cc | 985 +++++++ lib/jxl/fields.h | 300 ++ lib/jxl/fields_test.cc | 434 +++ lib/jxl/filters.cc | 112 + lib/jxl/filters.h | 348 +++ lib/jxl/filters_internal.h | 55 + lib/jxl/filters_internal_test.cc | 50 + lib/jxl/frame_header.cc | 369 +++ lib/jxl/frame_header.h | 492 ++++ lib/jxl/gaborish.cc | 70 + lib/jxl/gaborish.h | 26 + lib/jxl/gaborish_test.cc | 71 + lib/jxl/gamma_correct_test.cc | 37 + lib/jxl/gauss_blur.cc | 616 ++++ lib/jxl/gauss_blur.h | 94 + lib/jxl/gauss_blur_test.cc | 610 ++++ lib/jxl/gradient_test.cc | 205 ++ lib/jxl/headers.cc | 212 ++ lib/jxl/headers.h | 106 + lib/jxl/huffman_table.cc | 161 ++ lib/jxl/huffman_table.h | 28 + lib/jxl/huffman_tree.cc | 328 +++ lib/jxl/huffman_tree.h | 52 + lib/jxl/iaca_test.cc | 21 + lib/jxl/icc_codec.cc | 404 +++ lib/jxl/icc_codec.h | 64 + lib/jxl/icc_codec_common.cc | 192 ++ lib/jxl/icc_codec_common.h | 106 + lib/jxl/icc_codec_test.cc | 207 ++ lib/jxl/image.cc | 313 ++ lib/jxl/image.h | 443 +++ lib/jxl/image_bundle.cc | 149 + lib/jxl/image_bundle.h | 263 ++ lib/jxl/image_bundle_test.cc | 36 + lib/jxl/image_metadata.cc | 414 +++ lib/jxl/image_metadata.h | 410 +++ lib/jxl/image_ops.h | 814 ++++++ lib/jxl/image_ops_test.cc | 166 ++ lib/jxl/image_test_utils.h | 313 ++ lib/jxl/jpeg/dec_jpeg_data.cc | 140 + lib/jxl/jpeg/dec_jpeg_data.h | 19 + lib/jxl/jpeg/dec_jpeg_data_writer.cc | 983 +++++++ lib/jxl/jpeg/dec_jpeg_data_writer.h | 30 + lib/jxl/jpeg/dec_jpeg_output_chunk.h | 72 + lib/jxl/jpeg/dec_jpeg_serialization_state.h | 95 + lib/jxl/jpeg/enc_jpeg_data.cc | 370 +++ lib/jxl/jpeg/enc_jpeg_data.h | 28 + lib/jxl/jpeg/enc_jpeg_data_reader.cc | 1142 ++++++++ lib/jxl/jpeg/enc_jpeg_data_reader.h | 36 + lib/jxl/jpeg/enc_jpeg_huffman_decode.cc | 103 + lib/jxl/jpeg/enc_jpeg_huffman_decode.h | 41 + lib/jxl/jpeg/jpeg_data.cc | 448 +++ lib/jxl/jpeg/jpeg_data.h | 267 ++ lib/jxl/jxl.syms | 5 + lib/jxl/jxl.version | 17 + lib/jxl/jxl_inspection.h | 22 + lib/jxl/jxl_osx.syms | 1 + lib/jxl/jxl_test.cc | 1679 +++++++++++ lib/jxl/lehmer_code.h | 102 + lib/jxl/lehmer_code_test.cc | 98 + lib/jxl/libjxl.pc.in | 12 + lib/jxl/linalg.cc | 235 ++ lib/jxl/linalg.h | 294 ++ lib/jxl/linalg_test.cc | 149 + lib/jxl/loop_filter.cc | 87 + lib/jxl/loop_filter.h | 78 + lib/jxl/luminance.cc | 31 + lib/jxl/luminance.h | 21 + lib/jxl/memory_manager_internal.cc | 18 + lib/jxl/memory_manager_internal.h | 101 + lib/jxl/modular/encoding/context_predict.h | 653 +++++ lib/jxl/modular/encoding/dec_ma.cc | 107 + lib/jxl/modular/encoding/dec_ma.h | 66 + lib/jxl/modular/encoding/enc_encoding.cc | 549 ++++ lib/jxl/modular/encoding/enc_encoding.h | 49 + lib/jxl/modular/encoding/enc_ma.cc | 1043 +++++++ lib/jxl/modular/encoding/enc_ma.h | 157 + lib/jxl/modular/encoding/encoding.cc | 530 ++++ lib/jxl/modular/encoding/encoding.h | 140 + lib/jxl/modular/encoding/ma_common.h | 28 + lib/jxl/modular/modular_image.cc | 62 + lib/jxl/modular/modular_image.h | 107 + lib/jxl/modular/options.h | 172 ++ lib/jxl/modular/transform/enc_palette.cc | 562 ++++ lib/jxl/modular/transform/enc_palette.h | 22 + lib/jxl/modular/transform/enc_rct.cc | 71 + lib/jxl/modular/transform/enc_rct.h | 17 + lib/jxl/modular/transform/enc_squeeze.cc | 140 + lib/jxl/modular/transform/enc_squeeze.h | 20 + lib/jxl/modular/transform/enc_transform.cc | 46 + lib/jxl/modular/transform/enc_transform.h | 22 + lib/jxl/modular/transform/palette.h | 319 +++ lib/jxl/modular/transform/rct.h | 107 + lib/jxl/modular/transform/squeeze.cc | 329 +++ lib/jxl/modular/transform/squeeze.h | 94 + lib/jxl/modular/transform/transform.cc | 98 + lib/jxl/modular/transform/transform.h | 148 + lib/jxl/modular_test.cc | 171 ++ lib/jxl/noise.h | 60 + lib/jxl/noise_distributions.h | 138 + lib/jxl/opsin_image_test.cc | 127 + lib/jxl/opsin_inverse_test.cc | 55 + lib/jxl/opsin_params.cc | 44 + lib/jxl/opsin_params.h | 74 + lib/jxl/optimize.cc | 163 ++ lib/jxl/optimize.h | 218 ++ lib/jxl/optimize_test.cc | 109 + lib/jxl/padded_bytes_test.cc | 126 + lib/jxl/passes_state.cc | 68 + lib/jxl/passes_state.h | 138 + lib/jxl/passes_test.cc | 389 +++ lib/jxl/patch_dictionary_internal.h | 31 + lib/jxl/patch_dictionary_test.cc | 58 + lib/jxl/preview_test.cc | 83 + lib/jxl/progressive_split.cc | 128 + lib/jxl/progressive_split.h | 149 + lib/jxl/quant_weights.cc | 1184 ++++++++ lib/jxl/quant_weights.h | 469 +++ lib/jxl/quant_weights_test.cc | 240 ++ lib/jxl/quantizer-inl.h | 73 + lib/jxl/quantizer.cc | 146 + lib/jxl/quantizer.h | 178 ++ lib/jxl/quantizer_test.cc | 82 + lib/jxl/rational_polynomial-inl.h | 94 + lib/jxl/rational_polynomial_test.cc | 239 ++ lib/jxl/robust_statistics_test.cc | 150 + lib/jxl/roundtrip_test.cc | 615 ++++ lib/jxl/sanitizers.h | 246 ++ lib/jxl/speed_tier_test.cc | 112 + lib/jxl/splines.cc | 563 ++++ lib/jxl/splines.h | 146 + lib/jxl/splines_gbench.cc | 52 + lib/jxl/splines_test.cc | 339 +++ lib/jxl/test_utils.h | 390 +++ lib/jxl/testdata.h | 60 + lib/jxl/tf_gbench.cc | 143 + lib/jxl/toc.cc | 97 + lib/jxl/toc.h | 50 + lib/jxl/toc_test.cc | 93 + lib/jxl/transfer_functions-inl.h | 397 +++ lib/jxl/transpose-inl.h | 201 ++ lib/jxl/xorshift128plus-inl.h | 88 + lib/jxl/xorshift128plus_test.cc | 372 +++ lib/jxl_benchmark.cmake | 45 + lib/jxl_extras.cmake | 116 + lib/jxl_profiler.cmake | 31 + lib/jxl_tests.cmake | 137 + lib/jxl_threads.cmake | 100 + lib/lib.gni | 435 +++ lib/profiler/profiler.cc | 459 +++ lib/profiler/profiler.h | 165 ++ lib/profiler/tsc_timer.h | 141 + lib/threads/libjxl_threads.pc.in | 12 + lib/threads/resizable_parallel_runner.cc | 195 ++ lib/threads/thread_parallel_runner.cc | 101 + lib/threads/thread_parallel_runner_internal.cc | 219 ++ lib/threads/thread_parallel_runner_internal.h | 172 ++ lib/threads/thread_parallel_runner_test.cc | 115 + plugins/CMakeLists.txt | 12 + plugins/gdk-pixbuf/CMakeLists.txt | 76 + plugins/gdk-pixbuf/README.md | 50 + plugins/gdk-pixbuf/jxl.thumbnailer | 4 + plugins/gdk-pixbuf/loaders_test.cache | 16 + plugins/gdk-pixbuf/pixbufloader-jxl.c | 561 ++++ plugins/gdk-pixbuf/pixbufloader_test.cc | 41 + plugins/gimp/CMakeLists.txt | 28 + plugins/gimp/common.cc | 27 + plugins/gimp/common.h | 45 + plugins/gimp/file-jxl-load.cc | 340 +++ plugins/gimp/file-jxl-load.h | 19 + plugins/gimp/file-jxl-save.cc | 888 ++++++ plugins/gimp/file-jxl-save.h | 20 + plugins/gimp/file-jxl.cc | 157 + plugins/mime/CMakeLists.txt | 6 + plugins/mime/README.md | 20 + plugins/mime/image-jxl.xml | 13 + third_party/CMakeLists.txt | 223 ++ third_party/HEVCSoftware/README.md | 2 + third_party/HEVCSoftware/cfg/LICENSE | 31 + .../HEVCSoftware/cfg/encoder_intra_main_scc_10.cfg | 136 + third_party/dirent.cc | 142 + third_party/dirent.h | 49 + third_party/lcms2.cmake | 63 + third_party/lodepng.cmake | 22 + third_party/sjpeg.cmake | 23 + third_party/skcms.cmake | 51 + third_party/testdata/dots/ellipses.png | Bin 0 -> 7359 bytes .../testdata/imagecompression.info/LICENSE.txt | 8 + .../imagecompression.info/flower_foveon.png | Bin 0 -> 3222217 bytes .../flower_foveon.png.ffmpeg.y4m | 1526 ++++++++++ .../flower_foveon.png.im_q85_420.jpg | Bin 0 -> 226018 bytes .../flower_foveon.png.im_q85_420_progr.jpg | Bin 0 -> 205724 bytes .../flower_foveon.png.im_q85_422.jpg | Bin 0 -> 265590 bytes .../flower_foveon.png.im_q85_440.jpg | Bin 0 -> 262249 bytes .../flower_foveon.png.im_q85_444.jpg | Bin 0 -> 326916 bytes .../flower_foveon.png.im_q85_444_1x2.jpg | Bin 0 -> 329952 bytes .../flower_foveon.png.im_q85_asymmetric.jpg | Bin 0 -> 264737 bytes .../flower_foveon.png.im_q85_gray.jpg | Bin 0 -> 167025 bytes .../flower_foveon.png.im_q85_luma_subsample.jpg | Bin 0 -> 216069 bytes .../flower_foveon_cropped.jpg | Bin 0 -> 70681 bytes third_party/testdata/jxl/animation_patches.gif | Bin 0 -> 19125 bytes .../jxl/blending/cropped_traffic_light.jxl | Bin 0 -> 793 bytes .../jxl/blending/cropped_traffic_light_frame-0.png | Bin 0 -> 463 bytes .../jxl/blending/cropped_traffic_light_frame-1.png | Bin 0 -> 520 bytes .../jxl/blending/cropped_traffic_light_frame-2.png | Bin 0 -> 330 bytes .../jxl/blending/cropped_traffic_light_frame-3.png | Bin 0 -> 358 bytes .../jxl/blending/grayscale_patches_on_splines.png | Bin 0 -> 5140 bytes .../testdata/jxl/color_management/sRGB-D2700.icc | Bin 0 -> 924 bytes third_party/testdata/jxl/grayscale_patches.png | Bin 0 -> 18562 bytes third_party/testdata/jxl/spline_on_first_frame.jxl | Bin 0 -> 94 bytes third_party/testdata/jxl/spline_on_first_frame.png | Bin 0 -> 35342 bytes third_party/testdata/jxl/splines.png | Bin 0 -> 7708 bytes third_party/testdata/jxl/traffic_light.gif | Bin 0 -> 1088 bytes third_party/testdata/pngsuite/PngSuite.LICENSE | 9 + third_party/testdata/pngsuite/PngSuite.README | 25 + third_party/testdata/pngsuite/README.md | 15 + third_party/testdata/pngsuite/ccwn2c08.png | Bin 0 -> 1514 bytes third_party/testdata/pngsuite/ccwn3p08.png | Bin 0 -> 1554 bytes third_party/testdata/pngsuite/ct1n0g04.png | Bin 0 -> 792 bytes third_party/testdata/pngsuite/ctjn0g04.png | Bin 0 -> 941 bytes third_party/testdata/pngsuite/ctzn0g04.png | Bin 0 -> 753 bytes third_party/testdata/pngsuite/exif2c08.png | Bin 0 -> 1788 bytes third_party/testdata/pngsuite/g04n2c08.png | Bin 0 -> 377 bytes third_party/testdata/pngsuite/g10n3p04.png | Bin 0 -> 214 bytes .../testdata/position_encoding/bike_dots.txt | 156 + .../testdata/position_encoding/keong_dots.txt | 23 + .../testdata/position_encoding/non_square.txt | 6 + .../testdata/position_encoding/repeated_dots.txt | 6 + .../testdata/position_encoding/woman_dots.txt | 693 +++++ .../raw.pixls/DJI-FC6310-16bit_709_v4_krita.png | Bin 0 -> 21284 bytes .../raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png | Bin 0 -> 6438 bytes .../Google-Pixel2XL-16bit_acescg_g1_v4_krita.png | Bin 0 -> 22002 bytes .../Google-Pixel2XL-16bit_srgb8_v4_krita.png | Bin 0 -> 8626 bytes .../raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png | Bin 0 -> 23876 bytes .../raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png | Bin 0 -> 18167 bytes third_party/testdata/raw.pixls/LICENSE.md | 114 + .../raw.pixls/Nikon-D300-12bit_2020_g1_dt.png | Bin 0 -> 32348 bytes .../raw.pixls/Nikon-D300-12bit_srgb8_dt.png | Bin 0 -> 25108 bytes third_party/testdata/raw.pixls/README.md | 4 + .../Sony-DSC-RX1RM2-14bit_709_v4_krita.png | Bin 0 -> 21768 bytes .../Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png | Bin 0 -> 6952 bytes .../500px/cvo9xd_keong_macan_grayscale.png | Bin 0 -> 120209 bytes .../wesaturate/500px/cvo9xd_keong_macan_srgb8.png | Bin 0 -> 368318 bytes .../500px/tmshre_riaphotographs_alpha.png | Bin 0 -> 374287 bytes .../500px/tmshre_riaphotographs_srgb8.png | Bin 0 -> 314330 bytes .../wesaturate/500px/u76c0g_bliznaca_srgb8.png | Bin 0 -> 367925 bytes .../wesaturate/64px/Nikon-D3-14bit_2020_g1.png | Bin 0 -> 35582 bytes .../wesaturate/64px/Nikon-D3-14bit_srgb8.png | Bin 0 -> 31572 bytes .../wesaturate/64px/a2d1un_nkitzmiller_2020_g1.png | Bin 0 -> 61368 bytes .../wesaturate/64px/a2d1un_nkitzmiller_srgb8.png | Bin 0 -> 56885 bytes .../wesaturate/64px/cvo9xd_keong_macan_2020_g1.png | Bin 0 -> 47404 bytes .../wesaturate/64px/cvo9xd_keong_macan_srgb8.png | Bin 0 -> 42836 bytes .../wesaturate/64px/l1qnb5_nkitzmiller_2020_g1.png | Bin 0 -> 61317 bytes .../wesaturate/64px/l1qnb5_nkitzmiller_srgb8.png | Bin 0 -> 56080 bytes .../wesaturate/64px/phu1or_alfann24_2020_g1.png | Bin 0 -> 45675 bytes .../wesaturate/64px/phu1or_alfann24_srgb8.png | Bin 0 -> 37736 bytes .../wesaturate/64px/q3a0b3_d17ws_709_g1.png | Bin 0 -> 46202 bytes .../wesaturate/64px/q3a0b3_d17ws_srgb8.png | Bin 0 -> 45679 bytes .../wesaturate/64px/ra0ed45_alfann24_709_g1.png | Bin 0 -> 46975 bytes .../wesaturate/64px/ra0ed45_alfann24_srgb8.png | Bin 0 -> 40526 bytes .../wesaturate/64px/t0gho7_orlaustral_709_g1.png | Bin 0 -> 46561 bytes .../wesaturate/64px/t0gho7_orlaustral_srgb8.png | Bin 0 -> 46761 bytes .../64px/tmshre_riaphotographs_709_g1.png | Bin 0 -> 32961 bytes .../64px/tmshre_riaphotographs_srgb8.png | Bin 0 -> 26047 bytes .../wesaturate/64px/u76c0g_bliznaca_709_g1.png | Bin 0 -> 30231 bytes .../wesaturate/64px/u76c0g_bliznaca_srgb8.png | Bin 0 -> 26141 bytes .../testdata/wesaturate/64px/vgqcws_vin_709_g1.png | Bin 0 -> 55154 bytes .../testdata/wesaturate/64px/vgqcws_vin_srgb8.png | Bin 0 -> 56176 bytes third_party/testdata/wesaturate/LICENSE.md | 114 + third_party/testdata/wesaturate/README.md | 8 + third_party/testdata/wide-gamut-tests/LICENSE | 201 ++ .../testdata/wide-gamut-tests/P3-sRGB-blue.png | Bin 0 -> 295428 bytes .../wide-gamut-tests/P3-sRGB-color-bars.png | Bin 0 -> 507899 bytes .../wide-gamut-tests/P3-sRGB-color-ring.png | Bin 0 -> 722873 bytes .../testdata/wide-gamut-tests/P3-sRGB-green.png | Bin 0 -> 468399 bytes .../testdata/wide-gamut-tests/P3-sRGB-red.png | Bin 0 -> 488593 bytes .../testdata/wide-gamut-tests/R2020-sRGB-blue.png | Bin 0 -> 581900 bytes .../wide-gamut-tests/R2020-sRGB-color-bars.png | Bin 0 -> 554072 bytes .../wide-gamut-tests/R2020-sRGB-color-ring.png | Bin 0 -> 721512 bytes .../testdata/wide-gamut-tests/R2020-sRGB-green.png | Bin 0 -> 469705 bytes .../testdata/wide-gamut-tests/R2020-sRGB-red.png | Bin 0 -> 563684 bytes third_party/testdata/wide-gamut-tests/README.txt | 5 + .../testdata/wide-gamut-tests/rgb-to-gbr-test.png | Bin 0 -> 343047 bytes tools/CMakeLists.txt | 412 +++ tools/args.h | 159 ++ tools/benchmark/benchmark_args.cc | 277 ++ tools/benchmark/benchmark_args.h | 174 ++ tools/benchmark/benchmark_codec.cc | 192 ++ tools/benchmark/benchmark_codec.h | 102 + tools/benchmark/benchmark_codec_avif.cc | 344 +++ tools/benchmark/benchmark_codec_avif.h | 20 + tools/benchmark/benchmark_codec_custom.cc | 161 ++ tools/benchmark/benchmark_codec_custom.h | 46 + tools/benchmark/benchmark_codec_jpeg.cc | 123 + tools/benchmark/benchmark_codec_jpeg.h | 20 + tools/benchmark/benchmark_codec_jxl.cc | 404 +++ tools/benchmark/benchmark_codec_jxl.h | 23 + tools/benchmark/benchmark_codec_png.cc | 67 + tools/benchmark/benchmark_codec_png.h | 22 + tools/benchmark/benchmark_codec_webp.cc | 300 ++ tools/benchmark/benchmark_codec_webp.h | 23 + tools/benchmark/benchmark_file_io.cc | 232 ++ tools/benchmark/benchmark_file_io.h | 53 + tools/benchmark/benchmark_stats.cc | 368 +++ tools/benchmark/benchmark_stats.h | 81 + tools/benchmark/benchmark_utils.cc | 91 + tools/benchmark/benchmark_utils.h | 35 + tools/benchmark/benchmark_xl.cc | 1111 +++++++ tools/benchmark/hm/README.md | 12 + tools/benchmark/hm/decode.sh | 98 + tools/benchmark/hm/encode.sh | 97 + tools/benchmark/metrics/compute-hdrvdp.m | 17 + tools/benchmark/metrics/compute-pumetrics.m | 26 + tools/benchmark/metrics/compute_octave_metric.sh | 41 + tools/benchmark/metrics/dists-rgb.sh | 1 + tools/benchmark/metrics/fsim-rgb.sh | 1 + tools/benchmark/metrics/fsim-y.sh | 1 + tools/benchmark/metrics/gmsd-rgb.sh | 1 + tools/benchmark/metrics/hdr_plots.sh | 10 + tools/benchmark/metrics/hdrvdp-fixes.patch | 110 + tools/benchmark/metrics/hdrvdp.sh | 9 + tools/benchmark/metrics/iqa.py | 90 + tools/benchmark/metrics/iqa_wrapper.sh | 7 + tools/benchmark/metrics/lpips-rgb.sh | 1 + tools/benchmark/metrics/mrse.sh | 36 + tools/benchmark/metrics/msssim-rgb.sh | 1 + tools/benchmark/metrics/msssim-y.sh | 1 + tools/benchmark/metrics/nlpd-y.sh | 1 + tools/benchmark/metrics/plots.py | 259 ++ tools/benchmark/metrics/prepare_metrics.sh | 64 + tools/benchmark/metrics/pupsnr.sh | 9 + tools/benchmark/metrics/pussim.sh | 9 + tools/benchmark/metrics/run_all_hdr_metrics.sh | 30 + tools/benchmark/metrics/run_all_sdr_metrics.sh | 41 + tools/benchmark/metrics/sdr_plots.sh | 10 + tools/benchmark/metrics/ssim-rgb.sh | 1 + tools/benchmark/metrics/ssim-y.sh | 1 + tools/benchmark/metrics/ssimulacra.sh | 7 + tools/benchmark/metrics/vif-rgb.sh | 1 + tools/benchmark/metrics/vmaf.sh | 52 + tools/benchmark/metrics/vsi-rgb.sh | 1 + tools/box/CMakeLists.txt | 28 + tools/box/box.cc | 334 +++ tools/box/box.h | 122 + tools/box/box_list_main.cc | 88 + tools/box/box_test.cc | 77 + tools/build_cleaner.py | 316 ++ tools/build_stats.py | 412 +++ tools/butteraugli_main.cc | 142 + tools/check_author.py | 76 + tools/cjxl.cc | 851 ++++++ tools/cjxl.h | 107 + tools/cjxl_main.cc | 147 + tools/cmdline.cc | 95 + tools/cmdline.h | 322 +++ tools/codec_config.cc | 57 + tools/codec_config.h | 22 + tools/color_encoding_fuzzer.cc | 24 + tools/comparison_viewer/CMakeLists.txt | 74 + tools/comparison_viewer/codec_comparison_window.cc | 314 ++ tools/comparison_viewer/codec_comparison_window.h | 75 + tools/comparison_viewer/codec_comparison_window.ui | 170 ++ tools/comparison_viewer/compare_codecs.cc | 57 + tools/comparison_viewer/compare_images.cc | 113 + tools/comparison_viewer/image_loading.cc | 106 + tools/comparison_viewer/image_loading.h | 26 + tools/comparison_viewer/settings.cc | 51 + tools/comparison_viewer/settings.h | 40 + tools/comparison_viewer/settings.ui | 120 + tools/comparison_viewer/split_image_renderer.cc | 239 ++ tools/comparison_viewer/split_image_renderer.h | 90 + tools/comparison_viewer/split_image_view.cc | 71 + tools/comparison_viewer/split_image_view.h | 40 + tools/comparison_viewer/split_image_view.ui | 141 + tools/conformance/CMakeLists.txt | 21 + tools/conformance/conformance.py | 123 + tools/conformance/djxl_conformance.cc | 566 ++++ tools/conformance/generator.py | 119 + tools/conformance/tooling_test.sh | 52 + tools/cpu/cpu.cc | 420 +++ tools/cpu/cpu.h | 31 + tools/cpu/os_specific.cc | 373 +++ tools/cpu/os_specific.h | 62 + tools/decode_and_encode.cc | 50 + tools/decode_basic_info_fuzzer.cc | 58 + tools/demo_progressive_saliency_encoding.py | 182 ++ tools/demo_vardct_select.sh | 93 + tools/djxl.cc | 322 +++ tools/djxl.h | 89 + tools/djxl_fuzzer.cc | 508 ++++ tools/djxl_main.cc | 209 ++ tools/epf.cc | 70 + tools/epf.h | 21 + tools/epf_main.cc | 67 + tools/example_tree.txt | 50 + tools/fields_fuzzer.cc | 85 + tools/flicker_test/CMakeLists.txt | 38 + tools/flicker_test/main.cc | 22 + tools/flicker_test/parameters.cc | 83 + tools/flicker_test/parameters.h | 31 + tools/flicker_test/setup.cc | 149 + tools/flicker_test/setup.h | 44 + tools/flicker_test/setup.ui | 375 +++ tools/flicker_test/split_view.cc | 167 ++ tools/flicker_test/split_view.h | 84 + tools/flicker_test/test_window.cc | 183 ++ tools/flicker_test/test_window.h | 50 + tools/flicker_test/test_window.ui | 115 + tools/fuzzer_corpus.cc | 457 +++ tools/fuzzer_stub.cc | 45 + tools/git_version.cmake | 34 + tools/hdr/README.md | 86 + tools/hdr/pq_to_hlg.cc | 108 + tools/hdr/render_hlg.cc | 127 + tools/hdr/tone_map.cc | 82 + tools/icc_codec_fuzzer.cc | 68 + tools/icc_detect/icc_detect.h | 19 + tools/icc_detect/icc_detect_empty.cc | 14 + tools/icc_detect/icc_detect_win32.cc | 64 + tools/icc_detect/icc_detect_x11.cc | 77 + tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java | 39 + tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java | 73 + tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java | 127 + tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java | 25 + tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java | 13 + tools/jni/org/jpeg/jpegxl/wrapper/Status.java | 17 + tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java | 18 + tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc | 276 ++ tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h | 43 + .../org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc | 45 + tools/jxl_emcc.cc | 66 + tools/jxl_from_tree.cc | 490 ++++ tools/libjxl_test.c | 17 + tools/optimizer/simplex_fork.py | 255 ++ tools/ossfuzz-build.sh | 71 + tools/progressive_saliency.conf | 32 + tools/progressive_sizes.sh | 27 + tools/rans_fuzzer.cc | 46 + tools/reference_zip.sh | 74 + tools/set_from_bytes_fuzzer.cc | 33 + tools/speed_stats.cc | 107 + tools/speed_stats.h | 63 + tools/ssimulacra.cc | 325 +++ tools/ssimulacra.cpp | 382 +++ tools/ssimulacra.h | 34 + tools/ssimulacra_main.cc | 61 + tools/tool_version.cc | 18 + tools/tool_version.h | 22 + tools/transforms_fuzzer.cc | 147 + .../generate_upscaling_coefficients.py | 243 ++ tools/viewer/CMakeLists.txt | 39 + tools/viewer/load_jxl.cc | 174 ++ tools/viewer/load_jxl.h | 20 + tools/viewer/main.cc | 23 + tools/viewer/viewer_window.cc | 130 + tools/viewer/viewer_window.h | 41 + tools/viewer/viewer_window.ui | 125 + tools/xyb_range.cc | 78 + 811 files changed, 134728 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/build_test.yml create mode 100644 .github/workflows/debug_ci.yml create mode 100644 .github/workflows/fuzz.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitmodules create mode 100644 AUTHORS create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 PATENTS create mode 100644 README.Haiku.md create mode 100644 README.OSX.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100755 bash_test.sh create mode 100755 ci.sh create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/jxl.install create mode 100644 debian/libjxl-dev.install create mode 100644 debian/libjxl-gdk-pixbuf.install create mode 100644 debian/libjxl-gimp-plugin.install create mode 100644 debian/libjxl.install create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100755 deps.sh create mode 100644 doc/api.txt create mode 100644 doc/benchmarking.md create mode 100644 doc/building_and_testing.md create mode 100644 doc/building_wasm.md create mode 100644 doc/developing_in_debian.md create mode 100644 doc/developing_in_docker.md create mode 100644 doc/developing_in_github.md create mode 100644 doc/developing_in_windows_msys.md create mode 100644 doc/developing_in_windows_vcpkg.md create mode 100644 doc/developing_with_crossroad.md create mode 100644 doc/fuzzing.md create mode 100644 doc/jxl.svg create mode 100644 doc/man/cjxl.txt create mode 100644 doc/man/djxl.txt create mode 100644 doc/release.md create mode 100644 doc/software_support.md create mode 100644 doc/tables/adobe.md create mode 100644 doc/tables/all_tables.pdf create mode 100755 doc/tables/all_tables.sh create mode 100644 doc/tables/app0.md create mode 100644 doc/tables/brn_proto.md create mode 100644 doc/tables/context_modes.md create mode 100644 doc/tables/dct_gen.md create mode 100644 doc/tables/ducky.md create mode 100644 doc/tables/freq_context.md create mode 100644 doc/tables/icc.md create mode 100644 doc/tables/is_zero_base.md create mode 100644 doc/tables/markdown-pdf.css create mode 100644 doc/tables/nonzero_buckets.md create mode 100644 doc/tables/num_nonzero_context.md create mode 100644 doc/tables/num_nonzeros_base.md create mode 100644 doc/tables/quant.md create mode 100644 doc/tables/stock_counts.md create mode 100644 doc/tables/stock_quant.md create mode 100644 doc/tables/stock_values.md create mode 100644 doc/tables/symbol_order.md create mode 100644 doc/xl_overview.md create mode 100644 docker/Dockerfile.jpegxl-builder create mode 100644 docker/Dockerfile.jpegxl-builder-run-aarch64 create mode 100644 docker/README.md create mode 100755 docker/build.sh create mode 100644 docker/scripts/99_norecommends create mode 100644 docker/scripts/binutils_align_fix.patch create mode 100755 docker/scripts/emsdk_install.sh create mode 100755 docker/scripts/jpegxl_builder.sh create mode 100755 docker/scripts/msan_install.sh create mode 100755 docker/scripts/qemu_install.sh create mode 100644 examples/CMakeLists.txt create mode 100644 examples/decode_oneshot.cc create mode 100644 examples/encode_oneshot.cc create mode 100644 examples/examples.cmake create mode 100644 examples/jxlinfo.c create mode 100755 js-wasm-wrapper.sh create mode 100644 lib/CMakeLists.txt create mode 100644 lib/extras/README.md create mode 100644 lib/extras/codec.cc create mode 100644 lib/extras/codec.h create mode 100644 lib/extras/codec_apng.cc create mode 100644 lib/extras/codec_apng.h create mode 100644 lib/extras/codec_exr.cc create mode 100644 lib/extras/codec_exr.h create mode 100644 lib/extras/codec_gif.cc create mode 100644 lib/extras/codec_gif.h create mode 100644 lib/extras/codec_jpg.cc create mode 100644 lib/extras/codec_jpg.h create mode 100644 lib/extras/codec_pgx.cc create mode 100644 lib/extras/codec_pgx.h create mode 100644 lib/extras/codec_pgx_test.cc create mode 100644 lib/extras/codec_png.cc create mode 100644 lib/extras/codec_png.h create mode 100644 lib/extras/codec_pnm.cc create mode 100644 lib/extras/codec_pnm.h create mode 100644 lib/extras/codec_psd.cc create mode 100644 lib/extras/codec_psd.h create mode 100644 lib/extras/codec_test.cc create mode 100644 lib/extras/color_description.cc create mode 100644 lib/extras/color_description.h create mode 100644 lib/extras/color_description_test.cc create mode 100644 lib/extras/color_hints.cc create mode 100644 lib/extras/color_hints.h create mode 100644 lib/extras/time.cc create mode 100644 lib/extras/time.h create mode 100644 lib/extras/tone_mapping.cc create mode 100644 lib/extras/tone_mapping.h create mode 100644 lib/extras/tone_mapping_gbench.cc create mode 100644 lib/include/jxl/butteraugli.h create mode 100644 lib/include/jxl/butteraugli_cxx.h create mode 100644 lib/include/jxl/codestream_header.h create mode 100644 lib/include/jxl/color_encoding.h create mode 100644 lib/include/jxl/decode.h create mode 100644 lib/include/jxl/decode_cxx.h create mode 100644 lib/include/jxl/encode.h create mode 100644 lib/include/jxl/encode_cxx.h create mode 100644 lib/include/jxl/memory_manager.h create mode 100644 lib/include/jxl/parallel_runner.h create mode 100644 lib/include/jxl/resizable_parallel_runner.h create mode 100644 lib/include/jxl/resizable_parallel_runner_cxx.h create mode 100644 lib/include/jxl/thread_parallel_runner.h create mode 100644 lib/include/jxl/thread_parallel_runner_cxx.h create mode 100644 lib/include/jxl/types.h create mode 100644 lib/jxl.cmake create mode 100644 lib/jxl/ac_context.h create mode 100644 lib/jxl/ac_strategy.cc create mode 100644 lib/jxl/ac_strategy.h create mode 100644 lib/jxl/ac_strategy_test.cc create mode 100644 lib/jxl/adaptive_reconstruction_test.cc create mode 100644 lib/jxl/alpha.cc create mode 100644 lib/jxl/alpha.h create mode 100644 lib/jxl/alpha_test.cc create mode 100644 lib/jxl/ans_common.cc create mode 100644 lib/jxl/ans_common.h create mode 100644 lib/jxl/ans_common_test.cc create mode 100644 lib/jxl/ans_params.h create mode 100644 lib/jxl/ans_test.cc create mode 100644 lib/jxl/aux_out.cc create mode 100644 lib/jxl/aux_out.h create mode 100644 lib/jxl/aux_out_fwd.h create mode 100644 lib/jxl/base/arch_macros.h create mode 100644 lib/jxl/base/bits.h create mode 100644 lib/jxl/base/byte_order.h create mode 100644 lib/jxl/base/cache_aligned.cc create mode 100644 lib/jxl/base/cache_aligned.h create mode 100644 lib/jxl/base/compiler_specific.h create mode 100644 lib/jxl/base/data_parallel.cc create mode 100644 lib/jxl/base/data_parallel.h create mode 100644 lib/jxl/base/descriptive_statistics.cc create mode 100644 lib/jxl/base/descriptive_statistics.h create mode 100644 lib/jxl/base/file_io.h create mode 100644 lib/jxl/base/iaca.h create mode 100644 lib/jxl/base/os_macros.h create mode 100644 lib/jxl/base/override.h create mode 100644 lib/jxl/base/padded_bytes.cc create mode 100644 lib/jxl/base/padded_bytes.h create mode 100644 lib/jxl/base/profiler.h create mode 100644 lib/jxl/base/robust_statistics.h create mode 100644 lib/jxl/base/span.h create mode 100644 lib/jxl/base/status.cc create mode 100644 lib/jxl/base/status.h create mode 100644 lib/jxl/base/thread_pool_internal.h create mode 100644 lib/jxl/bit_reader_test.cc create mode 100644 lib/jxl/bits_test.cc create mode 100644 lib/jxl/blending.cc create mode 100644 lib/jxl/blending.h create mode 100644 lib/jxl/blending_test.cc create mode 100644 lib/jxl/butteraugli/butteraugli.cc create mode 100644 lib/jxl/butteraugli/butteraugli.h create mode 100644 lib/jxl/butteraugli_test.cc create mode 100644 lib/jxl/butteraugli_wrapper.cc create mode 100644 lib/jxl/byte_order_test.cc create mode 100644 lib/jxl/chroma_from_luma.cc create mode 100644 lib/jxl/chroma_from_luma.h create mode 100644 lib/jxl/codec_in_out.h create mode 100644 lib/jxl/coeff_order.cc create mode 100644 lib/jxl/coeff_order.h create mode 100644 lib/jxl/coeff_order_fwd.h create mode 100644 lib/jxl/coeff_order_test.cc create mode 100644 lib/jxl/color_encoding_internal.cc create mode 100644 lib/jxl/color_encoding_internal.h create mode 100644 lib/jxl/color_encoding_internal_test.cc create mode 100644 lib/jxl/color_management.cc create mode 100644 lib/jxl/color_management.h create mode 100644 lib/jxl/color_management_test.cc create mode 100644 lib/jxl/common.h create mode 100644 lib/jxl/compressed_dc.cc create mode 100644 lib/jxl/compressed_dc.h create mode 100644 lib/jxl/compressed_image_test.cc create mode 100644 lib/jxl/convolve-inl.h create mode 100644 lib/jxl/convolve.cc create mode 100644 lib/jxl/convolve.h create mode 100644 lib/jxl/convolve_test.cc create mode 100644 lib/jxl/data_parallel_test.cc create mode 100644 lib/jxl/dct-inl.h create mode 100644 lib/jxl/dct_block-inl.h create mode 100644 lib/jxl/dct_for_test.h create mode 100644 lib/jxl/dct_scales.cc create mode 100644 lib/jxl/dct_scales.h create mode 100644 lib/jxl/dct_test.cc create mode 100644 lib/jxl/dct_util.h create mode 100644 lib/jxl/dec_ans.cc create mode 100644 lib/jxl/dec_ans.h create mode 100644 lib/jxl/dec_bit_reader.h create mode 100644 lib/jxl/dec_cache.cc create mode 100644 lib/jxl/dec_cache.h create mode 100644 lib/jxl/dec_context_map.cc create mode 100644 lib/jxl/dec_context_map.h create mode 100644 lib/jxl/dec_external_image.cc create mode 100644 lib/jxl/dec_external_image.h create mode 100644 lib/jxl/dec_external_image_gbench.cc create mode 100644 lib/jxl/dec_file.cc create mode 100644 lib/jxl/dec_file.h create mode 100644 lib/jxl/dec_frame.cc create mode 100644 lib/jxl/dec_frame.h create mode 100644 lib/jxl/dec_group.cc create mode 100644 lib/jxl/dec_group.h create mode 100644 lib/jxl/dec_group_border.cc create mode 100644 lib/jxl/dec_group_border.h create mode 100644 lib/jxl/dec_huffman.cc create mode 100644 lib/jxl/dec_huffman.h create mode 100644 lib/jxl/dec_modular.cc create mode 100644 lib/jxl/dec_modular.h create mode 100644 lib/jxl/dec_noise.cc create mode 100644 lib/jxl/dec_noise.h create mode 100644 lib/jxl/dec_params.h create mode 100644 lib/jxl/dec_patch_dictionary.cc create mode 100644 lib/jxl/dec_patch_dictionary.h create mode 100644 lib/jxl/dec_reconstruct.cc create mode 100644 lib/jxl/dec_reconstruct.h create mode 100644 lib/jxl/dec_render_pipeline.h create mode 100644 lib/jxl/dec_transforms-inl.h create mode 100644 lib/jxl/dec_transforms_testonly.cc create mode 100644 lib/jxl/dec_transforms_testonly.h create mode 100644 lib/jxl/dec_upsample.cc create mode 100644 lib/jxl/dec_upsample.h create mode 100644 lib/jxl/dec_xyb-inl.h create mode 100644 lib/jxl/dec_xyb.cc create mode 100644 lib/jxl/dec_xyb.h create mode 100644 lib/jxl/decode.cc create mode 100644 lib/jxl/decode_test.cc create mode 100644 lib/jxl/decode_to_jpeg.cc create mode 100644 lib/jxl/decode_to_jpeg.h create mode 100644 lib/jxl/descriptive_statistics_test.cc create mode 100644 lib/jxl/docs/color_management.md create mode 100644 lib/jxl/docs/dc_predictor.md create mode 100644 lib/jxl/docs/entropy_coding_basic.md create mode 100644 lib/jxl/docs/file_format.md create mode 100644 lib/jxl/docs/upsample.md create mode 100644 lib/jxl/enc_ac_strategy.cc create mode 100644 lib/jxl/enc_ac_strategy.h create mode 100644 lib/jxl/enc_adaptive_quantization.cc create mode 100644 lib/jxl/enc_adaptive_quantization.h create mode 100644 lib/jxl/enc_ans.cc create mode 100644 lib/jxl/enc_ans.h create mode 100644 lib/jxl/enc_ans_params.h create mode 100644 lib/jxl/enc_ar_control_field.cc create mode 100644 lib/jxl/enc_ar_control_field.h create mode 100644 lib/jxl/enc_bit_writer.cc create mode 100644 lib/jxl/enc_bit_writer.h create mode 100644 lib/jxl/enc_butteraugli_comparator.cc create mode 100644 lib/jxl/enc_butteraugli_comparator.h create mode 100644 lib/jxl/enc_butteraugli_pnorm.cc create mode 100644 lib/jxl/enc_butteraugli_pnorm.h create mode 100644 lib/jxl/enc_cache.cc create mode 100644 lib/jxl/enc_cache.h create mode 100644 lib/jxl/enc_chroma_from_luma.cc create mode 100644 lib/jxl/enc_chroma_from_luma.h create mode 100644 lib/jxl/enc_cluster.cc create mode 100644 lib/jxl/enc_cluster.h create mode 100644 lib/jxl/enc_coeff_order.cc create mode 100644 lib/jxl/enc_coeff_order.h create mode 100644 lib/jxl/enc_color_management.cc create mode 100644 lib/jxl/enc_color_management.h create mode 100644 lib/jxl/enc_comparator.cc create mode 100644 lib/jxl/enc_comparator.h create mode 100644 lib/jxl/enc_context_map.cc create mode 100644 lib/jxl/enc_context_map.h create mode 100644 lib/jxl/enc_detect_dots.cc create mode 100644 lib/jxl/enc_detect_dots.h create mode 100644 lib/jxl/enc_dot_dictionary.cc create mode 100644 lib/jxl/enc_dot_dictionary.h create mode 100644 lib/jxl/enc_entropy_coder.cc create mode 100644 lib/jxl/enc_entropy_coder.h create mode 100644 lib/jxl/enc_external_image.cc create mode 100644 lib/jxl/enc_external_image.h create mode 100644 lib/jxl/enc_external_image_gbench.cc create mode 100644 lib/jxl/enc_external_image_test.cc create mode 100644 lib/jxl/enc_fast_heuristics.cc create mode 100644 lib/jxl/enc_file.cc create mode 100644 lib/jxl/enc_file.h create mode 100644 lib/jxl/enc_frame.cc create mode 100644 lib/jxl/enc_frame.h create mode 100644 lib/jxl/enc_gamma_correct.h create mode 100644 lib/jxl/enc_group.cc create mode 100644 lib/jxl/enc_group.h create mode 100644 lib/jxl/enc_heuristics.cc create mode 100644 lib/jxl/enc_heuristics.h create mode 100644 lib/jxl/enc_huffman.cc create mode 100644 lib/jxl/enc_huffman.h create mode 100644 lib/jxl/enc_icc_codec.cc create mode 100644 lib/jxl/enc_icc_codec.h create mode 100644 lib/jxl/enc_image_bundle.cc create mode 100644 lib/jxl/enc_image_bundle.h create mode 100644 lib/jxl/enc_jxl_skcms.h create mode 100644 lib/jxl/enc_modular.cc create mode 100644 lib/jxl/enc_modular.h create mode 100644 lib/jxl/enc_noise.cc create mode 100644 lib/jxl/enc_noise.h create mode 100644 lib/jxl/enc_params.h create mode 100644 lib/jxl/enc_patch_dictionary.cc create mode 100644 lib/jxl/enc_patch_dictionary.h create mode 100644 lib/jxl/enc_photon_noise.cc create mode 100644 lib/jxl/enc_photon_noise.h create mode 100644 lib/jxl/enc_photon_noise_test.cc create mode 100644 lib/jxl/enc_quant_weights.cc create mode 100644 lib/jxl/enc_quant_weights.h create mode 100644 lib/jxl/enc_splines.cc create mode 100644 lib/jxl/enc_splines.h create mode 100644 lib/jxl/enc_toc.cc create mode 100644 lib/jxl/enc_toc.h create mode 100644 lib/jxl/enc_transforms-inl.h create mode 100644 lib/jxl/enc_transforms.cc create mode 100644 lib/jxl/enc_transforms.h create mode 100644 lib/jxl/enc_xyb.cc create mode 100644 lib/jxl/enc_xyb.h create mode 100644 lib/jxl/encode.cc create mode 100644 lib/jxl/encode_internal.h create mode 100644 lib/jxl/encode_test.cc create mode 100644 lib/jxl/entropy_coder.cc create mode 100644 lib/jxl/entropy_coder.h create mode 100644 lib/jxl/entropy_coder_test.cc create mode 100644 lib/jxl/epf.cc create mode 100644 lib/jxl/epf.h create mode 100644 lib/jxl/fake_parallel_runner_testonly.h create mode 100644 lib/jxl/fast_math-inl.h create mode 100644 lib/jxl/fast_math_test.cc create mode 100644 lib/jxl/field_encodings.h create mode 100644 lib/jxl/fields.cc create mode 100644 lib/jxl/fields.h create mode 100644 lib/jxl/fields_test.cc create mode 100644 lib/jxl/filters.cc create mode 100644 lib/jxl/filters.h create mode 100644 lib/jxl/filters_internal.h create mode 100644 lib/jxl/filters_internal_test.cc create mode 100644 lib/jxl/frame_header.cc create mode 100644 lib/jxl/frame_header.h create mode 100644 lib/jxl/gaborish.cc create mode 100644 lib/jxl/gaborish.h create mode 100644 lib/jxl/gaborish_test.cc create mode 100644 lib/jxl/gamma_correct_test.cc create mode 100644 lib/jxl/gauss_blur.cc create mode 100644 lib/jxl/gauss_blur.h create mode 100644 lib/jxl/gauss_blur_test.cc create mode 100644 lib/jxl/gradient_test.cc create mode 100644 lib/jxl/headers.cc create mode 100644 lib/jxl/headers.h create mode 100644 lib/jxl/huffman_table.cc create mode 100644 lib/jxl/huffman_table.h create mode 100644 lib/jxl/huffman_tree.cc create mode 100644 lib/jxl/huffman_tree.h create mode 100644 lib/jxl/iaca_test.cc create mode 100644 lib/jxl/icc_codec.cc create mode 100644 lib/jxl/icc_codec.h create mode 100644 lib/jxl/icc_codec_common.cc create mode 100644 lib/jxl/icc_codec_common.h create mode 100644 lib/jxl/icc_codec_test.cc create mode 100644 lib/jxl/image.cc create mode 100644 lib/jxl/image.h create mode 100644 lib/jxl/image_bundle.cc create mode 100644 lib/jxl/image_bundle.h create mode 100644 lib/jxl/image_bundle_test.cc create mode 100644 lib/jxl/image_metadata.cc create mode 100644 lib/jxl/image_metadata.h create mode 100644 lib/jxl/image_ops.h create mode 100644 lib/jxl/image_ops_test.cc create mode 100644 lib/jxl/image_test_utils.h create mode 100644 lib/jxl/jpeg/dec_jpeg_data.cc create mode 100644 lib/jxl/jpeg/dec_jpeg_data.h create mode 100644 lib/jxl/jpeg/dec_jpeg_data_writer.cc create mode 100644 lib/jxl/jpeg/dec_jpeg_data_writer.h create mode 100644 lib/jxl/jpeg/dec_jpeg_output_chunk.h create mode 100644 lib/jxl/jpeg/dec_jpeg_serialization_state.h create mode 100644 lib/jxl/jpeg/enc_jpeg_data.cc create mode 100644 lib/jxl/jpeg/enc_jpeg_data.h create mode 100644 lib/jxl/jpeg/enc_jpeg_data_reader.cc create mode 100644 lib/jxl/jpeg/enc_jpeg_data_reader.h create mode 100644 lib/jxl/jpeg/enc_jpeg_huffman_decode.cc create mode 100644 lib/jxl/jpeg/enc_jpeg_huffman_decode.h create mode 100644 lib/jxl/jpeg/jpeg_data.cc create mode 100644 lib/jxl/jpeg/jpeg_data.h create mode 100644 lib/jxl/jxl.syms create mode 100644 lib/jxl/jxl.version create mode 100644 lib/jxl/jxl_inspection.h create mode 100644 lib/jxl/jxl_osx.syms create mode 100644 lib/jxl/jxl_test.cc create mode 100644 lib/jxl/lehmer_code.h create mode 100644 lib/jxl/lehmer_code_test.cc create mode 100644 lib/jxl/libjxl.pc.in create mode 100644 lib/jxl/linalg.cc create mode 100644 lib/jxl/linalg.h create mode 100644 lib/jxl/linalg_test.cc create mode 100644 lib/jxl/loop_filter.cc create mode 100644 lib/jxl/loop_filter.h create mode 100644 lib/jxl/luminance.cc create mode 100644 lib/jxl/luminance.h create mode 100644 lib/jxl/memory_manager_internal.cc create mode 100644 lib/jxl/memory_manager_internal.h create mode 100644 lib/jxl/modular/encoding/context_predict.h create mode 100644 lib/jxl/modular/encoding/dec_ma.cc create mode 100644 lib/jxl/modular/encoding/dec_ma.h create mode 100644 lib/jxl/modular/encoding/enc_encoding.cc create mode 100644 lib/jxl/modular/encoding/enc_encoding.h create mode 100644 lib/jxl/modular/encoding/enc_ma.cc create mode 100644 lib/jxl/modular/encoding/enc_ma.h create mode 100644 lib/jxl/modular/encoding/encoding.cc create mode 100644 lib/jxl/modular/encoding/encoding.h create mode 100644 lib/jxl/modular/encoding/ma_common.h create mode 100644 lib/jxl/modular/modular_image.cc create mode 100644 lib/jxl/modular/modular_image.h create mode 100644 lib/jxl/modular/options.h create mode 100644 lib/jxl/modular/transform/enc_palette.cc create mode 100644 lib/jxl/modular/transform/enc_palette.h create mode 100644 lib/jxl/modular/transform/enc_rct.cc create mode 100644 lib/jxl/modular/transform/enc_rct.h create mode 100644 lib/jxl/modular/transform/enc_squeeze.cc create mode 100644 lib/jxl/modular/transform/enc_squeeze.h create mode 100644 lib/jxl/modular/transform/enc_transform.cc create mode 100644 lib/jxl/modular/transform/enc_transform.h create mode 100644 lib/jxl/modular/transform/palette.h create mode 100644 lib/jxl/modular/transform/rct.h create mode 100644 lib/jxl/modular/transform/squeeze.cc create mode 100644 lib/jxl/modular/transform/squeeze.h create mode 100644 lib/jxl/modular/transform/transform.cc create mode 100644 lib/jxl/modular/transform/transform.h create mode 100644 lib/jxl/modular_test.cc create mode 100644 lib/jxl/noise.h create mode 100644 lib/jxl/noise_distributions.h create mode 100644 lib/jxl/opsin_image_test.cc create mode 100644 lib/jxl/opsin_inverse_test.cc create mode 100644 lib/jxl/opsin_params.cc create mode 100644 lib/jxl/opsin_params.h create mode 100644 lib/jxl/optimize.cc create mode 100644 lib/jxl/optimize.h create mode 100644 lib/jxl/optimize_test.cc create mode 100644 lib/jxl/padded_bytes_test.cc create mode 100644 lib/jxl/passes_state.cc create mode 100644 lib/jxl/passes_state.h create mode 100644 lib/jxl/passes_test.cc create mode 100644 lib/jxl/patch_dictionary_internal.h create mode 100644 lib/jxl/patch_dictionary_test.cc create mode 100644 lib/jxl/preview_test.cc create mode 100644 lib/jxl/progressive_split.cc create mode 100644 lib/jxl/progressive_split.h create mode 100644 lib/jxl/quant_weights.cc create mode 100644 lib/jxl/quant_weights.h create mode 100644 lib/jxl/quant_weights_test.cc create mode 100644 lib/jxl/quantizer-inl.h create mode 100644 lib/jxl/quantizer.cc create mode 100644 lib/jxl/quantizer.h create mode 100644 lib/jxl/quantizer_test.cc create mode 100644 lib/jxl/rational_polynomial-inl.h create mode 100644 lib/jxl/rational_polynomial_test.cc create mode 100644 lib/jxl/robust_statistics_test.cc create mode 100644 lib/jxl/roundtrip_test.cc create mode 100644 lib/jxl/sanitizers.h create mode 100644 lib/jxl/speed_tier_test.cc create mode 100644 lib/jxl/splines.cc create mode 100644 lib/jxl/splines.h create mode 100644 lib/jxl/splines_gbench.cc create mode 100644 lib/jxl/splines_test.cc create mode 100644 lib/jxl/test_utils.h create mode 100644 lib/jxl/testdata.h create mode 100644 lib/jxl/tf_gbench.cc create mode 100644 lib/jxl/toc.cc create mode 100644 lib/jxl/toc.h create mode 100644 lib/jxl/toc_test.cc create mode 100644 lib/jxl/transfer_functions-inl.h create mode 100644 lib/jxl/transpose-inl.h create mode 100644 lib/jxl/xorshift128plus-inl.h create mode 100644 lib/jxl/xorshift128plus_test.cc create mode 100644 lib/jxl_benchmark.cmake create mode 100644 lib/jxl_extras.cmake create mode 100644 lib/jxl_profiler.cmake create mode 100644 lib/jxl_tests.cmake create mode 100644 lib/jxl_threads.cmake create mode 100644 lib/lib.gni create mode 100644 lib/profiler/profiler.cc create mode 100644 lib/profiler/profiler.h create mode 100644 lib/profiler/tsc_timer.h create mode 100644 lib/threads/libjxl_threads.pc.in create mode 100644 lib/threads/resizable_parallel_runner.cc create mode 100644 lib/threads/thread_parallel_runner.cc create mode 100644 lib/threads/thread_parallel_runner_internal.cc create mode 100644 lib/threads/thread_parallel_runner_internal.h create mode 100644 lib/threads/thread_parallel_runner_test.cc create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/gdk-pixbuf/CMakeLists.txt create mode 100644 plugins/gdk-pixbuf/README.md create mode 100644 plugins/gdk-pixbuf/jxl.thumbnailer create mode 100644 plugins/gdk-pixbuf/loaders_test.cache create mode 100644 plugins/gdk-pixbuf/pixbufloader-jxl.c create mode 100644 plugins/gdk-pixbuf/pixbufloader_test.cc create mode 100644 plugins/gimp/CMakeLists.txt create mode 100644 plugins/gimp/common.cc create mode 100644 plugins/gimp/common.h create mode 100644 plugins/gimp/file-jxl-load.cc create mode 100644 plugins/gimp/file-jxl-load.h create mode 100644 plugins/gimp/file-jxl-save.cc create mode 100644 plugins/gimp/file-jxl-save.h create mode 100644 plugins/gimp/file-jxl.cc create mode 100644 plugins/mime/CMakeLists.txt create mode 100644 plugins/mime/README.md create mode 100644 plugins/mime/image-jxl.xml create mode 100644 third_party/CMakeLists.txt create mode 100644 third_party/HEVCSoftware/README.md create mode 100644 third_party/HEVCSoftware/cfg/LICENSE create mode 100644 third_party/HEVCSoftware/cfg/encoder_intra_main_scc_10.cfg create mode 100644 third_party/dirent.cc create mode 100644 third_party/dirent.h create mode 100644 third_party/lcms2.cmake create mode 100644 third_party/lodepng.cmake create mode 100644 third_party/sjpeg.cmake create mode 100644 third_party/skcms.cmake create mode 100644 third_party/testdata/dots/ellipses.png create mode 100644 third_party/testdata/imagecompression.info/LICENSE.txt create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.ffmpeg.y4m create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_420.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_420_progr.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_422.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_440.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_444.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_444_1x2.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_asymmetric.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_gray.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_luma_subsample.jpg create mode 100644 third_party/testdata/imagecompression.info/flower_foveon_cropped.jpg create mode 100644 third_party/testdata/jxl/animation_patches.gif create mode 100644 third_party/testdata/jxl/blending/cropped_traffic_light.jxl create mode 100644 third_party/testdata/jxl/blending/cropped_traffic_light_frame-0.png create mode 100644 third_party/testdata/jxl/blending/cropped_traffic_light_frame-1.png create mode 100644 third_party/testdata/jxl/blending/cropped_traffic_light_frame-2.png create mode 100644 third_party/testdata/jxl/blending/cropped_traffic_light_frame-3.png create mode 100644 third_party/testdata/jxl/blending/grayscale_patches_on_splines.png create mode 100644 third_party/testdata/jxl/color_management/sRGB-D2700.icc create mode 100644 third_party/testdata/jxl/grayscale_patches.png create mode 100644 third_party/testdata/jxl/spline_on_first_frame.jxl create mode 100644 third_party/testdata/jxl/spline_on_first_frame.png create mode 100644 third_party/testdata/jxl/splines.png create mode 100644 third_party/testdata/jxl/traffic_light.gif create mode 100644 third_party/testdata/pngsuite/PngSuite.LICENSE create mode 100644 third_party/testdata/pngsuite/PngSuite.README create mode 100644 third_party/testdata/pngsuite/README.md create mode 100644 third_party/testdata/pngsuite/ccwn2c08.png create mode 100644 third_party/testdata/pngsuite/ccwn3p08.png create mode 100644 third_party/testdata/pngsuite/ct1n0g04.png create mode 100644 third_party/testdata/pngsuite/ctjn0g04.png create mode 100644 third_party/testdata/pngsuite/ctzn0g04.png create mode 100644 third_party/testdata/pngsuite/exif2c08.png create mode 100644 third_party/testdata/pngsuite/g04n2c08.png create mode 100644 third_party/testdata/pngsuite/g10n3p04.png create mode 100644 third_party/testdata/position_encoding/bike_dots.txt create mode 100644 third_party/testdata/position_encoding/keong_dots.txt create mode 100644 third_party/testdata/position_encoding/non_square.txt create mode 100644 third_party/testdata/position_encoding/repeated_dots.txt create mode 100644 third_party/testdata/position_encoding/woman_dots.txt create mode 100644 third_party/testdata/raw.pixls/DJI-FC6310-16bit_709_v4_krita.png create mode 100644 third_party/testdata/raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png create mode 100644 third_party/testdata/raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png create mode 100644 third_party/testdata/raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png create mode 100644 third_party/testdata/raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png create mode 100644 third_party/testdata/raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png create mode 100644 third_party/testdata/raw.pixls/LICENSE.md create mode 100644 third_party/testdata/raw.pixls/Nikon-D300-12bit_2020_g1_dt.png create mode 100644 third_party/testdata/raw.pixls/Nikon-D300-12bit_srgb8_dt.png create mode 100644 third_party/testdata/raw.pixls/README.md create mode 100644 third_party/testdata/raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png create mode 100644 third_party/testdata/raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png create mode 100644 third_party/testdata/wesaturate/500px/cvo9xd_keong_macan_grayscale.png create mode 100644 third_party/testdata/wesaturate/500px/cvo9xd_keong_macan_srgb8.png create mode 100644 third_party/testdata/wesaturate/500px/tmshre_riaphotographs_alpha.png create mode 100644 third_party/testdata/wesaturate/500px/tmshre_riaphotographs_srgb8.png create mode 100644 third_party/testdata/wesaturate/500px/u76c0g_bliznaca_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/Nikon-D3-14bit_2020_g1.png create mode 100644 third_party/testdata/wesaturate/64px/Nikon-D3-14bit_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/a2d1un_nkitzmiller_2020_g1.png create mode 100644 third_party/testdata/wesaturate/64px/a2d1un_nkitzmiller_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/cvo9xd_keong_macan_2020_g1.png create mode 100644 third_party/testdata/wesaturate/64px/cvo9xd_keong_macan_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/l1qnb5_nkitzmiller_2020_g1.png create mode 100644 third_party/testdata/wesaturate/64px/l1qnb5_nkitzmiller_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/phu1or_alfann24_2020_g1.png create mode 100644 third_party/testdata/wesaturate/64px/phu1or_alfann24_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/q3a0b3_d17ws_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/q3a0b3_d17ws_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/ra0ed45_alfann24_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/ra0ed45_alfann24_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/t0gho7_orlaustral_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/t0gho7_orlaustral_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/tmshre_riaphotographs_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/tmshre_riaphotographs_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/u76c0g_bliznaca_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/u76c0g_bliznaca_srgb8.png create mode 100644 third_party/testdata/wesaturate/64px/vgqcws_vin_709_g1.png create mode 100644 third_party/testdata/wesaturate/64px/vgqcws_vin_srgb8.png create mode 100644 third_party/testdata/wesaturate/LICENSE.md create mode 100644 third_party/testdata/wesaturate/README.md create mode 100644 third_party/testdata/wide-gamut-tests/LICENSE create mode 100644 third_party/testdata/wide-gamut-tests/P3-sRGB-blue.png create mode 100644 third_party/testdata/wide-gamut-tests/P3-sRGB-color-bars.png create mode 100644 third_party/testdata/wide-gamut-tests/P3-sRGB-color-ring.png create mode 100644 third_party/testdata/wide-gamut-tests/P3-sRGB-green.png create mode 100644 third_party/testdata/wide-gamut-tests/P3-sRGB-red.png create mode 100644 third_party/testdata/wide-gamut-tests/R2020-sRGB-blue.png create mode 100644 third_party/testdata/wide-gamut-tests/R2020-sRGB-color-bars.png create mode 100644 third_party/testdata/wide-gamut-tests/R2020-sRGB-color-ring.png create mode 100644 third_party/testdata/wide-gamut-tests/R2020-sRGB-green.png create mode 100644 third_party/testdata/wide-gamut-tests/R2020-sRGB-red.png create mode 100644 third_party/testdata/wide-gamut-tests/README.txt create mode 100644 third_party/testdata/wide-gamut-tests/rgb-to-gbr-test.png create mode 100644 tools/CMakeLists.txt create mode 100644 tools/args.h create mode 100644 tools/benchmark/benchmark_args.cc create mode 100644 tools/benchmark/benchmark_args.h create mode 100644 tools/benchmark/benchmark_codec.cc create mode 100644 tools/benchmark/benchmark_codec.h create mode 100644 tools/benchmark/benchmark_codec_avif.cc create mode 100644 tools/benchmark/benchmark_codec_avif.h create mode 100644 tools/benchmark/benchmark_codec_custom.cc create mode 100644 tools/benchmark/benchmark_codec_custom.h create mode 100644 tools/benchmark/benchmark_codec_jpeg.cc create mode 100644 tools/benchmark/benchmark_codec_jpeg.h create mode 100644 tools/benchmark/benchmark_codec_jxl.cc create mode 100644 tools/benchmark/benchmark_codec_jxl.h create mode 100644 tools/benchmark/benchmark_codec_png.cc create mode 100644 tools/benchmark/benchmark_codec_png.h create mode 100644 tools/benchmark/benchmark_codec_webp.cc create mode 100644 tools/benchmark/benchmark_codec_webp.h create mode 100644 tools/benchmark/benchmark_file_io.cc create mode 100644 tools/benchmark/benchmark_file_io.h create mode 100644 tools/benchmark/benchmark_stats.cc create mode 100644 tools/benchmark/benchmark_stats.h create mode 100644 tools/benchmark/benchmark_utils.cc create mode 100644 tools/benchmark/benchmark_utils.h create mode 100644 tools/benchmark/benchmark_xl.cc create mode 100644 tools/benchmark/hm/README.md create mode 100755 tools/benchmark/hm/decode.sh create mode 100755 tools/benchmark/hm/encode.sh create mode 100644 tools/benchmark/metrics/compute-hdrvdp.m create mode 100644 tools/benchmark/metrics/compute-pumetrics.m create mode 100755 tools/benchmark/metrics/compute_octave_metric.sh create mode 120000 tools/benchmark/metrics/dists-rgb.sh create mode 120000 tools/benchmark/metrics/fsim-rgb.sh create mode 120000 tools/benchmark/metrics/fsim-y.sh create mode 120000 tools/benchmark/metrics/gmsd-rgb.sh create mode 100755 tools/benchmark/metrics/hdr_plots.sh create mode 100644 tools/benchmark/metrics/hdrvdp-fixes.patch create mode 100755 tools/benchmark/metrics/hdrvdp.sh create mode 100644 tools/benchmark/metrics/iqa.py create mode 100755 tools/benchmark/metrics/iqa_wrapper.sh create mode 120000 tools/benchmark/metrics/lpips-rgb.sh create mode 100755 tools/benchmark/metrics/mrse.sh create mode 120000 tools/benchmark/metrics/msssim-rgb.sh create mode 120000 tools/benchmark/metrics/msssim-y.sh create mode 120000 tools/benchmark/metrics/nlpd-y.sh create mode 100755 tools/benchmark/metrics/plots.py create mode 100755 tools/benchmark/metrics/prepare_metrics.sh create mode 100755 tools/benchmark/metrics/pupsnr.sh create mode 100755 tools/benchmark/metrics/pussim.sh create mode 100755 tools/benchmark/metrics/run_all_hdr_metrics.sh create mode 100755 tools/benchmark/metrics/run_all_sdr_metrics.sh create mode 100755 tools/benchmark/metrics/sdr_plots.sh create mode 120000 tools/benchmark/metrics/ssim-rgb.sh create mode 120000 tools/benchmark/metrics/ssim-y.sh create mode 100755 tools/benchmark/metrics/ssimulacra.sh create mode 120000 tools/benchmark/metrics/vif-rgb.sh create mode 100755 tools/benchmark/metrics/vmaf.sh create mode 120000 tools/benchmark/metrics/vsi-rgb.sh create mode 100644 tools/box/CMakeLists.txt create mode 100644 tools/box/box.cc create mode 100644 tools/box/box.h create mode 100644 tools/box/box_list_main.cc create mode 100644 tools/box/box_test.cc create mode 100755 tools/build_cleaner.py create mode 100755 tools/build_stats.py create mode 100644 tools/butteraugli_main.cc create mode 100755 tools/check_author.py create mode 100644 tools/cjxl.cc create mode 100644 tools/cjxl.h create mode 100644 tools/cjxl_main.cc create mode 100644 tools/cmdline.cc create mode 100644 tools/cmdline.h create mode 100644 tools/codec_config.cc create mode 100644 tools/codec_config.h create mode 100644 tools/color_encoding_fuzzer.cc create mode 100644 tools/comparison_viewer/CMakeLists.txt create mode 100644 tools/comparison_viewer/codec_comparison_window.cc create mode 100644 tools/comparison_viewer/codec_comparison_window.h create mode 100644 tools/comparison_viewer/codec_comparison_window.ui create mode 100644 tools/comparison_viewer/compare_codecs.cc create mode 100644 tools/comparison_viewer/compare_images.cc create mode 100644 tools/comparison_viewer/image_loading.cc create mode 100644 tools/comparison_viewer/image_loading.h create mode 100644 tools/comparison_viewer/settings.cc create mode 100644 tools/comparison_viewer/settings.h create mode 100644 tools/comparison_viewer/settings.ui create mode 100644 tools/comparison_viewer/split_image_renderer.cc create mode 100644 tools/comparison_viewer/split_image_renderer.h create mode 100644 tools/comparison_viewer/split_image_view.cc create mode 100644 tools/comparison_viewer/split_image_view.h create mode 100644 tools/comparison_viewer/split_image_view.ui create mode 100644 tools/conformance/CMakeLists.txt create mode 100755 tools/conformance/conformance.py create mode 100644 tools/conformance/djxl_conformance.cc create mode 100755 tools/conformance/generator.py create mode 100755 tools/conformance/tooling_test.sh create mode 100644 tools/cpu/cpu.cc create mode 100644 tools/cpu/cpu.h create mode 100644 tools/cpu/os_specific.cc create mode 100644 tools/cpu/os_specific.h create mode 100644 tools/decode_and_encode.cc create mode 100644 tools/decode_basic_info_fuzzer.cc create mode 100755 tools/demo_progressive_saliency_encoding.py create mode 100755 tools/demo_vardct_select.sh create mode 100644 tools/djxl.cc create mode 100644 tools/djxl.h create mode 100644 tools/djxl_fuzzer.cc create mode 100644 tools/djxl_main.cc create mode 100644 tools/epf.cc create mode 100644 tools/epf.h create mode 100644 tools/epf_main.cc create mode 100644 tools/example_tree.txt create mode 100644 tools/fields_fuzzer.cc create mode 100644 tools/flicker_test/CMakeLists.txt create mode 100644 tools/flicker_test/main.cc create mode 100644 tools/flicker_test/parameters.cc create mode 100644 tools/flicker_test/parameters.h create mode 100644 tools/flicker_test/setup.cc create mode 100644 tools/flicker_test/setup.h create mode 100644 tools/flicker_test/setup.ui create mode 100644 tools/flicker_test/split_view.cc create mode 100644 tools/flicker_test/split_view.h create mode 100644 tools/flicker_test/test_window.cc create mode 100644 tools/flicker_test/test_window.h create mode 100644 tools/flicker_test/test_window.ui create mode 100644 tools/fuzzer_corpus.cc create mode 100644 tools/fuzzer_stub.cc create mode 100644 tools/git_version.cmake create mode 100644 tools/hdr/README.md create mode 100644 tools/hdr/pq_to_hlg.cc create mode 100644 tools/hdr/render_hlg.cc create mode 100644 tools/hdr/tone_map.cc create mode 100644 tools/icc_codec_fuzzer.cc create mode 100644 tools/icc_detect/icc_detect.h create mode 100644 tools/icc_detect/icc_detect_empty.cc create mode 100644 tools/icc_detect/icc_detect_win32.cc create mode 100644 tools/icc_detect/icc_detect_x11.cc create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/Decoder.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/DecoderJni.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/DecoderTest.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/ImageData.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/PixelFormat.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/Status.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/StreamInfo.java create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.cc create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni.h create mode 100644 tools/jni/org/jpeg/jpegxl/wrapper/decoder_jni_onload.cc create mode 100644 tools/jxl_emcc.cc create mode 100644 tools/jxl_from_tree.cc create mode 100644 tools/libjxl_test.c create mode 100755 tools/optimizer/simplex_fork.py create mode 100755 tools/ossfuzz-build.sh create mode 100644 tools/progressive_saliency.conf create mode 100755 tools/progressive_sizes.sh create mode 100644 tools/rans_fuzzer.cc create mode 100755 tools/reference_zip.sh create mode 100644 tools/set_from_bytes_fuzzer.cc create mode 100644 tools/speed_stats.cc create mode 100644 tools/speed_stats.h create mode 100644 tools/ssimulacra.cc create mode 100644 tools/ssimulacra.cpp create mode 100644 tools/ssimulacra.h create mode 100644 tools/ssimulacra_main.cc create mode 100644 tools/tool_version.cc create mode 100644 tools/tool_version.h create mode 100644 tools/transforms_fuzzer.cc create mode 100755 tools/upscaling_coefficients/generate_upscaling_coefficients.py create mode 100644 tools/viewer/CMakeLists.txt create mode 100644 tools/viewer/load_jxl.cc create mode 100644 tools/viewer/load_jxl.h create mode 100644 tools/viewer/main.cc create mode 100644 tools/viewer/viewer_window.cc create mode 100644 tools/viewer/viewer_window.h create mode 100644 tools/viewer/viewer_window.ui create mode 100644 tools/xyb_range.cc diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a61b61c --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: Google +IncludeCategories: + - Regex: '^ +# instead . +# - modernize-return-braced-init-list: this often doesn't improve readability. +# - modernize-use-auto: is too aggressive towards using auto. +# - modernize-use-default-member-init: with a mix of constructors and default +# member initialization this can be confusing if enforced. +# - modernize-use-trailing-return-type: does not improve readability when used +# systematically. +# - modernize-use-using: typedefs are ok. +# +# - readability-else-after-return: It doesn't always improve readability. +# - readability-static-accessed-through-instance +# It is often more useful and readable to access a constant of a passed +# variable (like d.N) instead of using the type of the variable that could be +# long and complex. +# - readability-uppercase-literal-suffix: we write 1.0f, not 1.0F. + +Checks: >- + bugprone-*, + clang-*, + -clang-diagnostic-unused-command-line-argument, + google-*, + modernize-*, + performance-*, + readability-*, + -google-readability-todo, + -modernize-deprecated-headers, + -modernize-return-braced-init-list, + -modernize-use-auto, + -modernize-use-default-member-init, + -modernize-use-trailing-return-type, + -modernize-use-using, + -readability-else-after-return, + -readability-function-cognitive-complexity, + -readability-static-accessed-through-instance, + -readability-uppercase-literal-suffix, + + +WarningsAsErrors: >- + bugprone-argument-comment, + bugprone-macro-parentheses, + bugprone-suspicious-string-compare, + bugprone-use-after-move, + clang-*, + clang-analyzer-*, + -clang-diagnostic-unused-command-line-argument, + google-build-using-namespace, + google-explicit-constructor, + google-readability-braces-around-statements, + google-readability-namespace-comments, + modernize-use-override, + readability-inconsistent-declaration-parameter-name + +# We are only interested in the headers from this projects, excluding +# third_party/ and build/. +HeaderFilterRegex: '^.*/(lib|tools)/.*\.h$' + +CheckOptions: + - key: readability-braces-around-statements.ShortStatementLines + value: '2' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '2' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: '1' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: '1' diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9acff15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots or example input/output images to help explain your problem. + +**Environment** + - OS: [e.g. Windows] + - Compiler version: [e.g. clang 11.0.1] + - CPU type: [e.g. x86_64] + - cjxl/djxl version string: [e.g. cjxl [v0.3.7 | SIMD supported: SSE4,Scalar]] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml new file mode 100644 index 0000000..046be56 --- /dev/null +++ b/.github/workflows/build_test.yml @@ -0,0 +1,324 @@ +# 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. + +# Workflow for building and running tests. + +name: Build/Test +on: + push: + branches: + - main + - v*.*.x + pull_request: + types: [opened, reopened, labeled, synchronize] + +jobs: + ubuntu_build: + name: Ubuntu Build ${{ matrix.name }} + runs-on: [ubuntu-latest] + strategy: + matrix: + # We have one job per "name" in the matrix. Attributes are set on the + # specific job names. + name: [release, debug, asan, msan, scalar] + include: + - name: release + test_in_pr: true + # Track static stack size on build and check it doesn't exceed 3 kB. + env_stack_size: 1 + max_stack: 3000 + - name: debug + # Build scalar-only hwy instructions. + - name: scalar + mode: release + cxxflags: -DHWY_DISABLED_TARGETS=~HWY_SCALAR + # Disabling optional features to speed up msan build a little bit. + - name: msan + cmake_args: >- + -DJPEGXL_ENABLE_DEVTOOLS=OFF -DJPEGXL_ENABLE_PLUGINS=OFF + -DJPEGXL_ENABLE_VIEWERS=OFF + # Build with support for decoding to JPEG bytes disabled. Produces a + # smaller build if only decoding to pixels is needed. + - name: release-nojpeg + mode: release + cxxflags: -DJXL_DEBUG_ON_ABORT=0 + cmake_args: >- + -DJPEGXL_ENABLE_TRANSCODE_JPEG=OFF + -DJPEGXL_ENABLE_PLUGINS=OFF + -DJPEGXL_ENABLE_VIEWERS=OFF + # Builds with gcc in release mode + - name: release:gcc8 + mode: release + apt_pkgs: gcc-8 g++-8 + cmake_args: >- + -DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 + + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + # Whether we track the stack size. + STACK_SIZE: ${{ matrix.env_stack_size }} + + steps: + - name: Install build deps + run: | + sudo apt update + sudo apt install -y \ + ccache \ + clang-7 \ + cmake \ + doxygen \ + libbrotli-dev \ + libgdk-pixbuf2.0-dev \ + libgif-dev \ + libgtest-dev \ + libgtk2.0-dev \ + libjpeg-dev \ + libopenexr-dev \ + libpng-dev \ + libwebp-dev \ + ninja-build \ + pkg-config \ + xvfb \ + ${{ matrix.apt_pkgs }} \ + # + echo "CC=clang-7" >> $GITHUB_ENV + echo "CXX=clang++-7" >> $GITHUB_ENV + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 2 + - name: Git environment + id: git-env + run: | + echo "::set-output name=parent::$(git rev-parse ${{ github.sha }}^)" + shell: bash + - name: ccache + uses: actions/cache@v2 + with: + path: ${{ env.CCACHE_DIR }} + # When the cache hits the key it is not updated, so if this is a rebuild + # of the same Pull Request it will reuse the cache if still around. For + # either Pull Requests or new pushes to main, this will use the parent + # hash as the starting point from the restore-keys entry. + key: ${{ runner.os }}-${{ github.sha }}-${{ matrix.name }} + restore-keys: | + ${{ runner.os }}-${{ steps.git-env.outputs.parent }}-${{ matrix.name }} + - name: Build + run: | + mkdir -p ${CCACHE_DIR} + echo "max_size = 200M" > ${CCACHE_DIR}/ccache.conf + mode="${{ matrix.mode }}" + [[ -n "${mode}" ]] || mode="${{ matrix.name }}" + ./ci.sh ${mode} -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + ${{ matrix.cmake_args }} + env: + SKIP_TEST: 1 + CMAKE_CXX_FLAGS: ${{ matrix.cxxflags }} + - name: ccache stats + run: ccache --show-stats + - name: Build stats ${{ matrix.name }} + if: matrix.mode == 'release' || matrix.name == 'release' + run: | + tools/build_stats.py --save build/stats.json \ + --max-stack ${{ matrix.max_stack || '0' }} \ + cjxl djxl libjxl.so libjxl_dec.so + # Check that we can build the example project against the installed libs. + - name: Install and build examples + if: matrix.mode == 'release' || matrix.name == 'release' + run: | + set -x + sudo cmake --build build -- install + cmake -Bbuild-example -Hexamples -G Ninja + cmake --build build-example + if ldd build-example/decode_oneshot_static | grep libjxl; then + echo "decode_oneshot_static is not using the static lib" >&2 + exit 1 + fi + # Test that the built binaries run. + echo -e -n "PF\n1 1\n-1.0\nrrrrggggbbbb" > test.pfm + build-example/encode_oneshot test.pfm test.jxl + build-example/encode_oneshot_static test.pfm test-static.jxl + build-example/decode_oneshot test.jxl dec.pfm dec.icc + build-example/decode_oneshot_static test.jxl dec-static.pfm dec-static.icc + # Run the tests on push and when requested in pull_request. + - name: Test ${{ matrix.mode }} + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && ( + matrix.test_in_pr || + contains(github.event.pull_request.labels.*.names, 'CI:full'))) + run: | + if [[ "${{ matrix.name }}" == "debug" ]]; then + # Runs on AVX3 CPUs require more stack than others. Make sure to test + # on AVX3-enabled CPUs when changing this value. + export TEST_STACK_LIMIT=2048 + fi + ./ci.sh test + - name: Fast benchmark ${{ matrix.mode }} + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && ( + matrix.test_in_pr || + contains(github.event.pull_request.labels.*.names, 'CI:full'))) + run: | + if [[ "${{ matrix.name }}" == "debug" ]]; then + export TEST_STACK_LIMIT=2048 + fi + STORE_IMAGES=0 ./ci.sh fast_benchmark + + + cross_compile_ubuntu: + name: Cross-compiling ${{ matrix.build_target }} + runs-on: [ubuntu-latest] + strategy: + matrix: + include: + - arch: arm64 + build_target: aarch64-linux-gnu + cmake_args: -DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/qemu-aarch64-static + + - arch: armhf + build_target: arm-linux-gnueabihf + cmake_args: -DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/qemu-arm-static + + - arch: i386 + build_target: i686-linux-gnu + + env: + BUILD_DIR: build + + steps: + - name: Setup apt + shell: bash + run: | + set -x + sudo apt-get update -y + sudo apt-get install -y curl gnupg ca-certificates + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1E9377A2BA9EF27F + + if [[ "${{ matrix.arch }}" != "amd64" ]]; then + sudo dpkg --add-architecture "${{ matrix.arch }}" + + # Update the sources.list with the split of supported architectures. + bkplist="/etc/apt/sources.list.bkp" + sudo mv /etc/apt/sources.list "${bkplist}" + + newlist="/etc/apt/sources.list" + sudo rm -f "${newlist}" + + main_list="amd64" + port_list="" + if [[ "${{ matrix.arch }}" == "i386" ]]; then + main_list="amd64,i386" + else + port_list="${{ matrix.arch }}" + fi + + if [[ -n "${port_list}" ]]; then + port_url="http://ports.ubuntu.com/ubuntu-ports/" + grep -v -E '^#' "${bkplist}" | + sed -E "s;^deb (http[^ ]+) (.*)\$;deb [arch=${{ matrix.arch }}] ${port_url} \\2;" \ + | sudo tee -a "${newlist}" + fi + grep -v -E '^#' "${bkplist}" | + sed -E "s;^deb (http[^ ]+) (.*)\$;deb [arch=${main_list}] \\1 \\2\ndeb-src [arch=${main_list}] \\1 \\2;" \ + | sudo tee -a "${newlist}" + fi + + - name: Install build deps + shell: bash + run: | + set -x + sudo apt update + pkgs=( + # Build dependencies + cmake + doxygen + libgtest-dev:${{ matrix.arch }} + ninja-build + pkg-config + qemu-user-static + xvfb + + # Toolchain for cross-compiling. + clang-7 + # libclang-common-7-dev:${{ matrix.arch }} + libc6-dev-${{ matrix.arch }}-cross + libstdc++-9-dev-${{ matrix.arch }}-cross + libstdc++-9-dev:${{ matrix.arch }} + + # Dependencies + libbrotli-dev:${{ matrix.arch }} + libgif-dev:${{ matrix.arch }} + libjpeg-dev:${{ matrix.arch }} + libpng-dev:${{ matrix.arch }} + libwebp-dev:${{ matrix.arch }} + + # For OpenEXR: + libilmbase-dev:${{ matrix.arch }} + libopenexr-dev:${{ matrix.arch }} + + # GTK plugins + libgdk-pixbuf2.0-dev + libgtk2.0-dev + + # QT + libqt5x11extras5-dev:${{ matrix.arch }} + qtbase5-dev:${{ matrix.arch }} + ) + if [[ "${{ matrix.build_target }}" != "x86_64-linux-gnu" ]]; then + pkgs+=( + binutils-${{ matrix.build_target }} + gcc-${{ matrix.build_target }} + ) + fi + if [[ "${{ matrix.arch }}" != "i386" ]]; then + pkgs+=( + # TCMalloc + libgoogle-perftools-dev:${{ matrix.arch }} + libgoogle-perftools4:${{ matrix.arch }} + libtcmalloc-minimal4:${{ matrix.arch }} + libunwind-dev:${{ matrix.arch }} + ) + fi + DEBIAN_FRONTEND=noninteractive sudo apt install -y "${pkgs[@]}" + echo "CC=clang-7" >> $GITHUB_ENV + echo "CXX=clang++-7" >> $GITHUB_ENV + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + - name: Build + run: | + ./ci.sh release \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_ENABLE_JNI=OFF \ + ${{ matrix.cmake_args }} + env: + SKIP_TEST: 1 + BUILD_TARGET: ${{ matrix.build_target }} + - name: Build stats ${{ matrix.build_target }} + run: | + tools/build_stats.py --save build/stats.json \ + --binutils ${{ matrix.build_target }}- \ + --max-stack ${{ matrix.max_stack || '0' }} \ + cjxl djxl libjxl.so libjxl_dec.so + # Run the tests on push and when requested in pull_request. + - name: Test + # Some tests have a small floating point error on i686. + # TODO(deymo): Re-enable i686 tests. + if: | + matrix.build_target != 'i686-linux-gnu' && ( + github.event_name == 'push' || + (github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.names, 'CI:full'))) + run: | + ./ci.sh test + env: + BUILD_TARGET: ${{ matrix.build_target }} diff --git a/.github/workflows/debug_ci.yml b/.github/workflows/debug_ci.yml new file mode 100644 index 0000000..726a848 --- /dev/null +++ b/.github/workflows/debug_ci.yml @@ -0,0 +1,59 @@ +# 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. + +# Workflow for building and then debugging on a specific commit. + +name: Build and Test debugging +on: + push: + branches: + - ci-*-debug + +jobs: + ubuntu_build: + name: Ubuntu Build and SSH + runs-on: [ubuntu-latest] + + steps: + - name: Install build deps + run: | + sudo apt update + sudo apt install -y \ + ccache \ + clang-7 \ + cmake \ + doxygen \ + libbrotli-dev \ + libgdk-pixbuf2.0-dev \ + libgif-dev \ + libgtest-dev \ + libgtk2.0-dev \ + libjpeg-dev \ + libopenexr-dev \ + libpng-dev \ + libwebp-dev \ + ninja-build \ + pkg-config \ + xvfb \ + ${{ matrix.apt_pkgs }} \ + # + echo "CC=clang-7" >> $GITHUB_ENV + echo "CXX=clang++-7" >> $GITHUB_ENV + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 2 + - name: Build + run: | + ./ci.sh $(echo ${{ github.ref }} | sed 's_refs/heads/ci-\([a-z]*\)-debug_\1_') \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON + env: + SKIP_TEST: 1 + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + + + diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..1a773e4 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,52 @@ +# 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. + +# CI on pull-requests to run the fuzzer from oss-fuzz. See: +# +# https://google.github.io/oss-fuzz/getting-started/continuous-integration/ + +name: CIFuzz +on: + pull_request: + types: [opened, reopened, synchronize] + paths: + - '**.c' + - '**.cc' + - '**.cmake' + - '**.h' + - '**CMakeLists.txt' + - .github/workflows/fuzz.yml + +jobs: + fuzzing: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v2 + id: checkout + with: + # The build_fuzzers action checks out the code to the storage/libjxl + # directory already, but doesn't check out the submodules. This step + # is a workaround for checking out the submodules. + path: storage/libjxl + submodules: true + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'libjxl' + language: c++ + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'libjxl' + language: c++ + fuzz-seconds: 600 + - name: Upload Crash + uses: actions/upload-artifact@v1 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..b1214e1 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,42 @@ +# 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. + +# Workflow to run pull-requests specific checks. + +name: PR +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + # Checks that the AUTHORS files is updated with new contributors. + authors: + runs-on: [ubuntu-latest] + steps: + - name: Checkout the source + uses: actions/checkout@v2 + - name: Check AUTHORS file + run: + ./ci.sh authors + + format: + runs-on: [ubuntu-latest] + steps: + - name: Install build deps + run: | + sudo apt update + sudo apt install -y \ + clang-format \ + clang-format-7 \ + clang-format-8 \ + clang-format-9 \ + clang-format-10 \ + clang-format-11 \ + # + - name: Checkout the source + uses: actions/checkout@v2 + - name: clang-format + run: + ./ci.sh lint >&2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f0b0c89 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,352 @@ +# 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. + +# Workflow for building the release binaries. +# +# This workflow runs as a post-submit step, when pushing to main or the release +# branches (v*.*.x), and when creating a release in GitHub. +# +# In the GitHub release case, in addition to build the release binaries it also +# uploads the binaries to the given release automatically. + +name: Release build / deploy +on: + push: + branches: + - main + - v*.*.x + release: + types: [ published ] + +jobs: + ubuntu_static_x86_64: + name: Release linux x86_64 static + runs-on: [ubuntu-latest] + steps: + - name: Install build deps + run: | + sudo apt update + sudo apt install -y \ + asciidoc \ + clang \ + cmake \ + doxygen \ + libbrotli-dev \ + libgdk-pixbuf2.0-dev \ + libgif-dev \ + libgtest-dev \ + libgtk2.0-dev \ + libjpeg-dev \ + libopenexr-dev \ + libpng-dev \ + libwebp-dev \ + ninja-build \ + pkg-config \ + # + echo "CC=clang" >> $GITHUB_ENV + echo "CXX=clang++" >> $GITHUB_ENV + + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + + - name: Build + env: + SKIP_TEST: 1 + run: | + ./ci.sh release \ + -DJPEGXL_DEP_LICENSE_DIR=/usr/share/doc \ + -DJPEGXL_STATIC=ON \ + -DBUILD_TESTING=OFF \ + -DJPEGXL_ENABLE_VIEWERS=OFF \ + -DJPEGXL_ENABLE_PLUGINS=OFF \ + -DJPEGXL_ENABLE_OPENEXR=OFF \ + + - name: Package release tarball + run: | + cd build + tar -zcvf ${{ runner.workspace }}/release_file.tar.gz \ + LICENSE* tools/{cjxl,djxl,benchmark_xl} + ln -s ${{ runner.workspace }}/release_file.tar.gz \ + ${{ runner.workspace }}/jxl-linux-x86_64-static-${{ github.event.release.tag_name }}.tar.gz + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: jxl-linux-x86_64-static + path: ${{ runner.workspace }}/release_file.tar.gz + + - name: Upload binaries to release + if: github.event_name == 'release' + uses: AButler/upload-release-assets@v2.0 + with: + files: ${{ runner.workspace }}/jxl-linux-x86_64-static-${{ github.event.release.tag_name }}.tar.gz + repo-token: ${{ secrets.GITHUB_TOKEN }} + + + # Build .deb packages Ubuntu/Debian + release_ubuntu_pkg: + name: .deb packages / ${{ matrix.os }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: + - ubuntu:20.04 + - ubuntu:18.04 + - debian:buster + - debian:bullseye + - debian:bookworm + - debian:sid + + container: + image: ${{ matrix.os }} + + steps: + - name: Set env + shell: 'bash' + id: 'env' + run: | + artifact_name="jxl-debs-amd64-${matrix_os/:/-}" + echo ${artifact_name} + echo "::set-output name=artifact_name::${artifact_name}" + env: + matrix_os: ${{ matrix.os }} + + - name: Install build deps + run: | + apt update + DEBIAN_FRONTEND=noninteractive apt install -y \ + build-essential \ + devscripts \ + # + + - name: Install git (only 18.04) + if: matrix.os == 'ubuntu:18.04' + # Ubuntu 18.04 ships with git 2.17 but we need 2.18 or newer for + # actions/checkout@v2 to work + shell: 'bash' + run: | + apt install -y \ + libcurl4-openssl-dev \ + libexpat1-dev \ + libssl-dev \ + wget \ + zlib1g-dev \ + # + git_version="2.32.0" + wget -nv \ + "https://github.com/git/git/archive/refs/tags/v${git_version}.tar.gz" + tar -zxf "v${git_version}.tar.gz" + cd "git-${git_version}" + make prefix=/usr -j4 install + + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + + - name: Stamp non-release versions + # Stamps the built package with the commit date as part of the version + # after the version number so newer release candidates can override older + # ones. + if: github.event_name != 'release' + shell: 'bash' + run: | + # Committer timestamp. + set -x + commit_timestamp=$(git show -s --format=%ct) + commit_datetime=$(date --utc "--date=@${commit_timestamp}" '+%Y%m%d%H%M%S') + commit_ref=$(git rev-parse --short HEAD) + sem_version=$(dpkg-parsechangelog --show-field Version) + sem_version="${sem_version%%-*}" + deb_version="${sem_version}~alpha${commit_datetime}-0+git${commit_ref}" + dch -M --distribution unstable -b --newversion "${deb_version}" \ + "Stamping build with version ${deb_version}" + + - name: Stamp release versions + # Mark the version as released + if: github.event_name == 'release' + shell: 'bash' + run: | + if head -n1 debian/changelog | grep UNRELEASED; then + dch -M --distribution unstable --release '' + fi + + - name: Install gtest (only 18.04) + if: matrix.os == 'ubuntu:18.04' + # In Ubuntu 18.04 no package installed the libgtest.a. libgtest-dev + # installs the source files only. + run: | + apt install -y libgtest-dev cmake + for prj in googletest googlemock; do + (cd /usr/src/googletest/${prj}/ && + cmake CMakeLists.txt -DCMAKE_INSTALL_PREFIX=/usr && + make all install) + done + # Remove libgmock-dev dependency in Ubuntu 18.04. It doesn't exist there. + sed '/libgmock-dev,/d' -i debian/control + + - name: Remove libjxl-gimp-plugin package (only 18.04) + if: matrix.os == 'ubuntu:18.04' + run: | + # Gimp 2.8 is not supported. + sed -i '/Package: libjxl-gimp-plugin/,/^$/d' debian/control + + - name: Build hwy + run: | + apt build-dep -y ./third_party/highway + ./ci.sh debian_build highway + dpkg -i build/debs/libhwy-dev_*_amd64.deb + + - name: Build libjxl + run: | + apt build-dep -y . + ./ci.sh debian_build jpeg-xl + + - name: Stats + run: | + ./ci.sh debian_stats + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.env.outputs.artifact_name }} + path: | + build/debs/*jxl*.* + + - name: Package release tarball + if: github.event_name == 'release' + run: | + (cd build/debs/; find -maxdepth 1 -name '*jxl*.*') | \ + tar -zcvf release_file.tar.gz -C build/debs/ -T - + ln -s release_file.tar.gz \ + ${{ steps.env.outputs.artifact_name }}-${{ github.event.release.tag_name }}.tar.gz + + - name: Upload binaries to release + if: github.event_name == 'release' + uses: AButler/upload-release-assets@v2.0 + with: + files: ${{ steps.env.outputs.artifact_name }}-${{ github.event.release.tag_name }}.tar.gz + repo-token: ${{ secrets.GITHUB_TOKEN }} + + + windows_build: + name: Windows Build (vcpkg / ${{ matrix.triplet }}) + runs-on: [windows-latest] + strategy: + fail-fast: false + matrix: + include: + - triplet: x86-windows-static + arch: '-A Win32' + - triplet: x64-windows-static + arch: '-A x64' + + env: + VCPKG_VERSION: '2021.05.12' + VCPKG_ROOT: vcpkg + VCPKG_DISABLE_METRICS: 1 + + steps: + - name: Checkout the source + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 2 + + - uses: actions/cache@v2 + id: cache-vcpkg + with: + path: vcpkg + key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.triplet }} + + - name: Download vcpkg + if: steps.cache-vcpkg.outputs.cache-hit != 'true' + # wget doesn't seem to work under bash. + shell: 'powershell' + run: | + C:\msys64\usr\bin\wget.exe -nv ` + https://github.com/microsoft/vcpkg/archive/refs/tags/${{ env.VCPKG_VERSION }}.zip ` + -O vcpkg.zip + - name: Bootstrap vcpkg + if: steps.cache-vcpkg.outputs.cache-hit != 'true' + shell: 'bash' + run: | + set -x + unzip -q vcpkg.zip + rm -rf ${VCPKG_ROOT} + mv vcpkg-${VCPKG_VERSION} ${VCPKG_ROOT} + ${VCPKG_ROOT}/bootstrap-vcpkg.sh + + - name: Install libraries with vcpkg + shell: 'bash' + run: | + set -x + ${VCPKG_ROOT}/vcpkg --triplet ${{ matrix.triplet }} install \ + giflib \ + libjpeg-turbo \ + libpng \ + libwebp \ + # + + - name: Configure + shell: 'bash' + run: | + set -x + mkdir build + cmake -Bbuild -H. ${{ matrix.arch }} \ + -DBUILD_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=`pwd`/prefix \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DJPEGXL_ENABLE_OPENEXR=OFF \ + -DJPEGXL_ENABLE_PLUGINS=OFF \ + -DJPEGXL_ENABLE_TCMALLOC=OFF \ + -DJPEGXL_ENABLE_VIEWERS=OFF \ + -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} \ + # + - name: Build + shell: 'bash' + run: | + set -x + cmake --build build --config Release + - name: Install + shell: 'bash' + run: | + set -x + cmake --build build --config Release --target install + for pkg in giflib libjpeg-turbo libpng libwebp zlib; do + cp vcpkg/installed/${{matrix.triplet}}/share/${pkg}/copyright \ + prefix/bin/LICENSE.${pkg} + done + cp third_party/sjpeg/COPYING prefix/bin/LICENSE.sjpeg + cp third_party/skcms/LICENSE prefix/bin/LICENSE.skcms + cp third_party/highway/LICENSE prefix/bin/LICENSE.highway + cp third_party/brotli/LICENSE prefix/bin/LICENSE.brotli + cp LICENSE prefix/bin/LICENSE.libjxl + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: jxl-${{matrix.triplet}} + path: | + prefix/bin/* + + - name: Package release zip + if: github.event_name == 'release' + shell: 'powershell' + run: | + Compress-Archive -Path prefix\bin\* ` + -DestinationPath jxl-${{matrix.triplet}}.zip + + - name: Upload binaries to release + if: github.event_name == 'release' + uses: AButler/upload-release-assets@v2.0 + with: + files: jxl-${{matrix.triplet}}.zip + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a6acd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Build output directories +/build +/build* +/docker/*.log + +# The downloaded corpora files for benchmark. +/third_party/corpora + +# hdrvdp source code +third_party/hdrvdp-2.2.2 +third_party/hdrvdp-2.2.2.zip +third_party/hdrvdp-2.2.2.zip.tmp + +# Output plots +tools/benchmark/metrics/plots +tools/benchmark/metrics/results.csv diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..b447888 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,434 @@ +# 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. + +# We define only two stages for development. The "build" stage only compiles the +# code and doesn't run any target code. The "test" stage runs the code compiled +# by the previous stage. Stages form dependencies in a block, all the build +# targets in one stage need to be completed before any of the next stage can +# start. +stages: + - build + - test + - deploy + +variables: + # By default we don't fetch any third_party dependencies. Building requires to + # fetch all the dependencies but many other workflows don't. + GIT_SUBMODULE_STRATEGY: none + # Set the default checkout directory to a fixed one so it is the same across + # multiple jobs. cmake requires that build directory's realpath to not change. + GIT_CLONE_PATH: $CI_BUILDS_DIR/libjxl + +# A template for running in the generic cloud builders. These are tagged with +# "linux" and run on shared VMs. +.linux_host_template: &linux_host_template + image: &jpegxl-builder gcr.io/jpegxl/jpegxl-builder@sha256:439a05c41f86a82067042fca16d5c9c7cc5836a8a5d8dba7fa0383f3c1e603c2 + tags: + - linux + # By default all the workflows run on master and on request. This can be + # override by users of this template. + only: + - main + - master + - /^v[0-9]+\.[0-9]+\.x$/ + - tags + - schedules + # Retry on system failures. This can be typically caused by the runner or VM + # being evicted. + retry: + max: 2 + when: + - unknown_failure + - api_failure + - stuck_or_timeout_failure + - runner_system_failure + # Default variables. Note that adding a "variables:" section to a template + # using this template will completely discard this section, not just add new + # variables. + variables: + GIT_SUBMODULE_STRATEGY: none + BUILD_DIR: build + CC: clang-7 + CXX: clang++-7 + +# Common artifacts for the build stage +.default_build_paths: &default_build_paths + - build/libjxl.so* + - build/libjxl.dll + # Binary tools like cjxl are "cjxl.exe" in windows builds. These + # matches will match against either one. + - build/lib/jxl_gbench* + - build/tools/cjxl* + - build/tools/djxl* + - build/tools/benchmark_xl* + - build/tools/decode_and_encode* + - build/tools/comparison_viewer/compare_codecs* + - build/tools/comparison_viewer/compare_images* + - build/tools/viewer/viewer* + # Brotli shared dependencies if built + - build/third_party/brotli/libbrotli*so* + - build/third_party/brotli/libbrotli*.dll + # All the test binaries in the build/ directory and .cmake files used by + # ctest, compressed into this file together to save space when uploading. + - build/tests.tar.xz + # The coverage data produced at compile time. + - build/gcno.tar.xz + # The packed html doxygen documentation, only produced by the coverage build. + - build/htmldoc.tar.gz + - build/stats.json + - build/LICENSE* + # The JNI library + wrapper + test. + - build/tools/libjxl_jni*so* + - build/tools/libjxl_jni*.dll + - build/tools/jxl_jni_wrapper.jar + - build/tools/jxl_jni_wrapper_test.jar + +# Helper template to add the default artifacts paths to all build stages. If +# files in the "paths:" section are not present only a warning is generated. +.linux_host_build_template: &linux_host_build_template + <<: *linux_host_template + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: *default_build_paths + variables: + # Building always requires to fetch all the repositories. + GIT_SUBMODULE_STRATEGY: recursive + BUILD_DIR: build + CC: clang-7 + CXX: clang++-7 + +# Build from tarball source using README.md instructions. +build:x86_64:clang:tarball: + <<: *linux_host_build_template + stage: build + variables: + GIT_SUBMODULE_STRATEGY: none + BUILD_DIR: build + CC: clang-7 + CXX: clang++-7 + script: + # Simulate a tarball checkout by removing .git and not fetching any + # submodule. + - mv .git .git-ignore || true + - ./deps.sh + - apt update + - apt install -y cmake pkg-config libbrotli-dev + libgif-dev libjpeg-dev libopenexr-dev libpng-dev libwebp-dev + - mkdir build + - cd build + - cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. + - cmake --build . -- -j + +# linux x86_64 default Release mode. +build:x86_64:clang:release: + <<: *linux_host_build_template + stage: build + script: + # Check that the build files are up to date. + - tools/build_cleaner.py + - SKIP_TEST=1 PACK_TEST=1 STACK_SIZE=1 + ./ci.sh release -DCMAKE_INSTALL_PREFIX=`pwd`/prefix + # Test that the package can be installed. + - ninja -C build install + - tools/build_stats.py --save build/stats.json + cjxl djxl libjxl.so + +test:x86_64:clang:release: + <<: *linux_host_template + stage: test + dependencies: + - build:x86_64:clang:release + script: + - ./ci.sh test + # Quick run to make sure it doesn't crash. Not useful for actual benchmarks. + - ./ci.sh gbench --benchmark_min_time=0 + +# x86_84 test coverage build +build:x86_64:clang:coverage: + <<: *linux_host_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh coverage + - tar -zcf build/htmldoc.tar.gz -C build/html . + +test:x86_64:clang:coverage: + <<: *linux_host_template + stage: test + dependencies: + - build:x86_64:clang:coverage + script: + - ./ci.sh test + - ./ci.sh coverage_report + variables: + CC: clang-7 + CXX: clang++-7 + # Headers from submodules might be needed when generating the HTML reports. + GIT_SUBMODULE_STRATEGY: recursive + # Coverage builds use more stack for coverage tracking. + TEST_STACK_LIMIT: 1024 + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/coverage.txt + - build/coverage*.html + - build/coverage.xml + - build/htmldoc.tar.gz + +# Updates the "Pages" section in GitLab with the latest coverage report from +# master. +pages: + <<: *linux_host_template + stage: deploy + only: + - main + dependencies: + - test:x86_64:clang:coverage + script: + - mkdir public + # Unpack the doxygen documentation as "pages". + - tar -zxf build/htmldoc.tar.gz -C public + # Link from the index to the coverage.html file. + - sed -E "s,,Coverage for ${CI_COMMIT_SHA}
," -i public/index.html + # Copy the coverage files to public/coverage* + - mv build/coverage* public/ + - sed -E "s,GCC Code Coverage Report,Coverage Report for ${CI_COMMIT_SHORT_SHA}," -i public/coverage.html + artifacts: + paths: + - public + + +# i686 (x86 32-bit) builders +build:i686:clang:release: + <<: *linux_host_build_template + stage: build + script: + - BUILD_TARGET=i686-linux-gnu SKIP_TEST=1 PACK_TEST=1 STACK_SIZE=1 + ./ci.sh release -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_FUZZERS=OFF + - tools/build_stats.py --save build/stats.json + --binutils=i686-linux-gnu- + cjxl djxl libjxl.so + +test:i686:clang:release: + <<: *linux_host_template + stage: test + dependencies: + - build:i686:clang:release + script: + - ./ci.sh test + +# Windows builders. These are cross-compiled from a linux host. +build:win64:clang:release: + <<: *linux_host_build_template + stage: build + script: + - BUILD_TARGET=x86_64-w64-mingw32 + SKIP_TEST=1 PACK_TEST=1 + ./ci.sh release -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_FUZZERS=OFF + -DJPEGXL_ENABLE_PLUGINS=OFF + +test:win64:clang:release: + <<: *linux_host_template + stage: test + dependencies: + - build:win64:clang:release + script: + - BUILD_TARGET=x86_64-w64-mingw32 ./ci.sh test + +# aarch64 release runs only on master and on request. +build:aarch64:clang:release: + <<: *linux_host_build_template + stage: build + script: + - BUILD_TARGET=aarch64-linux-gnu + CMAKE_CXX_FLAGS="-DJXL_DISABLE_SLOW_TESTS" SKIP_TEST=1 PACK_TEST=1 + STACK_SIZE=1 + ./ci.sh release + - tools/build_stats.py --save build/stats.json + --binutils=aarch64-linux-gnu- + cjxl djxl libjxl.so + +test:aarch64:clang:release: + <<: *linux_host_template + stage: test + dependencies: + - build:aarch64:clang:release + script: + # LeakSanitizer doesn't work when running under qemu-arm. + - ASAN_OPTIONS=detect_leaks=0 ./ci.sh test + +build:aarch64:clang:release-nhp: + <<: *linux_host_build_template + stage: build + script: + - BUILD_TARGET=aarch64-linux-gnu + CMAKE_CXX_FLAGS="-DJXL_DISABLE_SLOW_TESTS -DJXL_HIGH_PRECISION=0" + SKIP_TEST=1 PACK_TEST=1 STACK_SIZE=1 + ./ci.sh release + - tools/build_stats.py --save build/stats.json + --binutils=aarch64-linux-gnu- + cjxl djxl libjxl.so + +test:aarch64:clang:release-nhp: + <<: *linux_host_template + stage: test + dependencies: + - build:aarch64:clang:release-nhp + script: + # LeakSanitizer doesn't work when running under qemu-arm. + - ASAN_OPTIONS=detect_leaks=0 ./ci.sh test + +# aarch64 asan build and test. +build:aarch64:clang:asan: + <<: *linux_host_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh asan + +test:aarch64:clang:asan: + <<: *linux_host_template + stage: test + dependencies: + - build:aarch64:clang:asan + # Disable asan test on arm since they timeout. + only: + - tags + script: + - ./ci.sh test + +build:x86_64:clang:tsan: + <<: *linux_host_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh tsan + +test:x86_64:clang:tsan: + <<: *linux_host_template + stage: test + dependencies: + - build:x86_64:clang:tsan + script: + # tsan test runs fail with a small stack. + - TEST_STACK_LIMIT=1024 ./ci.sh test + +# Faster benchmark over a smaller set of images. +.benchmark_x86_64_template: &benchmark_x86_64_template + <<: *linux_host_template + stage: test + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + +test:fast_benchmark:release: + <<: *benchmark_x86_64_template + dependencies: + - build:x86_64:clang:release + script: + - STORE_IMAGES=0 ./ci.sh fast_benchmark + +# This template runs on actual aarch64 hardware. +.benchmark_aarch64_template: &benchmark_aarch64_template + <<: *linux_host_template + image: gcr.io/jpegxl/jpegxl-builder-run-aarch64@sha256:27c2bb6319023ab94d66670135a971401869a3275a7e0dda177c9bb247d57e03 + stage: test + tags: + - aarch64 + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + - build/gbench.* + +test:aarch64:fast_benchmark:release: + <<: *benchmark_aarch64_template + dependencies: + - build:aarch64:clang:release + script: + # Bind to the big CPUs only. + - ./ci.sh cpuset "${RUNNER_CPU_BIG}" + - STORE_IMAGES=0 ./ci.sh fast_benchmark + # Running the gbench in the big CPUs only. + - ./ci.sh gbench + +# Benchmark test that runs separately on the big and little CPUs of the runner. +test:aarch64:arm_benchmark:release: + <<: *benchmark_aarch64_template + dependencies: + - build:aarch64:clang:release + script: + - ./ci.sh arm_benchmark + +# Benchmark ToT on nightly builds. These are scheduled at midnight UTC. +test:benchmark:release: + image: *jpegxl-builder + stage: test + only: + - schedules + variables: + GIT_SUBMODULE_STRATEGY: none + dependencies: + - build:x86_64:clang:release + script: + - STORE_IMAGES=0 ./ci.sh benchmark + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + +build:tidy:all: + <<: *linux_host_build_template + stage: build + script: + - CMAKE_BUILD_TYPE="Release" ./ci.sh tidy all + allow_failure: true + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + when: always + paths: + - build/clang-tidy.txt + +# Emscripten (WASM) build. +build:ems:all: + <<: *linux_host_build_template + stage: build + script: + - export V8=/opt/.jsvu/v8 + - source /opt/emsdk/emsdk_env.sh + - BUILD_TARGET=wasm32 SKIP_TEST=1 PACK_TEST=1 emconfigure ./ci.sh release + +test:ems:all: + <<: *linux_host_template + stage: test + dependencies: + - build:ems:all + script: + - export V8=/opt/.jsvu/v8 + - source /opt/emsdk/emsdk_env.sh + - BUILD_TARGET=wasm32 emconfigure ./ci.sh test + +# Emscripten (WASM + SIMD) build. +build:ems_simd:all: + <<: *linux_host_build_template + stage: build + script: + - export V8=/opt/.jsvu/v8 + - source /opt/emsdk/emsdk_env.sh + - BUILD_TARGET=wasm32 ENABLE_WASM_SIMD=1 SKIP_TEST=1 PACK_TEST=1 emconfigure ./ci.sh release + +test:ems_simd:all: + <<: *linux_host_template + stage: test + dependencies: + - build:ems_simd:all + script: + - export V8=/opt/.jsvu/v8 + - source /opt/emsdk/emsdk_env.sh + - BUILD_TARGET=wasm32 emconfigure ./ci.sh test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..561e7ab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,21 @@ +[submodule "third_party/brotli"] + path = third_party/brotli + url = https://github.com/google/brotli +[submodule "third_party/lodepng"] + path = third_party/lodepng + url = https://github.com/lvandeve/lodepng +[submodule "third_party/lcms"] + path = third_party/lcms + url = https://github.com/mm2/Little-CMS +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest +[submodule "third_party/sjpeg"] + path = third_party/sjpeg + url = https://github.com/webmproject/sjpeg.git +[submodule "third_party/skcms"] + path = third_party/skcms + url = https://skia.googlesource.com/skcms +[submodule "third_party/highway"] + path = third_party/highway + url = https://github.com/google/highway diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d929672 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,28 @@ +# List of the project authors for copyright purposes. When contributing to the +# project add your name or your organization's name to this list. See +# CONTRIBUTING.md for details. +# +# For organizations: +# Organization +# +# For individuals: +# Name +# +# Please keep each list sorted. If you wish to change your email address please +# send a pull request. + +# Organizations: +Cloudinary Ltd. <*@cloudinary.com> +Google LLC <*@google.com> + +# Individuals: +Alexander Sago +Dirk Lemstra +Jon Sneyers +Lovell Fuller +Marcin Konicki +Petr Diblík +Pieter Wuille +xiota +Ziemowit Zabawa +Andrius Lukas Narbutas diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8136bde --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,166 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.6.1] - 2021-10-29 +### Changed + - Security: Fix OOB read in splines rendering (#735 - + [CVE-2021-22563](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22563)) + - Security: Fix OOB copy (read/write) in out-of-order/multi-threaded decoding + (#708 - [CVE-2021-22564](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22564)) + - Fix segfault in `djxl` tool with `--allow_partial_files` flag (#781). + - Fix border in extra channels when using upsampling (#796) + +## [0.6] - 2021-10-04 +### Added + - API: New functions to decode extra channels: + `JxlDecoderExtraChannelBufferSize` and `JxlDecoderSetExtraChannelBuffer`. + - API: New function `JxlEncoderInitBasicInfo` to initialize `JxlBasicInfo` + (only needed when encoding). NOTE: it is now required to call this function + when using the encoder. Padding was added to the struct for forward + compatibility. + - API: Support for encoding oriented images. + - API: FLOAT16 support in the encoder API. + - Rewrite of the GDK pixbuf loader plugin. Added proper color management and + animation support. + - Rewrite of GIMP plugin. Added compression parameters dialog and switched to + using the public C API. + - Debian packages for GDK pixbuf loader (`libjxl-gdk-pixbuf`) and GIMP + (`libjxl-gimp-plugin`) plugins. + - `cjxl`/`djxl` support for `stdin` and `stdout`. + +### Changed + - API: Renamed the field `alpha_associated` in `JxlExtraChannelInfo` to + `alpha_premultiplied`, to match the corresponding name in `JxlBasicInfo`. + - Improved the 2x2 downscaling method in the encoder for the optional color + channel resampling for low bit rates. + - Fixed: the combination of floating point original data, XYB color encoding, + and Modular mode was broken (in both encoder and decoder). It now works. + NOTE: this can cause the current encoder to write jxl bitstreams that do + not decode with the old decoder. In particular this will happen when using + cjxl with PFM, EXR, or floating point PSD input, and a combination of XYB + and modular mode is used (which caused an encoder error before), e.g. + using options like `-m -q 80` (lossy modular), `-d 4.5` or `--progressive_dc=1` + (modular DC frame), or default lossy encoding on an image where patches + end up being used. There is no problem when using cjxl with PNG, JPEG, GIF, + APNG, PPM, PGM, PGX, or integer (8-bit or 16-bit) PSD input. + - `libjxl` static library now bundles skcms, fixing static linking in + downstream projects when skcms is used. + - Spline rendering performance improvements. + - Butteraugli changes for less visual masking. + +## [0.5] - 2021-08-02 +### Added + - API: New function to decode the image using a callback outputting a part of a + row per call. + - API: 16-bit float output support. + - API: `JxlDecoderRewind` and `JxlDecoderSkipFrames` functions to skip more + efficiently to earlier animation frames. + - API: `JxlDecoderSetPreferredColorProfile` function to choose color profile in + certain circumstances. + - encoder: Adding `center_x` and `center_y` flags for more control of the tile + order. + - New encoder speeds `lightning` (1) and `thunder` (2). + +### Changed + - Re-licensed the project under a BSD 3-Clause license. See the + [LICENSE](LICENSE) and [PATENTS](PATENTS) files for details. + - Full JPEG XL part 1 specification support: Implemented all the spec required + to decode files to pixels, including cases that are not used by the encoder + yet. Part 2 of the spec (container format) is final but not fully implemented + here. + - Butteraugli metric improvements. Exact numbers are different from previous + versions. + - Memory reductions during decoding. + - Reduce the size of the jxl_dec library by removing dependencies. + - A few encoding speedups. + - Clarify the security policy. + - Significant encoding improvements (~5 %) and less ringing. + - Butteraugli metric to have some less masking. + - `cjxl` flag `--speed` is deprecated and replaced by the `--effort` synonym. + +### Removed +- API for returning a downsampled DC was deprecated + (`JxlDecoderDCOutBufferSize` and `JxlDecoderSetDCOutBuffer`) and will be + removed in the next release. + +## [0.3.7] - 2021-03-29 +### Changed + - Fix a rounding issue in 8-bit decoding. + +## [0.3.6] - 2021-03-25 +### Changed + - Fix a bug that could result in the generation of invalid codestreams as + well as failure to decode valid streams. + +## [0.3.5] - 2021-03-23 +### Added + - New encode-time options for faster decoding at the cost of quality. + - Man pages for cjxl and djxl. + +### Changed + - Memory usage improvements. + - Faster decoding to 8-bit output with the C API. + - GIMP plugin: avoid the sRGB conversion dialog for sRGB images, do not show + a console window on Windows. + - Various bug fixes. + +## [0.3.4] - 2021-03-16 +### Changed + - Improved box parsing. + - Improved metadata handling. + - Performance and memory usage improvements. + +## [0.3.3] - 2021-03-05 +### Changed + - Performance improvements for small images. + - Add a (flag-protected) non-high-precision mode with better speed. + - Significantly speed up the PQ EOTF. + - Allow optional HDR tone mapping in djxl (--tone_map, --display_nits). + - Change the behavior of djxl -j to make it consistent with cjxl (#153). + - Improve image quality. + - Improve EXIF handling. + +## [0.3.2] - 2021-02-12 +### Changed + - Fix embedded ICC encoding regression + [#149](https://gitlab.com/wg1/jpeg-xl/-/issues/149). + +## [0.3.1] - 2021-02-10 +### Changed + - New experimental Butteraugli API (`jxl/butteraugli.h`). + - Encoder improvements to low quality settings. + - Bug fixes, including fuzzer-found potential security bug fixes. + - Fixed `-q 100` and `-d 0` not triggering lossless modes. + +## [0.3] - 2021-01-29 +### Changed + - Minor change to the Decoder C API to accommodate future work for other ways + to provide input. + - Future decoder C API changes will be backwards compatible. + - Lots of bug fixes since the previous version. + +## [0.2] - 2020-12-24 +### Added + - JPEG XL bitstream format is frozen. Files encoded with 0.2 will be supported + by future versions. + +### Changed + - Files encoded with previous versions are not supported. + +## [0.1.1] - 2020-12-01 + +## [0.1] - 2020-11-14 +### Added + - Initial release of an encoder (`cjxl`) and decoder (`djxl`) that work + together as well as a benchmark tool for comparison with other codecs + (`benchmark_xl`). + - Note: JPEG XL format is in the final stages of standardization, minor changes + to the codestream format are still possible but we are not expecting any + changes beyond what is required by bug fixing. + - API: new decoder API in C, check the `examples/` directory for its example + usage. The C API is a work in progress and likely to change both in API and + ABI in future releases. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c2790ab --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,385 @@ +# 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. + +# Ubuntu bionic ships with cmake 3.10. +cmake_minimum_required(VERSION 3.10) + +# Honor VISIBILITY_INLINES_HIDDEN on all types of targets. +if(POLICY CMP0063) + cmake_policy(SET CMP0063 NEW) +endif() +# Pass CMAKE_EXE_LINKER_FLAGS to CC and CXX compilers when testing if they work. +if(POLICY CMP0065) + cmake_policy(SET CMP0065 NEW) +endif() + +# Set PIE flags for POSITION_INDEPENDENT_CODE targets, added in 3.14. +if(POLICY CMP0083) + cmake_policy(SET CMP0083 NEW) +endif() + +project(LIBJXL LANGUAGES C CXX) + +include(CheckCXXSourceCompiles) +check_cxx_source_compiles( + "int main() { + #if !defined(__EMSCRIPTEN__) + static_assert(false, \"__EMSCRIPTEN__ is not defined\"); + #endif + return 0; + }" + JPEGXL_EMSCRIPTEN +) + +message(STATUS "CMAKE_SYSTEM_PROCESSOR is ${CMAKE_SYSTEM_PROCESSOR}") +include(CheckCXXCompilerFlag) +check_cxx_compiler_flag("-fsanitize=fuzzer-no-link" CXX_FUZZERS_SUPPORTED) +check_cxx_compiler_flag("-Xclang -mconstructor-aliases" CXX_CONSTRUCTOR_ALIASES_SUPPORTED) + +# Enabled PIE binaries by default if supported. +include(CheckPIESupported OPTIONAL RESULT_VARIABLE CHECK_PIE_SUPPORTED) +if(CHECK_PIE_SUPPORTED) + check_pie_supported(LANGUAGES CXX) + if(CMAKE_CXX_LINK_PIE_SUPPORTED) + set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) + endif() +endif() + +### Project build options: +if(${CXX_FUZZERS_SUPPORTED}) + # Enabled by default except on arm64, Windows and Apple builds. + set(ENABLE_FUZZERS_DEFAULT true) +endif() +find_package(PkgConfig) +if(NOT APPLE AND NOT WIN32 AND NOT HAIKU AND ${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") + pkg_check_modules(TCMallocMinimalVersionCheck QUIET IMPORTED_TARGET + libtcmalloc_minimal) + if(TCMallocMinimalVersionCheck_FOUND AND + NOT TCMallocMinimalVersionCheck_VERSION VERSION_EQUAL 2.8.0) + # Enabled by default except on Windows and Apple builds for + # tcmalloc != 2.8.0. tcmalloc 2.8.1 already has a fix for this issue. + set(ENABLE_TCMALLOC_DEFAULT true) + else() + message(STATUS + "tcmalloc version ${TCMallocMinimalVersionCheck_VERSION} -- " + "tcmalloc 2.8.0 disabled due to " + "https://github.com/gperftools/gperftools/issues/1204") + endif() +endif() + +set(WARNINGS_AS_ERRORS_DEFAULT false) + +set(JPEGXL_ENABLE_FUZZERS ${ENABLE_FUZZERS_DEFAULT} CACHE BOOL + "Build JPEGXL fuzzer targets.") +set(JPEGXL_ENABLE_DEVTOOLS false CACHE BOOL + "Build JPEGXL developer tools.") +set(JPEGXL_ENABLE_TOOLS true CACHE BOOL + "Build JPEGXL user tools: cjxl and djxl.") +set(JPEGXL_ENABLE_MANPAGES true CACHE BOOL + "Build and install man pages for the command-line tools.") +set(JPEGXL_ENABLE_BENCHMARK true CACHE BOOL + "Build JPEGXL benchmark tools.") +set(JPEGXL_ENABLE_EXAMPLES true CACHE BOOL + "Build JPEGXL library usage examples.") +set(JPEGXL_ENABLE_JNI true CACHE BOOL + "Build JPEGXL JNI Java wrapper, if Java dependencies are installed.") +set(JPEGXL_ENABLE_SJPEG true CACHE BOOL + "Build JPEGXL with support for encoding with sjpeg.") +set(JPEGXL_ENABLE_OPENEXR true CACHE BOOL + "Build JPEGXL with support for OpenEXR if available.") +set(JPEGXL_ENABLE_SKCMS true CACHE BOOL + "Build with skcms instead of lcms2.") +set(JPEGXL_BUNDLE_SKCMS true CACHE BOOL + "When building with skcms, bundle it into libjxl.a.") +set(JPEGXL_ENABLE_VIEWERS false CACHE BOOL + "Build JPEGXL viewer tools for evaluation.") +set(JPEGXL_ENABLE_TCMALLOC ${ENABLE_TCMALLOC_DEFAULT} CACHE BOOL + "Build JPEGXL using gperftools (tcmalloc) allocator.") +set(JPEGXL_ENABLE_PLUGINS false CACHE BOOL + "Build third-party plugings to support JPEG XL in other applications.") +set(JPEGXL_ENABLE_COVERAGE false CACHE BOOL + "Enable code coverage tracking for libjxl. This also enables debug and disables optimizations.") +set(JPEGXL_ENABLE_PROFILER false CACHE BOOL + "Builds in support for profiling (printed by tools if extra flags given") +set(JPEGXL_ENABLE_TRANSCODE_JPEG true CACHE BOOL + "Builds in support for decoding transcoded JXL files back to JPEG,\ + disabling it makes the decoder reject JXL_DEC_JPEG_RECONSTRUCTION events,\ + (default enabled)") +set(JPEGXL_STATIC false CACHE BOOL + "Build tools as static binaries.") +set(JPEGXL_WARNINGS_AS_ERRORS ${WARNINGS_AS_ERRORS_DEFAULT} CACHE BOOL + "Treat warnings as errors during compilation.") +set(JPEGXL_DEP_LICENSE_DIR "" CACHE STRING + "Directory where to search for system dependencies \"copyright\" files.") +set(JPEGXL_FORCE_NEON false CACHE BOOL + "Set flags to enable NEON in arm if not enabled by your toolchain.") + + +# Force system dependencies. +set(JPEGXL_FORCE_SYSTEM_GTEST false CACHE BOOL + "Force using system installed googletest (gtest/gmock) instead of third_party/googletest source.") +set(JPEGXL_FORCE_SYSTEM_BROTLI false CACHE BOOL + "Force using system installed brotli instead of third_party/brotli source.") +set(JPEGXL_FORCE_SYSTEM_HWY false CACHE BOOL + "Force using system installed highway (libhwy-dev) instead of third_party/highway source.") + +# Check minimum compiler versions. Older compilers are not supported and fail +# with hard to understand errors. +if (NOT ${CMAKE_C_COMPILER_ID} STREQUAL ${CMAKE_CXX_COMPILER_ID}) + message(FATAL_ERROR "Different C/C++ compilers set: " + "${CMAKE_C_COMPILER_ID} vs ${CMAKE_CXX_COMPILER_ID}") +endif() +if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + # Android NDK's toolchain.cmake fakes the clang version in + # CMAKE_CXX_COMPILER_VERSION with an incorrect number, so ignore this. + if (NOT ${CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION} MATCHES "clang" + AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 6) + message(FATAL_ERROR + "Minimum Clang version required is Clang 6, please update.") + endif() +elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") + if (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 7) + message(FATAL_ERROR + "Minimum GCC version required is 7, please update.") + endif() +endif() + +message(STATUS + "Compiled IDs C:${CMAKE_C_COMPILER_ID}, C++:${CMAKE_CXX_COMPILER_ID}") + +# CMAKE_EXPORT_COMPILE_COMMANDS is used to generate the compilation database +# used by clang-tidy. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(JPEGXL_STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) + set(BUILD_SHARED_LIBS 0) + # Clang developers say that in case to use "static" we have to build stdlib + # ourselves; for real use case we don't care about stdlib, as it is "granted", + # so just linking all other libraries is fine. + if (NOT APPLE) + set(CMAKE_EXE_LINKER_FLAGS + "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") + endif() +endif() # JPEGXL_STATIC + +# Threads +set(THREADS_PREFER_PTHREAD_FLAG YES) +find_package(Threads REQUIRED) + +if(JPEGXL_STATIC) + if (MINGW) + # In MINGW libstdc++ uses pthreads directly. When building statically a + # program (regardless of whether the source code uses pthread or not) the + # toolchain will add stdc++ and pthread to the linking step but stdc++ will + # be linked statically while pthread will be linked dynamically. + # To avoid this and have pthread statically linked with need to pass it in + # the command line with "-Wl,-Bstatic -lpthread -Wl,-Bdynamic" but the + # linker will discard it if not used by anything else up to that point in + # the linker command line. If the program or any dependency don't use + # pthread directly -lpthread is discarded and libstdc++ (added by the + # toolchain later) will then use the dynamic version. For this we also need + # to pass -lstdc++ explicitly before -lpthread. For pure C programs -lstdc++ + # will be discarded anyway. + # This adds these flags as dependencies for *all* targets. Adding this to + # CMAKE_EXE_LINKER_FLAGS instead would cause them to be included before any + # object files and therefore discarded. This should be set in the + # INTERFACE_LINK_LIBRARIES of Threads::Threads but some third_part targets + # don't depend on it. + link_libraries(-Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic) + elseif(CMAKE_USE_PTHREADS_INIT) + # "whole-archive" is not supported on OSX. + if (NOT APPLE) + # Set pthreads as a whole-archive, otherwise weak symbols in the static + # libraries will discard pthreads symbols leading to segmentation fault at + # runtime. + message(STATUS "Using -lpthread as --whole-archive") + set_target_properties(Threads::Threads PROPERTIES + INTERFACE_LINK_LIBRARIES + "-Wl,--whole-archive;-lpthread;-Wl,--no-whole-archive") + endif() + endif() +endif() # JPEGXL_STATIC + +if (MSVC) +# TODO(janwas): add flags +else () + +# Global compiler flags for all targets here and in subdirectories. +add_definitions( + # Avoid changing the binary based on the current time and date. + -D__DATE__="redacted" + -D__TIMESTAMP__="redacted" + -D__TIME__="redacted" +) + +# Avoid log spam from fopen etc. +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) +endif() + +if("${JPEGXL_ENABLE_FUZZERS}" OR "${JPEGXL_ENABLE_COVERAGE}") + add_definitions( + -DJXL_ENABLE_FUZZERS + ) +endif() # JPEGXL_ENABLE_FUZZERS + +# In CMake before 3.12 it is problematic to pass repeated flags like -Xclang. +# For this reason we place them in CMAKE_CXX_FLAGS instead. +# See https://gitlab.kitware.com/cmake/cmake/issues/15826 + +# Machine flags. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -funwind-tables") +if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xclang -mrelax-all") +endif() +if ("${CXX_CONSTRUCTOR_ALIASES_SUPPORTED}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xclang -mconstructor-aliases") +endif() + +if(WIN32) +# Not supported by clang-cl, but frame pointers are default on Windows +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer") +endif() + +# CPU flags - remove once we have NEON dynamic dispatch + +# TODO(janwas): this also matches M1, but only ARMv7 is intended/needed. +if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") +if(JPEGXL_FORCE_NEON) +# GCC requires these flags, otherwise __ARM_NEON is undefined. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -mfpu=neon-vfpv4 -mfloat-abi=hard") +endif() +endif() + +# Force build with optimizations in release mode. +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") + +add_compile_options( + # Ignore this to allow redefining __DATE__ and others. + -Wno-builtin-macro-redefined + + # Global warning settings. + -Wall +) + +if (JPEGXL_WARNINGS_AS_ERRORS) +add_compile_options(-Werror) +endif () +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. +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.jpeg-xl COPYONLY) + +# Enable tests regardless of where are they defined. +enable_testing() +include(CTest) + +# Libraries. +add_subdirectory(lib) + +if(BUILD_TESTING) +# Script to run tests over the source code in bash. +find_program (BASH_PROGRAM bash) +if(BASH_PROGRAM) + add_test( + NAME bash_test + COMMAND ${BASH_PROGRAM} ${CMAKE_CURRENT_SOURCE_DIR}/bash_test.sh) +endif() +endif() # BUILD_TESTING + +# Documentation generated by Doxygen +find_package(Doxygen) +if(DOXYGEN_FOUND) +set(DOXYGEN_GENERATE_HTML "YES") +set(DOXYGEN_GENERATE_XML "NO") +set(DOXYGEN_STRIP_FROM_PATH "${CMAKE_CURRENT_SOURCE_DIR}/include") +set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "README.md") +set(DOXYGEN_WARN_AS_ERROR "YES") +doxygen_add_docs(doc + "${CMAKE_CURRENT_SOURCE_DIR}/lib/include/jxl" + "${CMAKE_CURRENT_SOURCE_DIR}/doc/api.txt" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Generating C API documentation") +else() +# Create a "doc" target for compatibility since "doc" is not otherwise added to +# the build when doxygen is not installed. +add_custom_target(doc false + COMMENT "Error: Can't generate doc since Doxygen not installed.") +endif() # DOXYGEN_FOUND + +if(JPEGXL_ENABLE_MANPAGES) +find_program(ASCIIDOC a2x) +if(NOT "${ASCIIDOC}" STREQUAL "ASCIIDOC-NOTFOUND") +file(STRINGS "${ASCIIDOC}" ASCIIDOC_SHEBANG LIMIT_COUNT 1) +if(ASCIIDOC_SHEBANG MATCHES "python2") + find_package(Python2 COMPONENTS Interpreter) + set(ASCIIDOC_PY_FOUND "${Python2_Interpreter_FOUND}") + set(ASCIIDOC_PY Python2::Interpreter) +elseif(ASCIIDOC_SHEBANG MATCHES "python3") + find_package(Python3 COMPONENTS Interpreter) + set(ASCIIDOC_PY_FOUND "${Python3_Interpreter_FOUND}") + set(ASCIIDOC_PY Python3::Interpreter) +else() + find_package(Python COMPONENTS Interpreter QUIET) + if(NOT Python_Interpreter_FOUND) + find_program(ASCIIDOC_PY python) + if(NOT ASCIIDOC_PY STREQUAL "ASCIIDOC_PY-NOTFOUND") + set(ASCIIDOC_PY_FOUND ON) + endif() + else() + set(ASCIIDOC_PY_FOUND "${Python_Interpreter_FOUND}") + set(ASCIIDOC_PY Python::Interpreter) + endif() +endif() + +if (ASCIIDOC_PY_FOUND) + set(MANPAGE_FILES "") + set(MANPAGES "") + foreach(PAGE IN ITEMS cjxl djxl) + # Invoking the Python interpreter ourselves instead of running the a2x binary + # directly is necessary on MSYS2, otherwise it is run through cmd.exe which + # does not recognize it. + add_custom_command( + OUTPUT "${PAGE}.1" + COMMAND "${ASCIIDOC_PY}" + ARGS "${ASCIIDOC}" + --format manpage --destination-dir="${CMAKE_CURRENT_BINARY_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/doc/man/${PAGE}.txt" + MAIN_DEPENDENCY "${CMAKE_CURRENT_SOURCE_DIR}/doc/man/${PAGE}.txt") + list(APPEND MANPAGE_FILES "${CMAKE_CURRENT_BINARY_DIR}/${PAGE}.1") + list(APPEND MANPAGES "${PAGE}.1") + endforeach() + add_custom_target(manpages ALL DEPENDS ${MANPAGES}) + install(FILES ${MANPAGE_FILES} DESTINATION share/man/man1) +endif() # ASCIIDOC_PY_FOUND +else() + message(WARNING "asciidoc was not found, the man pages will not be installed.") +endif() # ASCIIDOC != "ASCIIDOC-NOTFOUND" +endif() # JPEGXL_ENABLE_MANPAGES + +# Example usage code. +if (${JPEGXL_ENABLE_EXAMPLES}) +include(examples/examples.cmake) +endif () + +# Plugins for third-party software +if (${JPEGXL_ENABLE_PLUGINS}) +add_subdirectory(plugins) +endif () + +# Binary tools +add_subdirectory(tools) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b2d81a3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,93 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to Jyrki Alakuijala , the +Project Steward(s) for JPEG XL. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cb64597 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,132 @@ +# Contributing to libjxl + +## Contributing with bug reports + +For security-related issues please see [SECURITY.md](SECURITY.md). + +We welcome suggestions, feature requests and bug reports. Before opening a new +issue please take a look if there is already an existing one in the following +link: + + * https://github.com/libjxl/libjxl/issues + +## Contributing with patches and Pull Requests + +We'd love to accept your contributions to the JPEG XL Project. Please read +through this section before sending a Pull Request. + +### Contributor License Agreements + +Our project is open source under the terms outlined in the [LICENSE](LICENSE) +and [PATENTS](PATENTS) files. Before we can accept your contributions, even for +small changes, there are just a few small guidelines you need to follow: + +Please fill out either the individual or corporate Contributor License Agreement +(CLA) with Google. JPEG XL Project is an an effort by multiple individuals and +companies, including the initial contributors Cloudinary and Google, but Google +is the legal entity in charge of receiving these CLA and relicensing this +software: + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual + CLA](https://code.google.com/legal/individual-cla-v1.0.html). + + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate + CLA](https://code.google.com/legal/corporate-cla-v1.0.html). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able +to accept your pull requests. + +***NOTE***: Only original source code from you and other people that have signed +the CLA can be accepted into the main repository. + +### License + +Contributions are licensed under the project's [LICENSE](LICENSE). Each new +file must include the following header when possible, with comment style adapted +to the language as needed: + +``` +// 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. +``` + +### Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +### Contribution philosophy + + * Prefer small changes, even if they don't implement a complete feature. Small + changes are easier to review and can be submitted faster. Think about what's + the smallest unit you can send that makes sense to review and submit in + isolation. For example, new modules that are not yet used by the tools but + have their own unittests are ok. If you have unrelated changes that + you discovered while working on something else, please send them in a + different Pull Request. If your are refactoring code and changing + functionality try to send the refactor first without any change in + functionality. Reviewers may ask you to split a Pull Request and it is + easier to create a smaller change from the beginning. + + * Describe your commits. Add a meaningful description to your commit message, explain what you are changing if it is not trivially obvious, but more importantly explain *why* you are making those changes. For example "Fix + build" is not a good commit message, describe what build and if it makes sense + why is this fixing it or why was it failing without this. It is very likely + that people far in the future without any context you have right now will be + looking at your commit trying to figure out why was the change introduced. If + related to an issue in this or another repository include a link to it. + + * Code Style: We follow the [Google C++ Coding + Style](https://google.github.io/styleguide/cppguide.html). A + [clang-format](https://clang.llvm.org/docs/ClangFormat.html) configuration + file is available to automatically format your code, you can invoke it with + the `./ci.sh lint` helper tool. + + * Testing: Test your change and explain in the commit message *how* your + commit was tested. For example adding unittests or in some cases just testing + with the existing ones is enough. In any case, mention what testing was + performed so reviewers can evaluate whether that's enough testing. In many + cases, testing that the Continuous Integration workflow passes is enough. + + * Make one commit per Pull Request / review, unless there's a good reason not + to. If you have multiple changes send multiple Pull Requests and each one can + have its own review. + + * When addressing comments from reviewers prefer to squash or fixup your + edits and force-push your commit. When merging changes into the repository we + don't want to include the history of code review back and forth changes or + typos. Reviewers can click on the "force-pushed" automatic comment on a Pull + Request to see the changes between versions. We use "Rebase and merge" policy + to keep a linear git history which is easier to reason about. + + * Your change must pass the build and test workflows. There's a `ci.sh` script + to help building and testing these configurations. See [building and + testing](doc/building_and_testing.md) for more details. + +### Contributing checklist. + + * Sign the CLA (only needed once per user, see above). + + * AUTHORS: If this is your first contribution, add your name or your + company name to the [AUTHORS](AUTHORS) file for copyright tracking purposes. + + * Style guide. Check `./ci.sh lint`. + + * Meaningful commit description: What and *why*, links to issues, testing + procedure. + + * Squashed multiple edits into a single commit. + + * Upload your changes to your fork and [create a Pull + Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). + +# Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google.com/conduct/). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..848096f --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,23 @@ +# This files lists individuals who made significant contributions to the JPEG XL +# code base, such as design, adding features, performing experiments, ... +# Small changes such as a small bugfix or fixing spelling errors are not +# included. If you'd like to be included in this file thanks to a significant +# contribution, feel free to send a pull request changing this file. +Alex Deymo +Alexander Rhatushnyak +Evgenii Kliuchnikov +Iulia-Maria Comșa +Jan Wassenberg +Jon Sneyers +Jyrki Alakuijala +Krzysztof Potempa +Lode Vandevenne +Luca Versari +Martin Bruse +Moritz Firsching +Renata Khasanova +Robert Obryk +Sami Boukortt +Sebastian Gomez-Gonzalez +Thomas Fischbacher +Zoltan Szabadka diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c66034b --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) the JPEG XL Project Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 0000000..c95b8f4 --- /dev/null +++ b/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the JPEG XL project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of JPEG XL, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of JPEG XL. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of JPEG XL or any code incorporated within this +implementation of JPEG XL constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of JPEG XL +shall terminate as of the date such litigation is filed. diff --git a/README.Haiku.md b/README.Haiku.md new file mode 100644 index 0000000..20111c5 --- /dev/null +++ b/README.Haiku.md @@ -0,0 +1,20 @@ +## Disclaimer + +Haiku builds are not officially supported, i.e. the build might not work at all, +some tests may fail and some sub-projects are excluded from build. + +This manual outlines Haiku-specific setup. For general building and testing +instructions see "[README](README.md)" and +"[Building and Testing changes](doc/building_and_testing.md)". + +## Dependencies + +```shell +pkgman install llvm9_clang ninja cmake doxygen libjpeg_turbo_devel giflib_devel +``` + +## Building + +```shell +TEST_STACK_LIMIT=none CMAKE_FLAGS="-I/boot/system/develop/tools/lib/gcc/x86_64-unknown-haiku/8.3.0/include/c++ -I/boot/system/develop/tools/lib/gcc/x86_64-unknown-haiku/8.3.0/include/c++/x86_64-unknown-haiku" CMAKE_SHARED_LINKER_FLAGS="-shared -Xlinker -soname=libjpegxl.so -lpthread" ./ci.sh opt +``` diff --git a/README.OSX.md b/README.OSX.md new file mode 100644 index 0000000..8c6dc5a --- /dev/null +++ b/README.OSX.md @@ -0,0 +1,41 @@ +## Disclaimer + +OSX builds have "best effort" support, i.e. build might not work at all, some +tests may fail and some sub-projects are excluded from build. + +This manual outlines OSX specific setup. For general building and testing +instructions see "[README](README.md)" and +"[Building and Testing changes](doc/building_and_testing.md)". + +[Homebrew](https://brew.sh/) is a popular package manager. JPEG XL library and +binaries could be installed using it: + +```bash +brew install jpeg-xl +``` + +## Dependencies + +Make sure that `brew doctor` does not report serious problems and up-to-date +version of XCode is installed. + +Installing (actually, building) `clang` might take a couple hours. + +```bash +brew install llvm +``` + +```bash +brew install coreutils cmake giflib jpeg-turbo libpng ninja zlib +``` + +Before building the project check that `which clang` is +`/usr/local/opt/llvm/bin/clang`, not the one provided by XCode. If not, update +`PATH` environment variable. + +Also, setting `CMAKE_PREFIX_PATH` might be necessary for correct include paths +resolving, e.g.: + +```bash +export CMAKE_PREFIX_PATH=`brew --prefix giflib`:`brew --prefix jpeg-turbo`:`brew --prefix libpng`:`brew --prefix zlib` +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcce3a3 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# JPEG XL reference implementation + +JXL logo + +This repository contains a reference implementation of JPEG XL (encoder and +decoder), called `libjxl`. This software library is +[used by many applications that support JPEG XL](doc/software_support.md). + +JPEG XL is in the final stages of standardization and its codestream and file format +are frozen. + +The library API, command line options, and tools in this repository are subject +to change, however files encoded with `cjxl` conform to the JPEG XL format +specification and can be decoded with current and future `djxl` decoders or +`libjxl` decoding library. + +## Quick start guide + +For more details and other workflows see the "Advanced guide" below. + +### Checking out the code + +```bash +git clone https://github.com/libjxl/libjxl.git --recursive +``` + +This repository uses git submodules to handle some third party dependencies +under `third_party`, that's why is important to pass `--recursive`. If you +didn't check out with `--recursive`, or any submodule has changed, run: + +```bash +git submodule update --init --recursive +``` + +Important: If you downloaded a zip file or tarball from the web interface you +won't get the needed submodules and the code will not compile. You can download +these external dependencies from source running `./deps.sh`. The git workflow +described above is recommended instead. + +### Installing dependencies + +Required dependencies for compiling the code, in a Debian/Ubuntu based +distribution run: + +```bash +sudo apt install cmake pkg-config libbrotli-dev +``` + +Optional dependencies for supporting other formats in the `cjxl`/`djxl` tools, +in a Debian/Ubuntu based distribution run: + +```bash +sudo apt install libgif-dev libjpeg-dev libopenexr-dev libpng-dev libwebp-dev +``` + +We recommend using a recent Clang compiler (version 7 or newer), for that +install clang and set `CC` and `CXX` variables. + +```bash +sudo apt install clang +export CC=clang CXX=clang++ +``` + +### Building + +```bash +cd libjxl +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF .. +cmake --build . -- -j$(nproc) +``` + +The encoder/decoder tools will be available in the `build/tools` directory. + +### Installing + +```bash +sudo cmake --install . +``` + +### Basic encoder/decoder + +To encode a source image to JPEG XL with default settings: + +```bash +build/tools/cjxl input.png output.jxl +``` + +For more settings run `build/tools/cjxl --help` or for a full list of options +run `build/tools/cjxl -v -v --help`. + +To decode a JPEG XL file run: + +```bash +build/tools/djxl input.jxl output.png +``` + +When possible `cjxl`/`djxl` are able to read/write the following +image formats: .exr, .gif, .jpeg/.jpg, .pfm, .pgm/.ppm, .pgx, .png. + +### Benchmarking + +For speed benchmarks on single images in single or multi-threaded decoding +`djxl` can print decoding speed information. See `djxl --help` for details +on the decoding options and note that the output image is optional for +benchmarking purposes. + +For more comprehensive benchmarking options, see the +[benchmarking guide](doc/benchmarking.md). + +## Advanced guide + +### Building with Docker + +We build a common environment based on Debian/Ubuntu using Docker. Other +systems may have different combinations of versions and dependencies that +have not been tested and may not work. For those cases we recommend using the +Docker container as explained in the +[step by step guide](doc/developing_in_docker.md). + +### Building JPEG XL for developers + +For experienced developers, we provide build instructions for several other environments: + +* [Building on Debian](doc/developing_in_debian.md) +* Building on Windows with [vcpkg](doc/developing_in_windows_vcpkg.md) (Visual Studio 2019) +* Building on Windows with [MSYS2](doc/developing_in_windows_msys.md) +* [Cross Compiling for Windows with Crossroad](doc/developing_with_crossroad.md) + +If you encounter any difficulties, please use Docker instead. + +## License + +This software is available under a 3-clause BSD license which can be found in +the [LICENSE](LICENSE) file, with an "Additional IP Rights Grant" as outlined in +the [PATENTS](PATENTS) file. + +Please note that the PATENTS file only mentions Google since Google is the legal +entity receiving the Contributor License Agreements (CLA) from all contributors +to the JPEG XL Project, including the initial main contributors to the JPEG XL +format: Cloudinary and Google. + +## Additional documentation + +### Codec description + +* [Introductory paper](https://www.spiedigitallibrary.org/proceedings/Download?fullDOI=10.1117%2F12.2529237) (open-access) +* [XL Overview](doc/xl_overview.md) - a brief introduction to the source code modules +* [JPEG XL white paper](http://ds.jpeg.org/whitepapers/jpeg-xl-whitepaper.pdf) +* [JPEG XL official website](https://jpeg.org/jpegxl) +* [JPEG XL community website](https://jpegxl.info) + +### Development process + +* [More information on testing/build options](doc/building_and_testing.md) +* [Git guide for JPEG XL](doc/developing_in_github.md) - for developers +* [Fuzzing](doc/fuzzing.md) - for developers +* [Building Web Assembly artifacts](doc/building_wasm.md) + +### Contact + +If you encounter a bug or other issue with the software, please open an Issue here. + +There is a [subreddit about JPEG XL](https://www.reddit.com/r/jpegxl/), and +informal chatting with developers and early adopters of `libjxl` can be done on the +[JPEG XL Discord server](https://discord.gg/DqkQgDRTFu). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e6616d1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security and Vulnerability Policy for JPEG XL + +The current focus of the reference implementation is to provide a vehicle for +evaluating the JPEG XL codec compression density, quality, features and its +actual performance on different platforms. With this focus in mind we provide +source code releases with improvements on performance and quality so developers +can evaluate the codec. + +At this time, **we don't provide security and vulnerability support** for any +of these releases. This means that the source code may contain bugs, including +security bugs, that may be added or fixed between releases and will **not** be +individually documented. All of these +[releases](https://gitlab.com/wg1/jpeg-xl/-/releases) include the following +note to that effect: + +* Note: This release is for evaluation purposes and may contain bugs, including + security bugs, that will *not* be individually documented when fixed. Always + prefer to use the latest release. Please provide feedback and report bugs + [here](https://gitlab.com/wg1/jpeg-xl/-/issues). + +To be clear, this means that because a release doesn't mention any CVE it +doesn't mean that no security issues in previous versions were fixed. You should +assume that any previous release contains security issues if that's a concern +for your use case. + +This however doesn't impede you from evaluating the codec with your own trusted +inputs, such as `.jxl` you encoded yourself, or when taking appropriate measures +for your application like sandboxing if processing untrusted inputs. + +## Future plans + +To help our users and developers integrating this implementation into their +software we plan to provide support for security and vulnerability tracking of +this implementation in the future. + +When we can provide such support we will update this Policy with the details and +expectations and clearly mention that fact in the release notes. diff --git a/bash_test.sh b/bash_test.sh new file mode 100755 index 0000000..c0656d0 --- /dev/null +++ b/bash_test.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# 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. + +# Tests implemented in bash. These typically will run checks about the source +# code rather than the compiled one. + +MYDIR=$(dirname $(realpath "$0")) + +set -u + +test_includes() { + local ret=0 + local f + for f in $(git ls-files | grep -E '(\.cc|\.cpp|\.h)$'); do + # Check that the public files (in lib/include/ directory) don't use the full + # path to the public header since users of the library will include the + # library as: #include "jxl/foobar.h". + if [[ "${f#lib/include/}" != "${f}" ]]; then + if grep -i -H -n -E '#include\s*[<"]lib/include/jxl' "$f" >&2; then + echo "Don't add \"include/\" to the include path of public headers." >&2 + ret=1 + fi + fi + + if [[ "${f#third_party/}" == "$f" ]]; then + # $f is not in third_party/ + + # Check that local files don't use the full path to third_party/ + # directory since the installed versions will not have that path. + # Add an exception for third_party/dirent.h. + if grep -v -F 'third_party/dirent.h' "$f" | \ + grep -i -H -n -E '#include\s*[<"]third_party/' >&2 && + [[ $ret -eq 0 ]]; then + cat >&2 <&2 + ret=1 + fi + done + return ${ret} +} + +test_copyright() { + local ret=0 + local f + for f in $( + git ls-files | grep -E \ + '(Dockerfile.*|\.c|\.cc|\.cpp|\.gni|\.h|\.java|\.sh|\.m|\.py|\.ui|\.yml)$'); do + if [[ "${f#third_party/}" == "$f" ]]; then + # $f is not in third_party/ + if ! head -n 10 "$f" | + grep -F 'Copyright (c) the JPEG XL Project Authors.' >/dev/null ; then + echo "$f: Missing Copyright blob near the top of the file." >&2 + ret=1 + fi + if ! head -n 10 "$f" | + grep -F 'Use of this source code is governed by a BSD-style' \ + >/dev/null ; then + echo "$f: Missing License blob near the top of the file." >&2 + ret=1 + fi + fi + done + return ${ret} +} + +# Check that "dec_" code doesn't depend on "enc_" headers. +test_dec_enc_deps() { + local ret=0 + local f + for f in $(git ls-files | grep -E '/dec_'); do + if [[ "${f#third_party/}" == "$f" ]]; then + # $f is not in third_party/ + if grep -n -H -E "#include.*/enc_" "$f" >&2; then + echo "$f: Don't include \"enc_*\" files from \"dec_*\" files." >&2 + ret=1 + fi + fi + done + return ${ret} +} + +# Check for git merge conflict markers. +test_merge_conflict() { + local ret=0 + TEXT_FILES='(\.cc|\.cpp|\.h|\.sh|\.m|\.py|\.md|\.txt|\.cmake)$' + for f in $(git ls-files | grep -E "${TEXT_FILES}"); do + if grep -E '^<<<<<<< ' "$f"; then + echo "$f: Found git merge conflict marker. Please resolve." >&2 + ret=1 + fi + done + return ${ret} +} + +# Check that the library and the package have the same version. This prevents +# accidentally having them out of sync. +get_version() { + local varname=$1 + local line=$(grep -F "set(${varname} " lib/CMakeLists.txt | head -n 1) + [[ -n "${line}" ]] + line="${line#set(${varname} }" + line="${line%)}" + echo "${line}" +} + +test_version() { + local major=$(get_version JPEGXL_MAJOR_VERSION) + local minor=$(get_version JPEGXL_MINOR_VERSION) + local patch=$(get_version JPEGXL_PATCH_VERSION) + # Check that the version is not empty + if [[ -z "${major}${minor}${patch}" ]]; then + echo "Couldn't parse version from CMakeLists.txt" >&2 + return 1 + fi + local pkg_version=$(head -n 1 debian/changelog) + # Get only the part between the first "jpeg-xl (" and the following ")". + pkg_version="${pkg_version#jpeg-xl (}" + pkg_version="${pkg_version%%)*}" + if [[ -z "${pkg_version}" ]]; then + echo "Couldn't parse version from debian package" >&2 + return 1 + fi + + local lib_version="${major}.${minor}.${patch}" + lib_version="${lib_version%.0}" + if [[ "${pkg_version}" != "${lib_version}"* ]]; then + echo "Debian package version (${pkg_version}) doesn't match library" \ + "version (${lib_version})." >&2 + return 1 + fi + return 0 +} + +# Check that the SHA versions in deps.sh matches the git submodules. +test_deps_version() { + while IFS= read -r line; do + if [[ "${line:0:10}" != "[submodule" ]]; then + continue + fi + line="${line#[submodule \"}" + line="${line%\"]}" + local varname=$(tr '[:lower:]' '[:upper:]' <<< "${line}") + varname="${varname/\//_}" + if ! grep -F "${varname}=" deps.sh >/dev/null; then + # Ignoring submodule not in deps.sh + continue + fi + local deps_sha=$(grep -F "${varname}=" deps.sh | cut -f 2 -d '"') + [[ -n "${deps_sha}" ]] + local git_sha=$(git ls-tree -r HEAD "${line}" | cut -f 1 | cut -f 3 -d ' ') + if [[ "${deps_sha}" != "${git_sha}" ]]; then + cat >&2 </dev/null; then + cat >&2 <&2; then + echo "Don't use \"%n\"." >&2 + ret=1 + fi + done + return ${ret} +} + +main() { + local ret=0 + cd "${MYDIR}" + + if ! git rev-parse >/dev/null 2>/dev/null; then + echo "Not a git checkout, skipping bash_test" + return 0 + fi + + IFS=$'\n' + for f in $(declare -F); do + local test_name=$(echo "$f" | cut -f 3 -d ' ') + # Runs all the local bash functions that start with "test_". + if [[ "${test_name}" == test_* ]]; then + echo "Test ${test_name}: Start" + if ${test_name}; then + echo "Test ${test_name}: PASS" + else + echo "Test ${test_name}: FAIL" + ret=1 + fi + fi + done + return ${ret} +} + +main "$@" diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..e4809e7 --- /dev/null +++ b/ci.sh @@ -0,0 +1,1516 @@ +#!/usr/bin/env bash +# 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. + +# Continuous integration helper module. This module is meant to be called from +# the .gitlab-ci.yml file during the continuous integration build, as well as +# from the command line for developers. + +set -eu + +OS=`uname -s` + +MYDIR=$(dirname $(realpath "$0")) + +### Environment parameters: +TEST_STACK_LIMIT="${TEST_STACK_LIMIT:-128}" +CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-RelWithDebInfo} +CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH:-} +CMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER:-} +CMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER:-} +CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM:-} +SKIP_TEST="${SKIP_TEST:-0}" +BUILD_TARGET="${BUILD_TARGET:-}" +ENABLE_WASM_SIMD="${ENABLE_WASM_SIMD:-0}" +if [[ -n "${BUILD_TARGET}" ]]; then + BUILD_DIR="${BUILD_DIR:-${MYDIR}/build-${BUILD_TARGET%%-*}}" +else + BUILD_DIR="${BUILD_DIR:-${MYDIR}/build}" +fi +# Whether we should post a message in the MR when the build fails. +POST_MESSAGE_ON_ERROR="${POST_MESSAGE_ON_ERROR:-1}" + +# Set default compilers to clang if not already set +export CC=${CC:-clang} +export CXX=${CXX:-clang++} + +# Time limit for the "fuzz" command in seconds (0 means no limit). +FUZZER_MAX_TIME="${FUZZER_MAX_TIME:-0}" + +SANITIZER="none" + +if [[ "${BUILD_TARGET}" == wasm* ]]; then + # Check that environment is setup for the WASM build target. + if [[ -z "${EMSCRIPTEN}" ]]; then + echo "'EMSCRIPTEN' is not defined. Use 'emconfigure' wrapper to setup WASM build environment" >&2 + return 1 + fi + # Remove the side-effect of "emconfigure" wrapper - it considers NodeJS environment. + unset EMMAKEN_JUST_CONFIGURE + EMS_TOOLCHAIN_FILE="${EMSCRIPTEN}/cmake/Modules/Platform/Emscripten.cmake" + if [[ -f "${EMS_TOOLCHAIN_FILE}" ]]; then + CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-${EMS_TOOLCHAIN_FILE}} + else + echo "Warning: EMSCRIPTEN CMake module not found" >&2 + fi + CMAKE_CROSSCOMPILING_EMULATOR="${MYDIR}/js-wasm-wrapper.sh" +fi + +if [[ "${BUILD_TARGET%%-*}" == "x86_64" || + "${BUILD_TARGET%%-*}" == "i686" ]]; then + # Default to building all targets, even if compiler baseline is SSE4 + HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-HWY_SCALAR} +else + HWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS:-} +fi + +# Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS +CMAKE_FLAGS=${CMAKE_FLAGS:-} +CMAKE_C_FLAGS="${CMAKE_C_FLAGS:-} ${CMAKE_FLAGS}" +CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS:-} ${CMAKE_FLAGS}" + +CMAKE_CROSSCOMPILING_EMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR:-} +CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} +CMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH:-} +CMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS:-} +CMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS:-} +CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-} + +if [[ "${ENABLE_WASM_SIMD}" -ne "0" ]]; then + CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -msimd128" + CMAKE_C_FLAGS="${CMAKE_C_FLAGS} -msimd128" + CMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS} -msimd128" +fi + +if [[ ! -z "${HWY_BASELINE_TARGETS}" ]]; then + CMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS} -DHWY_BASELINE_TARGETS=${HWY_BASELINE_TARGETS}" +fi + +# Version inferred from the CI variables. +CI_COMMIT_SHA=${CI_COMMIT_SHA:-${GITHUB_SHA:-}} +JPEGXL_VERSION=${JPEGXL_VERSION:-${CI_COMMIT_SHA:0:8}} + +# Benchmark parameters +STORE_IMAGES=${STORE_IMAGES:-1} +BENCHMARK_CORPORA="${MYDIR}/third_party/corpora" + +# Local flags passed to sanitizers. +UBSAN_FLAGS=( + -fsanitize=alignment + -fsanitize=bool + -fsanitize=bounds + -fsanitize=builtin + -fsanitize=enum + -fsanitize=float-cast-overflow + -fsanitize=float-divide-by-zero + -fsanitize=integer-divide-by-zero + -fsanitize=null + -fsanitize=object-size + -fsanitize=pointer-overflow + -fsanitize=return + -fsanitize=returns-nonnull-attribute + -fsanitize=shift-base + -fsanitize=shift-exponent + -fsanitize=unreachable + -fsanitize=vla-bound + + -fno-sanitize-recover=undefined + # Brunsli uses unaligned accesses to uint32_t, so alignment is just a warning. + -fsanitize-recover=alignment +) +# -fsanitize=function doesn't work on aarch64 and arm. +if [[ "${BUILD_TARGET%%-*}" != "aarch64" && + "${BUILD_TARGET%%-*}" != "arm" ]]; then + UBSAN_FLAGS+=( + -fsanitize=function + ) +fi +if [[ "${BUILD_TARGET%%-*}" != "arm" ]]; then + UBSAN_FLAGS+=( + -fsanitize=signed-integer-overflow + ) +fi + +CLANG_TIDY_BIN=$(which clang-tidy-6.0 clang-tidy-7 clang-tidy-8 clang-tidy | head -n 1) +# Default to "cat" if "colordiff" is not installed or if stdout is not a tty. +if [[ -t 1 ]]; then + COLORDIFF_BIN=$(which colordiff cat | head -n 1) +else + COLORDIFF_BIN="cat" +fi +FIND_BIN=$(which gfind find | head -n 1) +# "false" will disable wine64 when not installed. This won't allow +# cross-compiling. +WINE_BIN=$(which wine64 false | head -n 1) + +CLANG_VERSION="${CLANG_VERSION:-}" +# Detect the clang version suffix and store it in CLANG_VERSION. For example, +# "6.0" for clang 6 or "7" for clang 7. +detect_clang_version() { + if [[ -n "${CLANG_VERSION}" ]]; then + return 0 + fi + local clang_version=$("${CC:-clang}" --version | head -n1) + clang_version=${clang_version#"Debian "} + local llvm_tag + case "${clang_version}" in + "clang version 6."*) + CLANG_VERSION="6.0" + ;; + "clang version "*) + # Any other clang version uses just the major version number. + local suffix="${clang_version#clang version }" + CLANG_VERSION="${suffix%%.*}" + ;; + "emcc"*) + # We can't use asan or msan in the emcc case. + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac +} + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} + +# Executed on exit. +on_exit() { + local retcode="$1" + # Always cleanup the CLEANUP_FILES. + cleanup + + # Post a message in the MR when requested with POST_MESSAGE_ON_ERROR but only + # if the run failed and we are not running from a MR pipeline. + if [[ ${retcode} -ne 0 && -n "${CI_BUILD_NAME:-}" && + -n "${POST_MESSAGE_ON_ERROR}" && -z "${CI_MERGE_REQUEST_ID:-}" && + "${CI_BUILD_REF_NAME}" = "master" ]]; then + load_mr_vars_from_commit + { set +xeu; } 2>/dev/null + local message="**Run ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} failed.** + +Check the output of the job at ${CI_JOB_URL:-} to see if this was your problem. +If it was, please rollback this change or fix the problem ASAP, broken builds +slow down development. Check if the error already existed in the previous build +as well. + +Pipeline: ${CI_PIPELINE_URL} + +Previous build commit: ${CI_COMMIT_BEFORE_SHA} +" + cmd_post_mr_comment "${message}" + fi +} + +trap 'retcode=$?; { set +x; } 2>/dev/null; on_exit ${retcode}' INT TERM EXIT + + +# These variables are populated when calling merge_request_commits(). + +# The current hash at the top of the current branch or merge request branch (if +# running from a merge request pipeline). +MR_HEAD_SHA="" +# The common ancestor between the current commit and the tracked branch, such +# as master. This includes a list +MR_ANCESTOR_SHA="" + +# Populate MR_HEAD_SHA and MR_ANCESTOR_SHA. +merge_request_commits() { + { set +x; } 2>/dev/null + # GITHUB_SHA is the current reference being build in GitHub Actions. + if [[ -n "${GITHUB_SHA:-}" ]]; then + # GitHub normally does a checkout of a merge commit on a shallow repository + # by default. We want to get a bit more of the history to be able to diff + # changes on the Pull Request if needed. This fetches 10 more commits which + # should be enough given that PR normally should have 1 commit. + git -C "${MYDIR}" fetch -q origin "${GITHUB_SHA}" --depth 10 + MR_HEAD_SHA="$(git rev-parse "FETCH_HEAD^2" 2>/dev/null || + echo "${GITHUB_SHA}")" + else + # CI_BUILD_REF is the reference currently being build in the CI workflow. + MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") + fi + + if [[ -n "${CI_MERGE_REQUEST_IID:-}" ]]; then + # Merge request pipeline in CI. In this case the upstream is called "origin" + # but it refers to the forked project that's the source of the merge + # request. We need to get the target of the merge request, for which we need + # to query that repository using our CI_JOB_TOKEN. + echo "machine gitlab.com login gitlab-ci-token password ${CI_JOB_TOKEN}" \ + >> "${HOME}/.netrc" + git -C "${MYDIR}" fetch "${CI_MERGE_REQUEST_PROJECT_URL}" \ + "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) + elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then + # Pull request workflow in GitHub Actions. GitHub checkout action uses + # "origin" as the remote for the git checkout. + git -C "${MYDIR}" fetch -q origin "${GITHUB_BASE_REF}" + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) + else + # We are in a local branch, not a merge request. + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) + fi + + if [[ -z "${MR_ANCESTOR_SHA}" ]]; then + echo "Warning, not tracking any branch, using the last commit in HEAD.">&2 + # This prints the return value with just HEAD. + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^") + else + # GitHub runs the pipeline on a merge commit, no need to look for the common + # ancestor in that case. + if [[ -z "${GITHUB_BASE_REF:-}" ]]; then + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base \ + "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") + fi + fi + set -x +} + +# Load the MR iid from the landed commit message when running not from a +# merge request workflow. This is useful to post back results at the merge +# request when running pipelines from master. +load_mr_vars_from_commit() { + { set +x; } 2>/dev/null + if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then + local mr_iid=$(git rev-list --format=%B --max-count=1 HEAD | + grep -F "${CI_PROJECT_URL}" | grep -F "/merge_requests" | head -n 1) + # mr_iid contains a string like this if it matched: + # Part-of: + if [[ -n "${mr_iid}" ]]; then + mr_iid=$(echo "${mr_iid}" | + sed -E 's,^.*merge_requests/([0-9]+)>.*$,\1,') + CI_MERGE_REQUEST_IID="${mr_iid}" + CI_MERGE_REQUEST_PROJECT_ID=${CI_PROJECT_ID} + fi + fi + set -x +} + +# Posts a comment to the current merge request. +cmd_post_mr_comment() { + { set +x; } 2>/dev/null + local comment="$1" + if [[ -n "${BOT_TOKEN:-}" && -n "${CI_MERGE_REQUEST_IID:-}" ]]; then + local url="${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" + curl -X POST -g \ + -H "PRIVATE-TOKEN: ${BOT_TOKEN}" \ + --data-urlencode "body=${comment}" \ + --output /dev/null \ + "${url}" + fi + set -x +} + +# Set up and export the environment variables needed by the child processes. +export_env() { + if [[ "${BUILD_TARGET}" == *mingw32 ]]; then + # Wine needs to know the paths to the mingw dlls. These should be + # separated by ';'. + WINEPATH=$("${CC:-clang}" -print-search-dirs --target="${BUILD_TARGET}" \ + | grep -F 'libraries: =' | cut -f 2- -d '=' | tr ':' ';') + # We also need our own libraries in the wine path. + local real_build_dir=$(realpath "${BUILD_DIR}") + # Some library .dll dependencies are installed in /bin: + export WINEPATH="${WINEPATH};${real_build_dir};${real_build_dir}/third_party/brotli;/usr/${BUILD_TARGET}/bin" + + local prefix="${BUILD_DIR}/wineprefix" + mkdir -p "${prefix}" + export WINEPREFIX=$(realpath "${prefix}") + fi + # Sanitizers need these variables to print and properly format the stack + # traces: + LLVM_SYMBOLIZER=$("${CC:-clang}" -print-prog-name=llvm-symbolizer || true) + if [[ -n "${LLVM_SYMBOLIZER}" ]]; then + export ASAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + export MSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + export UBSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + fi +} + +cmake_configure() { + export_env + + if [[ "${STACK_SIZE:-0}" == 1 ]]; then + # Dump the stack size of each function in the .stack_sizes section for + # analysis. + CMAKE_C_FLAGS+=" -fstack-size-section" + CMAKE_CXX_FLAGS+=" -fstack-size-section" + fi + + local args=( + -B"${BUILD_DIR}" -H"${MYDIR}" + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" + -G Ninja + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" + -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" + -DCMAKE_MODULE_LINKER_FLAGS="${CMAKE_MODULE_LINKER_FLAGS}" + -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" + -DJPEGXL_VERSION="${JPEGXL_VERSION}" + -DSANITIZER="${SANITIZER}" + # These are not enabled by default in cmake. + -DJPEGXL_ENABLE_VIEWERS=ON + -DJPEGXL_ENABLE_PLUGINS=ON + -DJPEGXL_ENABLE_DEVTOOLS=ON + # We always use libfuzzer in the ci.sh wrapper. + -DJPEGXL_FUZZER_LINK_FLAGS="-fsanitize=fuzzer" + ) + if [[ "${BUILD_TARGET}" != *mingw32 ]]; then + args+=( + -DJPEGXL_WARNINGS_AS_ERRORS=ON + ) + fi + if [[ -n "${BUILD_TARGET}" ]]; then + local system_name="Linux" + if [[ "${BUILD_TARGET}" == *mingw32 ]]; then + # When cross-compiling with mingw the target must be set to Windows and + # run programs with wine. + system_name="Windows" + args+=( + -DCMAKE_CROSSCOMPILING_EMULATOR="${WINE_BIN}" + # Normally CMake automatically defines MINGW=1 when building with the + # mingw compiler (x86_64-w64-mingw32-gcc) but we are normally compiling + # with clang. + -DMINGW=1 + ) + fi + # EMSCRIPTEN toolchain sets the right values itself + if [[ "${BUILD_TARGET}" != wasm* ]]; then + # If set, BUILD_TARGET must be the target triplet such as + # x86_64-unknown-linux-gnu. + args+=( + -DCMAKE_C_COMPILER_TARGET="${BUILD_TARGET}" + -DCMAKE_CXX_COMPILER_TARGET="${BUILD_TARGET}" + # Only the first element of the target triplet. + -DCMAKE_SYSTEM_PROCESSOR="${BUILD_TARGET%%-*}" + -DCMAKE_SYSTEM_NAME="${system_name}" + ) + else + # sjpeg confuses WASM SIMD with SSE. + args+=( + -DSJPEG_ENABLE_SIMD=OFF + ) + fi + args+=( + # These are needed to make googletest work when cross-compiling. + -DCMAKE_CROSSCOMPILING=1 + -DHAVE_STD_REGEX=0 + -DHAVE_POSIX_REGEX=0 + -DHAVE_GNU_POSIX_REGEX=0 + -DHAVE_STEADY_CLOCK=0 + -DHAVE_THREAD_SAFETY_ATTRIBUTES=0 + ) + if [[ -z "${CMAKE_FIND_ROOT_PATH}" ]]; then + # find_package() will look in this prefix for libraries. + CMAKE_FIND_ROOT_PATH="/usr/${BUILD_TARGET}" + fi + if [[ -z "${CMAKE_PREFIX_PATH}" ]]; then + CMAKE_PREFIX_PATH="/usr/${BUILD_TARGET}" + fi + # Use pkg-config for the target. If there's no pkg-config available for the + # target we can set the PKG_CONFIG_PATH to the appropriate path in most + # linux distributions. + local pkg_config=$(which "${BUILD_TARGET}-pkg-config" || true) + if [[ -z "${pkg_config}" ]]; then + pkg_config=$(which pkg-config) + export PKG_CONFIG_LIBDIR="/usr/${BUILD_TARGET}/lib/pkgconfig" + fi + if [[ -n "${pkg_config}" ]]; then + args+=(-DPKG_CONFIG_EXECUTABLE="${pkg_config}") + fi + fi + if [[ -n "${CMAKE_CROSSCOMPILING_EMULATOR}" ]]; then + args+=( + -DCMAKE_CROSSCOMPILING_EMULATOR="${CMAKE_CROSSCOMPILING_EMULATOR}" + ) + fi + if [[ -n "${CMAKE_FIND_ROOT_PATH}" ]]; then + args+=( + -DCMAKE_FIND_ROOT_PATH="${CMAKE_FIND_ROOT_PATH}" + ) + fi + if [[ -n "${CMAKE_PREFIX_PATH}" ]]; then + args+=( + -DCMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" + ) + fi + if [[ -n "${CMAKE_C_COMPILER_LAUNCHER}" ]]; then + args+=( + -DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_C_COMPILER_LAUNCHER}" + ) + fi + if [[ -n "${CMAKE_CXX_COMPILER_LAUNCHER}" ]]; then + args+=( + -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CXX_COMPILER_LAUNCHER}" + ) + fi + if [[ -n "${CMAKE_MAKE_PROGRAM}" ]]; then + args+=( + -DCMAKE_MAKE_PROGRAM="${CMAKE_MAKE_PROGRAM}" + ) + fi + cmake "${args[@]}" "$@" +} + +cmake_build_and_test() { + # gtest_discover_tests() runs the test binaries to discover the list of tests + # at build time, which fails under qemu. + ASAN_OPTIONS=detect_leaks=0 cmake --build "${BUILD_DIR}" -- all doc + # Pack test binaries if requested. + if [[ "${PACK_TEST:-}" == "1" ]]; then + (cd "${BUILD_DIR}" + ${FIND_BIN} -name '*.cmake' -a '!' -path '*CMakeFiles*' + ${FIND_BIN} -type d -name tests -a '!' -path '*CMakeFiles*' + ) | tar -C "${BUILD_DIR}" -cf "${BUILD_DIR}/tests.tar.xz" -T - \ + --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" + du -h "${BUILD_DIR}/tests.tar.xz" + # Pack coverage data if also available. + touch "${BUILD_DIR}/gcno.sentinel" + (cd "${BUILD_DIR}"; echo gcno.sentinel; ${FIND_BIN} -name '*gcno') | \ + tar -C "${BUILD_DIR}" -cvf "${BUILD_DIR}/gcno.tar.xz" -T - \ + --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" + fi + + if [[ "${SKIP_TEST}" -ne "1" ]]; then + (cd "${BUILD_DIR}" + export UBSAN_OPTIONS=print_stacktrace=1 + [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" + ctest -j $(nproc --all || echo 1) --output-on-failure) + fi +} + +# Configure the build to strip unused functions. This considerably reduces the +# output size, specially for tests which only use a small part of the whole +# library. +strip_dead_code() { + # Emscripten does tree shaking without any extra flags. + if [[ "${CMAKE_TOOLCHAIN_FILE##*/}" == "Emscripten.cmake" ]]; then + return 0 + fi + # -ffunction-sections, -fdata-sections and -Wl,--gc-sections effectively + # discard all unreachable code, reducing the code size. For this to work, we + # need to also pass --no-export-dynamic to prevent it from exporting all the + # internal symbols (like functions) making them all reachable and thus not a + # candidate for removal. + CMAKE_CXX_FLAGS+=" -ffunction-sections -fdata-sections" + CMAKE_C_FLAGS+=" -ffunction-sections -fdata-sections" + if [[ "${OS}" == "Darwin" ]]; then + CMAKE_EXE_LINKER_FLAGS+=" -dead_strip" + CMAKE_SHARED_LINKER_FLAGS+=" -dead_strip" + else + CMAKE_EXE_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" + CMAKE_SHARED_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" + fi +} + +### Externally visible commands + +cmd_debug() { + CMAKE_BUILD_TYPE="Debug" + cmake_configure "$@" + cmake_build_and_test +} + +cmd_release() { + CMAKE_BUILD_TYPE="Release" + strip_dead_code + cmake_configure "$@" + cmake_build_and_test +} + +cmd_opt() { + CMAKE_BUILD_TYPE="RelWithDebInfo" + CMAKE_CXX_FLAGS+=" -DJXL_DEBUG_WARNING -DJXL_DEBUG_ON_ERROR" + cmake_configure "$@" + cmake_build_and_test +} + +cmd_coverage() { + # -O0 prohibits stack space reuse -> causes stack-overflow on dozens of tests. + TEST_STACK_LIMIT="none" + + cmd_release -DJPEGXL_ENABLE_COVERAGE=ON "$@" + + if [[ "${SKIP_TEST}" -ne "1" ]]; then + # If we didn't run the test we also don't print a coverage report. + cmd_coverage_report + fi +} + +cmd_coverage_report() { + LLVM_COV=$("${CC:-clang}" -print-prog-name=llvm-cov) + local real_build_dir=$(realpath "${BUILD_DIR}") + local gcovr_args=( + -r "${real_build_dir}" + --gcov-executable "${LLVM_COV} gcov" + # Only print coverage information for the jxl and fuif directories. The rest + # is not part of the code under test. + --filter '.*jxl/.*' + --exclude '.*_test.cc' + --object-directory "${real_build_dir}" + ) + + ( + cd "${real_build_dir}" + gcovr "${gcovr_args[@]}" --html --html-details \ + --output="${real_build_dir}/coverage.html" + gcovr "${gcovr_args[@]}" --print-summary | + tee "${real_build_dir}/coverage.txt" + gcovr "${gcovr_args[@]}" --xml --output="${real_build_dir}/coverage.xml" + ) +} + +cmd_test() { + export_env + # Unpack tests if needed. + if [[ -e "${BUILD_DIR}/tests.tar.xz" && ! -d "${BUILD_DIR}/tests" ]]; then + tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/tests.tar.xz" + fi + if [[ -e "${BUILD_DIR}/gcno.tar.xz" && ! -d "${BUILD_DIR}/gcno.sentinel" ]]; then + tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/gcno.tar.xz" + fi + (cd "${BUILD_DIR}" + export UBSAN_OPTIONS=print_stacktrace=1 + [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" + ctest -j $(nproc --all || echo 1) --output-on-failure "$@") +} + +cmd_gbench() { + export_env + (cd "${BUILD_DIR}" + export UBSAN_OPTIONS=print_stacktrace=1 + lib/jxl_gbench \ + --benchmark_counters_tabular=true \ + --benchmark_out_format=json \ + --benchmark_out=gbench.json "$@" + ) +} + +cmd_asanfuzz() { + CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link" + CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link" + cmd_asan -DJPEGXL_ENABLE_FUZZERS=ON "$@" +} + +cmd_msanfuzz() { + # Install msan if needed before changing the flags. + detect_clang_version + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then + # Install msan libraries for this version if needed or if an older version + # with libc++abi was installed. + cmd_msan_install + fi + + CMAKE_CXX_FLAGS+=" -fsanitize=fuzzer-no-link" + CMAKE_C_FLAGS+=" -fsanitize=fuzzer-no-link" + cmd_msan -DJPEGXL_ENABLE_FUZZERS=ON "$@" +} + +cmd_asan() { + SANITIZER="asan" + CMAKE_C_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ + -fsanitize=address ${UBSAN_FLAGS[@]}" + CMAKE_CXX_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ + -fsanitize=address ${UBSAN_FLAGS[@]}" + strip_dead_code + cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF + cmake_build_and_test +} + +cmd_tsan() { + SANITIZER="tsan" + local tsan_args=( + -DJXL_ENABLE_ASSERT=1 + -g + -DTHREAD_SANITIZER + ${UBSAN_FLAGS[@]} + -fsanitize=thread + ) + CMAKE_C_FLAGS+=" ${tsan_args[@]}" + CMAKE_CXX_FLAGS+=" ${tsan_args[@]}" + + CMAKE_BUILD_TYPE="RelWithDebInfo" + cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF + cmake_build_and_test +} + +cmd_msan() { + SANITIZER="msan" + detect_clang_version + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then + # Install msan libraries for this version if needed or if an older version + # with libc++abi was installed. + cmd_msan_install + fi + + local msan_c_flags=( + -fsanitize=memory + -fno-omit-frame-pointer + -fsanitize-memory-track-origins + + -DJXL_ENABLE_ASSERT=1 + -g + -DMEMORY_SANITIZER + + # Force gtest to not use the cxxbai. + -DGTEST_HAS_CXXABI_H_=0 + ) + local msan_cxx_flags=( + "${msan_c_flags[@]}" + + # Some C++ sources don't use the std at all, so the -stdlib=libc++ is unused + # in those cases. Ignore the warning. + -Wno-unused-command-line-argument + -stdlib=libc++ + + # We include the libc++ from the msan directory instead, so we don't want + # the std includes. + -nostdinc++ + -cxx-isystem"${msan_prefix}/include/c++/v1" + ) + + local msan_linker_flags=( + -L"${msan_prefix}"/lib + -Wl,-rpath -Wl,"${msan_prefix}"/lib/ + ) + + CMAKE_C_FLAGS+=" ${msan_c_flags[@]} ${UBSAN_FLAGS[@]}" + CMAKE_CXX_FLAGS+=" ${msan_cxx_flags[@]} ${UBSAN_FLAGS[@]}" + CMAKE_EXE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + CMAKE_MODULE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + CMAKE_SHARED_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + strip_dead_code + cmake_configure "$@" \ + -DCMAKE_CROSSCOMPILING=1 -DRUN_HAVE_STD_REGEX=0 -DRUN_HAVE_POSIX_REGEX=0 \ + -DJPEGXL_ENABLE_TCMALLOC=OFF + cmake_build_and_test +} + +# Install libc++ libraries compiled with msan in the msan_prefix for the current +# compiler version. +cmd_msan_install() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + # Detect the llvm to install: + export CC="${CC:-clang}" + export CXX="${CXX:-clang++}" + detect_clang_version + local llvm_tag="llvmorg-${CLANG_VERSION}.0.0" + case "${CLANG_VERSION}" in + "6.0") + llvm_tag="llvmorg-6.0.1" + ;; + "7") + llvm_tag="llvmorg-7.0.1" + ;; + esac + local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" + curl -L --show-error -o "${llvm_targz}" \ + "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" + tar -C "${tmpdir}" -zxf "${llvm_targz}" + local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + rm -rf "${msan_prefix}" + + declare -A CMAKE_EXTRAS + CMAKE_EXTRAS[libcxx]="\ + -DLIBCXX_CXX_ABI=libstdc++ \ + -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" + + for project in libcxx; do + local proj_build="${tmpdir}/build-${project}" + local proj_dir="${llvm_root}/${project}" + mkdir -p "${proj_build}" + cmake -B"${proj_build}" -H"${proj_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_USE_SANITIZER=Memory \ + -DLLVM_PATH="${llvm_root}/llvm" \ + -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ + head -n1)" \ + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ + -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" \ + -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ + ${CMAKE_EXTRAS[${project}]} + cmake --build "${proj_build}" + ninja -C "${proj_build}" install + done +} + +# Internal build step shared between all cmd_ossfuzz_* commands. +_cmd_ossfuzz() { + local sanitizer="$1" + shift + mkdir -p "${BUILD_DIR}" + local real_build_dir=$(realpath "${BUILD_DIR}") + + # oss-fuzz defines three directories: + # * /work, with the working directory to do re-builds + # * /src, with the source code to build + # * /out, with the output directory where to copy over the built files. + # We use $BUILD_DIR as the /work and the script directory as the /src. The + # /out directory is ignored as developers are used to look for the fuzzers in + # $BUILD_DIR/tools/ directly. + + if [[ "${sanitizer}" = "memory" && ! -d "${BUILD_DIR}/msan" ]]; then + sudo docker run --rm -i \ + --user $(id -u):$(id -g) \ + -v "${real_build_dir}":/work \ + gcr.io/oss-fuzz-base/msan-libs-builder \ + bash -c "cp -r /msan /work" + fi + + # Args passed to ninja. These will be evaluated as a string separated by + # spaces. + local jpegxl_extra_args="$@" + + sudo docker run --rm -i \ + -e JPEGXL_UID=$(id -u) \ + -e JPEGXL_GID=$(id -g) \ + -e FUZZING_ENGINE="${FUZZING_ENGINE:-libfuzzer}" \ + -e SANITIZER="${sanitizer}" \ + -e ARCHITECTURE=x86_64 \ + -e FUZZING_LANGUAGE=c++ \ + -e MSAN_LIBS_PATH="/work/msan" \ + -e JPEGXL_EXTRA_ARGS="${jpegxl_extra_args}" \ + -v "${MYDIR}":/src/libjxl \ + -v "${MYDIR}/tools/ossfuzz-build.sh":/src/build.sh \ + -v "${real_build_dir}":/work \ + gcr.io/oss-fuzz/libjxl +} + +cmd_ossfuzz_asan() { + _cmd_ossfuzz address "$@" +} +cmd_ossfuzz_msan() { + _cmd_ossfuzz memory "$@" +} +cmd_ossfuzz_ubsan() { + _cmd_ossfuzz undefined "$@" +} + +cmd_ossfuzz_ninja() { + [[ -e "${BUILD_DIR}/build.ninja" ]] + local real_build_dir=$(realpath "${BUILD_DIR}") + + if [[ -e "${BUILD_DIR}/msan" ]]; then + echo "ossfuzz_ninja doesn't work with msan builds. Use ossfuzz_msan." >&2 + exit 1 + fi + + sudo docker run --rm -i \ + --user $(id -u):$(id -g) \ + -v "${MYDIR}":/src/libjxl \ + -v "${real_build_dir}":/work \ + gcr.io/oss-fuzz/libjxl \ + ninja -C /work "$@" +} + +cmd_fast_benchmark() { + local small_corpus_tar="${BENCHMARK_CORPORA}/jyrki-full.tar" + mkdir -p "${BENCHMARK_CORPORA}" + curl --show-error -o "${small_corpus_tar}" -z "${small_corpus_tar}" \ + "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/jyrki-full.tar" + + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + tar -xf "${small_corpus_tar}" -C "${tmpdir}" + + run_benchmark "${tmpdir}" 1048576 +} + +cmd_benchmark() { + local nikon_corpus_tar="${BENCHMARK_CORPORA}/nikon-subset.tar" + mkdir -p "${BENCHMARK_CORPORA}" + curl --show-error -o "${nikon_corpus_tar}" -z "${nikon_corpus_tar}" \ + "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/nikon-subset.tar" + + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + tar -xvf "${nikon_corpus_tar}" -C "${tmpdir}" + + local sem_id="jpegxl_benchmark-$$" + local nprocs=$(nproc --all || echo 1) + images=() + local filename + while IFS= read -r filename; do + # This removes the './' + filename="${filename:2}" + local mode + if [[ "${filename:0:4}" == "srgb" ]]; then + mode="RGB_D65_SRG_Rel_SRG" + elif [[ "${filename:0:5}" == "adobe" ]]; then + mode="RGB_D65_Ado_Rel_Ado" + else + echo "Unknown image colorspace: ${filename}" >&2 + exit 1 + fi + png_filename="${filename%.ppm}.png" + png_filename=$(echo "${png_filename}" | tr '/' '_') + sem --bg --id "${sem_id}" -j"${nprocs}" -- \ + "${BUILD_DIR}/tools/decode_and_encode" \ + "${tmpdir}/${filename}" "${mode}" "${tmpdir}/${png_filename}" + images+=( "${png_filename}" ) + done < <(cd "${tmpdir}"; ${FIND_BIN} . -name '*.ppm' -type f) + sem --id "${sem_id}" --wait + + # We need about 10 GiB per thread on these images. + run_benchmark "${tmpdir}" 10485760 +} + +get_mem_available() { + if [[ "${OS}" == "Darwin" ]]; then + echo $(vm_stat | grep -F 'Pages free:' | awk '{print $3 * 4}') + else + echo $(grep -F MemAvailable: /proc/meminfo | awk '{print $2}') + fi +} + +run_benchmark() { + local src_img_dir="$1" + local mem_per_thread="${2:-10485760}" + + local output_dir="${BUILD_DIR}/benchmark_results" + mkdir -p "${output_dir}" + + # The memory available at the beginning of the benchmark run in kB. The number + # of threads depends on the available memory, and the passed memory per + # thread. We also add a 2 GiB of constant memory. + local mem_available="$(get_mem_available)" + # Check that we actually have a MemAvailable value. + [[ -n "${mem_available}" ]] + local num_threads=$(( (${mem_available} - 1048576) / ${mem_per_thread} )) + if [[ ${num_threads} -le 0 ]]; then + num_threads=1 + fi + + 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 + --output_dir "${output_dir}" + --noprofiler --show_progress + --num_threads="${num_threads}" + ) + if [[ "${STORE_IMAGES}" == "1" ]]; then + benchmark_args+=(--save_decompressed --save_compressed) + fi + ( + [[ "${TEST_STACK_LIMIT}" == "none" ]] || ulimit -s "${TEST_STACK_LIMIT}" + "${BUILD_DIR}/tools/benchmark_xl" "${benchmark_args[@]}" | \ + tee "${output_dir}/results.txt" + + # Check error code for benckmark_xl command. This will exit if not. + return ${PIPESTATUS[0]} + ) + + if [[ -n "${CI_BUILD_NAME:-}" ]]; then + { set +x; } 2>/dev/null + local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): + +$(cat "${output_dir}/results.txt") +" + cmd_post_mr_comment "${message}" + set -x + fi +} + +# Helper function to wait for the CPU temperature to cool down on ARM. +wait_for_temp() { + { set +x; } 2>/dev/null + local temp_limit=${1:-38000} + if [[ -z "${THERMAL_FILE:-}" ]]; then + echo "Must define the THERMAL_FILE with the thermal_zoneX/temp file" \ + "to read the temperature from. This is normally set in the runner." >&2 + exit 1 + fi + local org_temp=$(cat "${THERMAL_FILE}") + if [[ "${org_temp}" -ge "${temp_limit}" ]]; then + echo -n "Waiting for temp to get down from ${org_temp}... " + fi + local temp="${org_temp}" + local secs=0 + while [[ "${temp}" -ge "${temp_limit}" ]]; do + sleep 1 + temp=$(cat "${THERMAL_FILE}") + echo -n "${temp} " + secs=$((secs + 1)) + if [[ ${secs} -ge 5 ]]; then + break + fi + done + if [[ "${org_temp}" -ge "${temp_limit}" ]]; then + echo "Done, temp=${temp}" + fi + set -x +} + +# Helper function to set the cpuset restriction of the current process. +cmd_cpuset() { + [[ "${SKIP_CPUSET:-}" != "1" ]] || return 0 + local newset="$1" + local mycpuset=$(cat /proc/self/cpuset) + mycpuset="/dev/cpuset${mycpuset}" + # Check that the directory exists: + [[ -d "${mycpuset}" ]] + if [[ -e "${mycpuset}/cpuset.cpus" ]]; then + echo "${newset}" >"${mycpuset}/cpuset.cpus" + else + echo "${newset}" >"${mycpuset}/cpus" + fi +} + +# Return the encoding/decoding speed from the Stats output. +_speed_from_output() { + local speed="$1" + local unit="${2:-MP/s}" + if [[ "${speed}" == *"${unit}"* ]]; then + speed="${speed%% ${unit}*}" + speed="${speed##* }" + echo "${speed}" + fi +} + + +# Run benchmarks on ARM for the big and little CPUs. +cmd_arm_benchmark() { + # Flags used for cjxl encoder with .png inputs + local jxl_png_benchmarks=( + # Lossy options: + "--epf=0 --distance=1.0 --speed=cheetah" + "--epf=2 --distance=1.0 --speed=cheetah" + "--epf=0 --distance=8.0 --speed=cheetah" + "--epf=1 --distance=8.0 --speed=cheetah" + "--epf=2 --distance=8.0 --speed=cheetah" + "--epf=3 --distance=8.0 --speed=cheetah" + "--modular -Q 90" + "--modular -Q 50" + # Lossless options: + "--modular" + "--modular -E 0 -I 0" + "--modular -P 5" + "--modular --responsive=1" + # Near-lossless options: + "--epf=0 --distance=0.3 --speed=fast" + "--modular -Q 97" + ) + + # Flags used for cjxl encoder with .jpg inputs. These should do lossless + # JPEG recompression (of pixels or full jpeg). + local jxl_jpeg_benchmarks=( + "--num_reps=3" + ) + + local images=( + "third_party/testdata/imagecompression.info/flower_foveon.png" + ) + + local jpg_images=( + "third_party/testdata/imagecompression.info/flower_foveon.png.im_q85_420.jpg" + ) + + if [[ "${SKIP_CPUSET:-}" == "1" ]]; then + # Use a single cpu config in this case. + local cpu_confs=("?") + else + # Otherwise the CPU config comes from the environment: + local cpu_confs=( + "${RUNNER_CPU_LITTLE}" + "${RUNNER_CPU_BIG}" + # The CPU description is something like 3-7, so these configurations only + # take the first CPU of the group. + "${RUNNER_CPU_LITTLE%%-*}" + "${RUNNER_CPU_BIG%%-*}" + ) + # Check that RUNNER_CPU_ALL is defined. In the SKIP_CPUSET=1 case this will + # be ignored but still evaluated when calling cmd_cpuset. + [[ -n "${RUNNER_CPU_ALL}" ]] + fi + + local jpg_dirname="third_party/corpora/jpeg" + mkdir -p "${jpg_dirname}" + local jpg_qualities=( 50 80 95 ) + for src_img in "${images[@]}"; do + for q in "${jpg_qualities[@]}"; do + local jpeg_name="${jpg_dirname}/"$(basename "${src_img}" .png)"-q${q}.jpg" + convert -sampling-factor 1x1 -quality "${q}" \ + "${src_img}" "${jpeg_name}" + jpg_images+=("${jpeg_name}") + done + done + + local output_dir="${BUILD_DIR}/benchmark_results" + mkdir -p "${output_dir}" + local runs_file="${output_dir}/runs.txt" + + if [[ ! -e "${runs_file}" ]]; then + echo -e "binary\tflags\tsrc_img\tsrc size\tsrc pixels\tcpuset\tenc size (B)\tenc speed (MP/s)\tdec speed (MP/s)\tJPG dec speed (MP/s)\tJPG dec speed (MB/s)" | + tee -a "${runs_file}" + fi + + mkdir -p "${BUILD_DIR}/arm_benchmark" + local flags + local src_img + for src_img in "${jpg_images[@]}" "${images[@]}"; do + local src_img_hash=$(sha1sum "${src_img}" | cut -f 1 -d ' ') + local enc_binaries=("${BUILD_DIR}/tools/cjxl") + local src_ext="${src_img##*.}" + for enc_binary in "${enc_binaries[@]}"; do + local enc_binary_base=$(basename "${enc_binary}") + + # Select the list of flags to use for the current encoder/image pair. + local img_benchmarks + if [[ "${src_ext}" == "jpg" ]]; then + img_benchmarks=("${jxl_jpeg_benchmarks[@]}") + else + img_benchmarks=("${jxl_png_benchmarks[@]}") + fi + + for flags in "${img_benchmarks[@]}"; do + # Encoding step. + local enc_file_hash="${enc_binary_base} || $flags || ${src_img} || ${src_img_hash}" + enc_file_hash=$(echo "${enc_file_hash}" | sha1sum | cut -f 1 -d ' ') + local enc_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jxl" + + for cpu_conf in "${cpu_confs[@]}"; do + cmd_cpuset "${cpu_conf}" + # nproc returns the number of active CPUs, which is given by the cpuset + # mask. + local num_threads="$(nproc)" + + echo "Encoding with: ${enc_binary_base} img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" + local enc_output + if [[ "${flags}" == *"modular"* ]]; then + # We don't benchmark encoding speed in this case. + if [[ ! -f "${enc_file}" ]]; then + cmd_cpuset "${RUNNER_CPU_ALL:-}" + "${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" + mv "${enc_file}.tmp" "${enc_file}" + cmd_cpuset "${cpu_conf}" + fi + enc_output=" ?? MP/s" + else + wait_for_temp + enc_output=$("${enc_binary}" ${flags} "${src_img}" "${enc_file}.tmp" \ + 2>&1 | tee /dev/stderr | grep -F "MP/s [") + mv "${enc_file}.tmp" "${enc_file}" + fi + local enc_speed=$(_speed_from_output "${enc_output}") + local enc_size=$(stat -c "%s" "${enc_file}") + + echo "Decoding with: img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" + + local dec_output + wait_for_temp + dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ + --num_reps=5 --num_threads="${num_threads}" 2>&1 | tee /dev/stderr | + grep -E "M[BP]/s \[") + local img_size=$(echo "${dec_output}" | cut -f 1 -d ',') + local img_size_x=$(echo "${img_size}" | cut -f 1 -d ' ') + local img_size_y=$(echo "${img_size}" | cut -f 3 -d ' ') + local img_size_px=$(( ${img_size_x} * ${img_size_y} )) + local dec_speed=$(_speed_from_output "${dec_output}") + + # For JPEG lossless recompression modes (where the original is a JPEG) + # decode to JPG as well. + local jpeg_dec_mps_speed="" + local jpeg_dec_mbs_speed="" + if [[ "${src_ext}" == "jpg" ]]; then + wait_for_temp + local dec_file="${BUILD_DIR}/arm_benchmark/${enc_file_hash}.jpg" + dec_output=$("${BUILD_DIR}/tools/djxl" "${enc_file}" \ + "${dec_file}" --num_reps=5 --num_threads="${num_threads}" 2>&1 | \ + tee /dev/stderr | grep -E "M[BP]/s \[") + local jpeg_dec_mps_speed=$(_speed_from_output "${dec_output}") + local jpeg_dec_mbs_speed=$(_speed_from_output "${dec_output}" MB/s) + if ! cmp --quiet "${src_img}" "${dec_file}"; then + # Add a start at the end to signal that the files are different. + jpeg_dec_mbs_speed+="*" + fi + fi + + # Record entry in a tab-separated file. + local src_img_base=$(basename "${src_img}") + echo -e "${enc_binary_base}\t${flags}\t${src_img_base}\t${img_size}\t${img_size_px}\t${cpu_conf}\t${enc_size}\t${enc_speed}\t${dec_speed}\t${jpeg_dec_mps_speed}\t${jpeg_dec_mbs_speed}" | + tee -a "${runs_file}" + done + done + done + done + cmd_cpuset "${RUNNER_CPU_ALL:-}" + cat "${runs_file}" + + if [[ -n "${CI_BUILD_NAME:-}" ]]; then + load_mr_vars_from_commit + { set +x; } 2>/dev/null + local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): + +\`\`\` +$(column -t -s " " "${runs_file}") +\`\`\` +" + cmd_post_mr_comment "${message}" + set -x + fi +} + +# Generate a corpus and run the fuzzer on that corpus. +cmd_fuzz() { + local corpus_dir=$(realpath "${BUILD_DIR}/fuzzer_corpus") + local fuzzer_crash_dir=$(realpath "${BUILD_DIR}/fuzzer_crash") + mkdir -p "${corpus_dir}" "${fuzzer_crash_dir}" + # Generate step. + "${BUILD_DIR}/tools/fuzzer_corpus" "${corpus_dir}" + # Run step: + local nprocs=$(nproc --all || echo 1) + ( + cd "${BUILD_DIR}" + "tools/djxl_fuzzer" "${fuzzer_crash_dir}" "${corpus_dir}" \ + -max_total_time="${FUZZER_MAX_TIME}" -jobs=${nprocs} \ + -artifact_prefix="${fuzzer_crash_dir}/" + ) +} + +# Runs the linter (clang-format) on the pending CLs. +cmd_lint() { + merge_request_commits + { set +x; } 2>/dev/null + local versions=(${1:-6.0 7 8 9 10 11}) + local clang_format_bins=("${versions[@]/#/clang-format-}" clang-format) + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + + local ret=0 + local build_patch="${tmpdir}/build_cleaner.patch" + if ! "${MYDIR}/tools/build_cleaner.py" >"${build_patch}"; then + ret=1 + echo "build_cleaner.py findings:" >&2 + "${COLORDIFF_BIN}" <"${build_patch}" + echo "Run \`tools/build_cleaner.py --update\` to apply them" >&2 + fi + + local installed=() + local clang_patch + local clang_format + for clang_format in "${clang_format_bins[@]}"; do + if ! which "${clang_format}" >/dev/null; then + continue + fi + installed+=("${clang_format}") + local tmppatch="${tmpdir}/${clang_format}.patch" + # We include in this linter all the changes including the uncommitted changes + # to avoid printing changes already applied. + set -x + git -C "${MYDIR}" "${clang_format}" --binary "${clang_format}" \ + --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" + { set +x; } 2>/dev/null + + if grep -E '^--- ' "${tmppatch}">/dev/null; then + if [[ -n "${LINT_OUTPUT:-}" ]]; then + cp "${tmppatch}" "${LINT_OUTPUT}" + fi + clang_patch="${tmppatch}" + else + echo "clang-format check OK" >&2 + return ${ret} + fi + done + + if [[ ${#installed[@]} -eq 0 ]]; then + echo "You must install clang-format for \"git clang-format\"" >&2 + exit 1 + fi + + # clang-format is installed but found problems. + echo "clang-format findings:" >&2 + "${COLORDIFF_BIN}" < "${clang_patch}" + + echo "clang-format found issues in your patches from ${MR_ANCESTOR_SHA}" \ + "to the current patch. Run \`./ci.sh lint | patch -p1\` from the base" \ + "directory to apply them." >&2 + exit 1 +} + +# Runs clang-tidy on the pending CLs. If the "all" argument is passed it runs +# clang-tidy over all the source files instead. +cmd_tidy() { + local what="${1:-}" + + if [[ -z "${CLANG_TIDY_BIN}" ]]; then + echo "ERROR: You must install clang-tidy-7 or newer to use ci.sh tidy" >&2 + exit 1 + fi + + local git_args=() + if [[ "${what}" == "all" ]]; then + git_args=(ls-files) + shift + else + merge_request_commits + git_args=( + diff-tree --no-commit-id --name-only -r "${MR_ANCESTOR_SHA}" + "${MR_HEAD_SHA}" + ) + fi + + # Clang-tidy needs the compilation database generated by cmake. + if [[ ! -e "${BUILD_DIR}/compile_commands.json" ]]; then + # Generate the build options in debug mode, since we need the debug asserts + # enabled for the clang-tidy analyzer to use them. + CMAKE_BUILD_TYPE="Debug" + cmake_configure + # Build the autogen targets to generate the .h files from the .ui files. + local autogen_targets=( + $(ninja -C "${BUILD_DIR}" -t targets | grep -F _autogen: | + cut -f 1 -d :) + ) + if [[ ${#autogen_targets[@]} != 0 ]]; then + ninja -C "${BUILD_DIR}" "${autogen_targets[@]}" + fi + fi + + cd "${MYDIR}" + local nprocs=$(nproc --all || echo 1) + local ret=0 + if ! parallel -j"${nprocs}" --keep-order -- \ + "${CLANG_TIDY_BIN}" -p "${BUILD_DIR}" -format-style=file -quiet "$@" {} \ + < <(git "${git_args[@]}" | grep -E '(\.cc|\.cpp)$') \ + >"${BUILD_DIR}/clang-tidy.txt"; then + ret=1 + fi + { set +x; } 2>/dev/null + echo "Findings statistics:" >&2 + grep -E ' \[[A-Za-z\.,\-]+\]' -o "${BUILD_DIR}/clang-tidy.txt" | sort \ + | uniq -c >&2 + + if [[ $ret -ne 0 ]]; then + cat >&2 </dev/null + local debsdir="${BUILD_DIR}/debs" + local f + while IFS='' read -r -d '' f; do + echo "=====================================================================" + echo "Package $f:" + dpkg --info $f + dpkg --contents $f + done < <(find "${BUILD_DIR}/debs" -maxdepth 1 -mindepth 1 -type f \ + -name '*.deb' -print0) +} + +build_debian_pkg() { + local srcdir="$1" + local srcpkg="$2" + + local debsdir="${BUILD_DIR}/debs" + local builddir="${debsdir}/${srcpkg}" + + # debuild doesn't have an easy way to build out of tree, so we make a copy + # of with all symlinks on the first level. + mkdir -p "${builddir}" + for f in $(find "${srcdir}" -mindepth 1 -maxdepth 1 -printf '%P\n'); do + if [[ ! -L "${builddir}/$f" ]]; then + rm -f "${builddir}/$f" + ln -s "${srcdir}/$f" "${builddir}/$f" + fi + done + ( + cd "${builddir}" + debuild -b -uc -us + ) +} + +cmd_debian_build() { + local srcpkg="${1:-}" + + case "${srcpkg}" in + jpeg-xl) + build_debian_pkg "${MYDIR}" "jpeg-xl" + ;; + highway) + build_debian_pkg "${MYDIR}/third_party/highway" "highway" + ;; + *) + echo "ERROR: Must pass a valid source package name to build." >&2 + ;; + esac +} + +get_version() { + local varname=$1 + local line=$(grep -F "set(${varname} " lib/CMakeLists.txt | head -n 1) + [[ -n "${line}" ]] + line="${line#set(${varname} }" + line="${line%)}" + echo "${line}" +} + +cmd_bump_version() { + local newver="${1:-}" + + if ! which dch >/dev/null; then + echo "Run:\n sudo apt install debhelper" + exit 1 + fi + + if [[ -z "${newver}" ]]; then + local major=$(get_version JPEGXL_MAJOR_VERSION) + local minor=$(get_version JPEGXL_MINOR_VERSION) + local patch=0 + minor=$(( ${minor} + 1)) + else + local major="${newver%%.*}" + newver="${newver#*.}" + local minor="${newver%%.*}" + newver="${newver#${minor}}" + local patch="${newver#.}" + if [[ -z "${patch}" ]]; then + patch=0 + fi + fi + + newver="${major}.${minor}" + if [[ "${patch}" != "0" ]]; then + newver="${newver}.${patch}" + fi + echo "Bumping version to ${newver} (${major}.${minor}.${patch})" + sed -E \ + -e "s/(set\\(JPEGXL_MAJOR_VERSION) [0-9]+\\)/\\1 ${major})/" \ + -e "s/(set\\(JPEGXL_MINOR_VERSION) [0-9]+\\)/\\1 ${minor})/" \ + -e "s/(set\\(JPEGXL_PATCH_VERSION) [0-9]+\\)/\\1 ${patch})/" \ + -i lib/CMakeLists.txt + + # Update lib.gni + tools/build_cleaner.py --update + + # Mark the previous version as "unstable". + DEBCHANGE_RELEASE_HEURISTIC=log dch -M --distribution unstable --release '' + DEBCHANGE_RELEASE_HEURISTIC=log dch -M \ + --newversion "${newver}" \ + "Bump JPEG XL version to ${newver}." +} + +# Check that the AUTHORS file contains the email of the committer. +cmd_authors() { + merge_request_commits + # TODO(deymo): Handle multiple commits and check that they are all the same + # author. + local email=$(git log --format='%ae' "${MR_HEAD_SHA}^!") + local name=$(git log --format='%an' "${MR_HEAD_SHA}^!") + "${MYDIR}"/tools/check_author.py "${email}" "${name}" +} + +main() { + local cmd="${1:-}" + if [[ -z "${cmd}" ]]; then + cat >&2 < Build the given source package. + debian_stats Print stats about the built packages. + +oss-fuzz commands: + ossfuzz_asan Build the local source inside oss-fuzz docker with asan. + ossfuzz_msan Build the local source inside oss-fuzz docker with msan. + ossfuzz_ubsan Build the local source inside oss-fuzz docker with ubsan. + ossfuzz_ninja Run ninja on the BUILD_DIR inside the oss-fuzz docker. Extra + parameters are passed to ninja, for example "djxl_fuzzer" will + only build that ninja target. Use for faster build iteration + after one of the ossfuzz_*san commands. + +You can pass some optional environment variables as well: + - BUILD_DIR: The output build directory (by default "$$repo/build") + - BUILD_TARGET: The target triplet used when cross-compiling. + - CMAKE_FLAGS: Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS. + - CMAKE_PREFIX_PATH: Installation prefixes to be searched by the find_package. + - ENABLE_WASM_SIMD=1: enable experimental SIMD in WASM build (only). + - FUZZER_MAX_TIME: "fuzz" command fuzzer running timeout in seconds. + - LINT_OUTPUT: Path to the output patch from the "lint" command. + - SKIP_CPUSET=1: Skip modifying the cpuset in the arm_benchmark. + - SKIP_TEST=1: Skip the test stage. + - STORE_IMAGES=0: Makes the benchmark discard the computed images. + - TEST_STACK_LIMIT: Stack size limit (ulimit -s) during tests, in KiB. + - STACK_SIZE=1: Generate binaries with the .stack_sizes sections. + +These optional environment variables are forwarded to the cmake call as +parameters: + - CMAKE_BUILD_TYPE + - CMAKE_C_FLAGS + - CMAKE_CXX_FLAGS + - CMAKE_C_COMPILER_LAUNCHER + - CMAKE_CXX_COMPILER_LAUNCHER + - CMAKE_CROSSCOMPILING_EMULATOR + - CMAKE_FIND_ROOT_PATH + - CMAKE_EXE_LINKER_FLAGS + - CMAKE_MAKE_PROGRAM + - CMAKE_MODULE_LINKER_FLAGS + - CMAKE_SHARED_LINKER_FLAGS + - CMAKE_TOOLCHAIN_FILE + +Example: + BUILD_DIR=/tmp/build $0 opt +EOF + exit 1 + fi + + cmd="cmd_${cmd}" + shift + set -x + "${cmd}" "$@" +} + +main "$@" diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..e339977 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,83 @@ +jpeg-xl (0.6.1) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.6.1. + + -- JPEG XL Maintainers Fri, 29 Oct 2021 18:45:59 +0200 + +jpeg-xl (0.6) unstable; urgency=medium + + * Bump JPEG XL version to 0.6. + + -- JPEG XL Maintainers Thu, 12 Aug 2021 23:49:40 +0200 + +jpeg-xl (0.5.0) unstable; urgency=medium + + * Bump JPEG XL version to 0.5.0. + + -- JPEG XL Maintainers Thu, 12 Aug 2021 23:49:40 +0200 + +jpeg-xl (0.3.7) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.7. + + -- Sami Boukortt Mon, 29 Mar 2021 12:14:20 +0200 + +jpeg-xl (0.3.6) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.6. + + -- Sami Boukortt Thu, 25 Mar 2021 17:40:58 +0100 + +jpeg-xl (0.3.5) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.5. + + -- Sami Boukortt Tue, 23 Mar 2021 15:20:44 +0100 + +jpeg-xl (0.3.4) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.4. + + -- Sami Boukortt Tue, 16 Mar 2021 12:13:59 +0100 + +jpeg-xl (0.3.3) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.3. + + -- Sami Boukortt Fri, 5 Mar 2021 19:15:26 +0100 + +jpeg-xl (0.3.2) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.2. + + -- Alex Deymo Fri, 12 Feb 2021 21:00:12 +0100 + +jpeg-xl (0.3.1) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3.1. + + -- Alex Deymo Tue, 09 Feb 2021 09:48:43 +0100 + +jpeg-xl (0.3) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.3. + + -- Alex Deymo Wed, 27 Jan 2021 22:36:32 +0100 + +jpeg-xl (0.2) UNRELEASED; urgency=medium + + * Bump JPEG XL version to 0.2. + + -- Alex Deymo Wed, 23 Nov 2020 20:42:10 +0100 + +jpeg-xl (0.1) UNRELEASED; urgency=medium + + * JPEG XL format release candidate. + + -- Alex Deymo Fri, 13 Nov 2020 17:42:24 +0100 + +jpeg-xl (0.0.2-1) UNRELEASED; urgency=medium + + * Initial debian package. + + -- Alex Deymo Tue, 27 Oct 2020 15:27:59 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..b30268c --- /dev/null +++ b/debian/control @@ -0,0 +1,88 @@ +Source: jpeg-xl +Maintainer: JPEG XL Maintainers +Section: misc +Priority: optional +Standards-Version: 3.9.8 +Build-Depends: + asciidoc, + cmake, + debhelper (>= 9), + libbrotli-dev, + libgdk-pixbuf-2.0-dev | libgdk-pixbuf2.0-dev, + libgif-dev, + libgimp2.0-dev, + libgmock-dev, + libgoogle-perftools-dev, + libgtest-dev, + libhwy-dev, + libjpeg-dev, + libopenexr-dev, + libpng-dev, + libwebp-dev, + pkg-config, + xdg-utils, + xmlto, +Homepage: https://github.com/libjxl/libjxl +Rules-Requires-Root: no + +Package: jxl +Architecture: any +Section: utils +Depends: ${misc:Depends}, ${shlibs:Depends} +Description: JPEG XL Image Coding System - "JXL" (command line utility) + The JPEG XL Image Coding System (ISO/IEC 18181) is a lossy and + lossless image compression format. It has a rich feature set and is + particularly optimized for responsive web environments, so that + content renders well on a wide range of devices. Moreover, it includes + several features that help transition from the legacy JPEG format. + . + This package installs the command line utilities. + +Package: libjxl-dev +Architecture: any +Section: libdevel +Depends: libjxl (= ${binary:Version}), ${misc:Depends} + libhwy-dev, +Description: JPEG XL Image Coding System - "JXL" (development files) + The JPEG XL Image Coding System (ISO/IEC 18181) is a lossy and + lossless image compression format. It has a rich feature set and is + particularly optimized for responsive web environments, so that + content renders well on a wide range of devices. Moreover, it includes + several features that help transition from the legacy JPEG format. + . + This package installs development files. + +Package: libjxl +Architecture: any +Multi-Arch: same +Section: libs +Depends: ${shlibs:Depends}, ${misc:Depends} +Pre-Depends: ${misc:Pre-Depends} +Description: JPEG XL Image Coding System - "JXL" (shared libraries) + The JPEG XL Image Coding System (ISO/IEC 18181) is a lossy and + lossless image compression format. It has a rich feature set and is + particularly optimized for responsive web environments, so that + content renders well on a wide range of devices. Moreover, it includes + several features that help transition from the legacy JPEG format. + . + This package installs shared libraries. + +Package: libjxl-gdk-pixbuf +Architecture: any +Multi-Arch: same +Section: libs +Depends: ${shlibs:Depends}, ${misc:Depends} +Pre-Depends: ${misc:Pre-Depends} +Description: JPEG XL Plugin for gdk-pixbuf + This package installs the required files for reading JPEG XL files in + GTK applications. + +Package: libjxl-gimp-plugin +Architecture: any +Multi-Arch: same +Section: graphics +Depends: ${shlibs:Depends}, ${misc:Depends} +Pre-Depends: ${misc:Pre-Depends} +Enhances: gimp +Description: JPEG XL Import and Export Plugin for GIMP + This is a plugin for GIMP version 2.10.x to import and export JPEG XL images. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..84b5467 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,236 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: jpeg-xl + +Files: * +Copyright: 2020 the JPEG XL Project +License: BSD-3-clause + +Files: third_party/sjpeg/* +Copyright: 2017 Google, Inc +License: Apache-2.0 + +Files: third_party/lodepng/* +Copyright: 2005-2018 Lode Vandevenne +License: Zlib License + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + . + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + . + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + . + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + . + 3. This notice may not be removed or altered from any source + distribution. + +Files: third_party/skcms/* +Copyright: 2018 Google Inc. +License: BSD-3-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Files: third_party/testdata/imagecompression.info/* +Copyright: their respective owners. +License: License without any prohibitive copyright restrictions. + See https://imagecompression.info/test_images/ for details. + . + These Images are available without any prohibitive copyright restrictions. + . + These images are (c) there respective owners. You are granted full + redistribution and publication rights on these images provided: + . + 1. The origin of the pictures must not be misrepresented; you must not claim + that you took the original pictures. If you use, publish or redistribute them, + an acknowledgment would be appreciated but is not required. + 2. Altered versions must be plainly marked as such, and must not be + misinterpreted as being the originals. + 3. No payment is required for distribution of this material, it must be + available freely under the conditions stated here. That is, it is prohibited to + sell the material. + 4. This notice may not be removed or altered from any distribution. + +Files: third_party/testdata/pngsuite/* +Copyright: Willem van Schaik, 1996, 2011 +License: PngSuite License + See http://www.schaik.com/pngsuite/ for details. + . + Permission to use, copy, modify and distribute these images for any + purpose and without fee is hereby granted. + +Files: third_party/testdata/raw.pixls/* +Copyright: their respective owners listed in https://raw.pixls.us/ +License: CC0-1.0 + +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/ +Copyright: github.com/codelogic/wide-gamut-tests authors. +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache License, Version 2 + can be found in "/usr/share/common-licenses/Apache-2.0". + +License: CC0 + Creative Commons Zero v1.0 Universal + . + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL + SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT + RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" + BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS + DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS + LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE + INFORMATION OR WORKS PROVIDED HEREUNDER. + . + Statement of Purpose + . + The laws of most jurisdictions throughout the world automatically confer + exclusive Copyright and Related Rights (defined below) upon the creator and + subsequent owner(s) (each and all, an "owner") of an original work of + authorship and/or a database (each, a "Work"). + . + Certain owners wish to permanently relinquish those rights to a Work for the + purpose of contributing to a commons of creative, cultural and scientific + works ("Commons") that the public can reliably and without fear of later + claims of infringement build upon, modify, incorporate in other works, reuse + and redistribute as freely as possible in any form whatsoever and for any + purposes, including without limitation commercial purposes. These owners may + contribute to the Commons to promote the ideal of a free culture and the + further production of creative, cultural and scientific works, or to gain + reputation or greater distribution for their Work in part through the use + and efforts of others. + . + For these and/or other purposes and motivations, and without any expectation + of additional consideration or compensation, the person associating CC0 with + a Work (the "Affirmer"), to the extent that he or she is an owner of + Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to + the Work and publicly distribute the Work under its terms, with knowledge of + his or her Copyright and Related Rights in the Work and the meaning and + intended legal effect of CC0 on those rights. + . + 1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights ("Copyright and + Related Rights"). Copyright and Related Rights include, but are not limited + to, the following: + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); + iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national implementations + thereof. + . + 2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer's Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the "Waiver"). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer's heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer's express + Statement of Purpose. + . + 3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer's express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer's Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the "License"). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer's express Statement of Purpose. + . + 4. Limitations and Disclaimers. + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or the + absence of latent or other defects, accuracy, or the present or absence of + errors, whether or not discoverable, all to the greatest extent permissible + under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + . + For more information, please see: + http://creativecommons.org/publicdomain/zero/1.0/> + diff --git a/debian/jxl.install b/debian/jxl.install new file mode 100644 index 0000000..c3bae3e --- /dev/null +++ b/debian/jxl.install @@ -0,0 +1,3 @@ +usr/bin/* +usr/share/man/man1/cjxl.1 +usr/share/man/man1/djxl.1 diff --git a/debian/libjxl-dev.install b/debian/libjxl-dev.install new file mode 100644 index 0000000..b735ec2 --- /dev/null +++ b/debian/libjxl-dev.install @@ -0,0 +1,4 @@ +usr/include/jxl/*.h +usr/lib/*/*.a +usr/lib/*/*.so +usr/lib/*/pkgconfig/*.pc diff --git a/debian/libjxl-gdk-pixbuf.install b/debian/libjxl-gdk-pixbuf.install new file mode 100644 index 0000000..12d2ab2 --- /dev/null +++ b/debian/libjxl-gdk-pixbuf.install @@ -0,0 +1,3 @@ +usr/lib/*/gdk-pixbuf-*/*/loaders/* +usr/share/mime/packages/image-jxl.xml +usr/share/thumbnailers/jxl.thumbnailer diff --git a/debian/libjxl-gimp-plugin.install b/debian/libjxl-gimp-plugin.install new file mode 100644 index 0000000..353431d --- /dev/null +++ b/debian/libjxl-gimp-plugin.install @@ -0,0 +1 @@ +usr/lib/gimp diff --git a/debian/libjxl.install b/debian/libjxl.install new file mode 100644 index 0000000..cd157a7 --- /dev/null +++ b/debian/libjxl.install @@ -0,0 +1 @@ +usr/lib/*/libjxl*.so.* diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..efed75d --- /dev/null +++ b/debian/rules @@ -0,0 +1,17 @@ +#!/usr/bin/make -f + +include /usr/share/dpkg/pkg-info.mk + +%: + dh $@ --buildsystem=cmake + +override_dh_auto_configure: + # TODO(deymo): Remove the DCMAKE_BUILD_TYPE once builds without NDEBUG + # are as useful as Release builds. + dh_auto_configure -- \ + -DJPEGXL_VERSION=$(DEB_VERSION) \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DJPEGXL_FORCE_SYSTEM_GTEST=ON \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_HWY=ON \ + -DJPEGXL_ENABLE_PLUGINS=ON diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/deps.sh b/deps.sh new file mode 100755 index 0000000..6de69f7 --- /dev/null +++ b/deps.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# 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 file downloads the dependencies needed to build JPEG XL into third_party. +# These dependencies are normally pulled by gtest. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +# Git revisions we use for the given submodules. Update these whenever you +# update a git submodule. +THIRD_PARTY_HIGHWAY="e2397743fe092df68b760d358253773699a16c93" +THIRD_PARTY_LODEPNG="48e5364ef48ec2408f44c727657ac1b6703185f8" +THIRD_PARTY_SKCMS="64374756e03700d649f897dbd98c95e78c30c7da" +THIRD_PARTY_SJPEG="868ab558fad70fcbe8863ba4e85179eeb81cc840" + +# Download the target revision from GitHub. +download_github() { + local path="$1" + local project="$2" + + local varname="${path^^}" + varname="${varname/\//_}" + local sha + eval "sha=\${${varname}}" + + local down_dir="${MYDIR}/downloads" + local local_fn="${down_dir}/${sha}.tar.gz" + if [[ -e "${local_fn}" && -d "${MYDIR}/${path}" ]]; then + echo "${path} already up to date." >&2 + return 0 + fi + + local url + local strip_components=0 + if [[ "${project:0:4}" == "http" ]]; then + # "project" is a googlesource.com base url. + url="${project}${sha}.tar.gz" + else + # GitHub files have a top-level directory + strip_components=1 + url="https://github.com/${project}/tarball/${sha}" + fi + + echo "Downloading ${path} version ${sha}..." >&2 + mkdir -p "${down_dir}" + curl -L --show-error -o "${local_fn}.tmp" "${url}" + mkdir -p "${MYDIR}/${path}" + tar -zxf "${local_fn}.tmp" -C "${MYDIR}/${path}" \ + --strip-components="${strip_components}" + mv "${local_fn}.tmp" "${local_fn}" +} + + +main() { + if git -C "${MYDIR}" rev-parse; then + cat >&2 <> ~/.ssh/config <github.com/*{{USERNAME}}*/libjxl + +where {{USERNAME}} denotes your GitHub username. + +### Checkout the JPEG XL code from GitHub + +To get the source code on your computer you need to "clone" it. There are two +repositories at play here, the upstream repository (`libjxl/lbjxl`) and your +fork (`{{USERNAME}}/libjxl`). You will be normally fetching new changes from +the upstream repository and push changes to your fork. Getting your changes from +your fork to the upstream repository is done through the Web interface, via Pull +Requests. + +The [Fork a +repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) +goes in great detail, but uses the git remote names `upstream` for the shared +upstream repository and `origin` for your work. This guide proposes an +alternative naming scheme, used in the examples below. + +In this guide `origin` is the upstream shared repository and `myfork` is your +fork. You can use any other name for your fork if you want. Use the following +commands to set things up, replacing `{{USERNAME}}` with your GitHub username: + +```bash +git clone git https://github.com/libjxl/libjxl --recursive +cd libjxl +git remote set-url --push origin git@github.com:{{USERNAME}}/libjxl.git +git remote add myfork git@github.com:{{USERNAME}}/libjxl.git +git remote -vv +``` + +These commands did three things: + + * Created the repository with `origin` as the upstream remote, + * Changed the "push" URL to point to your fork, and + * Create a new remote pointing to your fork. + +The last step is optional. Since the "fetch" URL of `origin` points to the +shared repository and the "push" URL points to your fork, fetching from `origin` +always gets the latest changes from the upstream repository regardless of the +contents of your fork. + +Having a second origin called `myfork` is only useful if you need to download +pending changes from your fork from a different computer. For example, if you +work on multiple computers, each one with this setup, you can push to your +fork from one, and then fetch from `myfork` from another computer to get those. + +# Life of a Pull Request + +The general [GitHub flow +guide](https://docs.github.com/en/github/getting-started-with-github/github-flow) +applies to sending Pull Requests to this project. + +All the commands here assume you are in a git checkout as setup here. + +### Sync to the latest version + +```bash +git fetch origin +``` + +The last upstream version is now on `origin/main` and none of your local +branches have been modified by this command. + +### Start a new branch + +To start a new change you need a local branch. Each branch will represent a list +of individual commits which can then be requested to be merged as a single merge +request. So in general one branch is one code review, but each branch can have +multiple individual commits in it. + +```bash +git checkout origin/main -b mybranch +``` + +This will create a new branch `mybranch` tracking `origin/main`. A branch can +track any remove or local branch, which is used by some tools. Running `git +branch -vv` will show all the branches you have have, what are they tracking and +how many commits are ahead or behind. If you create a branch without tracking +any other, you can add or change the tracking branch of the current branch +running `git branch --set-upstream-to=...`. + +### Add changes to your branch + +Follow any of the many online tutorials, for example +[The basics](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) +chapter from the https://git-scm.com/doc website is a good starting guide. +Create, change or delete files and do a git commit with a message. + +The commit message is required. A commit message should follow the 50/72 rule: + +* First line is 50 characters or less. +* Then a blank line. +* Remaining text should be wrapped at 72 characters. + +The first line should identify your commit, since that's what most tools will +show to the user. First lines like "Some fixes" are not useful. Explain what the +commit contains and why. + +We follow the [Google C++ Coding +Style](https://google.github.io/styleguide/cppguide.html). A +[clang-format](https://clang.llvm.org/docs/ClangFormat.html) configuration +file is available to automatically format your code, you can invoke it with +the `./ci.sh lint` helper tool. + +Read the [CONTRIBUTING.md](../CONTRIBUTING.md) file for more information about +contributing to libjxl. + +### Upload your changes for review + +The first step is a local review of your changes to see what will you be sending +for review. `gitg` is a nice Gtk UI for reviewing your local changes, or `tig` +for similar ncurses console-based interface. Otherwise, from the terminal you +can run: + +```bash +git branch -vv +``` + +To show the current status of your local branches. In particular, since your +branch is tracking origin/main (as seen in the output) git will tell you that +you are one commit ahead of the tracking branch. + +``` +* mybranch e74ae1a [origin/main: ahead 1] Improved decoding speed by 40% +``` + +It is a good idea before uploading to sync again with upstream (`git fetch +origin`) and then run `git branch -vv` to check whether there are new changes +upstream. If that is the case, you will see a "behind" flag in the output: + +``` +* mybranch e74ae1a [origin/main: ahead 1, behind 2] Improved decoding speed by 40% +``` + +To sync your changes on top of the latest changes in upstream you need to +rebase: + +```bash +git rebase +``` + +This will by default rebase your current branch changes on top of the tracking +branch. In this case, this will try to apply the current commit on top of the +latest origin/main (which has 2 more commits than the ones we have in our +branch) and your branch will now include that. There could be conflicts that you +have to deal with. A shortcut to do both fetch and rebase is to run `git pull +-r`, where the `-r` stands for "rebase" and will rebase the local commits on top +of the remote ones. + +Before uploading a patch, make sure your patch conforms to the +[contributing guidelines](../CONTRIBUTING.md) and it +[builds and passes tests](building_and_testing.md). + +Once you are ready to send your branch for review, upload it to *your* fork: + +```bash +git push origin mybranch +``` + +This will push your local branch "mybranch" to a remote in your fork called +"mybranch". The name can be anything, but keep in mind that it is public. A link +to the URL to create a merge request will be displayed. + +``` +Enumerating objects: 627, done. +Counting objects: 100% (627/627), done. +Delta compression using up to 56 threads +Compressing objects: 100% (388/388), done. +Writing objects: 100% (389/389), 10.71 MiB | 8.34 MiB/s, done. +Total 389 (delta 236), reused 0 (delta 0) +emote: +remote: Create a pull request for 'mybranch' on GitHub by visiting: +remote: https://github.com/{{USERNAME}}/libjxl/pull/new/mybranch +remote: +To github.com:{{USERNAME}}/libjxl.git + * [new branch] mybranch -> mybranch +``` + +### Updating submodules + +The repository uses submodules for external library dependencies in +third_party. Each submodule points to a particular external commit of the +external repository by the hash code of that external commit. Just like +regular source code files, this hash code is part of the current branch and +jpeg xl commit you have checked out. + +When changing branches or when doing `git rebase`, git will unfortunately +*not* automatically set those hashes to the ones of the branch or jpeg xl +commit you changed to nor set the source files of the third_party submodules +to the new state. That is, even though git will have updated the jpeg xl +source code files on your disk to the new ones, it will leave the submodule +hashes and the files in third_party in your workspace to the ones they were +before you changed branches. This will show up in a git diff because this +is seen as a change compared to the branch you switched to. The git diff shows +the difference in hash codes (as if you are changing to the old ones), it does +not show changes in files inside the third_party directory. + +This mismatch can cause at least two problems: + +*) the jpeg xl codebase may not compile due to third_party library version +mismatch if e.g. API changed or a submodule was added/removed. + +*) when using `commit -a` your commit, which may be a technical change +unrelated to submodule changes, will unintentionally contain a change to the +submodules hash code, which is undesired unless you actually want to change +the version of third_party libraries. + +To resolve this, the submodules must be updated manually with +the following command after those actions (at least when the submodules +changed): + +``` +git submodule update --init --recursive +``` + +Here, the init flag ensures new modules get added when encessary and the +recursive flag is required for the submodules depending on other submodules. + +If you checkout a different branch, you can spot that submodules changed +when it shows a message similar to this: + +``` +M third_party/brotli +M third_party/lcms +``` + +If you do a rebase you may end up in a harder to solve situation, where +`git submodule update --init --recursive` itself fails with errors such as: + +``` +Unable to checkout '35ef5c554d888bef217d449346067de05e269b30' in submodule path 'third_party/brotli' +``` + +In that case, you can use the force flag: + +``` +git submodule update --init --recursive --force +``` + +### Iterating changes in your merge request + +To address reviewer changes you need to amend the local changes in your branch +first. Make the changes you need in your commit locally by running `git commit +--amend file1 file2 file3 ...` or `git commit --amend -a` to amend all the +changes from all the staged files. + +Once you have the new version of the "mybranch" branch to re-upload, you need to +force push it to the same branch in your fork. Since you are pushing a different +version of the same commit (as opposed to another commit on top of the existing +ones), you need to force the operation to replace the old version. + +```bash +git push origin mybranch --force +``` + +The merge request should now be updated with the new changes. + +### Merging your changes + +We use "rebase" as a merge policy, which means that there a no "merge" commits +(commits with more than one parent) but instead only a linear history of +changes. + +It is possible that other changes where added to the main branch since the last +time you rebased your changes. These changes could create a conflict with your +Pull Request, if so you need to `git fetch`, `git rebase` and push again your +changes which need to go through the continuous integration workflow again to +verify that all the tests pass again after including the latest changes. + +### Trying locally a pending Pull Request + +If you want to review in your computer a pending pull request proposed by +another user you can fetch the merge request commit with the following command, +replacing `NNNN` with the pull request number: + +```bash +git fetch origin refs/pull/NNNN/head +git checkout FETCH_HEAD +``` + +The first command will add to your local git repository the remote commit for +the pending pull request and store a temporary reference called `FETCH_HEAD`. +The second command then checks out that reference. From this point you can +review the files in your computer, create a local branch for this FETCH_HEAD or +build on top of it. diff --git a/doc/developing_in_windows_msys.md b/doc/developing_in_windows_msys.md new file mode 100644 index 0000000..45b63c8 --- /dev/null +++ b/doc/developing_in_windows_msys.md @@ -0,0 +1,168 @@ +# Developing for Windows with MSYS2 + +[MSYS2](https://www.msys2.org/) ("minimal system 2") is a software distribution and a development platform based on MinGW and Cygwin. It provides a Unix-like environment to build code on Windows. These instructions were written with a 64-bit instance of Windows 10 running on a VM. They may also work on native instances of Windows and other versions of Windows. + +## Build Environments + +MSYS2 provides multiple development [environments](https://www.msys2.org/docs/environments/). By convention, they are referred to in uppercase. They target slightly different platforms, runtime libraries, and compiler toolchains. For example, to build for 32-bit Windows, use the MINGW32 environment. For interoperability with Visual Studio projects, use the UCRT64 environment. + +Since all of the build environments are built on top of the MSYS environment, **all updates and package installation must be done from within the MSYS environment**. After making any package changes, `exit` all MSYS2 terminals and restart the desired build-environment. This reminder is repeated multiple times throughout this guide. + +* **MINGW32:** To compile for 32-bit Windows (on 64-bit Windows), use packages from the `mingw32` group. Package names are prefixed with `mingw-w64-i686`. The naming scheme may be different on the 32-bit version of MSYS2. + +* **MINGW64:** This is the primary environment to building for 64-bit Windows. It uses the older MSVCRT runtime, which is widely available across Windows systems. Package names are prefixed with `mingw-w64-x86_64`. + +* **UCRT64:** The Universal C Runtime (UCRT) is used by recent versions of Microsoft Visual Studio. It ships by default with Windows 10. For older versions of Windows, it must be provided with the application or installed by the user. Package names are prefixed with `mingw-w64-ucrt-x86_64`. + +* **CLANG64:** Unfortunately, the `gimp` packages are not available for the CLANG64 environment. However, `libjxl` will otherwise build in this environment if the appropriate packages are installed. Packages are prefixed with `mingw-w64-clang-x86_64`. + +## Install and Upgrade MSYS2 + +Download MSYS2 from the homepage. Install at a location without any spaces on a drive with ample free space. After installing the packages used in this guide, MSYS2 used about 15GB of space. + +Toward the end of installation, select the option to run MSYS2 now. A command-line window will open. Run the following command, and answer the prompts to update the repository and close the terminal. + +```bash +pacman -Syu +``` + +Now restart the MSYS environment and run the following command to complete updates: + +```bash +pacman -Su +``` + +## Package Management + +Packages are organized in groups, which share the build environment name, but in lower case. Then they have name prefixes that indicate which group they belong to. Consider this package search: `pacman -Ss cmake` + +``` +mingw32/mingw-w64-i686-cmake +mingw64/mingw-w64-x86_64-cmake +ucrt64/mingw-w64-ucrt-x86_64-cmake +clang64/mingw-w64-clang-x86_64-cmake +msys/cmake +``` + +We can see the organization `group/prefix-name`. When installing packages, the group name is optional. + +```bash +pacman -S mingw-w64-x86_64-cmake +``` + +For tools that need to be aware of the compiler to function, install the package that corresponds with the specific build-environment you plan to use. For `cmake`, install the `mingw64` version. The generic `msys/cmake` will not function correctly because it will not find the compiler. For other tools, the generic `msys` version is adequate, like `msys/git`. + +To remove packages, use: + +```bash +pacman -Rsc [package-name] +``` + +## Worst-Case Scenario... + +If packages management is done within a build environment other than MSYS, the environment structure will be disrupted and compilation will likely fail. If this happens, it may be necessary to reinstall MSYS2. + +1. Rename the `msys64` folder to `msys64.bak`. + +2. Use the installer to reinstall MSYS2 to `msys64`. + +3. Copy packages from `msys64.bak/var/cache/pacman/pkg/` to the new installation to save download time and bandwidth. + +4. Use `pacman` from within the MSYS environment to install and update packages. + +5. After successfully building a project, it is safe to delete `msys64.bak` + +## The MING64 Environment + +Next set up the MING64 environment. The following commands should be run within the MSYS environment. `pacman -S` is used to install packages. The `--needed` argument prevents packages from being reinstalled. + +```bash +pacman -S --needed base-devel mingw-w64-x86_64-toolchain +pacman -S git mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \ + mingw-w64-x86_64-gtest mingw-w64-x86_64-giflib \ + mingw-w64-x86_64-libpng mingw-w64-x86_64-libjpeg-turbo +``` + +## Build `libjxl` + +Download the source from the libjxl [releases](https://github.com/libjxl/libjxl/releases) page. Alternatively, you may obtain the latest development version with `git`. Run `./deps.sh` to ensure additional third-party dependencies are downloaded. + +Start the MINGW64 environment, create a build directory within the source directory, and configure with `cmake`. + +```bash +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_PLUGINS=ON -DJPEGXL_ENABLE_MANPAGES=OFF \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_GTEST=ON .. +``` + +Check the output to see if any dependencies were missed and need to be installed. Adding `-G Ninja` may be helpful, but on my computer, Ninja was selected by default. Remember that package changes must be done from the MSYS environment. Then exit all MSYS2 terminals and restart the build environment. + +If all went well, you may now run `cmake` to build `libjxl`: + +```bash +cmake --build . +``` + +Do not be alarmed by the compiler warnings. They are a caused by differences between gcc/g++ and clang. The build should complete successfully. Then `cjxl`, `djxl`, `jxlinfo`, and others can be run from within the build environment. Moving them into the native Windows environment requires resolving `dll` issues that are beyond the scope of this document. + +## The `clang` Compiler + +To use the `clang` compiler, install the packages that correspond with the environment you wish to use. Remember to make package changes from within the MSYS environment. + +``` +mingw-w64-i686-clang +mingw-w64-i686-clang-tools-extra +mingw-w64-i686-clang-compiler-rt + +mingw-w64-x86_64-clang +mingw-w64-x86_64-clang-tools-extra +mingw-w64-x86_64-clang-compiler-rt + +mingw-w64-ucrt64-x86_64-clang +mingw-w64-ucrt64-x86_64-clang-tools-extra +mingw-w64-ucrt64-x86_64-clang-compiler-rt +``` + +After the `clang` compiler is installed, 'libjxl' can be built with the `./ci.sh` script. + +```bash +./ci.sh opt -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_MANPAGES=OFF -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_GTEST=ON +``` + +On my computer, `doxygen` packages needed to be installed to proceed with building. Use `pacman -Ss doxygen` to find the packages to install. + +## The GIMP Plugin + +To build the GIMP plugin, install the relevant `gimp` package. This will also install dependencies. Again, perform package management tasks from only the MSYS environment. Then restart the build environment. + +```bash +pacman -S mingw-w64-i686-gimp +pacman -S mingw-w64-x86_64-gimp +pacman -S mingw-w64-ucrt-x86_64-gimp +``` + +If `clang` is installed, you can use the `./ci.sh` script to build. Otherwise, navigate to the build directory to reconfigure and build with `cmake`. + +```bash +cd build +rm -r C* +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_PLUGINS=ON -DJPEGXL_ENABLE_MANPAGES=OFF \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_GTEST=ON .. +``` + +Fortunately, the plugin works without installing `dll` files. To test the plugin: + +1. [Download](https://www.gimp.org/downloads/) and install the stable version of GIMP (currently 2.10.24). + +2. Create a new folder: `C:\Program Files\GIMP 2\lib\gimp\2.0\plug-ins\file-jxl` + +3. Copy `build/plugins/gimp/file-jxl.exe` to the new folder. diff --git a/doc/developing_in_windows_vcpkg.md b/doc/developing_in_windows_vcpkg.md new file mode 100644 index 0000000..332e451 --- /dev/null +++ b/doc/developing_in_windows_vcpkg.md @@ -0,0 +1,91 @@ +# Developing on Windows with Visual Studio 2019 + +These instructions assume an up-to-date Windows 10 (e.g. build 19041.928) with +**Microsoft Visual Studio 2019** (e.g. Version 16.9.0 Preview 4.0) installed. If +unavailable, please use another build environment: + +* [Docker container](developing_in_docker.md) +* [MSYS2 on Windows](developing_in_windows_msys.md) +* [Crossroad on Linux](developing_with_crossroad.md) (cross compilation for Windows) + +## Minimum build dependencies + +Apart from the dependencies in third_party, some of the tools use external +dependencies that need to be installed in your system first. + +Please install [vcpkg](https://vcpkg.readthedocs.io/en/latest/examples/installing-and-using-packages/) +(tested with version 2019.07.18), and use it to install the following libraries: + +``` +vcpkg install gtest:x64-windows +vcpkg install giflib:x64-windows +vcpkg install libjpeg-turbo:x64-windows +vcpkg install libpng:x64-windows +vcpkg install zlib:x64-windows +``` + +## Building + +From Visual Studio, open the CMakeLists.txt in the JPEG XL root directory. +Right-click the CMakeLists.txt entry in the Folder View of the Solution +Explorer. In the context menu, select CMake Settings. Click on the green plus +to add an x64-Clang configuration and the red minus to remove any non-Clang +configuration (the MSVC compiler is currently not supported). Click on the blue +hyperlink marked "CMakeSettings.json" and an editor will open. Insert the +following text after replacing $VCPKG with the directory where you installed +vcpkg above. + +``` +{ + "configurations": [ + { + "name": "x64-Clang-Release", + "generator": "Ninja", + "configurationType": "MinSizeRel", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "-DCMAKE_TOOLCHAIN_FILE=$VCPKG/scripts/buildsystems/vcpkg.cmake", + "buildCommandArgs": "-v", + "ctestCommandArgs": "", + "inheritEnvironments": [ "clang_cl_x64" ], + "variables": [ + { + "name": "VCPKG_TARGET_TRIPLET", + "value": "x64-windows", + "type": "STRING" + }, + { + "name": "JPEGXL_ENABLE_TCMALLOC", + "value": "False", + "type": "BOOL" + }, + { + "name": "BUILD_GMOCK", + "value": "True", + "type": "BOOL" + }, + { + "name": "gtest_force_shared_crt", + "value": "True", + "type": "BOOL" + }, + { + "name": "JPEGXL_ENABLE_FUZZERS", + "value": "False", + "type": "BOOL" + }, + { + "name": "JPEGXL_ENABLE_VIEWERS", + "value": "False", + "type": "BOOL" + } + ] + } + ] +} +``` + +The project is now ready for use. To build, simply press F7 (or choose +Build All from the Build menu). This writes binaries to +`out/build/x64-Clang-Release/tools`. The main [README.md](../README.md) explains +how to use the encoder/decoder and benchmark binaries. diff --git a/doc/developing_with_crossroad.md b/doc/developing_with_crossroad.md new file mode 100644 index 0000000..80c8ca0 --- /dev/null +++ b/doc/developing_with_crossroad.md @@ -0,0 +1,115 @@ +# Cross Compiling for Windows with Crossroad + +[Crossroad](https://pypi.org/project/crossroad/) is a tool to set up cross-compilation environments on GNU/Linux distributions. These instructions assume a Debian/Ubuntu system. However, they can likely be adapted to other Linux environments. Since Ubuntu can be run on Windows through WSL, these instruction may be useful for developing directly on Windows. + +## Install Crossroad + +Crossroad requires tools included with `python3-docutils` and `mingw-w64`. They may be installed using: + +```bash +sudo aptitude install python3-docutils mingw-w64 +``` + +The `zstandard` python package is also required, but is not available in the repositories. It may be installed using `pip`. + +```bash +pip3 install zstandard +``` + +After the dependencies are installed, crossroad itself maybe installed with `pip`. + +```bash +pip3 install crossroad +``` + +If there are errors while running crossroad, it may need to be downloaded and installed directly using `setup.py`. Instructions are on the crossroad homepage. + +## Update Debian Alternatives + +Since `libjxl` uses C++ features that require posix threads, the symlinks used by the Debian alternative system need to be updated: + +```bash +sudo update-alternatives --config x86_64-w64-mingw32-g++ +``` + +Select the option that indicates `posix` usage. Repeat for `gcc` and `i686`: + +```bash +sudo update-alternatives --config x86_64-w64-mingw32-gcc +sudo update-alternatives --config i686-w64-mingw32-gcc +sudo update-alternatives --config i686-w64-mingw32-g++ +``` + +## Create a New Crossroad Project + +Crossroad supports the following platforms: + +``` +native Native platform (x86_64 GNU/Linux) +android-x86 Generic Android/Bionic on x86 +android-mips64 Generic Android/Bionic on MIPS64 +android-x86-64 Generic Android/Bionic on x86-64 +w64 Windows 64-bit +w32 Windows 32-bit +android-arm64 Generic Android/Bionic on ARM64 +android-mips Generic Android/Bionic on MIPS +android-arm Generic Android/Bionic on ARM +``` + +To begin cross compiling for Windows, a new project needs to be created: + +```bash +crossroad w64 [project-name] +``` + +## Install Dependencies + +Since the `gimp` development package is required to build the GIMP plugin and also includes most of the packages required by `libjxl`, install it first. + +```bash +crossroad install gimp +``` + +`gtest` and `brotli` are also required. + +```bash +crossroad install gtest brotli +``` + +If any packages are later found to be missing, you may search for them using: + +```bash +crossroad search [...] +``` + +## Build `libjxl` + +Download the source from the libjxl [releases](https://github.com/libjxl/libjxl/releases) page. Alternatively, you may obtain the latest development version with `git`. Run `./deps.sh` to ensure additional third-party dependencies are downloaded. Unfortunately, the script `./ci.sh` does not work with Crossroad, so `cmake` will need to be called directly. + +Create a build directory within the source directory. If you haven't already, start your crossroad project and run `cmake`: + +```bash +mkdir build +cd build +crossroad w64 libjxl +crossroad cmake -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_PLUGINS=ON -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_GTEST=ON .. +``` + +Check the output to see if any dependencies were missed and need to be installed. If all went well, you may now run `cmake` to build `libjxl`: + +```bash +cmake --build . +``` + +## Try out the GIMP Plugin + +To install and try out out the GIMP plugin: + +1. [Download](https://www.gimp.org/downloads/) and install the stable version of GIMP (currently 2.10.24). + +2. Create a new folder: `C:\Program Files\GIMP 2\lib\gimp\2.0\plug-ins\file-jxl` + +3. Copy `build/plugins/gimp/file-jxl.exe` to the new folder. diff --git a/doc/fuzzing.md b/doc/fuzzing.md new file mode 100644 index 0000000..af92659 --- /dev/null +++ b/doc/fuzzing.md @@ -0,0 +1,184 @@ +# Fuzzing + +Fuzzing is a technique to find potential bugs by providing randomly generated +invalid inputs. To detect potential bugs such as programming errors we use +fuzzing in combination with ASan (Address Sanitizer), MSan (Memory Sanitizer), +UBSan (Undefined Behavior Sanitizer) and asserts in the code. An invalid input +will likely produce a decoding error (some API function returning error), which +is absolutely not a problem, but what it should not do is access memory out of +bounds, use uninitialized memory or hit a false assert condition. + +## Automated Fuzzing with oss-fuzz + +libjxl fuzzing is integrated into [oss-fuzz](https://github.com/google/oss-fuzz) +as the project `libjxl`. oss-fuzz regularly runs the fuzzers on the `main` +branch and reports bugs into their bug tracker which remains private until the +bugs are fixed in main. + +## Fuzzer targets + +There are several fuzzer executable targets defined in the `tools/` directory +to fuzz different parts of the code. The main one is `djxl_fuzzer`, which uses +the public C decoder API to attempt to decode an image. The fuzzer input is not +directly the .jxl file, the last few bytes of the fuzzer input are used to +decide *how* will the API be used (if preview is requested, the pixel format +requested, if the .jxl input data is provided altogether, etc) and the rest of +the fuzzer input is provided as the .jxl file to the decoder. Some bugs might +reproduce only if the .jxl input is decoded in certain way. + +The remaining fuzzer targets execute a specific portion the codec that might be +easier to fuzz independently from the whole codec. + +## Reproducing fuzzer bugs + +A fuzzer target, like `djxl_fuzzer` accepts as a parameter one or more files +that will be used as inputs. This runs the fuzzer program in test-only mode +where no new inputs are generated and only the provided files are tested. This +is the easiest way to reproduce a bug found by the fuzzer using the generated +test case from the bug report. + +oss-fuzz uses a specific compiler version and flags, and it is built using +Docker. Different compiler versions will have different support for detecting +certain actions as errors, so we want to reproduce the build from oss-fuzz as +close as possible. To reproduce the build as generated by oss-fuzz there are a +few helper commands in `ci.sh` as explained below. + +### Generate the gcr.io/oss-fuzz/libjxl image + +First you need the ossfuzz libjxl builder image. This is the base oss-fuzz +builder image with a few dependencies installed. To generate it you need to +check out the oss-fuzz project and build it: + +```bash +git clone https://github.com/google/oss-fuzz.git ~/oss-fuzz +cd ~/oss-fuzz +sudo infra/helper.py build_image libjxl +``` + +This will create the `gcr.io/oss-fuzz/libjxl` docker image. You can check if it +was created verifying that it is listed in the output of the `sudo docker image +ls` command. + +### Build the fuzzer targets with oss-fuzz + +To build the fuzzer targets from the current libjxl source checkout, use the +`./ci.sh ossfuzz_msan` command for MSan, `./ci.sh ossfuzz_asan` command for ASan +or `./ci.sh ossfuzz_ubsan` command for UBSan. All the `JXL_ASSERT` and +`JXL_DASSERT` calls are enabled in all the three modes. These ci.sh helpers will +reproduce the oss-fuzz docker call to build libjxl mounting the current source +directory into the Docker container. Ideally you will run this command in a +different build directory separated from your regular builds. + +For example, for MSan builds run: + +```bash +BUILD_DIR=build-fuzzmsan ./ci.sh ossfuzz_msan +``` + +After this, the fuzzer program will be generated in the build directory like +for other build modes: `build-fuzzmsan/tools/djxl_fuzzer`. + +### Iterating changes with oss-fuzz builds + +After modifying the source code to fix the fuzzer-found bug, or to include more +debug information, you can rebuild only a specific fuzzer target to save on +rebuilding time and immediately run the test case again. For example, for +rebuilding and testing only `djxl_fuzzer` in MSan mode we can run: + +```bash +BUILD_DIR=build-fuzzmsan ./ci.sh ossfuzz_msan djxl_fuzzer && build-fuzzmsan/tools/djxl_fuzzer path/to/testcase.bin +``` + +When MSan and ASan fuzzers fail they will print a stack trace at the point where +the error occurred, and some related information. To make these these stack +traces useful we need to convert the addresses to function names and source file +names and lines, which is done with the "symbolizer". For UBSan to print a stack +trace we need to set the `UBSAN_OPTIONS` environment variables when running the +fuzzer. + +Set the following environment variables when testing the fuzzer binaries. Here +`clang` should match the compiler version used by the container, you can pass a +different compiler version in the following example by first installing the +clang package for that version outside the container and using `clang-NN` +(for example `clang-11`) instead of `clang` in the following commands: + +```bash +symbolizer=$($(realpath $(which clang)) -print-prog-name=llvm-symbolizer) +export MSAN_SYMBOLIZER_PATH="${symbolizer}" +export UBSAN_SYMBOLIZER_PATH="${symbolizer}" +export ASAN_SYMBOLIZER_PATH="${symbolizer}" +export ASAN_OPTIONS=detect_leaks=1 +export UBSAN_OPTIONS=print_stacktrace=1 +``` + +Note: The symbolizer binary must be a program called `llvm-symbolizer`, any +other file name will fail. There are normally symlinks already installed with +the right name which the `-print-prog-name` would print. + +## Running the fuzzers locally + +Running the fuzzer targets in fuzzing mode can be achieved by running them with +no parameters, or better with a parameter with the path to a *directory* +containing a seed of files to use as a starting point. Note that passing a +directory is considered a corpus to use for fuzzing while passing a file is +considered an input to evaluate. Multi-process fuzzing is also supported. For +details about all the fuzzing options run: + +```bash +build-fuzzmsan/tools/djxl_fuzzer -help=1 +``` + +## Writing fuzzer-friendly code + +Fuzzing on itself can't find programming bugs unless an input makes the program +perform an invalid operation (read/write out of bounds, perform an undefined +behavior operation, etc). You can help the fuzzer find invalid situations by +adding asserts: + + * `JXL_ASSERT()` is enabled in Release mode by default. It can be disabled + with `-DJXL_ENABLE_ASSERT=0` but the intention is that it will run for all + the users in released code. If performance of the check is not an issue (like + checks done once per image, once per channel, once per group, etc) a + JXL_ASSERT is appropriate. A failed assert is preferable to an out of bounds + write. + + * `JXL_DASSERT()` is only enabled in Debug builds, which includes all the ASan, + MSan and UBSan builds. Performance of these checks is not an issue if kept + within reasonable limits (automated msan/asan test should finish withing 1 + hour for example). Fuzzing is more effective when the given input runs + faster, so keep that in mind when adding a complex DASSERT that runs multiple + times per output pixel. + + * For MSan builds it is also possible to specify that certain values must be + initialized. This is automatic for values that are used to make decisions + (like when used in an `if` statement or in the ternary operator condition) + but those checks can be made explicit for image data using the + `JXL_CHECK_IMAGE_INITIALIZED(image, rect)` macro. This helps document and + check (only in MSan builds) that a given portion of the image is expected to + be initialized, allowing to catch errors earlier in the process. + +## Dealing with use-of-uninitialized memory + +In MSan builds it is considered an error to *use* uninitialized memory. Using +the memory normally requires something like a decision / branch based on the +uninitialized value, just running `memcpy()` or simple arithmetic over +uninitialized memory is not a problem. Notably, computing `DemoteTo()`, +`NearestInt()` or similar expressions that create a branch based on the value of +the uninitialized memory will trigger an MSan error. + +In libjxl we often run vectorized operations over a series of values, rounding +up to the next multiple of a vector size, thus operating over uninitialized +values past the end of the requested region. These values are part of the image +padding but are not initialized. This behavior would not create an MSan error +unless the processing includes operations like `NearestInt()`. For such cases +the preferred solution is to use `msan::UnpoisonMemory` over the portion of +memory of the last SIMD vector before processing, and then running +`msan::PoisonMemory` over the corresponding value in the output side. A note +including why this is safe to do must be added, for example if the processing +doesn't involve any cross-lane computation. + +Initializing padding memory in MSan builds is discouraged because it may hide +bugs in functions that weren't supposed to read from the padding. Initializing +padding memory in all builds, including Release builds, would mitigate the +MSan potential security issue but it would hide the logic bug for a longer time +and potentially incur in a performance hit. diff --git a/doc/jxl.svg b/doc/jxl.svg new file mode 100644 index 0000000..a80778b --- /dev/null +++ b/doc/jxl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/man/cjxl.txt b/doc/man/cjxl.txt new file mode 100644 index 0000000..261742a --- /dev/null +++ b/doc/man/cjxl.txt @@ -0,0 +1,102 @@ +cjxl(1) +======= +:doctype: manpage + +Name +---- + +cjxl - compress images to JPEG XL + +Synopsis +-------- + +*cjxl* ['options'...] 'input' ['output.jxl'] + +Description +----------- + +`cjxl` compresses an image or animation to the JPEG XL format. It is intended to +spare users the trouble of determining a set of optimal parameters for each +individual image. Instead, for a given target quality, it should provide +consistent visual results across various kinds of images. The defaults have been +chosen to be sensible, so that the following commands should give satisfactory +results in most cases: + +---- +cjxl input.png output.jxl +cjxl input.jpg output.jxl +cjxl input.gif output.jxl +---- + +Options +------- + +-h:: +--help:: + Displays the options that `cjxl` supports. On its own, it will only show + basic options. It can be combined with `-v` or `-v -v` to show increasingly + advanced options as well. + +-v:: +--verbose:: + Increases verbosity. Can be repeated to increase it further, and also + applies to `--help`. + +-d 'distance':: +--distance='distance':: + The preferred way to specify quality. It is specified in multiples of a + just-noticeable difference. That is, `-d 0` is mathematically lossless, + `-d 1` should be visually lossless, and higher distances yield denser and + denser files with lower and lower fidelity. Lossy sources such as JPEG and + GIF files are compressed losslessly by default, and in the case of JPEG + files specifically, the original JPEG can then be reconstructed bit-for-bit. + For lossless sources, `-d 1` is the default. + +-q 'quality':: +--quality='quality':: + Alternative way to indicate the desired quality. 100 is lossless and lower + values yield smaller files. There is no lower bound to this quality + parameter, but positive values should approximately match the quality + setting of libjpeg. + +-e 'effort':: +--effort='effort':: + Controls the amount of effort that goes into producing an ``optimal'' file + in terms of quality/size. That is to say, all other parameters being equal, + a higher effort should yield a file that is at least as dense and possibly + denser, and with at least as high and possibly higher quality. ++ +Recognized effort settings, from fastest to slowest, are: ++ +- 1 or ``lightning'' +- 2 or ``thunder'' +- 3 or ``falcon'' +- 4 or ``cheetah'' +- 5 or ``hare'' +- 6 or ``wombat'' +- 7 or ``squirrel'' (default) +- 8 or ``kitten'' +- 9 or ``tortoise'' + +Examples +-------- + +---- +# Compress a PNG file to a high-quality JPEG XL version. +$ cjxl input.png output.jxl + +# Compress it at a slightly lower quality, appropriate for web use. +$ cjxl -d 2 input.png output.jxl + +# Compress it losslessly. These are equivalent. +$ cjxl -d 0 input.png lossless.jxl +$ cjxl -q 100 input.png lossless.jxl + +# Compress a JPEG file losslessly. +$ cjxl input.jpeg lossless-jpeg.jxl +---- + +See also +-------- + +*djxl*(1) diff --git a/doc/man/djxl.txt b/doc/man/djxl.txt new file mode 100644 index 0000000..bd57b44 --- /dev/null +++ b/doc/man/djxl.txt @@ -0,0 +1,61 @@ +djxl(1) +======= +:doctype: manpage + +Name +---- + +djxl - decompress JPEG XL images + +Synopsis +-------- + +*djxl* ['options'...] 'input.jxl' ['output'] + +Description +----------- + +`djxl` decompresses a JPEG XL image or animation. The output format is determined +by the extension of the output file, which can be `.png`, `.jpg`, `.ppm`, `.pfm`. +If the JPEG XL input file contains an animation, multiple output files will be +produced, with names of the form "'output'-*framenumber*.ext". + + +Options +------- + +-h:: +--help:: + Displays the options that `djxl` supports. + +-j:: +--pixels_to_jpeg:: + By default, if the input JPEG XL contains a recompressed JPEG file, + djxl reconstructs the exact original JPEG file if the output file has the + `.jpg` (or `.jpeg`) filename extension. + This flag causes the decoder to instead decode the image to pixels and + encode a new (lossy) JPEG in this case. + + +-q 'quality':: +--jpeg_quality='quality':: + When decoding to `.jpg`, use this output quality. This option implicitly + enables the --pixels_to_jpeg option. + + +Examples +-------- + +---- +# Decompress a JPEG XL file to PNG +$ djxl input.jxl output.png + +# Reconstruct a losslessly-recompressed JPEG file +$ djxl lossless-jpeg.jxl reconstructed.jpeg +---- + + +See also +-------- + +*cjxl*(1) diff --git a/doc/release.md b/doc/release.md new file mode 100644 index 0000000..2e645da --- /dev/null +++ b/doc/release.md @@ -0,0 +1,262 @@ +# libjxl release process + +This guide documents the release process for the libjxl project. + +libjxl follows the [semantic versioning](https://semver.org/spec/v2.0.0.html) +specification for released versions. Releases are distributed as tags in the git +repository with the semantic version prefixed by the letter "v". For example, +release version "0.3.7" will have a git tag "v0.3.7". + +The public API is explicitly defined as C headers in the `lib/include` +directory, normally installed in your include path. All other headers are +internal API and are not covered by the versioning rules. + +## Development and release workflow + +New code development is performed on the `main` branch of the git repository. +Pre-submit checks enforce minimum build and test requirements for new patches +that balance impact and test latency, but not all checks are performed before +pull requests are merged. Several slower checks only run *after* the code has +been merged to `main`, resulting in some errors being detected hours after the +code is merged or even days after in the case of fuzzer-detected bugs. + +Release tags are cut from *release branches*. Each MAJOR.MINOR version has its +own release branch, for example releases `0.5`, `0.5.1`, `0.5.2`, ... would have +tags `v0.5`, `v0.5.1`, `v0.5.2`, ... on commits from the `v0.5.x` branch. +`v0.5.x` is a branch name, not a tag name, and doesn't represent a released +version since semantic versioning requires that the PATCH is a non-negative +number. Released tags don't each one have their own release branch, all releases +from the same MAJOR.MINOR version will share the same branch. + +The main purpose of the release branch is to stabilize the code before a +release. This involves including fixes to existing bugs but **not** including +new features. New features often come with new bugs which take time to fix, so +having a release branch allows us to cherry-pick *bug fixes* from the `main` +branch into the release branch without including the new *features* from `main`. +For this reason it is important to make small commits in `main` and separate bug +fixes from new features. + +After the initial minor release (`M.N`, for example `0.5.0` or just `0.5`) the +release branch is used to continue to cherry-pick fixes to be included in a +patch release, for example a version `0.5.1` release. Patch fixes are only meant +to fix security bugs or other critical bugs that can't wait until the next major +or minor release. + +Release branches *may* continue to be maintained even after the next minor or +major version has been released to support users that can't update to a newer +minor release. In that case, the same process applies to all the maintained +release branches. + +A release branch with specific cherry-picks from `main` means that the release +code is actually a version of the code that never existed in the `main` branch, +so it needs to be tested independently. Pre-submit and post-submit tests run on +release branches (branches matching `v*.*.x`) but extra manual checks should be +performed before a release, specially if multiple bug fixes interact with each +other. Take this into account when selecting which commits to include in a +release. The objective is to have a stable version that can be used without +problems for months. Having the latest improvements at the time the release tag +is created is a non-goal. + +## Creating a release branch + +A new release branch is needed before creating a new major or minor release, +that is, a new release where the MAJOR or MINOR numbers are increased. Patch +releases, where only the PATCH number is increased, reuse the branch from the +previous release of the same MAJOR and MINOR numbers. + +The following instructions assume that you followed the recommended [libjxl git +setup](developing_in_github.md) where `origin` points to the upstream +libjxl/libjxl project, otherwise use the name of your upstream remote repository +instead of `origin`. + +The release branch is normally created from the latest work in `main` at the +time the branch is created, but it is possible to create the branch from an +older commit if the current `main` is particularly unstable or includes commits +that were not intended to be included in the release. The following example +creates the branch `v0.5.x` from the latest commit in main (`origin/main`), if a +different commit is to be used then replace `origin/main` with the SHA of that +commit. Change the `v0.5.x` branch name to the one you are creating. + +```bash +git fetch origin main +git push git@github.com:libjxl/libjxl.git origin/main:refs/heads/v0.5.x +``` + +Here we use the SSH URL explicitly since you are pushing to the `libjxl/libjxl` +project directly to a branch there. If you followed the guide `origin` will have +the HTTPS URL which wouldn't normally let you push since you wouldn't be +authenticated. The `v*.*.x` branches are [GitHub protected +branches](https://docs.github.com/en/github/administering-a-repository/defining-the-mergeability-of-pull-requests/about-protected-branches) +in our repository, however you can push to a protected branch when *creating* it +but you can't directly push to it after it is created. To include more changes +in the release branch see the "Cherry-picking fixes to a release" section below. + +## Creating a merge label + +We use GitHub labels in Pull Requests to keep track of the changes that should +be merged into a given release branch. For this purpose create a new label for +each new MAJOR.MINOR release branch called `merge-MAJOR.MINOR`, for example, +`merge-0.5`. + +In the [edit labels](https://github.com/libjxl/libjxl/issues/labels) page, click +on "New label" and create the label. Pick your favorite color. + +Labels are a GitHub-only concept and are not represented in git. You can add the +label to a Pull Request even after it was merged, whenever it is decided that +the Pull Request should be included in the given release branch. Adding the +label doesn't automatically merge it to the release branch. + +## Update the versioning number + +The version number (as returned by `JxlDecoderVersion`) in the source code in +`main` must match the semantic versioning of a release. After the release +branch is created the code in `main` will only be included in the next major +or minor release. Right after a release branch update the version targeting the +next release. Artifacts from `main` should include the new (unreleased) version, +so it is important to update it. For example, after the `v0.5.x` branch is +created from main, you should update the version on `main` to `0.6`. + +To help update it, run this helper command (in a Debian-based system): + +```bash +./ci.sh bump_version 0.6 +``` + +This will update the version in the following files: + + * `lib/CMakeLists.txt` + * `lib/lib.gni`, automatically updated with `tools/build_cleaner.py --update`. + * `debian/changelog` to create the Debian package release with the new version. + Debian changelog shouldn't repeat the library changelog, instead it should + include changes to the packaging scripts. + +## Cherry-pick fixes to a release + +After a Pull Request that should be included in a release branch has been merged +to `main` it can be cherry-picked to the release branch. Before cherry-picking a +change to a release branch it is important to check that it doesn't introduce +more problems, in particular it should run for some time in `main` to make sure +post-submit tests and the fuzzers run on it. Waiting for a day is a good idea. + +Most of the testing is done on the `main` branch, so be careful with what +commits are cherry-picked to a branch. Refactoring code is often not a good +candidate to cherry-pick. + +To cherry-pick a single commit to a release branch (in this example to `v0.5.x`) +you can run: + +```bash +git fetch origin +git checkout origin/v0.5.x -b merge_to_release +git cherry-pick -x SHA_OF_MAIN_COMMIT +# -x will annotate the cherry-pick with the original SHA_OF_MAIN_COMMIT value. +# If not already mentioned in the original commit, add the original PR number to +# the commit, for example add "(cherry picked from PR #NNNN)". +git commit --amend +``` + +The `SHA_OF_MAIN_COMMIT` is the hash of the commit as it landed in main. Use +`git log origin/main` to list the recent main commits and their hashes. + +Making sure that the commit message on the cherry-picked commit contains a +reference to the original pull request (like `#NNNN`) is important. It creates +an automatic comment in the original pull request notifying that it was +mentioned in another commit, helping keep track of the merged pull requests. If +the original commit was merged with the "Squash and merge" policy it will +automatically contain the pull request number on the first line, if this is not +the case you can amend the commit message of the cherry-pick to include a +reference. + +Multiple commits can be cherry-picked and tested at once to save time. Continue +running `git cherry-pick` and `git commit --amend` multiple times for all the +commits you need to cherry-pick, ideally in the same order they were merged on +the `main` branch. At the end you will have a local branch with multiple commits +on top of the release branch. + +Finally, upload your changes to *your fork* like normal, except that when +creating a pull request select the desired release branch as a target: + +```bash +git push myfork merge_to_release +``` + +If you used the [guide](developing_in_github.md) `myfork` would be `origin` in +that example. Click on the URL displayed, which will be something like + + `https://github.com/mygithubusername/libjxl/pull/new/merge_to_release` + +In the "Open a pull request" page, change the drop-down base branch from +"base: main" (the default) to the release branch you are targeting. + +The pull request approval and pre-submit rules apply as with normal pull +requests to the `main` branch. + +**Important:** When merging multiple cherry-picks use "Rebase and merge" policy, +not the squash one since otherwise you would discard the individual commit +message references from the git history in the release branch. + +## Publishing a release + +Once a release tag is created it must not be modified, so you need to prepare +the changes before creating the release. Make sure you checked the following: + + * The semantic version number in the release branch (see `lib/CMakeLists.txt`) + matches the number you intend to release, all three MAJOR, MINOR and PATCH + should match. Otherwise send a pull request to the release branch to + update them. + + * The GitHub Actions checks pass on the release branch. Look for the green + tick next to the last commit on the release branch. This should be visible + on the branch page, for example: https://github.com/libjxl/libjxl/tree/v0.5.x + + * There no open fuzzer-found bugs for the release branch. The most effective + way is to [run the fuzzer](fuzzing.md) on the release branch for a while. You + can seed the fuzzer with corpus generated by oss-fuzz by [downloading + it](https://google.github.io/oss-fuzz/advanced-topics/corpora/#downloading-the-corpus), + for example `djxl_fuzzer` with libFuzzer will use: + gs://libjxl-corpus.clusterfuzz-external.appspot.com/libFuzzer/libjxl_djxl_fuzzer + + * Manually check that images encode/decode ok. + + * Manually check that downstream projects compile with our code. Sometimes + bugs on build scripts are only detected when other projects try to use our + library. For example, test compiling + [imagemagick](https://github.com/ImageMagick/ImageMagick) and Chrome. + +A [GitHub +"release"](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/about-releases) +consists of two different concepts: + + * a git "tag": this is a name (`v` plus the semantic version number) with a + commit hash associated, defined in the git repository. Most external projects + will use git tags or HTTP URLs to these tags to fetch the code. + + * a GitHub "release": this is a GitHub-only concept and is not represented in + git other than by having a git tag associated with the release. A GitHub + release has a given source code commit SHA associated (through the tag) but + it *also* contains release notes and optional binary files attached to the + release. + +Releases from the older GitLab repository only have a git tag in GitHub, while +newer releases have both a git tag and a release entry in GitHub. + +To publish a release open the [New Release +page](https://github.com/libjxl/libjxl/releases/new) and follow these +instructions: + + * Set the "Tag version" as "v" plus the semantic version number. Omit the ".0" + when the PATCH version is 0, for example use "v0.5" or "v0.5.1" but not + "v0.5.0". + + * Select the "Target" as your release branch. For a "v0.5" release tag you + would use the "v0.5.x" branch. + + * Use the version number as the release title. + + * Copy-paste the relevant section of the [CHANGELOG.md](../CHANGELOG.md) to the + release notes into the release notes. Add any other information pertaining + the release itself that are not included in the CHANGELOG.md, although prefer + to include those in the CHANGELOG.md file. You can switch to the Preview tab + to see the results. + + * Finally click "Publish release" and go celebrate with the team. 🎉 diff --git a/doc/software_support.md b/doc/software_support.md new file mode 100644 index 0000000..4aa54ea --- /dev/null +++ b/doc/software_support.md @@ -0,0 +1,55 @@ +# JPEG XL software support + +This document attempts to keep track of software that is using libjxl to support JPEG XL. +This list serves several purposes: + +- thank/acknowledge other projects for integrating jxl support +- point end-users to software that can read/write jxl +- keep track of the adoption status of jxl +- in case of a (security) bug in libjxl, it's easier to see who might be affected and check if they are updated (in case they use static linking) + +Please add missing software to this list. + +## Browsers + +- Chromium: behind a flag since version 91, [tracking bug](https://bugs.chromium.org/p/chromium/issues/detail?id=1178058) +- Firefox: behind a flag since version 90, [tracking bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1539075) +- Safari: not supported, [tracking bug](https://bugs.webkit.org/show_bug.cgi?id=208235) +- Edge: behind a flag since version 91, start with `.\msedge.exe --enable-features=JXL` +- Opera: behind a flag since version 77. + +## Image libraries + +- [ImageMagick](https://imagemagick.org/): supported since 7.0.10-54 +- [libvips](https://libvips.github.io/libvips/): supported since 8.11 +- [Imlib2](https://github.com/alistair7/imlib2-jxl) + +## OS-level support / UI frameworks / file browser plugins + +- Qt / KDE: [plugin available](https://github.com/novomesk/qt-jpegxl-image-plugin) +- GDK-pixbuf: plugin available in libjxl repo +- [gThumb](https://ubuntuhandbook.org/index.php/2021/04/gthumb-3-11-3-adds-jpeg-xl-support/) +- [MacOS viewer/QuickLook plugin](https://github.com/yllan/JXLook) +- [Windows Imaging Component](https://github.com/mirillis/jpegxl-wic) +- [Windows thumbnail handler](https://github.com/saschanaz/jxl-winthumb) +- [OpenMandriva Lx (since 4.3 RC)](https://www.openmandriva.org/en/news/article/openmandriva-lx-4-3-rc-available-for-testing) +- [KaOS (since 2021.06)](https://news.itsfoss.com/kaos-2021-06-release/) + +## Image editors + +- GIMP: plugin available in libjxl repo, no official support, [tracking bug](https://gitlab.gnome.org/GNOME/gimp/-/issues/4681) +- Photoshop: no plugin available yet, no official support yet + +## Image viewers + +- [XnView](https://www.xnview.com/en/) +- [ImageGlass](https://imageglass.org/) +- Any viewer based on Qt, KDE, GDK-pixbuf, ImageMagick, libvips or imlib2 (see above) + - Qt viewers: gwenview, digiKam, KolourPaint, KPhotoAlbum, LXImage-Qt, qimgv, qView, nomacs, VookiImageViewer, PhotoQt + - GTK viewers: Eye of Gnome (eog), gThumb, Geeqie + +## Online tools + +- [Squoosh](https://squoosh.app/) +- [Cloudinary](https://cloudinary.com/blog/cloudinary_supports_jpeg_xl) +- [MConverter](https://mconverter.eu/) diff --git a/doc/tables/adobe.md b/doc/tables/adobe.md new file mode 100644 index 0000000..f3beef7 --- /dev/null +++ b/doc/tables/adobe.md @@ -0,0 +1,6 @@ +#### Table M.8 – "Adobe" marker template + +``` +0xEE, 0x00, 0x0E, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x01 +``` + diff --git a/doc/tables/all_tables.pdf b/doc/tables/all_tables.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b02c5b25b884054c09256be304128b171c1fa164 GIT binary patch literal 164368 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 all_tables.md diff --git a/doc/tables/app0.md b/doc/tables/app0.md new file mode 100644 index 0000000..266f210 --- /dev/null +++ b/doc/tables/app0.md @@ -0,0 +1,6 @@ +#### Table M.4 – APP0 template + +``` +0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00 +``` + diff --git a/doc/tables/brn_proto.md b/doc/tables/brn_proto.md new file mode 100644 index 0000000..b5f80b6 --- /dev/null +++ b/doc/tables/brn_proto.md @@ -0,0 +1,23 @@ +#### Table M.3 – Protocol Buffer descriptor of top-level structure of losslessly compressed JPEG stream + +```protobuf +message Header { + optional uint64 width = 1; + optional uint64 height = 2; + required uint64 version_and_component_count_code = 3; + optional uint64 subsampling_code = 4; +} + +message Jpeg { + required bytes signature = 1; + required Header header = 2; + optional bytes meta_data = 3; + optional bytes jpeg1_internals = 4; + optional bytes quant_data = 5; + optional bytes histogram_data = 6; + optional bytes dc_data = 7; + optional bytes ac_data = 8; + optional bytes original_jpg = 9; +} +``` + diff --git a/doc/tables/context_modes.md b/doc/tables/context_modes.md new file mode 100644 index 0000000..59bff36 --- /dev/null +++ b/doc/tables/context_modes.md @@ -0,0 +1,13 @@ +#### Table M.29 – context_modes table + +``` +0, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, +0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, +0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0 +``` + +``` +0, 1, 1, 1, 1, 0, 0, 0, 2, 3, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, +0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +``` diff --git a/doc/tables/dct_gen.md b/doc/tables/dct_gen.md new file mode 100644 index 0000000..f3b59e2 --- /dev/null +++ b/doc/tables/dct_gen.md @@ -0,0 +1,241 @@ +#### Electronic Insert I.1 – DCT-II / DCT-III code generator + +```python +####################################################################### +# DCT-II / DCT-III generator +# +# Based on: +# "A low multiplicative complexity fast recursive DCT-2 algorithm" +# by Maxim Vashkevich and Alexander Petrovsky / arXiv / 20 Jul 2012 +####################################################################### + +import math +import sys +N = 8 + +####################################################################### +# Base transforms / generators +####################################################################### + +CNTR = 0 +def makeTmp(): + global CNTR + result = "t{:02d}".format(CNTR) + CNTR = CNTR + 1 + return result + +def makeVar(i): + return "i{:02d}".format(i) + +def add(x, y): + tmp = makeTmp() + print(tmp + " = " + x + " + " + y + ";") + return tmp + +def sub(x, y): + tmp = makeTmp() + print(tmp + " = " + x + " - " + y + ";") + return tmp + +def mul(x, c): + tmp = makeTmp() + print(tmp + " = " + x + " * " + c + ";") + return tmp + +# 2.0 * math.cos((a + 0.0) / (b + 0.0) * math.pi) +def C2(a, b): + return "c_c2_" + str(a) + "_" + str(b) + +# 1.0 / C2(a, b) +def iC2(a, b): + return "c_ic2_" + str(a) + "_" + str(b) + +####################################################################### +# Utilities +####################################################################### + +# Generate identity matrix. Usually this matrix is passed to +# DCT algorithm to generate "basis" vectors of the transform. +def makeVars(): + return [makeVar(i) for i in range(N)] + +# Split list of variables info halves. +def split(x): + m = len(x) + m2 = m // 2 + return (x[0 : m2], x[m2 : m]) + +# Make a list of variables in a reverse order. +def reverse(varz): + m = len(varz) + result = [0] * m + for i in range(m): + result[i] = varz[m - 1 - i] + return result + +# Apply permutation +def permute(x, p): + return [x[p[i]] for i in range(len(p))] + +def transposePermutation(p): + n = len(p) + result = [0] * n + for i in range(n): + result[p[i]] = i + return result + +# See paper. Split even-odd elements. +def P(n): + if n == 1: + return [0] + n2 = n // 2 + return [2 * i for i in range(n2)] + [2 * i + 1 for i in range(n2)] + +# See paper. Interleave first and second half. +def Pt(n): + return transposePermutation(P(n)) + +####################################################################### +# Scheme +####################################################################### + +def B2(x): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + bottom = reverse(bottom) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + return t + b + +def iB2(x): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + return t + reverse(b) + +def B4(x, rn): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + rbottom = reverse(bottom) + t = [sub(top[i], rbottom[i]) for i in range(n2)] + b = [mul(bottom[i], C2(rn, 2 * N)) for i in range(n2)] + top = [add(t[i], b[i]) for i in range(n2)] + bottom = [sub(t[i], b[i]) for i in range(n2)] + return top + bottom + +def iB4(x, rn): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + bottom = [mul(b[i], iC2(rn, 2 * N)) for i in range(n2)] + rbottom = reverse(bottom) + top = [add(t[i], rbottom[i]) for i in range(n2)] + return top + bottom + +def P4(n): + if n == 1: + return [0] + if n == 2: + return [0, 1] + n2 = n // 2 + result = [0] * n + tc = 0 + bc = 0 + i = 0 + result[i] = tc; tc = tc + 1; i = i + 1 + turn = True + while i < n - 1: + if turn: + result[i] = n2 + bc; bc = bc + 1; i = i + 1 + result[i] = n2 + bc; bc = bc + 1; i = i + 1 + else: + result[i] = tc; tc = tc + 1; i = i + 1 + result[i] = tc; tc = tc + 1; i = i + 1 + turn = not turn + result[i] = tc; tc = tc + 1; i = i + 1 + return result + +def iP4(n): + return transposePermutation(P4(n)) + +def d2n(x): + n = len(x) + if n == 1: + return x + y = B2(x) + (top, bottom) = split(y) + return permute(d2n(top) + d4n(bottom, N // 2), Pt(n)) + +def id2n(x): + n = len(x) + if n == 1: + return x + (top, bottom) = split(permute(x, P(n))) + return iB2(id2n(top) + id4n(bottom, N // 2)) + +def d4n(x, rn): + n = len(x) + if n == 1: + return x + y = B4(x, rn) + (top, bottom) = split(y) + rn2 = rn // 2 + return permute(d4n(top, rn2) + d4n(bottom, N - rn2), P4(n)) + +def id4n(x, rn): + n = len(x) + if n == 1: + return x + (top, bottom) = split(permute(x, iP4(n))) + rn2 = rn // 2 + y = id4n(top, rn2) + id4n(bottom, N -rn2) + return iB4(y, rn) + +####################################################################### +# Main. +####################################################################### + +def help(): + print("Usage: %s [N [T]]" % sys.argv[0]) + print(" N should be the power of 2, default is 8") + print(" T is one of {2, 3}, default is 2") + sys.exit() + +def parseInt(s): + try: + return int(s) + except ValueError: + help() + +if __name__ == "__main__": + if len(sys.argv) < 1 or len(sys.argv) > 3: help() + if len(sys.argv) >= 2: + N = parseInt(sys.argv[1]) + if (N & (N - 1)) != 0: help() + type = 0 + if len(sys.argv) >= 3: + typeOption = sys.argv[2] + if len(typeOption) != 1: help() + type = "23".index(typeOption) + if type == -1: help() + if type == 0: + vars = d2n(makeVars()) + else: # type == 1 + vars = id2n(makeVars()) + print("Output vector: " + str(vars)) +``` + diff --git a/doc/tables/ducky.md b/doc/tables/ducky.md new file mode 100644 index 0000000..307f688 --- /dev/null +++ b/doc/tables/ducky.md @@ -0,0 +1,6 @@ +#### Table M.7 – "Ducky" marker template + +``` +0xEC, 0x00, 0x11, 0x44, 0x75, 0x63, 0x6B, 0x79, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00 +``` + diff --git a/doc/tables/freq_context.md b/doc/tables/freq_context.md new file mode 100644 index 0000000..3e218fb --- /dev/null +++ b/doc/tables/freq_context.md @@ -0,0 +1,54 @@ +#### Table M.15 – freq_context + +`scheme == 0`: +``` +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +``` + +`scheme == 1`: +``` +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 +``` + +`scheme == 2`: +``` +0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, +3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1 +``` + +`scheme == 3`: +``` +0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, +6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, +7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 2, 2, 2 +``` + +`scheme == 4`: +``` + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 8, 8, 9, 9, + 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, +13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, +15, 15, 15, 15, 15, 15, 15, 15, 15, 15 +``` + +`scheme == 5`: +``` + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16, +17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 24, 24, +25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, +29, 29, 30, 30, 30, 30, 31, 31, 31, 31 +``` + +`scheme == 6`: +``` + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, +18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, +36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, +54, 55, 56, 57, 58, 59, 60, 61, 62, 63 +``` + diff --git a/doc/tables/icc.md b/doc/tables/icc.md new file mode 100644 index 0000000..1f3b4cd --- /dev/null +++ b/doc/tables/icc.md @@ -0,0 +1,6 @@ +#### Table M.6 – common ICC profile template + +``` +0xE2, 0x0C, 0x58, 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0x01, 0x01, 0x00, 0x00, 0x0C, 0x48, 0x4C, 0x69, 0x6E, 0x6F, 0x02, 0x10, 0x00, 0x00, 0x6D, 0x6E, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5A, 0x20, 0x07, 0xCE, 0x00, 0x02, 0x00, 0x09, 0x00, 0x06, 0x00, 0x31, 0x00, 0x00, 0x61, 0x63, 0x73, 0x70, 0x4D, 0x53, 0x46, 0x54, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x43, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xF6, 0xD6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xD3, 0x2D, 0x48, 0x50, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x33, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, 0x84, 0x00, 0x00, 0x00, 0x6C, 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x00, 0x14, 0x62, 0x6B, 0x70, 0x74, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x14, 0x72, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x18, 0x00, 0x00, 0x00, 0x14, 0x67, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x2C, 0x00, 0x00, 0x00, 0x14, 0x62, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x14, 0x64, 0x6D, 0x6E, 0x64, 0x00, 0x00, 0x02, 0x54, 0x00, 0x00, 0x00, 0x70, 0x64, 0x6D, 0x64, 0x64, 0x00, 0x00, 0x02, 0xC4, 0x00, 0x00, 0x00, 0x88, 0x76, 0x75, 0x65, 0x64, 0x00, 0x00, 0x03, 0x4C, 0x00, 0x00, 0x00, 0x86, 0x76, 0x69, 0x65, 0x77, 0x00, 0x00, 0x03, 0xD4, 0x00, 0x00, 0x00, 0x24, 0x6C, 0x75, 0x6D, 0x69, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x14, 0x6D, 0x65, 0x61, 0x73, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x00, 0x24, 0x74, 0x65, 0x63, 0x68, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x00, 0x0C, 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x28, 0x63, 0x29, 0x20, 0x31, 0x39, 0x39, 0x38, 0x20, 0x48, 0x65, 0x77, 0x6C, 0x65, 0x74, 0x74, 0x2D, 0x50, 0x61, 0x63, 0x6B, 0x61, 0x72, 0x64, 0x20, 0x43, 0x6F, 0x6D, 0x70, 0x61, 0x6E, 0x79, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x73, 0x52, 0x47, 0x42, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x73, 0x52, 0x47, 0x42, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x51, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x16, 0xCC, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xA2, 0x00, 0x00, 0x38, 0xF5, 0x00, 0x00, 0x03, 0x90, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x99, 0x00, 0x00, 0xB7, 0x85, 0x00, 0x00, 0x18, 0xDA, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0xA0, 0x00, 0x00, 0x0F, 0x84, 0x00, 0x00, 0xB6, 0xCF, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x49, 0x45, 0x43, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x65, 0x63, 0x2E, 0x63, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x49, 0x45, 0x43, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x65, 0x63, 0x2E, 0x63, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x49, 0x45, 0x43, 0x20, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x20, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x20, 0x52, 0x47, 0x42, 0x20, 0x63, 0x6F, 0x6C, 0x6F, 0x75, 0x72, 0x20, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x2D, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x49, 0x45, 0x43, 0x20, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x20, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x20, 0x52, 0x47, 0x42, 0x20, 0x63, 0x6F, 0x6C, 0x6F, 0x75, 0x72, 0x20, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x2D, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x63, 0x65, 0x20, 0x56, 0x69, 0x65, 0x77, 0x69, 0x6E, 0x67, 0x20, 0x43, 0x6F, 0x6E, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x63, 0x65, 0x20, 0x56, 0x69, 0x65, 0x77, 0x69, 0x6E, 0x67, 0x20, 0x43, 0x6F, 0x6E, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xA4, 0xFE, 0x00, 0x14, 0x5F, 0x2E, 0x00, 0x10, 0xCF, 0x14, 0x00, 0x03, 0xED, 0xCC, 0x00, 0x04, 0x13, 0x0B, 0x00, 0x03, 0x5C, 0x9E, 0x00, 0x00, 0x00, 0x01, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x09, 0x56, 0x00, 0x50, 0x00, 0x00, 0x00, 0x57, 0x1F, 0xE7, 0x6D, 0x65, 0x61, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x8F, 0x00, 0x00, 0x00, 0x02, 0x73, 0x69, 0x67, 0x20, 0x00, 0x00, 0x00, 0x00, 0x43, 0x52, 0x54, 0x20, 0x63, 0x75, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x0F, 0x00, 0x14, 0x00, 0x19, 0x00, 0x1E, 0x00, 0x23, 0x00, 0x28, 0x00, 0x2D, 0x00, 0x32, 0x00, 0x37, 0x00, 0x3B, 0x00, 0x40, 0x00, 0x45, 0x00, 0x4A, 0x00, 0x4F, 0x00, 0x54, 0x00, 0x59, 0x00, 0x5E, 0x00, 0x63, 0x00, 0x68, 0x00, 0x6D, 0x00, 0x72, 0x00, 0x77, 0x00, 0x7C, 0x00, 0x81, 0x00, 0x86, 0x00, 0x8B, 0x00, 0x90, 0x00, 0x95, 0x00, 0x9A, 0x00, 0x9F, 0x00, 0xA4, 0x00, 0xA9, 0x00, 0xAE, 0x00, 0xB2, 0x00, 0xB7, 0x00, 0xBC, 0x00, 0xC1, 0x00, 0xC6, 0x00, 0xCB, 0x00, 0xD0, 0x00, 0xD5, 0x00, 0xDB, 0x00, 0xE0, 0x00, 0xE5, 0x00, 0xEB, 0x00, 0xF0, 0x00, 0xF6, 0x00, 0xFB, 0x01, 0x01, 0x01, 0x07, 0x01, 0x0D, 0x01, 0x13, 0x01, 0x19, 0x01, 0x1F, 0x01, 0x25, 0x01, 0x2B, 0x01, 0x32, 0x01, 0x38, 0x01, 0x3E, 0x01, 0x45, 0x01, 0x4C, 0x01, 0x52, 0x01, 0x59, 0x01, 0x60, 0x01, 0x67, 0x01, 0x6E, 0x01, 0x75, 0x01, 0x7C, 0x01, 0x83, 0x01, 0x8B, 0x01, 0x92, 0x01, 0x9A, 0x01, 0xA1, 0x01, 0xA9, 0x01, 0xB1, 0x01, 0xB9, 0x01, 0xC1, 0x01, 0xC9, 0x01, 0xD1, 0x01, 0xD9, 0x01, 0xE1, 0x01, 0xE9, 0x01, 0xF2, 0x01, 0xFA, 0x02, 0x03, 0x02, 0x0C, 0x02, 0x14, 0x02, 0x1D, 0x02, 0x26, 0x02, 0x2F, 0x02, 0x38, 0x02, 0x41, 0x02, 0x4B, 0x02, 0x54, 0x02, 0x5D, 0x02, 0x67, 0x02, 0x71, 0x02, 0x7A, 0x02, 0x84, 0x02, 0x8E, 0x02, 0x98, 0x02, 0xA2, 0x02, 0xAC, 0x02, 0xB6, 0x02, 0xC1, 0x02, 0xCB, 0x02, 0xD5, 0x02, 0xE0, 0x02, 0xEB, 0x02, 0xF5, 0x03, 0x00, 0x03, 0x0B, 0x03, 0x16, 0x03, 0x21, 0x03, 0x2D, 0x03, 0x38, 0x03, 0x43, 0x03, 0x4F, 0x03, 0x5A, 0x03, 0x66, 0x03, 0x72, 0x03, 0x7E, 0x03, 0x8A, 0x03, 0x96, 0x03, 0xA2, 0x03, 0xAE, 0x03, 0xBA, 0x03, 0xC7, 0x03, 0xD3, 0x03, 0xE0, 0x03, 0xEC, 0x03, 0xF9, 0x04, 0x06, 0x04, 0x13, 0x04, 0x20, 0x04, 0x2D, 0x04, 0x3B, 0x04, 0x48, 0x04, 0x55, 0x04, 0x63, 0x04, 0x71, 0x04, 0x7E, 0x04, 0x8C, 0x04, 0x9A, 0x04, 0xA8, 0x04, 0xB6, 0x04, 0xC4, 0x04, 0xD3, 0x04, 0xE1, 0x04, 0xF0, 0x04, 0xFE, 0x05, 0x0D, 0x05, 0x1C, 0x05, 0x2B, 0x05, 0x3A, 0x05, 0x49, 0x05, 0x58, 0x05, 0x67, 0x05, 0x77, 0x05, 0x86, 0x05, 0x96, 0x05, 0xA6, 0x05, 0xB5, 0x05, 0xC5, 0x05, 0xD5, 0x05, 0xE5, 0x05, 0xF6, 0x06, 0x06, 0x06, 0x16, 0x06, 0x27, 0x06, 0x37, 0x06, 0x48, 0x06, 0x59, 0x06, 0x6A, 0x06, 0x7B, 0x06, 0x8C, 0x06, 0x9D, 0x06, 0xAF, 0x06, 0xC0, 0x06, 0xD1, 0x06, 0xE3, 0x06, 0xF5, 0x07, 0x07, 0x07, 0x19, 0x07, 0x2B, 0x07, 0x3D, 0x07, 0x4F, 0x07, 0x61, 0x07, 0x74, 0x07, 0x86, 0x07, 0x99, 0x07, 0xAC, 0x07, 0xBF, 0x07, 0xD2, 0x07, 0xE5, 0x07, 0xF8, 0x08, 0x0B, 0x08, 0x1F, 0x08, 0x32, 0x08, 0x46, 0x08, 0x5A, 0x08, 0x6E, 0x08, 0x82, 0x08, 0x96, 0x08, 0xAA, 0x08, 0xBE, 0x08, 0xD2, 0x08, 0xE7, 0x08, 0xFB, 0x09, 0x10, 0x09, 0x25, 0x09, 0x3A, 0x09, 0x4F, 0x09, 0x64, 0x09, 0x79, 0x09, 0x8F, 0x09, 0xA4, 0x09, 0xBA, 0x09, 0xCF, 0x09, 0xE5, 0x09, 0xFB, 0x0A, 0x11, 0x0A, 0x27, 0x0A, 0x3D, 0x0A, 0x54, 0x0A, 0x6A, 0x0A, 0x81, 0x0A, 0x98, 0x0A, 0xAE, 0x0A, 0xC5, 0x0A, 0xDC, 0x0A, 0xF3, 0x0B, 0x0B, 0x0B, 0x22, 0x0B, 0x39, 0x0B, 0x51, 0x0B, 0x69, 0x0B, 0x80, 0x0B, 0x98, 0x0B, 0xB0, 0x0B, 0xC8, 0x0B, 0xE1, 0x0B, 0xF9, 0x0C, 0x12, 0x0C, 0x2A, 0x0C, 0x43, 0x0C, 0x5C, 0x0C, 0x75, 0x0C, 0x8E, 0x0C, 0xA7, 0x0C, 0xC0, 0x0C, 0xD9, 0x0C, 0xF3, 0x0D, 0x0D, 0x0D, 0x26, 0x0D, 0x40, 0x0D, 0x5A, 0x0D, 0x74, 0x0D, 0x8E, 0x0D, 0xA9, 0x0D, 0xC3, 0x0D, 0xDE, 0x0D, 0xF8, 0x0E, 0x13, 0x0E, 0x2E, 0x0E, 0x49, 0x0E, 0x64, 0x0E, 0x7F, 0x0E, 0x9B, 0x0E, 0xB6, 0x0E, 0xD2, 0x0E, 0xEE, 0x0F, 0x09, 0x0F, 0x25, 0x0F, 0x41, 0x0F, 0x5E, 0x0F, 0x7A, 0x0F, 0x96, 0x0F, 0xB3, 0x0F, 0xCF, 0x0F, 0xEC, 0x10, 0x09, 0x10, 0x26, 0x10, 0x43, 0x10, 0x61, 0x10, 0x7E, 0x10, 0x9B, 0x10, 0xB9, 0x10, 0xD7, 0x10, 0xF5, 0x11, 0x13, 0x11, 0x31, 0x11, 0x4F, 0x11, 0x6D, 0x11, 0x8C, 0x11, 0xAA, 0x11, 0xC9, 0x11, 0xE8, 0x12, 0x07, 0x12, 0x26, 0x12, 0x45, 0x12, 0x64, 0x12, 0x84, 0x12, 0xA3, 0x12, 0xC3, 0x12, 0xE3, 0x13, 0x03, 0x13, 0x23, 0x13, 0x43, 0x13, 0x63, 0x13, 0x83, 0x13, 0xA4, 0x13, 0xC5, 0x13, 0xE5, 0x14, 0x06, 0x14, 0x27, 0x14, 0x49, 0x14, 0x6A, 0x14, 0x8B, 0x14, 0xAD, 0x14, 0xCE, 0x14, 0xF0, 0x15, 0x12, 0x15, 0x34, 0x15, 0x56, 0x15, 0x78, 0x15, 0x9B, 0x15, 0xBD, 0x15, 0xE0, 0x16, 0x03, 0x16, 0x26, 0x16, 0x49, 0x16, 0x6C, 0x16, 0x8F, 0x16, 0xB2, 0x16, 0xD6, 0x16, 0xFA, 0x17, 0x1D, 0x17, 0x41, 0x17, 0x65, 0x17, 0x89, 0x17, 0xAE, 0x17, 0xD2, 0x17, 0xF7, 0x18, 0x1B, 0x18, 0x40, 0x18, 0x65, 0x18, 0x8A, 0x18, 0xAF, 0x18, 0xD5, 0x18, 0xFA, 0x19, 0x20, 0x19, 0x45, 0x19, 0x6B, 0x19, 0x91, 0x19, 0xB7, 0x19, 0xDD, 0x1A, 0x04, 0x1A, 0x2A, 0x1A, 0x51, 0x1A, 0x77, 0x1A, 0x9E, 0x1A, 0xC5, 0x1A, 0xEC, 0x1B, 0x14, 0x1B, 0x3B, 0x1B, 0x63, 0x1B, 0x8A, 0x1B, 0xB2, 0x1B, 0xDA, 0x1C, 0x02, 0x1C, 0x2A, 0x1C, 0x52, 0x1C, 0x7B, 0x1C, 0xA3, 0x1C, 0xCC, 0x1C, 0xF5, 0x1D, 0x1E, 0x1D, 0x47, 0x1D, 0x70, 0x1D, 0x99, 0x1D, 0xC3, 0x1D, 0xEC, 0x1E, 0x16, 0x1E, 0x40, 0x1E, 0x6A, 0x1E, 0x94, 0x1E, 0xBE, 0x1E, 0xE9, 0x1F, 0x13, 0x1F, 0x3E, 0x1F, 0x69, 0x1F, 0x94, 0x1F, 0xBF, 0x1F, 0xEA, 0x20, 0x15, 0x20, 0x41, 0x20, 0x6C, 0x20, 0x98, 0x20, 0xC4, 0x20, 0xF0, 0x21, 0x1C, 0x21, 0x48, 0x21, 0x75, 0x21, 0xA1, 0x21, 0xCE, 0x21, 0xFB, 0x22, 0x27, 0x22, 0x55, 0x22, 0x82, 0x22, 0xAF, 0x22, 0xDD, 0x23, 0x0A, 0x23, 0x38, 0x23, 0x66, 0x23, 0x94, 0x23, 0xC2, 0x23, 0xF0, 0x24, 0x1F, 0x24, 0x4D, 0x24, 0x7C, 0x24, 0xAB, 0x24, 0xDA, 0x25, 0x09, 0x25, 0x38, 0x25, 0x68, 0x25, 0x97, 0x25, 0xC7, 0x25, 0xF7, 0x26, 0x27, 0x26, 0x57, 0x26, 0x87, 0x26, 0xB7, 0x26, 0xE8, 0x27, 0x18, 0x27, 0x49, 0x27, 0x7A, 0x27, 0xAB, 0x27, 0xDC, 0x28, 0x0D, 0x28, 0x3F, 0x28, 0x71, 0x28, 0xA2, 0x28, 0xD4, 0x29, 0x06, 0x29, 0x38, 0x29, 0x6B, 0x29, 0x9D, 0x29, 0xD0, 0x2A, 0x02, 0x2A, 0x35, 0x2A, 0x68, 0x2A, 0x9B, 0x2A, 0xCF, 0x2B, 0x02, 0x2B, 0x36, 0x2B, 0x69, 0x2B, 0x9D, 0x2B, 0xD1, 0x2C, 0x05, 0x2C, 0x39, 0x2C, 0x6E, 0x2C, 0xA2, 0x2C, 0xD7, 0x2D, 0x0C, 0x2D, 0x41, 0x2D, 0x76, 0x2D, 0xAB, 0x2D, 0xE1, 0x2E, 0x16, 0x2E, 0x4C, 0x2E, 0x82, 0x2E, 0xB7, 0x2E, 0xEE, 0x2F, 0x24, 0x2F, 0x5A, 0x2F, 0x91, 0x2F, 0xC7, 0x2F, 0xFE, 0x30, 0x35, 0x30, 0x6C, 0x30, 0xA4, 0x30, 0xDB, 0x31, 0x12, 0x31, 0x4A, 0x31, 0x82, 0x31, 0xBA, 0x31, 0xF2, 0x32, 0x2A, 0x32, 0x63, 0x32, 0x9B, 0x32, 0xD4, 0x33, 0x0D, 0x33, 0x46, 0x33, 0x7F, 0x33, 0xB8, 0x33, 0xF1, 0x34, 0x2B, 0x34, 0x65, 0x34, 0x9E, 0x34, 0xD8, 0x35, 0x13, 0x35, 0x4D, 0x35, 0x87, 0x35, 0xC2, 0x35, 0xFD, 0x36, 0x37, 0x36, 0x72, 0x36, 0xAE, 0x36, 0xE9, 0x37, 0x24, 0x37, 0x60, 0x37, 0x9C, 0x37, 0xD7, 0x38, 0x14, 0x38, 0x50, 0x38, 0x8C, 0x38, 0xC8, 0x39, 0x05, 0x39, 0x42, 0x39, 0x7F, 0x39, 0xBC, 0x39, 0xF9, 0x3A, 0x36, 0x3A, 0x74, 0x3A, 0xB2, 0x3A, 0xEF, 0x3B, 0x2D, 0x3B, 0x6B, 0x3B, 0xAA, 0x3B, 0xE8, 0x3C, 0x27, 0x3C, 0x65, 0x3C, 0xA4, 0x3C, 0xE3, 0x3D, 0x22, 0x3D, 0x61, 0x3D, 0xA1, 0x3D, 0xE0, 0x3E, 0x20, 0x3E, 0x60, 0x3E, 0xA0, 0x3E, 0xE0, 0x3F, 0x21, 0x3F, 0x61, 0x3F, 0xA2, 0x3F, 0xE2, 0x40, 0x23, 0x40, 0x64, 0x40, 0xA6, 0x40, 0xE7, 0x41, 0x29, 0x41, 0x6A, 0x41, 0xAC, 0x41, 0xEE, 0x42, 0x30, 0x42, 0x72, 0x42, 0xB5, 0x42, 0xF7, 0x43, 0x3A, 0x43, 0x7D, 0x43, 0xC0, 0x44, 0x03, 0x44, 0x47, 0x44, 0x8A, 0x44, 0xCE, 0x45, 0x12, 0x45, 0x55, 0x45, 0x9A, 0x45, 0xDE, 0x46, 0x22, 0x46, 0x67, 0x46, 0xAB, 0x46, 0xF0, 0x47, 0x35, 0x47, 0x7B, 0x47, 0xC0, 0x48, 0x05, 0x48, 0x4B, 0x48, 0x91, 0x48, 0xD7, 0x49, 0x1D, 0x49, 0x63, 0x49, 0xA9, 0x49, 0xF0, 0x4A, 0x37, 0x4A, 0x7D, 0x4A, 0xC4, 0x4B, 0x0C, 0x4B, 0x53, 0x4B, 0x9A, 0x4B, 0xE2, 0x4C, 0x2A, 0x4C, 0x72, 0x4C, 0xBA, 0x4D, 0x02, 0x4D, 0x4A, 0x4D, 0x93, 0x4D, 0xDC, 0x4E, 0x25, 0x4E, 0x6E, 0x4E, 0xB7, 0x4F, 0x00, 0x4F, 0x49, 0x4F, 0x93, 0x4F, 0xDD, 0x50, 0x27, 0x50, 0x71, 0x50, 0xBB, 0x51, 0x06, 0x51, 0x50, 0x51, 0x9B, 0x51, 0xE6, 0x52, 0x31, 0x52, 0x7C, 0x52, 0xC7, 0x53, 0x13, 0x53, 0x5F, 0x53, 0xAA, 0x53, 0xF6, 0x54, 0x42, 0x54, 0x8F, 0x54, 0xDB, 0x55, 0x28, 0x55, 0x75, 0x55, 0xC2, 0x56, 0x0F, 0x56, 0x5C, 0x56, 0xA9, 0x56, 0xF7, 0x57, 0x44, 0x57, 0x92, 0x57, 0xE0, 0x58, 0x2F, 0x58, 0x7D, 0x58, 0xCB, 0x59, 0x1A, 0x59, 0x69, 0x59, 0xB8, 0x5A, 0x07, 0x5A, 0x56, 0x5A, 0xA6, 0x5A, 0xF5, 0x5B, 0x45, 0x5B, 0x95, 0x5B, 0xE5, 0x5C, 0x35, 0x5C, 0x86, 0x5C, 0xD6, 0x5D, 0x27, 0x5D, 0x78, 0x5D, 0xC9, 0x5E, 0x1A, 0x5E, 0x6C, 0x5E, 0xBD, 0x5F, 0x0F, 0x5F, 0x61, 0x5F, 0xB3, 0x60, 0x05, 0x60, 0x57, 0x60, 0xAA, 0x60, 0xFC, 0x61, 0x4F, 0x61, 0xA2, 0x61, 0xF5, 0x62, 0x49, 0x62, 0x9C, 0x62, 0xF0, 0x63, 0x43, 0x63, 0x97, 0x63, 0xEB, 0x64, 0x40, 0x64, 0x94, 0x64, 0xE9, 0x65, 0x3D, 0x65, 0x92, 0x65, 0xE7, 0x66, 0x3D, 0x66, 0x92, 0x66, 0xE8, 0x67, 0x3D, 0x67, 0x93, 0x67, 0xE9, 0x68, 0x3F, 0x68, 0x96, 0x68, 0xEC, 0x69, 0x43, 0x69, 0x9A, 0x69, 0xF1, 0x6A, 0x48, 0x6A, 0x9F, 0x6A, 0xF7, 0x6B, 0x4F, 0x6B, 0xA7, 0x6B, 0xFF, 0x6C, 0x57, 0x6C, 0xAF, 0x6D, 0x08, 0x6D, 0x60, 0x6D, 0xB9, 0x6E, 0x12, 0x6E, 0x6B, 0x6E, 0xC4, 0x6F, 0x1E, 0x6F, 0x78, 0x6F, 0xD1, 0x70, 0x2B, 0x70, 0x86, 0x70, 0xE0, 0x71, 0x3A, 0x71, 0x95, 0x71, 0xF0, 0x72, 0x4B, 0x72, 0xA6, 0x73, 0x01, 0x73, 0x5D, 0x73, 0xB8, 0x74, 0x14, 0x74, 0x70, 0x74, 0xCC, 0x75, 0x28, 0x75, 0x85, 0x75, 0xE1, 0x76, 0x3E, 0x76, 0x9B, 0x76, 0xF8, 0x77, 0x56, 0x77, 0xB3, 0x78, 0x11, 0x78, 0x6E, 0x78, 0xCC, 0x79, 0x2A, 0x79, 0x89, 0x79, 0xE7, 0x7A, 0x46, 0x7A, 0xA5, 0x7B, 0x04, 0x7B, 0x63, 0x7B, 0xC2, 0x7C, 0x21, 0x7C, 0x81, 0x7C, 0xE1, 0x7D, 0x41, 0x7D, 0xA1, 0x7E, 0x01, 0x7E, 0x62, 0x7E, 0xC2, 0x7F, 0x23, 0x7F, 0x84, 0x7F, 0xE5, 0x80, 0x47, 0x80, 0xA8, 0x81, 0x0A, 0x81, 0x6B, 0x81, 0xCD, 0x82, 0x30, 0x82, 0x92, 0x82, 0xF4, 0x83, 0x57, 0x83, 0xBA, 0x84, 0x1D, 0x84, 0x80, 0x84, 0xE3, 0x85, 0x47, 0x85, 0xAB, 0x86, 0x0E, 0x86, 0x72, 0x86, 0xD7, 0x87, 0x3B, 0x87, 0x9F, 0x88, 0x04, 0x88, 0x69, 0x88, 0xCE, 0x89, 0x33, 0x89, 0x99, 0x89, 0xFE, 0x8A, 0x64, 0x8A, 0xCA, 0x8B, 0x30, 0x8B, 0x96, 0x8B, 0xFC, 0x8C, 0x63, 0x8C, 0xCA, 0x8D, 0x31, 0x8D, 0x98, 0x8D, 0xFF, 0x8E, 0x66, 0x8E, 0xCE, 0x8F, 0x36, 0x8F, 0x9E, 0x90, 0x06, 0x90, 0x6E, 0x90, 0xD6, 0x91, 0x3F, 0x91, 0xA8, 0x92, 0x11, 0x92, 0x7A, 0x92, 0xE3, 0x93, 0x4D, 0x93, 0xB6, 0x94, 0x20, 0x94, 0x8A, 0x94, 0xF4, 0x95, 0x5F, 0x95, 0xC9, 0x96, 0x34, 0x96, 0x9F, 0x97, 0x0A, 0x97, 0x75, 0x97, 0xE0, 0x98, 0x4C, 0x98, 0xB8, 0x99, 0x24, 0x99, 0x90, 0x99, 0xFC, 0x9A, 0x68, 0x9A, 0xD5, 0x9B, 0x42, 0x9B, 0xAF, 0x9C, 0x1C, 0x9C, 0x89, 0x9C, 0xF7, 0x9D, 0x64, 0x9D, 0xD2, 0x9E, 0x40, 0x9E, 0xAE, 0x9F, 0x1D, 0x9F, 0x8B, 0x9F, 0xFA, 0xA0, 0x69, 0xA0, 0xD8, 0xA1, 0x47, 0xA1, 0xB6, 0xA2, 0x26, 0xA2, 0x96, 0xA3, 0x06, 0xA3, 0x76, 0xA3, 0xE6, 0xA4, 0x56, 0xA4, 0xC7, 0xA5, 0x38, 0xA5, 0xA9, 0xA6, 0x1A, 0xA6, 0x8B, 0xA6, 0xFD, 0xA7, 0x6E, 0xA7, 0xE0, 0xA8, 0x52, 0xA8, 0xC4, 0xA9, 0x37, 0xA9, 0xA9, 0xAA, 0x1C, 0xAA, 0x8F, 0xAB, 0x02, 0xAB, 0x75, 0xAB, 0xE9, 0xAC, 0x5C, 0xAC, 0xD0, 0xAD, 0x44, 0xAD, 0xB8, 0xAE, 0x2D, 0xAE, 0xA1, 0xAF, 0x16, 0xAF, 0x8B, 0xB0, 0x00, 0xB0, 0x75, 0xB0, 0xEA, 0xB1, 0x60, 0xB1, 0xD6, 0xB2, 0x4B, 0xB2, 0xC2, 0xB3, 0x38, 0xB3, 0xAE, 0xB4, 0x25, 0xB4, 0x9C, 0xB5, 0x13, 0xB5, 0x8A, 0xB6, 0x01, 0xB6, 0x79, 0xB6, 0xF0, 0xB7, 0x68, 0xB7, 0xE0, 0xB8, 0x59, 0xB8, 0xD1, 0xB9, 0x4A, 0xB9, 0xC2, 0xBA, 0x3B, 0xBA, 0xB5, 0xBB, 0x2E, 0xBB, 0xA7, 0xBC, 0x21, 0xBC, 0x9B, 0xBD, 0x15, 0xBD, 0x8F, 0xBE, 0x0A, 0xBE, 0x84, 0xBE, 0xFF, 0xBF, 0x7A, 0xBF, 0xF5, 0xC0, 0x70, 0xC0, 0xEC, 0xC1, 0x67, 0xC1, 0xE3, 0xC2, 0x5F, 0xC2, 0xDB, 0xC3, 0x58, 0xC3, 0xD4, 0xC4, 0x51, 0xC4, 0xCE, 0xC5, 0x4B, 0xC5, 0xC8, 0xC6, 0x46, 0xC6, 0xC3, 0xC7, 0x41, 0xC7, 0xBF, 0xC8, 0x3D, 0xC8, 0xBC, 0xC9, 0x3A, 0xC9, 0xB9, 0xCA, 0x38, 0xCA, 0xB7, 0xCB, 0x36, 0xCB, 0xB6, 0xCC, 0x35, 0xCC, 0xB5, 0xCD, 0x35, 0xCD, 0xB5, 0xCE, 0x36, 0xCE, 0xB6, 0xCF, 0x37, 0xCF, 0xB8, 0xD0, 0x39, 0xD0, 0xBA, 0xD1, 0x3C, 0xD1, 0xBE, 0xD2, 0x3F, 0xD2, 0xC1, 0xD3, 0x44, 0xD3, 0xC6, 0xD4, 0x49, 0xD4, 0xCB, 0xD5, 0x4E, 0xD5, 0xD1, 0xD6, 0x55, 0xD6, 0xD8, 0xD7, 0x5C, 0xD7, 0xE0, 0xD8, 0x64, 0xD8, 0xE8, 0xD9, 0x6C, 0xD9, 0xF1, 0xDA, 0x76, 0xDA, 0xFB, 0xDB, 0x80, 0xDC, 0x05, 0xDC, 0x8A, 0xDD, 0x10, 0xDD, 0x96, 0xDE, 0x1C, 0xDE, 0xA2, 0xDF, 0x29, 0xDF, 0xAF, 0xE0, 0x36, 0xE0, 0xBD, 0xE1, 0x44, 0xE1, 0xCC, 0xE2, 0x53, 0xE2, 0xDB, 0xE3, 0x63, 0xE3, 0xEB, 0xE4, 0x73, 0xE4, 0xFC, 0xE5, 0x84, 0xE6, 0x0D, 0xE6, 0x96, 0xE7, 0x1F, 0xE7, 0xA9, 0xE8, 0x32, 0xE8, 0xBC, 0xE9, 0x46, 0xE9, 0xD0, 0xEA, 0x5B, 0xEA, 0xE5, 0xEB, 0x70, 0xEB, 0xFB, 0xEC, 0x86, 0xED, 0x11, 0xED, 0x9C, 0xEE, 0x28, 0xEE, 0xB4, 0xEF, 0x40, 0xEF, 0xCC, 0xF0, 0x58, 0xF0, 0xE5, 0xF1, 0x72, 0xF1, 0xFF, 0xF2, 0x8C, 0xF3, 0x19, 0xF3, 0xA7, 0xF4, 0x34, 0xF4, 0xC2, 0xF5, 0x50, 0xF5, 0xDE, 0xF6, 0x6D, 0xF6, 0xFB, 0xF7, 0x8A, 0xF8, 0x19, 0xF8, 0xA8, 0xF9, 0x38, 0xF9, 0xC7, 0xFA, 0x57, 0xFA, 0xE7, 0xFB, 0x77, 0xFC, 0x07, 0xFC, 0x98, 0xFD, 0x29, 0xFD, 0xBA, 0xFE, 0x4B, 0xFE, 0xDC, 0xFF, 0x6D, 0xFF, 0xFF +``` + diff --git a/doc/tables/is_zero_base.md b/doc/tables/is_zero_base.md new file mode 100644 index 0000000..7e2d081 --- /dev/null +++ b/doc/tables/is_zero_base.md @@ -0,0 +1,9 @@ +#### Table M.1 – is_zero_base table + +``` +228, 216, 216, 195, 192, 189, 182, 184, 179, 176, 171, 168, 166, 159, +156, 151, 151, 150, 150, 146, 144, 138, 138, 137, 135, 131, 127, 126, +124, 123, 124, 123, 122, 121, 118, 117, 114, 115, 116, 116, 115, 115, +114, 111, 111, 111, 112, 111, 110, 110, 110, 111, 111, 114, 110, 111, +112, 113, 116, 120, 126, 131, 147, 160 +``` diff --git a/doc/tables/markdown-pdf.css b/doc/tables/markdown-pdf.css new file mode 100644 index 0000000..c1efc1c --- /dev/null +++ b/doc/tables/markdown-pdf.css @@ -0,0 +1,36 @@ +/* + settings.json: + "markdown-pdf.styles": ["markdown-pdf.css",], + "markdown-pdf.format": "Letter", + "markdown-pdf.margin.top": "1in", + "markdown-pdf.margin.bottom": "1in", + "markdown-pdf.margin.left": "1in", + "markdown-pdf.margin.right": "1in", + "markdown-pdf.stylesRelativePathFile" : true, + "markdown-pdf.displayHeaderFooter": false, + */ + +body { + font-family: "Times"; + font-size: 10pt; + padding: 0; +} + +h4 { + font-family: "Times New Roman"; + font-size: 10pt; + font-weight: bold; +} + +code { + font-family: Consolas, "Source Code Pro"; + font-size: 10pt; +} + +pre.hljs code > div { + padding: 0px; +} + +:not(pre):not(.hljs) > code { + color: #4d4d4c; +} diff --git a/doc/tables/nonzero_buckets.md b/doc/tables/nonzero_buckets.md new file mode 100644 index 0000000..77a5a39 --- /dev/null +++ b/doc/tables/nonzero_buckets.md @@ -0,0 +1,9 @@ +#### Table M.17 – nonzero_buckets + +``` + 0, 1, 2, 3, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, + 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 +``` + diff --git a/doc/tables/num_nonzero_context.md b/doc/tables/num_nonzero_context.md new file mode 100644 index 0000000..b73d48c --- /dev/null +++ b/doc/tables/num_nonzero_context.md @@ -0,0 +1,60 @@ +#### Table M.16 – num_nonzero_context + +`scheme == 0`: +``` +0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, +6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, +7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 +``` + +`scheme == 1`: +``` + 0, 2, 2, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, +10, 10, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, +12, 12, 12, 12, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, +14, 14, 14, 14, 14, 14, 14, 14, 14, 14 +``` + +`scheme == 2`: +``` + 0, 4, 4, 8, 8, 8, 12, 12, 12, 12, 16, 16, 16, 16, 16, 16, 20, 20, +20, 20, 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, +24, 24, 24, 24, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, +28, 28, 28, 28, 28, 28, 28, 28, 28, 28 +``` + +`scheme == 3`: +``` + 0, 8, 8, 16, 16, 16, 24, 24, 24, 24, 32, 32, 32, 32, 32, 32, 40, 40, +40, 40, 40, 40, 40, 40, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, +48, 48, 48, 48, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, +55, 55, 55, 55, 55, 55, 55, 55, 55, 55 +``` + +`scheme == 4`: +``` + 0, 16, 16, 32, 32, 32, 48, 48, 48, 48, 64, 64, 64, 64, + 64, 64, 80, 80, 80, 80, 80, 80, 80, 80, 95, 95, 95, 95, + 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 109, 109, +109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, +109, 109, 109, 109, 109, 109, 109, 109 +``` + +`scheme == 5`: +``` + 0, 32, 32, 64, 64, 64, 96, 96, 96, 96, 127, 127, 127, 127, +127, 127, 157, 157, 157, 157, 157, 157, 157, 157, 185, 185, 185, 185, +185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 211, 211, +211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, +211, 211, 211, 211, 211, 211, 211, 211 +``` + +`scheme == 6`: +``` + 0, 64, 64, 127, 127, 127, 188, 188, 188, 188, 246, 246, 246, 246, +246, 246, 300, 300, 300, 300, 300, 300, 300, 300, 348, 348, 348, 348, +348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 388, 388, +388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, +388, 388, 388, 388, 388, 388, 388, 388 +``` + diff --git a/doc/tables/num_nonzeros_base.md b/doc/tables/num_nonzeros_base.md new file mode 100644 index 0000000..165c738 --- /dev/null +++ b/doc/tables/num_nonzeros_base.md @@ -0,0 +1,258 @@ +#### Table M.2 – num_nonzeros_base table + +``` +251, 252, 117, 249, 161, 136, 83, 238, 184, 126, 137, 129, 140, 119, + 70, 213, 160, 175, 174, 130, 166, 134, 122, 125, 131, 144, 136, 133, +139, 123, 79, 216, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +254, 252, 174, 232, 189, 155, 122, 177, 204, 173, 146, 149, 141, 133, +103, 109, 167, 187, 168, 142, 154, 147, 125, 139, 144, 138, 138, 153, +141, 133, 90, 121, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +251, 240, 197, 176, 184, 177, 114, 89, 194, 165, 153, 161, 158, 136, + 92, 95, 123, 171, 160, 140, 148, 136, 129, 139, 145, 136, 143, 134, +138, 124, 92, 154, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +247, 220, 201, 110, 194, 176, 147, 59, 175, 171, 156, 157, 152, 146, +115, 114, 88, 151, 164, 141, 153, 135, 141, 131, 146, 139, 140, 145, +138, 137, 112, 184, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +238, 179, 203, 63, 194, 173, 149, 71, 139, 169, 154, 159, 150, 146, +117, 143, 78, 122, 152, 137, 149, 138, 138, 133, 134, 142, 142, 142, +148, 128, 118, 199, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +227, 127, 200, 44, 192, 170, 148, 100, 102, 161, 156, 153, 148, 149, +124, 160, 88, 101, 134, 132, 149, 145, 134, 134, 136, 141, 138, 142, +144, 137, 116, 208, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +214, 86, 195, 44, 187, 163, 148, 126, 81, 147, 156, 152, 150, 144, +121, 172, 96, 95, 117, 122, 145, 152, 136, 133, 135, 135, 131, 142, +141, 135, 114, 217, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +198, 56, 191, 54, 171, 162, 147, 144, 74, 128, 152, 149, 150, 142, +119, 177, 101, 100, 106, 111, 135, 154, 136, 137, 136, 132, 133, 142, +144, 130, 117, 222, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +176, 40, 189, 73, 147, 159, 148, 152, 79, 106, 147, 149, 151, 139, +123, 188, 108, 110, 106, 97, 125, 151, 137, 138, 135, 135, 134, 136, +140, 131, 116, 221, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +148, 33, 185, 88, 117, 158, 145, 163, 95, 91, 137, 146, 150, 140, +120, 197, 115, 116, 114, 92, 114, 144, 130, 133, 132, 133, 129, 140, +138, 130, 111, 224, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +117, 31, 180, 104, 93, 150, 143, 166, 99, 85, 124, 139, 148, 142, +118, 201, 105, 120, 120, 90, 107, 135, 127, 130, 131, 131, 132, 140, +142, 133, 114, 229, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 87, 35, 170, 110, 78, 141, 144, 176, 106, 90, 112, 132, 143, 138, +119, 204, 111, 121, 125, 90, 105, 131, 124, 122, 129, 128, 129, 137, +138, 133, 114, 227, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 63, 42, 159, 123, 73, 127, 142, 191, 105, 91, 105, 123, 139, 137, +120, 209, 117, 110, 122, 98, 110, 125, 115, 123, 122, 126, 128, 134, +141, 129, 113, 229, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 45, 53, 146, 135, 71, 114, 138, 193, 100, 98, 98, 113, 133, 135, +118, 222, 113, 111, 139, 103, 107, 126, 111, 119, 121, 122, 127, 135, +141, 128, 114, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 33, 60, 132, 138, 75, 100, 134, 203, 112, 99, 98, 105, 126, 131, +115, 229, 107, 93, 121, 106, 108, 122, 106, 109, 114, 116, 127, 133, +143, 128, 110, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 24, 70, 118, 134, 76, 87, 130, 201, 110, 96, 99, 97, 119, 130, +111, 229, 97, 104, 125, 102, 112, 125, 101, 109, 113, 114, 125, 129, +142, 127, 112, 241, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 17, 65, 100, 121, 80, 75, 124, 174, 117, 100, 94, 93, 114, 128, +110, 216, 103, 94, 113, 122, 118, 126, 113, 108, 105, 108, 122, 128, +141, 125, 113, 238, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 12, 70, 82, 132, 78, 65, 118, 155, 136, 103, 97, 89, 106, 124, +111, 215, 115, 123, 129, 99, 104, 127, 110, 108, 101, 109, 118, 126, +136, 123, 110, 233, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 8, 66, 61, 117, 91, 59, 108, 195, 101, 112, 99, 99, 99, 116, +106, 230, 127, 99, 144, 101, 118, 137, 117, 111, 106, 104, 116, 121, +134, 122, 110, 223, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 6, 78, 42, 146, 101, 54, 94, 201, 116, 102, 110, 94, 92, 108, +103, 214, 108, 111, 127, 102, 121, 132, 120, 121, 95, 98, 110, 121, +129, 117, 107, 235, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 5, 93, 29, 145, 102, 52, 77, 216, 108, 115, 108, 102, 89, 97, + 94, 229, 89, 103, 139, 120, 103, 151, 102, 100, 97, 96, 99, 111, +125, 116, 104, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 4, 105, 21, 145, 100, 54, 64, 217, 100, 122, 128, 87, 88, 91, + 87, 230, 112, 80, 148, 95, 146, 123, 96, 140, 90, 91, 98, 106, +122, 111, 100, 249, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 4, 130, 14, 142, 104, 56, 51, 208, 116, 135, 100, 89, 82, 84, + 75, 239, 85, 85, 122, 125, 94, 144, 151, 136, 92, 97, 104, 109, +113, 110, 91, 246, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 126, 9, 172, 105, 57, 39, 219, 95, 120, 118, 96, 93, 75, + 66, 241, 102, 134, 96, 156, 146, 162, 130, 112, 82, 89, 97, 101, +116, 103, 82, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 149, 7, 182, 122, 54, 29, 224, 103, 100, 113, 96, 90, 74, + 55, 250, 127, 94, 118, 93, 135, 160, 113, 130, 95, 117, 106, 96, +111, 97, 77, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 150, 4, 170, 138, 59, 20, 229, 91, 150, 107, 98, 92, 68, + 48, 245, 113, 64, 114, 111, 134, 127, 102, 104, 85, 118, 103, 107, +102, 91, 72, 245, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 171, 3, 165, 137, 62, 14, 211, 96, 127, 132, 121, 95, 62, + 37, 248, 102, 57, 144, 85, 127, 191, 102, 97, 127, 104, 91, 102, +107, 81, 64, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 166, 2, 196, 122, 65, 10, 243, 102, 93, 117, 92, 96, 63, + 29, 251, 169, 159, 149, 96, 91, 139, 157, 40, 100, 89, 120, 92, +109, 79, 58, 247, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 176, 2, 189, 118, 48, 7, 219, 68, 43, 109, 96, 129, 75, + 19, 254, 2, 3, 185, 6, 102, 127, 127, 127, 1, 131, 83, 99, +107, 80, 45, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 1, 205, 2, 208, 64, 89, 4, 223, 29, 169, 29, 123, 118, 76, + 11, 240, 202, 243, 65, 6, 12, 243, 96, 55, 102, 102, 114, 102, +107, 74, 31, 247, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 1, 216, 1, 214, 127, 94, 2, 234, 145, 3, 127, 106, 155, 80, + 4, 247, 4, 65, 86, 127, 127, 127, 127, 102, 127, 143, 143, 108, +113, 80, 16, 216, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 199, 1, 222, 93, 94, 1, 232, 2, 65, 74, 139, 201, 48, + 2, 254, 169, 127, 52, 243, 251, 249, 102, 86, 202, 153, 65, 65, +146, 69, 8, 238, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + diff --git a/doc/tables/quant.md b/doc/tables/quant.md new file mode 100644 index 0000000..1fb80d7 --- /dev/null +++ b/doc/tables/quant.md @@ -0,0 +1,19 @@ +#### Table M.13 – template quant tables + +`is_luma == true`: +``` + 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, + 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, + 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, +104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, +112, 100, 103, 99 +``` + +`is_luma == false`: +``` +17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, +56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, +99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, +99, 99, 99, 99, 99, 99, 99, 99, 99, 99 +``` + diff --git a/doc/tables/stock_counts.md b/doc/tables/stock_counts.md new file mode 100644 index 0000000..6e8e445 --- /dev/null +++ b/doc/tables/stock_counts.md @@ -0,0 +1,22 @@ +#### Table M.9 – stock counts arrays + +`is_ac == 0`, `stock_index == 0`: +``` +0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0 +``` + +`is_ac == 0`, `stock_index == 1`: +``` +0, 0, 1, 5, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0 +``` + +`is_ac == 1`, `stock_index == 0`: +``` +0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 126 +``` + +`is_ac == 1`, `stock_index == 1`: +``` +0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 120 +``` + diff --git a/doc/tables/stock_quant.md b/doc/tables/stock_quant.md new file mode 100644 index 0000000..b32fd3c --- /dev/null +++ b/doc/tables/stock_quant.md @@ -0,0 +1,130 @@ +#### Table M.12 – stock quant tables + +`is_luma == true`, `stock_index == 0`: +``` + 3, 2, 2, 3, 5, 8, 10, 12, 2, 2, 3, 4, 5, 12, 12, 11, 3, 3, + 3, 5, 8, 11, 14, 11, 3, 3, 4, 6, 10, 17, 16, 12, 4, 4, 7, 11, +14, 22, 21, 15, 5, 7, 11, 13, 16, 21, 23, 18, 10, 13, 16, 17, 21, 24, +24, 20, 14, 18, 19, 20, 22, 20, 21, 20 +``` + +`is_luma == true`, `stock_index == 1`: +``` + 8, 6, 5, 8, 12, 20, 26, 31, 6, 6, 7, 10, 13, 29, 30, 28, 7, 7, + 8, 12, 20, 29, 35, 28, 7, 9, 11, 15, 26, 44, 40, 31, 9, 11, 19, 28, +34, 55, 52, 39, 12, 18, 28, 32, 41, 52, 57, 46, 25, 32, 39, 44, 52, 61, +60, 51, 36, 46, 48, 49, 56, 50, 52, 50 +``` + +`is_luma == true`, `stock_index == 2`: +``` + 6, 4, 4, 6, 10, 16, 20, 24, 5, 5, 6, 8, 10, 23, 24, 22, 6, 5, + 6, 10, 16, 23, 28, 22, 6, 7, 9, 12, 20, 35, 32, 25, 7, 9, 15, 22, +27, 44, 41, 31, 10, 14, 22, 26, 32, 42, 45, 37, 20, 26, 31, 35, 41, 48, +48, 40, 29, 37, 38, 39, 45, 40, 41, 40 +``` + +`is_luma == true`, `stock_index == 3`: +``` + 5, 3, 3, 5, 7, 12, 15, 18, 4, 4, 4, 6, 8, 17, 18, 17, 4, 4, + 5, 7, 12, 17, 21, 17, 4, 5, 7, 9, 15, 26, 24, 19, 5, 7, 11, 17, +20, 33, 31, 23, 7, 11, 17, 19, 24, 31, 34, 28, 15, 19, 23, 26, 31, 36, +36, 30, 22, 28, 29, 29, 34, 30, 31, 30 +``` + +`is_luma == true`, `stock_index == 4`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +``` + +`is_luma == true`, `stock_index == 5`: +``` + 2, 1, 1, 2, 2, 4, 5, 6, 1, 1, 1, 2, 3, 6, 6, 6, 1, 1, + 2, 2, 4, 6, 7, 6, 1, 2, 2, 3, 5, 9, 8, 6, 2, 2, 4, 6, + 7, 11, 10, 8, 2, 4, 6, 6, 8, 10, 11, 9, 5, 6, 8, 9, 10, 12, +12, 10, 7, 9, 10, 10, 11, 10, 10, 10 +``` + +`is_luma == true`, `stock_index == 6`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, + 1, 2, 2, 3, 1, 1, 1, 1, 2, 2, 3, 3, 1, 1, 1, 2, 2, 3, + 3, 3, 1, 1, 2, 2, 3, 3, 3, 3 +``` + +`is_luma == true`, `stock_index == 7`: +``` +10, 7, 6, 10, 14, 24, 31, 37, 7, 7, 8, 11, 16, 35, 36, 33, 8, 8, +10, 14, 24, 34, 41, 34, 8, 10, 13, 17, 31, 52, 48, 37, 11, 13, 22, 34, +41, 65, 62, 46, 14, 21, 33, 38, 49, 62, 68, 55, 29, 38, 47, 52, 62, 73, +72, 61, 43, 55, 57, 59, 67, 60, 62, 59 +``` + +`is_luma == false`, `stock_index == 0`: +``` + 9, 9, 9, 12, 11, 12, 24, 13, 13, 24, 50, 33, 28, 33, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50 +``` + +`is_luma == false`, `stock_index == 1`: +``` + 3, 4, 5, 9, 20, 20, 20, 20, 4, 4, 5, 13, 20, 20, 20, 20, 5, 5, +11, 20, 20, 20, 20, 20, 9, 13, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, +20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, +20, 20, 20, 20, 20, 20, 20, 20, 20, 20 +``` + +`is_luma == false`, `stock_index == 2`: +``` + 9, 9, 12, 24, 50, 50, 50, 50, 9, 11, 13, 33, 50, 50, 50, 50, 12, 13, +28, 50, 50, 50, 50, 50, 24, 33, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50 +``` + +`is_luma == false`, `stock_index == 3`: +``` + 5, 5, 7, 14, 30, 30, 30, 30, 5, 6, 8, 20, 30, 30, 30, 30, 7, 8, +17, 30, 30, 30, 30, 30, 14, 20, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, +30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, +30, 30, 30, 30, 30, 30, 30, 30, 30, 30 +``` + +`is_luma == false`, `stock_index == 4`: +``` + 7, 7, 10, 19, 40, 40, 40, 40, 7, 8, 10, 26, 40, 40, 40, 40, 10, 10, +22, 40, 40, 40, 40, 40, 19, 26, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, +40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, +40, 40, 40, 40, 40, 40, 40, 40, 40, 40 +``` + +`is_luma == false`, `stock_index == 5`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +``` + +`is_luma == false`, `stock_index == 6`: +``` + 2, 2, 2, 5, 10, 10, 10, 10, 2, 2, 3, 7, 10, 10, 10, 10, 2, 3, + 6, 10, 10, 10, 10, 10, 5, 7, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +10, 10, 10, 10, 10, 10, 10, 10, 10, 10 +``` + +`is_luma == false`, `stock_index == 7`: +``` +10, 11, 14, 28, 59, 59, 59, 59, 11, 13, 16, 40, 59, 59, 59, 59, 14, 16, +34, 59, 59, 59, 59, 59, 28, 40, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, +59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, +59, 59, 59, 59, 59, 59, 59, 59, 59, 59 +``` + diff --git a/doc/tables/stock_values.md b/doc/tables/stock_values.md new file mode 100644 index 0000000..8e67cff --- /dev/null +++ b/doc/tables/stock_values.md @@ -0,0 +1,44 @@ +#### Table M.10 – stock values arrays + +`is_ac == 0`, `stock_index == 0`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 256 +``` + +`is_ac == 0`, `stock_index == 1`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 256 +``` + +`is_ac == 1`, `stock_index == 0`: +``` + 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6, 19, 81, + 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193, + 21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, + 25, 26, 37, 38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, + 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, + 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, +118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, 138, 146, +147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, +168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, +196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, +217, 218, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 241, 242, +243, 244, 245, 246, 247, 248, 249, 250, 256 +``` + +`is_ac == 1`, `stock_index == 1`: +``` + 0, 1, 2, 3, 17, 4, 5, 33, 49, 6, 18, 65, 81, 7, + 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177, 193, 9, + 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37, +241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, + 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, + 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, +117, 118, 119, 120, 121, 122, 130, 131, 132, 133, 134, 135, 136, 137, +138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, +166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, +194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, +215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, +243, 244, 245, 246, 247, 248, 249, 250, 256 +``` + diff --git a/doc/tables/symbol_order.md b/doc/tables/symbol_order.md new file mode 100644 index 0000000..a196c0f --- /dev/null +++ b/doc/tables/symbol_order.md @@ -0,0 +1,30 @@ +#### Table M.11 – predefined symbol order + +`is_ac == 0`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 +``` + +`is_ac == 1`: +``` + 1, 0, 2, 3, 17, 4, 5, 33, 18, 49, 65, 6, 81, 19, + 97, 7, 34, 113, 50, 129, 20, 145, 161, 8, 35, 66, 177, 193, + 21, 82, 209, 240, 36, 51, 98, 114, 9, 130, 10, 22, 52, 225, + 23, 37, 241, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, + 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, + 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, +116, 117, 118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, +138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, +166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, +194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, +215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, +243, 244, 245, 246, 247, 248, 249, 250, 16, 32, 48, 64, 80, 96, +112, 128, 144, 160, 176, 192, 208, 11, 12, 13, 14, 15, 27, 28, + 29, 30, 31, 43, 44, 45, 46, 47, 59, 60, 61, 62, 63, 75, + 76, 77, 78, 79, 91, 92, 93, 94, 95, 107, 108, 109, 110, 111, +123, 124, 125, 126, 127, 139, 140, 141, 142, 143, 155, 156, 157, 158, +159, 171, 172, 173, 174, 175, 187, 188, 189, 190, 191, 203, 204, 205, +206, 207, 219, 220, 221, 222, 223, 224, 235, 236, 237, 238, 239, 251, +252, 253, 254, 255 +``` + diff --git a/doc/xl_overview.md b/doc/xl_overview.md new file mode 100644 index 0000000..cfcfcb5 --- /dev/null +++ b/doc/xl_overview.md @@ -0,0 +1,181 @@ +# XL Overview + +## Requirements + +JPEG XL was designed for two main requirements: + +* high quality: visually lossless at reasonable bitrates; +* decoding speed: multithreaded decoding should be able to reach around + 400 Megapixel/s on large images. + +These goals apply to various types of images, including HDR content, whose +support is made possible by full-precision (float32) computations and extensive +support of color spaces and transfer functions. + +High performance is achieved by designing the format with careful consideration +of memory bandwidth usage and ease of SIMD/GPU implementation. + +The full requirements for JPEG XL are listed in document wg1m82079. + +## General architecture + +The architecture follows the traditional block transform model with improvements +in the individual components. For a quick overview, we sketch a "block diagram" +of the lossy format decoder in the form of module names in **bold** followed by +a brief description. Note that post-processing modules in [brackets] are +optional - they are unnecessary or even counterproductive at very high quality +settings. + +**Header**: decode metadata (e.g. image dimensions) from compressed fields +(smaller than Exp-Golomb thanks to per-field encodings). The compression and +small number of required fields enables very compact headers - much smaller than +JFIF and HEVC. The container supports multiple images (e.g. animations/bursts) +and passes (progressive). + +**Bitstream**: decode transform coefficient residuals using rANS-encoded +<#bits,bits> symbols + +**Dequantize**: from adaptive quant map side information, plus chroma from luma + +**DC prediction**: expand DC residuals using adaptive (history-based) predictors + +**Chroma from luma**: restore predicted X from B and Y from B + +**IDCT:** 2x2..32x32, floating-point + +**[Gaborish]**: additional deblocking convolution with 3x3 kernel + +**[Edge preserving filter]**: nonlinear adaptive smoothing controlled by side +information + +**[Noise injection]**: add perceptually pleasing noise according to a per-image +noise model + +**Color space conversion**: from perceptual opsin XYB to linear RGB + +**[Converting to other color spaces via ICC]** + +The encoder is basically the reverse: + +**Color space conversion**: from linear RGB to perceptual opsin XYB + +**[Noise estimation]**: compute a noise model for the image + +**[Gaborish]**: sharpening to counteract the blurring on the decoder side + +**DCT**: transform sizes communicated via per-block side information + +**Chroma from luma**: find the best multipliers of Y for X and B channels of +entire image + +**Adaptive quantization**: iterative search for quant map that yields the best +perceived restoration + +**Quantize**: store 16-bit prediction residuals + +**DC prediction**: store residuals (prediction happens in quantized space) + +**Entropy coding**: rANS and context modeling with clustering + + +# File Structure + +A codestream begins with a `FileHeader` followed by one or more "passes" +(= scans: e.g. DC or AC_LF) which are then added together (summing the +respective color components in Opsin space) to form the final image. There is no +limit to the number of passes, so an encoder could choose to send salient parts +first, followed by arbitrary decompositions of the final image (in terms of +resolution, bit depth, quality or spatial location). + +Each pass contains groups of AC and DC data. A group is a subset of pixels that +can be decoded in parallel. DC groups contain 256x256 DCs (from 2048x2048 input +pixels), AC groups cover 256x256 input pixels. + +Each pass starts with a table of contents (sizes of each of their DC+AC +groups), which enables parallel decoding and/or the decoding of a subset. +However, there is no higher-level TOC of passes, as that would prevent +appending additional images and could be too constraining for the encoder. + + +## Lossless + +JPEG XL supports tools for lossless coding designed by Alexander Rhatushnyak and +Jon Sneyers. They are about 60-75% of size of PNG, and smaller than WebP +lossless for photos. + +An adaptive predictor computes 4 from the NW, N, NE and W pixels and combines +them with weights based on previous errors. The error value is encoded in a +bucket chosen based on a heuristic max error. The result is entropy-coded using +the ANS encoder. + +## Current Reference Implementation + +### Conventions + +The software is written in C++ and built using CMake 3.6 or later. + +Error handling is done by having functions return values of type `jxl::Status` +(a thin wrapper around bool which checks that it is not ignored). A convenience +macro named `JXL_RETURN_IF_ERROR` makes this more convenient by automatically +forwarding errors, and another macro named `JXL_FAILURE` exits with an error +message if reached, with no effect in optimized builds. + +To diagnose the cause of encoder/decoder failures (which often only result in a +generic "decode failed" message), build using the following command: + +```bash +CMAKE_FLAGS="-DJXL_CRASH_ON_ERROR" ./ci.sh opt +``` + +In such builds, the first JXL_FAILURE will print a message identifying where the +problem is and the program will exit immediately afterwards. + +### Architecture + +Getting back to the earlier block diagram: + +**Header** handling is implemented in `headers.h` and `field*`. + +**Bitstream**: `entropy_coder.h`, `dec_ans_*`. + +**(De)quantize**: `quantizer.h`. + +**DC prediction**: `predictor.h`. + +**Chroma from luma**: `chroma_from_luma.h` + +**(I)DCT**: `dct*.h`. Instead of operating directly on blocks of memory, the +functions operate on thin wrappers which can handle blocks spread across +multiple image lines. + +**DCT size selection**: `ac_strategy.cc` + +**[Gaborish]**: `gaborish.h`. + +**[Edge preserving filter]**: `epf.h` + +**[Noise injection]**: `noise*` (currently disabled) + +**Color space conversion**: `color_*`, `dec_xyb.h`. + +## Decoder overview + +After decoding headers, the decoder begins processing frames (`dec_frame.cc`). + +For each pass, it will read the DC group table of contents (TOC) and start +decoding, dequantizing and restoring color correlation of each DC group +(covering 2048x2048 pixels in the input image) in parallel +(`compressed_dc.cc`). The DC is split into parts corresponding to each AC group +(with 1px of extra border); the AC group TOC is read and each AC group (256x256 +pixels) is processed in parallel (`dec_group.cc`). + +In each AC group, the decoder reads per-block side information indicating the +kind of DCT transform; this is followed by the quantization field. Then, AC +coefficients are read, dequantized and have color correlation restored on a +tile per tile basis for better locality. + +After all the groups are read, postprocessing is applied: Gaborish smoothing +and edge preserving filter, to reduce blocking and other artifacts. + +Finally, the image is converted back from the XYB color space +(`dec_xyb.cc`) and saved to the output image (`codec_*.cc`). diff --git a/docker/Dockerfile.jpegxl-builder b/docker/Dockerfile.jpegxl-builder new file mode 100644 index 0000000..16e0077 --- /dev/null +++ b/docker/Dockerfile.jpegxl-builder @@ -0,0 +1,21 @@ +# 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. + +# Build an Ubuntu-based docker image with the installed software needed to +# develop and test JPEG XL. + +FROM ubuntu:bionic + +# Set a prompt for when using it locally. +ENV PS1="\[\033[01;33m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ " + +COPY scripts/99_norecommends /etc/apt/apt.conf.d/99_norecommends + +COPY scripts /jpegxl_scripts + +ARG DEBIAN_FRONTEND=noninteractive + +RUN /jpegxl_scripts/jpegxl_builder.sh && \ + rm -rf /jpegxl_scripts diff --git a/docker/Dockerfile.jpegxl-builder-run-aarch64 b/docker/Dockerfile.jpegxl-builder-run-aarch64 new file mode 100644 index 0000000..a9f38a4 --- /dev/null +++ b/docker/Dockerfile.jpegxl-builder-run-aarch64 @@ -0,0 +1,37 @@ +# 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. + +# Build an Ubuntu-based docker image for aarch64 with the installed software +# needed to run JPEG XL. This is only useful when running on actual aarch64 +# hardware. + +FROM arm64v8/ubuntu:bionic + +COPY scripts/99_norecommends /etc/apt/apt.conf.d/99_norecommends + +# Set a prompt for when using it locally. +ENV PS1="\[\033[01;33m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ " + +ARG DEBIAN_FRONTEND=noninteractive + +RUN set -ex; \ + apt-get update -y; \ + apt-get install -y \ + bsdmainutils \ + cmake \ + curl \ + ca-certificates \ + extra-cmake-modules \ + git \ + imagemagick \ + libjpeg8 \ + libgif7 \ + libgoogle-perftools4 \ + libopenexr22 \ + libpng16-16 \ + libqt5x11extras5 \ + libsdl2-2.0-0 \ + parallel; \ + rm -rf /var/lib/apt/lists/*; diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..874df1c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,7 @@ +### Docker container infrastructure for JPEG XL + +This directory contains the requirements to build a docker image for the +JPEG XL project builder. + +Docker images need to be created and upload manually. See ./build.sh for +details. diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..3d4727f --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# 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. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +declare -a TARGETS + +load_targets() { + # Built-in OSX "find" does not support "-m". + FIND=$(which "gfind" || which "find") + for f in $(${FIND} -maxdepth 1 -name 'Dockerfile.*' | sort); do + local target="${f#*Dockerfile.}" + TARGETS+=("${target}") + done +} + +usage() { + cat >&2 <&2 + done +} + +build_target() { + local target="$1" + + local dockerfile="${MYDIR}/Dockerfile.${target}" + # JPEG XL builder images are stored in the gcr.io/jpegxl project. + local tag="gcr.io/jpegxl/${target}" + + echo "Building ${target}" + if ! sudo docker build --no-cache -t "${tag}" -f "${dockerfile}" "${MYDIR}" \ + >"${target}.log" 2>&1; then + echo "${target} failed. See ${target}.log" >&2 + else + echo "Done, to upload image run:" >&2 + echo " sudo docker push ${tag}" + if [[ "${JPEGXL_PUSH:-}" == "1" ]]; then + echo "sudo docker push ${tag}" >&2 + sudo docker push "${tag}" + # The RepoDigest is only created after it is pushed. + local fulltag=$(sudo docker inspect --format="{{.RepoDigests}}" "${tag}") + fulltag="${fulltag#[}" + fulltag="${fulltag%]}" + echo "Updating .gitlab-ci.yml to ${fulltag}" >&2 + sed -E "s;${tag}@sha256:[0-9a-f]+;${fulltag};" \ + -i "${MYDIR}/../.gitlab-ci.yml" + fi + fi +} + +main() { + cd "${MYDIR}" + local target="${1:-}" + + load_targets + if [[ -z "${target}" ]]; then + usage $0 + exit 1 + fi + + if [[ "${target}" == "all" ]]; then + for target in "${TARGETS[@]}"; do + build_target "${target}" + done + else + for target in "$@"; do + build_target "${target}" + done + fi +} + +main "$@" diff --git a/docker/scripts/99_norecommends b/docker/scripts/99_norecommends new file mode 100644 index 0000000..96d6728 --- /dev/null +++ b/docker/scripts/99_norecommends @@ -0,0 +1 @@ +APT::Install-Recommends "false"; diff --git a/docker/scripts/binutils_align_fix.patch b/docker/scripts/binutils_align_fix.patch new file mode 100644 index 0000000..6066252 --- /dev/null +++ b/docker/scripts/binutils_align_fix.patch @@ -0,0 +1,28 @@ +Description: fix lack of alignment in relocations (crashes on mingw) +See https://sourceware.org/git/?p=binutils-gdb.git;a=patch;h=73af69e74974eaa155eec89867e3ccc77ab39f6d +From: Marc +Date: Fri, 9 Nov 2018 11:13:50 +0000 +Subject: [PATCH] Allow for compilers that do not produce aligned .rdat + sections in PE format files. + +--- a/upstream/ld/scripttempl/pe.sc 2020-05-12 18:45:12.000000000 +0200 ++++ b/upstream/ld/scripttempl/pe.sc 2020-05-12 18:47:12.000000000 +0200 +@@ -143,6 +143,7 @@ + .rdata ${RELOCATING+BLOCK(__section_alignment__)} : + { + ${R_RDATA} ++ . = ALIGN(4); + ${RELOCATING+__rt_psrelocs_start = .;} + ${RELOCATING+KEEP(*(.rdata_runtime_pseudo_reloc))} + ${RELOCATING+__rt_psrelocs_end = .;} +--- a/upstream/ld/scripttempl/pep.sc 2020-05-12 18:45:19.000000000 +0200 ++++ b/upstream/ld/scripttempl/pep.sc 2020-05-12 18:47:18.000000000 +0200 +@@ -143,6 +143,7 @@ + .rdata ${RELOCATING+BLOCK(__section_alignment__)} : + { + ${R_RDATA} ++ . = ALIGN(4); + ${RELOCATING+__rt_psrelocs_start = .;} + ${RELOCATING+KEEP(*(.rdata_runtime_pseudo_reloc))} + ${RELOCATING+__rt_psrelocs_end = .;} + diff --git a/docker/scripts/emsdk_install.sh b/docker/scripts/emsdk_install.sh new file mode 100755 index 0000000..6cf225a --- /dev/null +++ b/docker/scripts/emsdk_install.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# 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. + +EMSDK_URL="https://github.com/emscripten-core/emsdk/archive/main.tar.gz" +EMSDK_DIR="/opt/emsdk" + +EMSDK_RELEASE="2.0.23" + +set -eu -x + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +main() { + local workdir=$(mktemp -d --suffix=emsdk) + CLEANUP_FILES+=("${workdir}") + + local emsdktar="${workdir}/emsdk.tar.gz" + curl --output "${emsdktar}" "${EMSDK_URL}" --location + mkdir -p "${EMSDK_DIR}" + tar -zxf "${emsdktar}" -C "${EMSDK_DIR}" --strip-components=1 + + cd "${EMSDK_DIR}" + ./emsdk install --shallow "${EMSDK_RELEASE}" + ./emsdk activate --embedded "${EMSDK_RELEASE}" +} + +main "$@" diff --git a/docker/scripts/jpegxl_builder.sh b/docker/scripts/jpegxl_builder.sh new file mode 100755 index 0000000..bf9f19d --- /dev/null +++ b/docker/scripts/jpegxl_builder.sh @@ -0,0 +1,518 @@ +#!/usr/bin/env bash +# 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. + +# Main entry point for all the Dockerfile for jpegxl-builder. This centralized +# file helps sharing code and configuration between Dockerfiles. + +set -eux + +MYDIR=$(dirname $(realpath "$0")) + +# libjpeg-turbo. +JPEG_TURBO_RELEASE="2.0.4" +JPEG_TURBO_URL="https://github.com/libjpeg-turbo/libjpeg-turbo/archive/${JPEG_TURBO_RELEASE}.tar.gz" +JPEG_TURBO_SHA256="7777c3c19762940cff42b3ba4d7cd5c52d1671b39a79532050c85efb99079064" + +# zlib (dependency of libpng) +ZLIB_RELEASE="1.2.11" +ZLIB_URL="https://www.zlib.net/zlib-${ZLIB_RELEASE}.tar.gz" +ZLIB_SHA256="c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1" +# The name in the .pc and the .dll generated don't match in zlib for Windows +# because they use different .dll names in Windows. We avoid that by defining +# UNIX=1. We also install all the .dll files to ${prefix}/lib instead of the +# default ${prefix}/bin. +ZLIB_FLAGS='-DUNIX=1 -DINSTALL_PKGCONFIG_DIR=/${CMAKE_INSTALL_PREFIX}/lib/pkgconfig -DINSTALL_BIN_DIR=/${CMAKE_INSTALL_PREFIX}/lib' + +# libpng +LIBPNG_RELEASE="1.6.37" +LIBPNG_URL="https://github.com/glennrp/libpng/archive/v${LIBPNG_RELEASE}.tar.gz" +LIBPNG_SHA256="ca74a0dace179a8422187671aee97dd3892b53e168627145271cad5b5ac81307" + +# giflib +GIFLIB_RELEASE="5.2.1" +GIFLIB_URL="https://netcologne.dl.sourceforge.net/project/giflib/giflib-${GIFLIB_RELEASE}.tar.gz" +GIFLIB_SHA256="31da5562f44c5f15d63340a09a4fd62b48c45620cd302f77a6d9acf0077879bd" + +# A patch needed to compile GIFLIB in mingw. +GIFLIB_PATCH_URL="https://github.com/msys2/MINGW-packages/raw/3afde38fcee7b3ba2cafd97d76cca8f06934504f/mingw-w64-giflib/001-mingw-build.patch" +GIFLIB_PATCH_SHA256="2b2262ddea87fc07be82e10aeb39eb699239f883c899aa18a16e4d4e40af8ec8" + +# webp +WEBP_RELEASE="1.0.2" +WEBP_URL="https://codeload.github.com/webmproject/libwebp/tar.gz/v${WEBP_RELEASE}" +WEBP_SHA256="347cf85ddc3497832b5fa9eee62164a37b249c83adae0ba583093e039bf4881f" + +# Google benchmark +BENCHMARK_RELEASE="1.5.2" +BENCHMARK_URL="https://github.com/google/benchmark/archive/v${BENCHMARK_RELEASE}.tar.gz" +BENCHMARK_SHA256="dccbdab796baa1043f04982147e67bb6e118fe610da2c65f88912d73987e700c" +BENCHMARK_FLAGS="-DGOOGLETEST_PATH=${MYDIR}/../../third_party/googletest" +# attribute(format(__MINGW_PRINTF_FORMAT, ...)) doesn't work in our +# environment, so we disable the warning. +BENCHMARK_FLAGS="-DCMAKE_BUILD_TYPE=Release -DBENCHMARK_ENABLE_TESTING=OFF \ + -DCMAKE_CXX_FLAGS=-Wno-ignored-attributes \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON" + +# V8 +V8_VERSION="9.3.22" + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +# List of Ubuntu arch names supported by the builder (such as "i386"). +LIST_ARCHS=( + amd64 + i386 + arm64 + armhf +) + +# List of target triplets supported by the builder. +LIST_TARGETS=( + x86_64-linux-gnu + i686-linux-gnu + arm-linux-gnueabihf + aarch64-linux-gnu +) +LIST_MINGW_TARGETS=( + i686-w64-mingw32 + x86_64-w64-mingw32 +) +LIST_WASM_TARGETS=( + wasm32 +) + +# Setup the apt repositories and supported architectures. +setup_apt() { + apt-get update -y + apt-get install -y curl gnupg ca-certificates + + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1E9377A2BA9EF27F + + # node sources. + cat >/etc/apt/sources.list.d/nodesource.list <>"${newlist}" + fi + + main_list=$(echo "${main_list[@]}" | tr ' ' ,) + grep -v -E '^#' "${bkplist}" | + sed -E "s;^deb (http[^ ]+) (.*)\$;deb [arch=${main_list}] \\1 \\2\ndeb-src [arch=${main_list}] \\1 \\2;" \ + >>"${newlist}" + mv "${newlist}" /etc/apt/sources.list +} + +install_pkgs() { + packages=( + # Native compilers (minimum for SIMD is clang-7) + clang-7 clang-format-7 clang-tidy-7 + + # TODO: Consider adding clang-8 to every builder: + # clang-8 clang-format-8 clang-tidy-8 + + # For cross-compiling to Windows with mingw. + mingw-w64 + wine64 + wine-binfmt + + # Native tools. + bsdmainutils + cmake + extra-cmake-modules + git + llvm + nasm + ninja-build + parallel + pkg-config + + # For compiling / testing JNI wrapper. JDK8 is almost 2x smaller than JDK11 + # openjdk-8-jdk-headless would be 50MB smaller, unfortunately, CMake + # does mistakenly thinks it does not contain JNI feature. + openjdk-8-jdk + + # These are used by the ./ci.sh lint in the native builder. + clang-format-7 + clang-format-8 + + # For coverage builds + gcovr + + # For compiling giflib documentation. + xmlto + + # Common libraries. + libstdc++-8-dev + + # We don't use tcmalloc on archs other than amd64. This installs + # libgoogle-perftools4:amd64. + google-perftools + + # NodeJS for running WASM tests + nodejs + + # To generate API documentation. + doxygen + + # Freezes version that builds (passes tests). Newer version + # (2.30-21ubuntu1~18.04.4) claims to fix "On Intel Skylake + # (-march=native) generated avx512 instruction can be wrong", + # but newly added tests does not pass. Perhaps the problem is + # that mingw package is not updated. + binutils-source=2.30-15ubuntu1 + ) + + # Install packages that are arch-dependent. + local ubarch + for ubarch in "${LIST_ARCHS[@]}"; do + packages+=( + # Library dependencies. These normally depend on the target architecture + # we are compiling for and can't usually be installed for multiple + # architectures at the same time. + libgif7:"${ubarch}" + libjpeg-dev:"${ubarch}" + libpng-dev:"${ubarch}" + libqt5x11extras5-dev:"${ubarch}" + + libstdc++-8-dev:"${ubarch}" + qtbase5-dev:"${ubarch}" + + # For OpenEXR: + libilmbase12:"${ubarch}" + libopenexr22:"${ubarch}" + + # TCMalloc dependency + libunwind-dev:"${ubarch}" + + # Cross-compiling tools per arch. + libc6-dev-"${ubarch}"-cross + libstdc++-8-dev-"${ubarch}"-cross + ) + done + + local target + for target in "${LIST_TARGETS[@]}"; do + # Per target cross-compiling tools. + if [[ "${target}" != "x86_64-linux-gnu" ]]; then + packages+=( + binutils-"${target}" + gcc-"${target}" + ) + fi + done + + # Install all the manual packages via "apt install" for the main arch. These + # will be installed for other archs via manual download and unpack. + apt install -y "${packages[@]}" "${UNPACK_PKGS[@]}" +} + +# binutils <2.32 need a patch. +install_binutils() { + local workdir=$(mktemp -d --suffix=_install) + CLEANUP_FILES+=("${workdir}") + pushd "${workdir}" + apt source binutils-mingw-w64 + apt -y build-dep binutils-mingw-w64 + cd binutils-mingw-w64-8ubuntu1 + cp "${MYDIR}/binutils_align_fix.patch" debian/patches + echo binutils_align_fix.patch >> debian/patches/series + dpkg-buildpackage -b + cd .. + dpkg -i *deb + popd +} + +# Install a library from the source code for multiple targets. +# Usage: install_from_source [] +install_from_source() { + local package="$1" + shift + + local url + eval "url=\${${package}_URL}" + local sha256 + eval "sha256=\${${package}_SHA256}" + # Optional package flags + local pkgflags + eval "pkgflags=\${${package}_FLAGS:-}" + + local workdir=$(mktemp -d --suffix=_install) + CLEANUP_FILES+=("${workdir}") + + local tarfile="${workdir}"/$(basename "${url}") + curl -L --output "${tarfile}" "${url}" + if ! echo "${sha256} ${tarfile}" | sha256sum -c --status -; then + echo "SHA256 mismatch for ${url}: expected ${sha256} but found:" + sha256sum "${tarfile}" + exit 1 + fi + + local target + for target in "$@"; do + echo "Installing ${package} for target ${target} from ${url}" + + local srcdir="${workdir}/source-${target}" + mkdir -p "${srcdir}" + tar -zxf "${tarfile}" -C "${srcdir}" --strip-components=1 + + local prefix="/usr" + if [[ "${target}" != "x86_64-linux-gnu" ]]; then + prefix="/usr/${target}" + fi + + # Apply patches to buildfiles. + if [[ "${package}" == "GIFLIB" && "${target}" == *mingw32 ]]; then + # GIFLIB Makefile has several problems so we need to fix them here. We are + # using a patch from MSYS2 that already fixes the compilation for mingw. + local make_patch="${srcdir}/libgif.patch" + curl -L "${GIFLIB_PATCH_URL}" -o "${make_patch}" + echo "${GIFLIB_PATCH_SHA256} ${make_patch}" | sha256sum -c --status - + patch "${srcdir}/Makefile" < "${make_patch}" + elif [[ "${package}" == "LIBPNG" && "${target}" == wasm* ]]; then + # Cut the dependency to libm; there is pull request to fix it, so this + # might not be needed in the future. + sed -i 's/APPLE/EMSCRIPTEN/g' "${srcdir}/CMakeLists.txt" + fi + + local cmake_args=() + local export_args=("CC=clang-7" "CXX=clang++-7") + local cmake="cmake" + local make="make" + local system_name="Linux" + if [[ "${target}" == *mingw32 ]]; then + system_name="Windows" + # When compiling with clang, CMake doesn't detect that we are using mingw. + cmake_args+=( + -DMINGW=1 + # Googletest needs this when cross-compiling to windows + -DCMAKE_CROSSCOMPILING=1 + -DHAVE_STD_REGEX=0 + -DHAVE_POSIX_REGEX=0 + -DHAVE_GNU_POSIX_REGEX=0 + ) + local windres=$(which ${target}-windres || true) + if [[ -n "${windres}" ]]; then + cmake_args+=(-DCMAKE_RC_COMPILER="${windres}") + fi + fi + if [[ "${target}" == wasm* ]]; then + system_name="WASM" + cmake="emcmake cmake" + make="emmake make" + export_args=() + cmake_args+=( + -DCMAKE_FIND_ROOT_PATH="${prefix}" + -DCMAKE_PREFIX_PATH="${prefix}" + ) + # Static and shared library link to the same file -> race condition. + nproc=1 + else + nproc=`nproc --all` + fi + cmake_args+=(-DCMAKE_SYSTEM_NAME="${system_name}") + + if [[ "${target}" != "x86_64-linux-gnu" ]]; then + # Cross-compiling. + cmake_args+=( + -DCMAKE_C_COMPILER_TARGET="${target}" + -DCMAKE_CXX_COMPILER_TARGET="${target}" + -DCMAKE_SYSTEM_PROCESSOR="${target%%-*}" + ) + fi + + if [[ -e "${srcdir}/CMakeLists.txt" ]]; then + # Most packages use cmake for building which is easier to configure for + # cross-compiling. + if [[ "${package}" == "JPEG_TURBO" && "${target}" == wasm* ]]; then + # JT erroneously detects WASM CPU as i386 and tries to use asm. + # Wasm/Emscripten support for dynamic linking is incomplete; disable + # to avoid CMake warning. + cmake_args+=(-DWITH_SIMD=0 -DENABLE_SHARED=OFF) + fi + ( + cd "${srcdir}" + export ${export_args[@]} + ${cmake} \ + -DCMAKE_INSTALL_PREFIX="${prefix}" \ + "${cmake_args[@]}" ${pkgflags} + ${make} -j${nproc} + ${make} install + ) + elif [[ "${package}" == "GIFLIB" ]]; then + # GIFLIB doesn't yet have a cmake build system. There is a pull + # request in giflib for adding CMakeLists.txt so this might not be + # needed in the future. + ( + cd "${srcdir}" + local giflib_make_flags=( + CFLAGS="-O2 --target=${target} -std=gnu99" + PREFIX="${prefix}" + ) + if [[ "${target}" != wasm* ]]; then + giflib_make_flags+=(CC=clang-7) + fi + # giflib make dependencies are not properly set up so parallel building + # doesn't work for everything. + ${make} -j${nproc} libgif.a "${giflib_make_flags[@]}" + ${make} -j${nproc} all "${giflib_make_flags[@]}" + ${make} install "${giflib_make_flags[@]}" + ) + else + echo "Don't know how to install ${package}" + exit 1 + fi + + # CMake mistakenly uses ".so" libraries and EMCC fails to link properly. + if [[ "${target}" == wasm* ]]; then + rm -f "${prefix}/lib"/*.so* + fi + done +} + +# Packages that are manually unpacked for each architecture. +UNPACK_PKGS=( + libgif-dev + libclang-common-7-dev + + # For OpenEXR: + libilmbase-dev + libopenexr-dev + + # TCMalloc + libgoogle-perftools-dev + libtcmalloc-minimal4 + libgoogle-perftools4 +) + +# Main script entry point. +main() { + cd "${MYDIR}" + + # Configure the repositories with the sources for multi-arch cross + # compilation. + setup_apt + apt-get update -y + apt-get dist-upgrade -y + + install_pkgs + install_binutils + apt clean + + # Remove prebuilt Java classes cache. + rm /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server/classes.jsa + + # Manually extract packages for the target arch that can't install it directly + # at the same time as the native ones. + local ubarch + for ubarch in "${LIST_ARCHS[@]}"; do + if [[ "${ubarch}" != "amd64" ]]; then + local pkg + for pkg in "${UNPACK_PKGS[@]}"; do + apt download "${pkg}":"${ubarch}" + dpkg -x "${pkg}"_*_"${ubarch}".deb / + done + fi + done + # TODO: Add clang from the llvm repos. This is problematic since we are + # installing libclang-common-7-dev:"${ubarch}" from the ubuntu ports repos + # which is not available in the llvm repos so it might have a different + # version than the ubuntu ones. + + # Remove the win32 libgcc version. The gcc-mingw-w64-x86-64 (and i686) + # packages install two libgcc versions: + # /usr/lib/gcc/x86_64-w64-mingw32/7.3-posix + # /usr/lib/gcc/x86_64-w64-mingw32/7.3-win32 + # (exact libgcc version number depends on the package version). + # + # Clang will pick the best libgcc, sorting by version, but it doesn't + # seem to be a way to specify one or the other one, except by passing + # -nostdlib and setting all the include paths from the command line. + # To check which one is being used you can run: + # clang++-7 --target=x86_64-w64-mingw32 -v -print-libgcc-file-name + # We need to use the "posix" versions for thread support, so here we + # just remove the other one. + local target + for target in "${LIST_MINGW_TARGETS[@]}"; do + update-alternatives --set "${target}-gcc" $(which "${target}-gcc-posix") + local gcc_win32_path=$("${target}-cpp-win32" -print-libgcc-file-name) + rm -rf $(dirname "${gcc_win32_path}") + done + + # TODO: Add msan for the target when cross-compiling. This only installs it + # for amd64. + ./msan_install.sh + + # Build and install qemu user-linux targets. + ./qemu_install.sh + + # Install emscripten SDK. + ./emsdk_install.sh + + # Setup environment for building WASM libraries from sources. + source /opt/emsdk/emsdk_env.sh + + # Install some dependency libraries manually for the different targets. + + install_from_source JPEG_TURBO "${LIST_MINGW_TARGETS[@]}" "${LIST_WASM_TARGETS[@]}" + install_from_source ZLIB "${LIST_MINGW_TARGETS[@]}" "${LIST_WASM_TARGETS[@]}" + install_from_source LIBPNG "${LIST_MINGW_TARGETS[@]}" "${LIST_WASM_TARGETS[@]}" + install_from_source GIFLIB "${LIST_MINGW_TARGETS[@]}" "${LIST_WASM_TARGETS[@]}" + # webp in Ubuntu is relatively old so we install it from source for everybody. + install_from_source WEBP "${LIST_TARGETS[@]}" "${LIST_MINGW_TARGETS[@]}" + + install_from_source BENCHMARK "${LIST_TARGETS[@]}" "${LIST_MINGW_TARGETS[@]}" + + # Install v8. v8 has better WASM SIMD support than NodeJS 14 (LTS). + # First we need the installer to install v8. + npm install jsvu -g + # install specific version; + HOME=/opt jsvu --os=linux64 "v8@${V8_VERSION}" + ln -s "/opt/.jsvu/v8-${V8_VERSION}" "/opt/.jsvu/v8" + + # Cleanup. + find /var/lib/apt/lists/ -mindepth 1 -delete +} + +main "$@" diff --git a/docker/scripts/msan_install.sh b/docker/scripts/msan_install.sh new file mode 100755 index 0000000..0216f62 --- /dev/null +++ b/docker/scripts/msan_install.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# 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. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +# Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS +CMAKE_FLAGS=${CMAKE_FLAGS:-} +CMAKE_C_FLAGS=${CMAKE_C_FLAGS:-${CMAKE_FLAGS}} +CMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS:-${CMAKE_FLAGS}} +CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} + +CLANG_VERSION="${CLANG_VERSION:-}" +# Detect the clang version suffix and store it in CLANG_VERSION. For example, +# "6.0" for clang 6 or "7" for clang 7. +detect_clang_version() { + if [[ -n "${CLANG_VERSION}" ]]; then + return 0 + fi + local clang_version=$("${CC:-clang}" --version | head -n1) + local llvm_tag + case "${clang_version}" in + "clang version 6."*) + CLANG_VERSION="6.0" + ;; + "clang version 7."*) + CLANG_VERSION="7" + ;; + "clang version 8."*) + CLANG_VERSION="8" + ;; + "clang version 9."*) + CLANG_VERSION="9" + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac +} + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +# Install libc++ libraries compiled with msan in the msan_prefix for the current +# compiler version. +cmd_msan_install() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + # Detect the llvm to install: + export CC="${CC:-clang}" + export CXX="${CXX:-clang++}" + detect_clang_version + local llvm_tag + case "${CLANG_VERSION}" in + "6.0") + llvm_tag="llvmorg-6.0.1" + ;; + "7") + llvm_tag="llvmorg-7.0.1" + ;; + "8") + llvm_tag="llvmorg-8.0.0" + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac + local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" + curl -L --show-error -o "${llvm_targz}" \ + "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" + tar -C "${tmpdir}" -zxf "${llvm_targz}" + local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + rm -rf "${msan_prefix}" + + declare -A CMAKE_EXTRAS + CMAKE_EXTRAS[libcxx]="\ + -DLIBCXX_CXX_ABI=libstdc++ \ + -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" + + for project in libcxx; do + local proj_build="${tmpdir}/build-${project}" + local proj_dir="${llvm_root}/${project}" + mkdir -p "${proj_build}" + cmake -B"${proj_build}" -H"${proj_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_USE_SANITIZER=Memory \ + -DLLVM_PATH="${llvm_root}/llvm" \ + -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ + head -n1)" \ + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ + -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ + ${CMAKE_EXTRAS[${project}]} + cmake --build "${proj_build}" + ninja -C "${proj_build}" install + done +} + +main() { + set -x + for version in 6.0 7 8; do + if ! which "clang-${version}" >/dev/null; then + echo "Skipping msan install for clang version ${version}" + continue + fi + ( + trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + export CLANG_VERSION=${version} + export CC=clang-${version} + export CXX=clang++-${version} + cmd_msan_install + ) & + done + wait +} + +main "$@" diff --git a/docker/scripts/qemu_install.sh b/docker/scripts/qemu_install.sh new file mode 100755 index 0000000..8106c44 --- /dev/null +++ b/docker/scripts/qemu_install.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# 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. + +QEMU_RELEASE="4.1.0" +QEMU_URL="https://download.qemu.org/qemu-${QEMU_RELEASE}.tar.xz" +QEMU_ARCHS=( + aarch64 + arm + i386 + # TODO: Consider adding these: + # aarch64_be + # mips64el + # mips64 + # mips + # ppc64 + # ppc +) + +# Ubuntu packages not installed that are needed to build qemu. +QEMU_BUILD_DEPS=( + libglib2.0-dev + libpixman-1-dev + flex + bison +) + +set -eu -x + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +main() { + local workdir=$(mktemp -d --suffix=qemu) + CLEANUP_FILES+=("${workdir}") + + apt install -y "${QEMU_BUILD_DEPS[@]}" + + local qemutar="${workdir}/qemu.tar.gz" + curl --output "${qemutar}" "${QEMU_URL}" + tar -Jxf "${qemutar}" -C "${workdir}" + local srcdir="${workdir}/qemu-${QEMU_RELEASE}" + + local builddir="${workdir}/build" + local prefixdir="${workdir}/prefix" + mkdir -p "${builddir}" + + # List of targets to build. + local targets="" + local make_targets=() + local target + for target in "${QEMU_ARCHS[@]}"; do + targets="${targets} ${target}-linux-user" + # Build just the linux-user targets. + make_targets+=("${target}-linux-user/all") + done + + cd "${builddir}" + "${srcdir}/configure" \ + --prefix="${prefixdir}" \ + --static --disable-system --enable-linux-user \ + --target-list="${targets}" + + make -j $(nproc --all || echo 1) "${make_targets[@]}" + + # Manually install these into the non-standard location. This script runs as + # root anyway. + for target in "${QEMU_ARCHS[@]}"; do + cp "${target}-linux-user/qemu-${target}" "/usr/bin/qemu-${target}-static" + done + + apt autoremove -y --purge "${QEMU_BUILD_DEPS[@]}" +} + +main "$@" diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..64510e5 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,56 @@ +# 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. + +# Example project using libjxl. + +cmake_minimum_required(VERSION 3.10) + +project(SAMPLE_LIBJXL LANGUAGES C CXX) + +# Use pkg-config to find libjxl. +find_package(PkgConfig) +pkg_check_modules(Jxl REQUIRED IMPORTED_TARGET libjxl) +pkg_check_modules(JxlThreads REQUIRED IMPORTED_TARGET libjxl_threads) + +# Build the example encoder/decoder binaries using the default shared libraries +# installed. +add_executable(decode_oneshot decode_oneshot.cc) +target_link_libraries(decode_oneshot 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 +# depends on the platform, and building static binaries in general has problems. +# If you don't need static binaries you can remove this section. +add_library(StaticJxl INTERFACE IMPORTED GLOBAL) +set_target_properties(StaticJxl PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Jxl_STATIC_INCLUDE_DIR}" + INTERFACE_COMPILE_OPTIONS "${Jxl_STATIC_CFLAGS_OTHER}" + INTERFACE_LINK_LIBRARIES "${Jxl_STATIC_LDFLAGS}" +) +add_library(StaticJxlThreads INTERFACE IMPORTED GLOBAL) +set_target_properties(StaticJxlThreads PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${JxlThreads_STATIC_INCLUDE_DIR}" + INTERFACE_COMPILE_OPTIONS "${JxlThreads_STATIC_CFLAGS_OTHER}" + # libgcc uses weak symbols for pthread which means that -lpthread is not + # linked when compiling a static binary. This is a platform-specific fix for + # that. + INTERFACE_LINK_LIBRARIES + "${JxlThreads_STATIC_LDFLAGS} -Wl,--whole-archive -lpthread -Wl,--no-whole-archive" +) + +add_executable(decode_oneshot_static decode_oneshot.cc) +target_link_libraries(decode_oneshot_static + -static StaticJxl StaticJxlThreads) + +add_executable(encode_oneshot_static encode_oneshot.cc) +target_link_libraries(encode_oneshot_static + -static StaticJxl StaticJxlThreads) diff --git a/examples/decode_oneshot.cc b/examples/decode_oneshot.cc new file mode 100644 index 0000000..360c8d2 --- /dev/null +++ b/examples/decode_oneshot.cc @@ -0,0 +1,245 @@ +// 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 C++ example decodes a JPEG XL image in one shot (all input bytes +// available at once). The example outputs the pixels and color information to a +// floating point image and an ICC profile on disk. + +#include +#include +#include +#include + +#include + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/resizable_parallel_runner.h" +#include "jxl/resizable_parallel_runner_cxx.h" + +/** Decodes JPEG XL image to floating point pixels and ICC Profile. Pixel are + * stored as floating point, as interleaved RGBA (4 floating point values per + * pixel), line per line from top to bottom. Pixel values have nominal range + * 0..1 but may go beyond this range for HDR or wide gamut. The ICC profile + * describes the color format of the pixel data. + */ +bool DecodeJpegXlOneShot(const uint8_t* jxl, size_t size, + std::vector* pixels, size_t* xsize, + size_t* ysize, std::vector* icc_profile) { + // Multi-threaded parallel runner. + auto runner = JxlResizableParallelRunnerMake(nullptr); + + auto dec = JxlDecoderMake(nullptr); + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)) { + fprintf(stderr, "JxlDecoderSubscribeEvents failed\n"); + return false; + } + + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), + JxlResizableParallelRunner, + runner.get())) { + fprintf(stderr, "JxlDecoderSetParallelRunner failed\n"); + return false; + } + + JxlBasicInfo info; + JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + + JxlDecoderSetInput(dec.get(), jxl, size); + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + + if (status == JXL_DEC_ERROR) { + fprintf(stderr, "Decoder error\n"); + return false; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + fprintf(stderr, "Error, already provided all input\n"); + return false; + } else if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) { + fprintf(stderr, "JxlDecoderGetBasicInfo failed\n"); + return false; + } + *xsize = info.xsize; + *ysize = info.ysize; + JxlResizableParallelRunnerSetThreads( + runner.get(), + JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); + } else if (status == JXL_DEC_COLOR_ENCODING) { + // Get the ICC color profile of the pixel data + size_t icc_size; + if (JXL_DEC_SUCCESS != + JxlDecoderGetICCProfileSize( + dec.get(), &format, JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) { + fprintf(stderr, "JxlDecoderGetICCProfileSize failed\n"); + return false; + } + icc_profile->resize(icc_size); + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( + dec.get(), &format, + JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile->data(), icc_profile->size())) { + fprintf(stderr, "JxlDecoderGetColorAsICCProfile failed\n"); + return false; + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + size_t buffer_size; + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { + fprintf(stderr, "JxlDecoderImageOutBufferSize failed\n"); + return false; + } + if (buffer_size != *xsize * *ysize * 16) { + fprintf(stderr, "Invalid out buffer size %zu %zu\n", buffer_size, + *xsize * *ysize * 16); + return false; + } + pixels->resize(*xsize * *ysize * 4); + void* pixels_buffer = (void*)pixels->data(); + size_t pixels_buffer_size = pixels->size() * sizeof(float); + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, + pixels_buffer, + pixels_buffer_size)) { + fprintf(stderr, "JxlDecoderSetImageOutBuffer failed\n"); + return false; + } + } else if (status == JXL_DEC_FULL_IMAGE) { + // Nothing to do. Do not yet return. If the image is an animation, more + // full frames may be decoded. This example only keeps the last one. + } else if (status == JXL_DEC_SUCCESS) { + // All decoding successfully finished. + // It's not required to call JxlDecoderReleaseInput(dec.get()) here since + // the decoder will be destroyed. + return true; + } else { + fprintf(stderr, "Unknown decoder status\n"); + return false; + } + } +} + +/** Writes to .pfm file (Portable FloatMap). Gimp, tev viewer and ImageMagick + * support viewing this format. + * The input pixels are given as 32-bit floating point with 4-channel RGBA. + * The alpha channel will not be written since .pfm does not support it. + */ +bool WritePFM(const char* filename, const float* pixels, size_t xsize, + size_t ysize) { + FILE* file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, "Could not open %s for writing", filename); + return false; + } + uint32_t endian_test = 1; + uint8_t little_endian[4]; + memcpy(little_endian, &endian_test, 4); + + fprintf(file, "PF\n%d %d\n%s\n", (int)xsize, (int)ysize, + little_endian[0] ? "-1.0" : "1.0"); + for (int y = ysize - 1; y >= 0; y--) { + for (size_t x = 0; x < xsize; x++) { + for (size_t c = 0; c < 3; c++) { + const float* f = &pixels[(y * xsize + x) * 4 + c]; + fwrite(f, 4, 1, file); + } + } + } + if (fclose(file) != 0) { + return false; + } + return true; +} + +bool LoadFile(const char* filename, std::vector* out) { + FILE* file = fopen(filename, "rb"); + if (!file) { + return false; + } + + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + + long size = ftell(file); + // Avoid invalid file or directory. + if (size >= LONG_MAX || size < 0) { + fclose(file); + return false; + } + + if (fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return false; + } + + out->resize(size); + size_t readsize = fread(out->data(), 1, size, file); + if (fclose(file) != 0) { + return false; + } + + return readsize == static_cast(size); +} + +bool WriteFile(const char* filename, const uint8_t* data, size_t size) { + FILE* file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, "Could not open %s for writing", filename); + return false; + } + fwrite(data, 1, size, file); + if (fclose(file) != 0) { + return false; + } + return true; +} + +int main(int argc, char* argv[]) { + if (argc != 4) { + fprintf(stderr, + "Usage: %s \n" + "Where:\n" + " jxl = input JPEG XL image filename\n" + " pfm = output Portable FloatMap image filename\n" + " icc = output ICC color profile filename\n" + "Output files will be overwritten.\n", + argv[0]); + return 1; + } + + const char* jxl_filename = argv[1]; + const char* pfm_filename = argv[2]; + const char* icc_filename = argv[3]; + + std::vector jxl; + if (!LoadFile(jxl_filename, &jxl)) { + fprintf(stderr, "couldn't load %s\n", jxl_filename); + return 1; + } + + std::vector pixels; + std::vector icc_profile; + size_t xsize = 0, ysize = 0; + if (!DecodeJpegXlOneShot(jxl.data(), jxl.size(), &pixels, &xsize, &ysize, + &icc_profile)) { + fprintf(stderr, "Error while decoding the jxl file\n"); + return 1; + } + if (!WritePFM(pfm_filename, pixels.data(), xsize, ysize)) { + fprintf(stderr, "Error while writing the PFM image file\n"); + return 1; + } + if (!WriteFile(icc_filename, icc_profile.data(), icc_profile.size())) { + fprintf(stderr, "Error while writing the ICC profile file\n"); + return 1; + } + printf("Successfully wrote %s and %s\n", pfm_filename, icc_filename); + return 0; +} diff --git a/examples/encode_oneshot.cc b/examples/encode_oneshot.cc new file mode 100644 index 0000000..e3c9fdb --- /dev/null +++ b/examples/encode_oneshot.cc @@ -0,0 +1,272 @@ +// 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 encodes a file containing a floating point image to another +// file containing JPEG XL image with a single frame. + +#include +#include + +#include +#include +#include + +#include "jxl/encode.h" +#include "jxl/encode_cxx.h" +#include "jxl/thread_parallel_runner.h" +#include "jxl/thread_parallel_runner_cxx.h" + +/** + * Reads from .pfm file (Portable FloatMap) + * + * @param filename name of the file to read + * @param pixels vector to fill with loaded pixels as 32-bit floating point with + * 3-channel RGB + * @param xsize set to width of loaded image + * @param ysize set to height of loaded image + */ +bool ReadPFM(const char* filename, std::vector* pixels, uint32_t* xsize, + uint32_t* ysize) { + FILE* file = fopen(filename, "rb"); + if (!file) { + fprintf(stderr, "Could not open %s for reading.\n", filename); + return false; + } + uint32_t endian_test = 1; + uint8_t little_endian[4]; + memcpy(little_endian, &endian_test, 4); + + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + + long size = ftell(file); + // Avoid invalid file or directory. + if (size >= LONG_MAX || size < 0) { + fclose(file); + return false; + } + + if (fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return false; + } + + std::vector data; + data.resize(size); + + size_t readsize = fread(data.data(), 1, size, file); + if ((long)readsize != size) { + return false; + } + if (fclose(file) != 0) { + return false; + } + + std::stringstream datastream; + std::string datastream_content(data.data(), data.size()); + datastream.str(datastream_content); + + std::string pf_token; + getline(datastream, pf_token, '\n'); + if (pf_token != "PF") { + fprintf(stderr, + "%s doesn't seem to be a 3 channel Portable FloatMap file (missing " + "'PF\\n' " + "bytes).\n", + filename); + return false; + } + + std::string xsize_token; + getline(datastream, xsize_token, ' '); + *xsize = std::stoi(xsize_token); + + std::string ysize_token; + getline(datastream, ysize_token, '\n'); + *ysize = std::stoi(ysize_token); + + std::string endianness_token; + getline(datastream, endianness_token, '\n'); + bool input_little_endian; + if (endianness_token == "1.0") { + input_little_endian = false; + } else if (endianness_token == "-1.0") { + input_little_endian = true; + } else { + fprintf(stderr, + "%s doesn't seem to be a Portable FloatMap file (endianness token " + "isn't '1.0' or '-1.0').\n", + filename); + return false; + } + + size_t offset = pf_token.size() + 1 + xsize_token.size() + 1 + + ysize_token.size() + 1 + endianness_token.size() + 1; + + if (data.size() != *ysize * *xsize * 3 * 4 + offset) { + fprintf(stderr, + "%s doesn't seem to be a Portable FloatMap file (pixel data bytes " + "are %d, but expected %d * %d * 3 * 4 + %d (%d).\n", + filename, (int)data.size(), (int)*ysize, (int)*xsize, (int)offset, + (int)(*ysize * *xsize * 3 * 4 + offset)); + return false; + } + + if (!!little_endian[0] != input_little_endian) { + fprintf(stderr, + "%s has a different endianness than we do, conversion is not " + "supported.\n", + filename); + return false; + } + + pixels->resize(*ysize * *xsize * 3); + + for (int y = *ysize - 1; y >= 0; y--) { + for (int x = 0; x < (int)*xsize; x++) { + for (int c = 0; c < 3; c++) { + memcpy(pixels->data() + (y * *xsize + x) * 3 + c, data.data() + offset, + sizeof(float)); + offset += sizeof(float); + } + } + } + + return true; +} + +/** + * Compresses the provided pixels. + * + * @param pixels input pixels + * @param xsize width of the input image + * @param ysize height of the input image + * @param compressed will be populated with the compressed bytes + */ +bool EncodeJxlOneshot(const std::vector& pixels, const uint32_t xsize, + const uint32_t ysize, std::vector* compressed) { + auto enc = JxlEncoderMake(/*memory_manager=*/nullptr); + auto runner = JxlThreadParallelRunnerMake( + /*memory_manager=*/nullptr, + JxlThreadParallelRunnerDefaultNumWorkerThreads()); + if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(), + JxlThreadParallelRunner, + runner.get())) { + fprintf(stderr, "JxlEncoderSetParallelRunner failed\n"); + return false; + } + + JxlPixelFormat pixel_format = {3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + + JxlBasicInfo basic_info; + JxlEncoderInitBasicInfo(&basic_info); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.bits_per_sample = 32; + basic_info.exponent_bits_per_sample = 8; + basic_info.uses_original_profile = JXL_FALSE; + if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), &basic_info)) { + fprintf(stderr, "JxlEncoderSetBasicInfo failed\n"); + return false; + } + + JxlColorEncoding color_encoding = {}; + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) { + fprintf(stderr, "JxlEncoderSetColorEncoding failed\n"); + return false; + } + + if (JXL_ENC_SUCCESS != + JxlEncoderAddImageFrame(JxlEncoderOptionsCreate(enc.get(), nullptr), + &pixel_format, (void*)pixels.data(), + sizeof(float) * pixels.size())) { + fprintf(stderr, "JxlEncoderAddImageFrame failed\n"); + return false; + } + + compressed->resize(64); + uint8_t* next_out = compressed->data(); + size_t avail_out = compressed->size() - (next_out - compressed->data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed->data(); + compressed->resize(compressed->size() * 2); + next_out = compressed->data() + offset; + avail_out = compressed->size() - offset; + } + } + compressed->resize(next_out - compressed->data()); + if (JXL_ENC_SUCCESS != process_result) { + fprintf(stderr, "JxlEncoderProcessOutput failed\n"); + return false; + } + + return true; +} + +/** + * Writes bytes to file. + */ +bool WriteFile(const std::vector& bytes, const char* filename) { + FILE* file = fopen(filename, "wb"); + if (!file) { + fprintf(stderr, "Could not open %s for writing\n", filename); + return false; + } + if (fwrite(bytes.data(), sizeof(uint8_t), bytes.size(), file) != + bytes.size()) { + fprintf(stderr, "Could not write bytes to %s\n", filename); + return false; + } + if (fclose(file) != 0) { + fprintf(stderr, "Could not close %s\n", filename); + return false; + } + return true; +} + +int main(int argc, char* argv[]) { + if (argc != 3) { + fprintf(stderr, + "Usage: %s \n" + "Where:\n" + " pfm = input Portable FloatMap image filename\n" + " jxl = output JPEG XL image filename\n" + "Output files will be overwritten.\n", + argv[0]); + return 1; + } + + const char* pfm_filename = argv[1]; + const char* jxl_filename = argv[2]; + + std::vector pixels; + uint32_t xsize; + uint32_t ysize; + if (!ReadPFM(pfm_filename, &pixels, &xsize, &ysize)) { + fprintf(stderr, "Couldn't load %s\n", pfm_filename); + return 2; + } + + std::vector compressed; + if (!EncodeJxlOneshot(pixels, xsize, ysize, &compressed)) { + fprintf(stderr, "Couldn't encode jxl\n"); + return 3; + } + + if (!WriteFile(compressed, jxl_filename)) { + fprintf(stderr, "Couldn't write jxl file\n"); + return 4; + } + + return 0; +} diff --git a/examples/examples.cmake b/examples/examples.cmake new file mode 100644 index 0000000..726727c --- /dev/null +++ b/examples/examples.cmake @@ -0,0 +1,19 @@ +# 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. + +add_executable(decode_oneshot ${CMAKE_CURRENT_LIST_DIR}/decode_oneshot.cc) +target_link_libraries(decode_oneshot 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" diff --git a/examples/jxlinfo.c b/examples/jxlinfo.c new file mode 100644 index 0000000..5827974 --- /dev/null +++ b/examples/jxlinfo.c @@ -0,0 +1,317 @@ +// 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 +#include +#include + +#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)) { + 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); + } 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); + } + 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_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; + } + free(name); + printf(" name: %s\n", 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: %zu\n", 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; + } + free(name); + printf(" name: %s\n", 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); + } + } + + // This is the last expected event, no need to read the rest of the file. + } 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 \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)) { + fprintf(stderr, "Couldn't print basic info\n"); + return 1; + } + + return 0; +} diff --git a/js-wasm-wrapper.sh b/js-wasm-wrapper.sh new file mode 100755 index 0000000..fb91c55 --- /dev/null +++ b/js-wasm-wrapper.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# 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. + +# Continuous integration helper module. This module is meant to be called from +# the .gitlab-ci.yml file during the continuous integration build, as well as +# from the command line for developers. + +# This wrapper is used to enable WASM SIMD when running tests. +# Unfortunately, it is impossible to pass the option directly via the +# CMAKE_CROSSCOMPILING_EMULATOR variable. + +# Fallback to default v8 binary, if override is not set. +V8="${V8:-$(which v8)}" +SCRIPT="$1" +shift +"${V8}" --experimental-wasm-simd "${SCRIPT}" -- "$@" diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..52dd05f --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,160 @@ +# 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. + +set(JPEGXL_MAJOR_VERSION 0) +set(JPEGXL_MINOR_VERSION 6) +set(JPEGXL_PATCH_VERSION 1) +set(JPEGXL_LIBRARY_VERSION + "${JPEGXL_MAJOR_VERSION}.${JPEGXL_MINOR_VERSION}.${JPEGXL_PATCH_VERSION}") + +# This is the library API/ABI compatibility version. Changing this value makes +# the shared library incompatible with previous version. A program linked +# against this shared library SOVERSION will not run with an older SOVERSION. +# It is important to update this value when making incompatible API/ABI changes +# so that programs that depend on libjxl can update their dependencies. Semantic +# versioning allows 0.y.z to have incompatible changes in minor versions. +set(JPEGXL_SO_MINOR_VERSION 6) +if (JPEGXL_MAJOR_VERSION EQUAL 0) +set(JPEGXL_LIBRARY_SOVERSION + "${JPEGXL_MAJOR_VERSION}.${JPEGXL_SO_MINOR_VERSION}") +else() +set(JPEGXL_LIBRARY_SOVERSION "${JPEGXL_MAJOR_VERSION}") +endif() + + +# List of warning and feature flags for our library and tests. +if (MSVC) +set(JPEGXL_INTERNAL_FLAGS + # TODO(janwas): add flags +) +else () +set(JPEGXL_INTERNAL_FLAGS + # F_FLAGS + -fmerge-all-constants + -fno-builtin-fwrite + -fno-builtin-fread + + # WARN_FLAGS + -Wall + -Wextra + -Wc++11-compat + -Warray-bounds + -Wformat-security + -Wimplicit-fallthrough + -Wno-register # Needed by public headers in lcms + -Wno-unused-function + -Wno-unused-parameter + -Wnon-virtual-dtor + -Woverloaded-virtual + -Wvla +) + +# Warning flags supported by clang. +if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + list(APPEND JPEGXL_INTERNAL_FLAGS + -Wdeprecated-increment-bool + # TODO(deymo): Add -Wextra-semi once we update third_party/highway. + # -Wextra-semi + -Wfloat-overflow-conversion + -Wfloat-zero-conversion + -Wfor-loop-analysis + -Wgnu-redeclared-enum + -Winfinite-recursion + -Wliteral-conversion + -Wno-c++98-compat + -Wno-unused-command-line-argument + -Wprivate-header + -Wself-assign + -Wstring-conversion + -Wtautological-overlap-compare + -Wthread-safety-analysis + -Wundefined-func-template + -Wunreachable-code + -Wunused-comparison + ) + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 5.0) + list(APPEND HWY_FLAGS -Wc++2a-extensions) + endif() +endif() # Clang + +if (WIN32) + list(APPEND JPEGXL_INTERNAL_FLAGS + -Wno-c++98-compat-pedantic + -Wno-cast-align + -Wno-double-promotion + -Wno-float-equal + -Wno-format-nonliteral + -Wno-global-constructors + -Wno-language-extension-token + -Wno-missing-prototypes + -Wno-shadow + -Wno-shadow-field-in-constructor + -Wno-sign-conversion + -Wno-unused-member-function + -Wno-unused-template + -Wno-used-but-marked-unused + -Wno-zero-as-null-pointer-constant + ) +else() # WIN32 + list(APPEND JPEGXL_INTERNAL_FLAGS + -fsized-deallocation + -fno-exceptions + + # Language flags + -fmath-errno + ) + + if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + list(APPEND JPEGXL_INTERNAL_FLAGS + -fnew-alignment=8 + -fno-cxx-exceptions + -fno-slp-vectorize + -fno-vectorize + + -disable-free + -disable-llvm-verifier + ) + endif() # Clang +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 + -DJXL_ENABLE_ASSERT=0 -DJXL_ENABLE_CHECK=0 +) +endif() # JPEGXL_ENABLE_COVERAGE +endif() #!MSVC + +# The jxl library definition. +include(jxl.cmake) + +# Other libraries outside the core jxl library. +include(jxl_extras.cmake) +include(jxl_threads.cmake) + +# Install all the library headers from the source and the generated ones. There +# is no distinction on which libraries use which header since it is expected +# that all developer libraries are available together at build time. +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/jxl + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/jxl + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + +# Profiler for libjxl +include(jxl_profiler.cmake) + +if(BUILD_TESTING) +# Unittests +cmake_policy(SET CMP0057 NEW) # https://gitlab.kitware.com/cmake/cmake/issues/18198 +include(GoogleTest) + +# Tests for the jxl library. +include(jxl_tests.cmake) + +# Google benchmark for the jxl library +include(jxl_benchmark.cmake) + +endif() # BUILD_TESTING diff --git a/lib/extras/README.md b/lib/extras/README.md new file mode 100644 index 0000000..06a9b5e --- /dev/null +++ b/lib/extras/README.md @@ -0,0 +1,5 @@ +## JPEG XL "extras" + +The files in this directory do not form part of the library or codec and are +only used by tests or specific internal tools that have access to the internals +of the library. diff --git a/lib/extras/codec.cc b/lib/extras/codec.cc new file mode 100644 index 0000000..54f98bb --- /dev/null +++ b/lib/extras/codec.cc @@ -0,0 +1,240 @@ +// 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.h" + +#include "lib/jxl/base/file_io.h" +#if JPEGXL_ENABLE_APNG +#include "lib/extras/codec_apng.h" +#endif +#if JPEGXL_ENABLE_EXR +#include "lib/extras/codec_exr.h" +#endif +#if JPEGXL_ENABLE_GIF +#include "lib/extras/codec_gif.h" +#endif +#include "lib/extras/codec_jpg.h" +#include "lib/extras/codec_pgx.h" +#include "lib/extras/codec_png.h" +#include "lib/extras/codec_pnm.h" +#include "lib/extras/codec_psd.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { +namespace { + +// Any valid encoding is larger (ensures codecs can read the first few bytes) +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(const std::string& extension, + size_t* JXL_RESTRICT bits_per_sample) { + if (extension == ".png") return Codec::kPNG; + + if (extension == ".jpg") return Codec::kJPG; + if (extension == ".jpeg") return Codec::kJPG; + + if (extension == ".pgx") return Codec::kPGX; + + if (extension == ".pbm") { + *bits_per_sample = 1; + return Codec::kPNM; + } + if (extension == ".pgm") return Codec::kPNM; + if (extension == ".ppm") return Codec::kPNM; + if (extension == ".pfm") { + *bits_per_sample = 32; + return Codec::kPNM; + } + + if (extension == ".gif") return Codec::kGIF; + + if (extension == ".exr") return Codec::kEXR; + + if (extension == ".psd") return Codec::kPSD; + + return Codec::kUnknown; +} + +Status SetFromBytes(const Span bytes, + const ColorHints& color_hints, CodecInOut* io, + ThreadPool* pool, Codec* orig_codec) { + if (bytes.size() < kMinBytes) return JXL_FAILURE("Too few bytes"); + + io->metadata.m.bit_depth.bits_per_sample = 0; // (For is-set check below) + + Codec codec; + if (extras::DecodeImagePNG(bytes, color_hints, pool, io)) { + codec = Codec::kPNG; + } +#if JPEGXL_ENABLE_APNG + else if (extras::DecodeImageAPNG(bytes, color_hints, pool, io)) { + codec = Codec::kPNG; + } +#endif + else if (extras::DecodeImagePGX(bytes, color_hints, pool, io)) { + codec = Codec::kPGX; + } else if (extras::DecodeImagePNM(bytes, color_hints, pool, io)) { + codec = Codec::kPNM; + } +#if JPEGXL_ENABLE_GIF + else if (extras::DecodeImageGIF(bytes, color_hints, pool, io)) { + codec = Codec::kGIF; + } +#endif + else if (io->dec_target == DecodeTarget::kQuantizedCoeffs && + extras::DecodeImageJPGCoefficients(bytes, io)) { + // TODO(deymo): In this case the tools should use a different API to + // transcode the input JPEG to JXL. + codec = Codec::kJPG; + } else if (io->dec_target == DecodeTarget::kPixels && + extras::DecodeImageJPG(bytes, color_hints, pool, io)) { + codec = Codec::kJPG; + } else if (extras::DecodeImagePSD(bytes, color_hints, pool, io)) { + codec = Codec::kPSD; + } +#if JPEGXL_ENABLE_EXR + else if (extras::DecodeImageEXR(bytes, color_hints, pool, io)) { + codec = Codec::kEXR; + } +#endif + else { + return JXL_FAILURE("Codecs failed to decode"); + } + if (orig_codec) *orig_codec = codec; + + io->CheckMetadata(); + return true; +} + +Status SetFromFile(const std::string& pathname, const ColorHints& color_hints, + CodecInOut* io, ThreadPool* pool, Codec* orig_codec) { + PaddedBytes encoded; + JXL_RETURN_IF_ERROR(ReadFile(pathname, &encoded)); + JXL_RETURN_IF_ERROR(SetFromBytes(Span(encoded), color_hints, + io, pool, orig_codec)); + return true; +} + +Status Encode(const CodecInOut& io, const Codec codec, + const ColorEncoding& c_desired, size_t bits_per_sample, + PaddedBytes* bytes, ThreadPool* pool) { + JXL_CHECK(!io.Main().c_current().ICC().empty()); + JXL_CHECK(!c_desired.ICC().empty()); + io.CheckMetadata(); + if (io.Main().IsJPEG() && codec != Codec::kJPG) { + return JXL_FAILURE( + "Output format has to be JPEG for losslessly recompressed JPEG " + "reconstruction"); + } + + switch (codec) { + case Codec::kPNG: + return extras::EncodeImagePNG(&io, c_desired, bits_per_sample, pool, + bytes); + case Codec::kJPG: + if (io.Main().IsJPEG()) { + return extras::EncodeImageJPGCoefficients(&io, bytes); + } else { +#if JPEGXL_ENABLE_JPEG + return EncodeImageJPG(&io, + io.use_sjpeg ? extras::JpegEncoder::kSJpeg + : extras::JpegEncoder::kLibJpeg, + io.jpeg_quality, YCbCrChromaSubsampling(), pool, + bytes); +#else + return JXL_FAILURE("JPEG XL was built without JPEG support"); +#endif + } + case Codec::kPNM: + return extras::EncodeImagePNM(&io, c_desired, bits_per_sample, pool, + bytes); + case Codec::kPGX: + return extras::EncodeImagePGX(&io, c_desired, bits_per_sample, pool, + bytes); + case Codec::kGIF: + return JXL_FAILURE("Encoding to GIF is not implemented"); + case Codec::kPSD: + return extras::EncodeImagePSD(&io, c_desired, bits_per_sample, pool, + bytes); + case Codec::kEXR: +#if JPEGXL_ENABLE_EXR + return extras::EncodeImageEXR(&io, c_desired, pool, bytes); +#else + return JXL_FAILURE("JPEG XL was built without OpenEXR support"); +#endif + case Codec::kUnknown: + return JXL_FAILURE("Cannot encode using Codec::kUnknown"); + } + + return JXL_FAILURE("Invalid codec"); +} + +Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, + size_t bits_per_sample, const std::string& pathname, + ThreadPool* pool) { + const std::string extension = Extension(pathname); + const Codec codec = CodecFromExtension(extension, &bits_per_sample); + + // Warn about incorrect usage of PBM/PGM/PGX/PPM - only the latter supports + // color, but CodecFromExtension lumps them all together. + if (codec == Codec::kPNM && extension != ".pfm") { + if (!io.Main().IsGray() && extension != ".ppm") { + JXL_WARNING("For color images, the filename should end with .ppm.\n"); + } else if (io.Main().IsGray() && extension == ".ppm") { + JXL_WARNING( + "For grayscale images, the filename should not end with .ppm.\n"); + } + if (bits_per_sample > 16) { + JXL_WARNING("PPM only supports up to 16 bits per sample"); + bits_per_sample = 16; + } + } else if (codec == Codec::kPGX && !io.Main().IsGray()) { + JXL_WARNING("Storing color image to PGX - use .ppm extension instead.\n"); + } + if (bits_per_sample > 16 && codec == Codec::kPNG) { + JXL_WARNING("PNG only supports up to 16 bits per sample"); + bits_per_sample = 16; + } + + PaddedBytes encoded; + return Encode(io, codec, c_desired, bits_per_sample, &encoded, pool) && + WriteFile(encoded, pathname); +} + +Status EncodeToFile(const CodecInOut& io, const std::string& pathname, + ThreadPool* pool) { + // TODO(lode): need to take the floating_point_sample field into account + return EncodeToFile(io, io.metadata.m.color_encoding, + io.metadata.m.bit_depth.bits_per_sample, pathname, pool); +} + +} // namespace jxl diff --git a/lib/extras/codec.h b/lib/extras/codec.h new file mode 100644 index 0000000..fcd81bd --- /dev/null +++ b/lib/extras/codec.h @@ -0,0 +1,94 @@ +// 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_H_ +#define LIB_EXTRAS_CODEC_H_ + +// Facade for image encoders/decoders (PNG, PNM, ...). + +#include +#include + +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.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" +#include "lib/jxl/field_encodings.h" // MakeBit + +namespace jxl { + +// Codecs supported by CodecInOut::Encode. +enum class Codec : uint32_t { + kUnknown, // for CodecFromExtension + kPNG, + kPNM, + kPGX, + kJPG, + kGIF, + kEXR, + kPSD +}; + +static inline constexpr uint64_t EnumBits(Codec /*unused*/) { + // Return only fully-supported codecs (kGIF is decode-only). + return MakeBit(Codec::kPNM) | MakeBit(Codec::kPNG) +#if JPEGXL_ENABLE_JPEG + | MakeBit(Codec::kJPG) +#endif +#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(const std::string& extension, + size_t* JXL_RESTRICT bits_per_sample); + +// Decodes "bytes" and sets io->metadata.m. +// color_space_hint may specify the color space, otherwise, defaults to sRGB. +Status SetFromBytes(const Span bytes, + const ColorHints& color_hints, CodecInOut* io, + ThreadPool* pool, Codec* orig_codec); +// Helper function to use no color_space_hint. +JXL_INLINE Status SetFromBytes(const Span bytes, CodecInOut* io, + ThreadPool* pool = nullptr, + Codec* orig_codec = nullptr) { + return SetFromBytes(bytes, ColorHints(), io, pool, orig_codec); +} + +// Reads from file and calls SetFromBytes. +Status SetFromFile(const std::string& pathname, const ColorHints& color_hints, + CodecInOut* io, ThreadPool* pool = nullptr, + Codec* orig_codec = nullptr); + +// Replaces "bytes" with an encoding of pixels transformed from c_current +// color space to c_desired. +Status Encode(const CodecInOut& io, Codec codec, const ColorEncoding& c_desired, + size_t bits_per_sample, PaddedBytes* bytes, + ThreadPool* pool = nullptr); + +// Deduces codec, calls Encode and writes to file. +Status EncodeToFile(const CodecInOut& io, const ColorEncoding& c_desired, + size_t bits_per_sample, const std::string& pathname, + ThreadPool* pool = nullptr); +// Same, but defaults to metadata.original color_encoding and bits_per_sample. +Status EncodeToFile(const CodecInOut& io, const std::string& pathname, + ThreadPool* pool = nullptr); + +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_H_ diff --git a/lib/extras/codec_apng.cc b/lib/extras/codec_apng.cc new file mode 100644 index 0000000..196bddf --- /dev/null +++ b/lib/extras/codec_apng.cc @@ -0,0 +1,409 @@ +// 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_apng.h" + +// Parts of this code are taken from apngdis, which has the following license: +/* APNG Disassembler 2.8 + * + * Deconstructs APNG files into individual frames. + * + * http://apngdis.sourceforge.net + * + * Copyright (c) 2010-2015 Max Stepin + * maxst at users.sourceforge.net + * + * zlib license + * ------------ + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * + */ + +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/luminance.h" +#include "png.h" /* original (unpatched) libpng is ok */ + +namespace jxl { +namespace extras { + +namespace { + +constexpr bool isAbc(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} +#define notabc(c) ((c) < 65 || (c) > 122 || ((c) > 90 && (c) < 97)) + +constexpr uint32_t kId_IHDR = 0x52444849; +constexpr uint32_t kId_acTL = 0x4C546361; +constexpr uint32_t kId_fcTL = 0x4C546366; +constexpr uint32_t kId_IDAT = 0x54414449; +constexpr uint32_t kId_fdAT = 0x54416466; +constexpr uint32_t kId_IEND = 0x444E4549; + +struct CHUNK { + unsigned char* p; + unsigned int size; +}; + +struct APNGFrame { + unsigned char *p, **rows; + unsigned int w, h, delay_num, delay_den; +}; + +struct Reader { + const uint8_t* next; + const uint8_t* last; + bool Read(void* data, size_t len) { + size_t cap = last - next; + size_t to_copy = std::min(cap, len); + memcpy(data, next, to_copy); + next += to_copy; + return (len == to_copy); + } + bool Eof() { return next == last; } +}; + +const unsigned long cMaxPNGSize = 1000000UL; +const size_t kMaxPNGChunkSize = 100000000; // 100 MB + +void info_fn(png_structp png_ptr, png_infop info_ptr) { + png_set_expand(png_ptr); + png_set_strip_16(png_ptr); + png_set_gray_to_rgb(png_ptr); + png_set_palette_to_rgb(png_ptr); + png_set_add_alpha(png_ptr, 0xff, PNG_FILLER_AFTER); + (void)png_set_interlace_handling(png_ptr); + png_read_update_info(png_ptr, info_ptr); +} + +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); + png_progressive_combine_row(png_ptr, frame->rows[row_num], new_row); +} + +inline unsigned int read_chunk(Reader* r, CHUNK* pChunk) { + unsigned char len[4]; + pChunk->size = 0; + pChunk->p = 0; + if (r->Read(&len, 4)) { + const auto size = png_get_uint_32(len); + // Check first, to avoid overflow. + if (size > kMaxPNGChunkSize) { + JXL_WARNING("APNG chunk size is too big"); + return 0; + } + pChunk->size = size + 12; + pChunk->p = new unsigned char[pChunk->size]; + memcpy(pChunk->p, len, 4); + if (r->Read(pChunk->p + 4, pChunk->size - 4)) { + return *(unsigned int*)(pChunk->p + 4); + } + } + return 0; +} + +int processing_start(png_structp& png_ptr, png_infop& info_ptr, void* frame_ptr, + bool hasInfo, CHUNK& chunkIHDR, + std::vector& 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); + info_ptr = png_create_info_struct(png_ptr); + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + return 1; + } + + 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); + + png_process_data(png_ptr, info_ptr, header, 8); + png_process_data(png_ptr, info_ptr, chunkIHDR.p, chunkIHDR.size); + + if (hasInfo) { + for (unsigned int i = 0; i < chunksInfo.size(); i++) { + png_process_data(png_ptr, info_ptr, chunksInfo[i].p, chunksInfo[i].size); + } + } + return 0; +} + +int processing_data(png_structp png_ptr, png_infop info_ptr, unsigned char* p, + unsigned int size) { + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + return 1; + } + + png_process_data(png_ptr, info_ptr, p, size); + return 0; +} + +int processing_finish(png_structp png_ptr, png_infop info_ptr) { + unsigned char footer[12] = {0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130}; + + if (!png_ptr || !info_ptr) return 1; + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + return 1; + } + + png_process_data(png_ptr, info_ptr, footer, 12); + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + + return 0; +} + +} // namespace + +Status DecodeImageAPNG(Span bytes, const ColorHints& color_hints, + ThreadPool* pool, CodecInOut* io) { + Reader r; + unsigned int id, i, j, w, h, w0, h0, x0, y0; + unsigned int delay_num, delay_den, dop, bop, rowbytes, imagesize; + unsigned char sig[8]; + png_structp png_ptr; + png_infop info_ptr; + CHUNK chunk; + CHUNK chunkIHDR; + std::vector chunksInfo; + bool isAnimated = false; + bool skipFirst = false; + bool hasInfo = false; + bool all_dispose_bg = true; + APNGFrame frameRaw = {}; + + r = {bytes.data(), bytes.data() + bytes.size()}; + // Not an aPNG => not an error + unsigned char png_signature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + if (!r.Read(sig, 8) || memcmp(sig, png_signature, 8) != 0) { + return false; + } + id = read_chunk(&r, &chunkIHDR); + + io->frames.clear(); + io->dec_pixels = 0; + io->metadata.m.SetUintSamples(8); + io->metadata.m.SetAlphaBits(8); + io->metadata.m.color_encoding = + ColorEncoding::SRGB(); // todo: get data from png metadata + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/true, + /*is_gray=*/false, io)); + + bool errorstate = true; + if (id == kId_IHDR && chunkIHDR.size == 25) { + w0 = w = png_get_uint_32(chunkIHDR.p + 8); + h0 = h = png_get_uint_32(chunkIHDR.p + 12); + + if (w > cMaxPNGSize || h > cMaxPNGSize) { + return false; + } + + x0 = 0; + y0 = 0; + delay_num = 1; + delay_den = 10; + dop = 0; + bop = 0; + rowbytes = w * 4; + imagesize = h * rowbytes; + + frameRaw.p = new unsigned char[imagesize]; + frameRaw.rows = new png_bytep[h * sizeof(png_bytep)]; + for (j = 0; j < h; j++) frameRaw.rows[j] = frameRaw.p + j * rowbytes; + + if (!processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo, + chunkIHDR, chunksInfo)) { + bool last_base_was_none = true; + while (!r.Eof()) { + id = read_chunk(&r, &chunk); + if (!id) break; + JXL_ASSERT(chunk.p != nullptr); + + if (id == kId_acTL && !hasInfo && !isAnimated) { + isAnimated = true; + skipFirst = true; + io->metadata.m.have_animation = true; + io->metadata.m.animation.tps_numerator = 1000; + } else if (id == kId_IEND || + (id == kId_fcTL && (!hasInfo || isAnimated))) { + if (hasInfo) { + if (!processing_finish(png_ptr, info_ptr)) { + ImageBundle bundle(&io->metadata.m); + bundle.duration = delay_num * 1000 / delay_den; + bundle.origin.x0 = x0; + bundle.origin.y0 = y0; + // TODO(veluca): this could in principle be implemented. + if (last_base_was_none && !all_dispose_bg && + (x0 != 0 || y0 != 0 || w0 != w || h0 != h || bop != 0)) { + return JXL_FAILURE( + "APNG with dispose-to-0 is not supported for non-full or " + "blended frames"); + } + switch (dop) { + case 0: + bundle.use_for_next_frame = true; + last_base_was_none = false; + all_dispose_bg = false; + break; + case 2: + bundle.use_for_next_frame = false; + all_dispose_bg = false; + break; + default: + bundle.use_for_next_frame = false; + last_base_was_none = true; + } + bundle.blend = bop != 0; + io->dec_pixels += w0 * h0; + + Image3F sub_frame(w0, h0); + ImageF sub_frame_alpha(w0, h0); + for (size_t y = 0; y < h0; ++y) { + float* const JXL_RESTRICT row_r = sub_frame.PlaneRow(0, y); + float* const JXL_RESTRICT row_g = sub_frame.PlaneRow(1, y); + float* const JXL_RESTRICT row_b = sub_frame.PlaneRow(2, y); + float* const JXL_RESTRICT row_alpha = sub_frame_alpha.Row(y); + uint8_t* const f = frameRaw.rows[y]; + for (size_t x = 0; x < w0; ++x) { + if (f[4 * x + 3] == 0) { + row_alpha[x] = 0; + row_r[x] = 0; + row_g[x] = 0; + row_b[x] = 0; + continue; + } + row_r[x] = f[4 * x + 0] * (1.f / 255); + row_g[x] = f[4 * x + 1] * (1.f / 255); + row_b[x] = f[4 * x + 2] * (1.f / 255); + row_alpha[x] = f[4 * x + 3] * (1.f / 255); + } + } + bundle.SetFromImage(std::move(sub_frame), ColorEncoding::SRGB()); + bundle.SetAlpha(std::move(sub_frame_alpha), + /*alpha_is_premultiplied=*/false); + io->frames.push_back(std::move(bundle)); + } else { + delete[] chunk.p; + break; + } + } + + if (id == kId_IEND) { + errorstate = false; + break; + } + // At this point the old frame is done. Let's start a new one. + w0 = png_get_uint_32(chunk.p + 12); + h0 = png_get_uint_32(chunk.p + 16); + x0 = png_get_uint_32(chunk.p + 20); + y0 = png_get_uint_32(chunk.p + 24); + delay_num = png_get_uint_16(chunk.p + 28); + delay_den = png_get_uint_16(chunk.p + 30); + dop = chunk.p[32]; + bop = chunk.p[33]; + + if (!delay_den) delay_den = 100; + + if (w0 > cMaxPNGSize || h0 > cMaxPNGSize || x0 > cMaxPNGSize || + y0 > cMaxPNGSize || x0 + w0 > w || y0 + h0 > h || dop > 2 || + bop > 1) { + delete[] chunk.p; + break; + } + + if (hasInfo) { + memcpy(chunkIHDR.p + 8, chunk.p + 12, 8); + if (processing_start(png_ptr, info_ptr, (void*)&frameRaw, hasInfo, + chunkIHDR, chunksInfo)) { + delete[] chunk.p; + break; + } + } else + skipFirst = false; + + if (io->frames.size() == (skipFirst ? 1 : 0)) { + bop = 0; + if (dop == 2) dop = 1; + } + } else if (id == kId_IDAT) { + hasInfo = true; + if (processing_data(png_ptr, info_ptr, chunk.p, chunk.size)) { + delete[] chunk.p; + break; + } + } else if (id == kId_fdAT && isAnimated) { + png_save_uint_32(chunk.p + 4, chunk.size - 16); + memcpy(chunk.p + 8, "IDAT", 4); + if (processing_data(png_ptr, info_ptr, chunk.p + 4, chunk.size - 4)) { + delete[] chunk.p; + break; + } + } else if (!isAbc(chunk.p[4]) || !isAbc(chunk.p[5]) || + !isAbc(chunk.p[6]) || !isAbc(chunk.p[7])) { + delete[] chunk.p; + break; + } else if (!hasInfo) { + if (processing_data(png_ptr, info_ptr, chunk.p, chunk.size)) { + delete[] chunk.p; + break; + } + chunksInfo.push_back(chunk); + continue; + } + delete[] chunk.p; + } + } + delete[] frameRaw.rows; + delete[] frameRaw.p; + } + + for (i = 0; i < chunksInfo.size(); i++) delete[] chunksInfo[i].p; + + chunksInfo.clear(); + delete[] chunkIHDR.p; + + if (errorstate) return false; + SetIntensityTarget(io); + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_apng.h b/lib/extras/codec_apng.h new file mode 100644 index 0000000..10da778 --- /dev/null +++ b/lib/extras/codec_apng.h @@ -0,0 +1,31 @@ +// 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_APNG_H_ +#define LIB_EXTRAS_CODEC_APNG_H_ + +// Decodes APNG images in memory. + +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `io`. color_space_hint is ignored. +Status DecodeImageAPNG(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_APNG_H_ diff --git a/lib/extras/codec_exr.cc b/lib/extras/codec_exr.cc new file mode 100644 index 0000000..7e09a7f --- /dev/null +++ b/lib/extras/codec_exr.cc @@ -0,0 +1,352 @@ +// 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_exr.h" + +#include +#include +#include +#include + +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" + +namespace jxl { +namespace extras { + +namespace { + +namespace OpenEXR = OPENEXR_IMF_NAMESPACE; +namespace Imath = IMATH_NAMESPACE; + +// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using +// uint64_t as recommended causes build failures with previous OpenEXR versions +// on macOS, where the definition for OpenEXR::Int64 was actually not equivalent +// to uint64_t. This alternative should work in all cases. +using ExrInt64 = decltype(std::declval().tellg()); + +constexpr int kExrBitsPerSample = 16; +constexpr int kExrAlphaBits = 16; + +float GetIntensityTarget(const CodecInOut& io, + const OpenEXR::Header& exr_header) { + if (OpenEXR::hasWhiteLuminance(exr_header)) { + const float exr_luminance = OpenEXR::whiteLuminance(exr_header); + if (io.target_nits != 0) { + JXL_WARNING( + "overriding OpenEXR whiteLuminance of %g with user-specified value " + "of %g", + exr_luminance, io.target_nits); + return io.target_nits; + } + return exr_luminance; + } + if (io.target_nits != 0) { + return io.target_nits; + } + JXL_WARNING( + "no OpenEXR whiteLuminance tag found and no intensity_target specified, " + "defaulting to %g", + kDefaultIntensityTarget); + return kDefaultIntensityTarget; +} + +size_t GetNumThreads(ThreadPool* pool) { + size_t exr_num_threads = 1; + RunOnPool( + pool, 0, 1, + [&](size_t num_threads) { + exr_num_threads = num_threads; + return true; + }, + [&](const int /* task */, const int /*thread*/) {}, + "DecodeImageEXRThreads"); + return exr_num_threads; +} + +class InMemoryIStream : public OpenEXR::IStream { + public: + // The data pointed to by `bytes` must outlive the InMemoryIStream. + explicit InMemoryIStream(const Span bytes) + : IStream(/*fileName=*/""), bytes_(bytes) {} + + bool isMemoryMapped() const override { return true; } + char* readMemoryMapped(const int n) override { + JXL_ASSERT(pos_ + n <= bytes_.size()); + char* const result = + const_cast(reinterpret_cast(bytes_.data() + pos_)); + pos_ += n; + return result; + } + bool read(char c[], const int n) override { + std::copy_n(readMemoryMapped(n), n, c); + return pos_ < bytes_.size(); + } + + ExrInt64 tellg() override { return pos_; } + void seekg(const ExrInt64 pos) override { + JXL_ASSERT(pos + 1 <= bytes_.size()); + pos_ = pos; + } + + private: + const Span bytes_; + size_t pos_ = 0; +}; + +class InMemoryOStream : public OpenEXR::OStream { + public: + // `bytes` must outlive the InMemoryOStream. + explicit InMemoryOStream(PaddedBytes* const bytes) + : OStream(/*fileName=*/""), bytes_(*bytes) {} + + void write(const char c[], const int n) override { + if (bytes_.size() < pos_ + n) { + bytes_.resize(pos_ + n); + } + std::copy_n(c, n, bytes_.begin() + pos_); + pos_ += n; + } + + ExrInt64 tellp() override { return pos_; } + void seekp(const ExrInt64 pos) override { + if (bytes_.size() + 1 < pos) { + bytes_.resize(pos - 1); + } + pos_ = pos; + } + + private: + PaddedBytes& bytes_; + size_t pos_ = 0; +}; + +} // namespace + +Status DecodeImageEXR(Span bytes, const ColorHints& color_hints, + ThreadPool* pool, CodecInOut* io) { + // 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 + std::unique_ptr input_ptr; + try { + input_ptr.reset(new OpenEXR::RgbaInputFile(is)); + } catch (...) { + return JXL_FAILURE("OpenEXR failed to parse input"); + } + OpenEXR::RgbaInputFile& input = *input_ptr; +#else + OpenEXR::RgbaInputFile input(is); +#endif + + if ((input.channels() & OpenEXR::RgbaChannels::WRITE_RGB) != + OpenEXR::RgbaChannels::WRITE_RGB) { + return JXL_FAILURE("only RGB OpenEXR files are supported"); + } + const bool has_alpha = (input.channels() & OpenEXR::RgbaChannels::WRITE_A) == + OpenEXR::RgbaChannels::WRITE_A; + + const float intensity_target = GetIntensityTarget(*io, input.header()); + + auto image_size = input.displayWindow().size(); + // Size is computed as max - min, but both bounds are inclusive. + ++image_size.x; + ++image_size.y; + Image3F image(image_size.x, image_size.y); + ZeroFillImage(&image); + ImageF alpha; + if (has_alpha) { + alpha = ImageF(image_size.x, image_size.y); + FillImage(1.f, &alpha); + } + + const int row_size = input.dataWindow().size().x + 1; + // Number of rows to read at a time. + // https://www.openexr.com/documentation/ReadingAndWritingImageFiles.pdf + // recommends reading the whole file at once. + const int y_chunk_size = input.displayWindow().size().y + 1; + std::vector input_rows(row_size * y_chunk_size); + for (int start_y = + std::max(input.dataWindow().min.y, input.displayWindow().min.y); + start_y <= + std::min(input.dataWindow().max.y, input.displayWindow().max.y); + start_y += y_chunk_size) { + // Inclusive. + const int end_y = std::min( + start_y + y_chunk_size - 1, + std::min(input.dataWindow().max.y, input.displayWindow().max.y)); + input.setFrameBuffer( + input_rows.data() - input.dataWindow().min.x - start_y * row_size, + /*xStride=*/1, /*yStride=*/row_size); + input.readPixels(start_y, end_y); + RunOnPool( + pool, start_y, end_y + 1, ThreadPool::SkipInit(), + [&](const int exr_y, const int /*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]; + float* const JXL_RESTRICT rows[] = { + image.PlaneRow(0, image_y), + image.PlaneRow(1, image_y), + image.PlaneRow(2, image_y), + }; + float* const JXL_RESTRICT alpha_row = + has_alpha ? alpha.Row(image_y) : nullptr; + 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; + const OpenEXR::Rgba& pixel = + input_row[exr_x - input.dataWindow().min.x]; + rows[0][image_x] = pixel.r; + rows[1][image_x] = pixel.g; + rows[2][image_x] = pixel.b; + if (has_alpha) { + alpha_row[image_x] = pixel.a; + } + } + }, + "DecodeImageEXR"); + } + + ColorEncoding color_encoding; + color_encoding.tf.SetTransferFunction(TransferFunction::kLinear); + color_encoding.SetColorSpace(ColorSpace::kRGB); + PrimariesCIExy primaries = ColorEncoding::SRGB().GetPrimaries(); + CIExy white_point = ColorEncoding::SRGB().GetWhitePoint(); + if (OpenEXR::hasChromaticities(input.header())) { + const auto& chromaticities = OpenEXR::chromaticities(input.header()); + primaries.r.x = chromaticities.red.x; + primaries.r.y = chromaticities.red.y; + primaries.g.x = chromaticities.green.x; + primaries.g.y = chromaticities.green.y; + primaries.b.x = chromaticities.blue.x; + primaries.b.y = chromaticities.blue.y; + white_point.x = chromaticities.white.x; + white_point.y = chromaticities.white.y; + } + JXL_RETURN_IF_ERROR(color_encoding.SetPrimaries(primaries)); + JXL_RETURN_IF_ERROR(color_encoding.SetWhitePoint(white_point)); + JXL_RETURN_IF_ERROR(color_encoding.CreateICC()); + + io->metadata.m.bit_depth.bits_per_sample = kExrBitsPerSample; + // EXR uses binary16 or binary32 floating point format. + io->metadata.m.bit_depth.exponent_bits_per_sample = + kExrBitsPerSample == 16 ? 5 : 8; + io->metadata.m.bit_depth.floating_point_sample = true; + io->SetFromImage(std::move(image), color_encoding); + io->metadata.m.color_encoding = color_encoding; + io->metadata.m.SetIntensityTarget(intensity_target); + if (has_alpha) { + io->metadata.m.SetAlphaBits(kExrAlphaBits, /*alpha_is_premultiplied=*/true); + io->Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/true); + } + return true; +} + +Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, + ThreadPool* pool, PaddedBytes* bytes) { + // As in `DecodeImageEXR`, `pool` is only used for pixel conversion, not for + // actual OpenEXR I/O. + OpenEXR::setGlobalThreadCount(GetNumThreads(pool)); + + ColorEncoding c_linear = c_desired; + c_linear.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_RETURN_IF_ERROR(c_linear.CreateICC()); + ImageMetadata metadata = io->metadata.m; + ImageBundle store(&metadata); + const ImageBundle* linear; + JXL_RETURN_IF_ERROR( + TransformIfNeeded(io->Main(), c_linear, pool, &store, &linear)); + + const bool has_alpha = io->Main().HasAlpha(); + const bool alpha_is_premultiplied = io->Main().AlphaIsPremultiplied(); + + OpenEXR::Header header(io->xsize(), io->ysize()); + const PrimariesCIExy& primaries = c_linear.HasPrimaries() + ? c_linear.GetPrimaries() + : ColorEncoding::SRGB().GetPrimaries(); + OpenEXR::Chromaticities chromaticities; + chromaticities.red = Imath::V2f(primaries.r.x, primaries.r.y); + chromaticities.green = Imath::V2f(primaries.g.x, primaries.g.y); + chromaticities.blue = Imath::V2f(primaries.b.x, primaries.b.y); + chromaticities.white = + Imath::V2f(c_linear.GetWhitePoint().x, c_linear.GetWhitePoint().y); + OpenEXR::addChromaticities(header, chromaticities); + OpenEXR::addWhiteLuminance(header, io->metadata.m.IntensityTarget()); + + // Ensure that the destructor of RgbaOutputFile has run before we look at the + // size of `bytes`. + { + InMemoryOStream os(bytes); + OpenEXR::RgbaOutputFile output( + os, header, has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB); + // How many rows to write at once. Again, the OpenEXR documentation + // recommends writing the whole image in one call. + const int y_chunk_size = io->ysize(); + std::vector output_rows(io->xsize() * y_chunk_size); + + for (size_t start_y = 0; start_y < io->ysize(); start_y += y_chunk_size) { + // Inclusive. + const size_t end_y = + std::min(start_y + y_chunk_size - 1, io->ysize() - 1); + output.setFrameBuffer(output_rows.data() - start_y * io->xsize(), + /*xStride=*/1, /*yStride=*/io->xsize()); + RunOnPool( + pool, start_y, end_y + 1, ThreadPool::SkipInit(), + [&](const int y, const int /*thread*/) { + const float* const JXL_RESTRICT input_rows[] = { + linear->color().ConstPlaneRow(0, y), + linear->color().ConstPlaneRow(1, y), + linear->color().ConstPlaneRow(2, y), + }; + OpenEXR::Rgba* const JXL_RESTRICT row_data = + &output_rows[(y - start_y) * io->xsize()]; + if (has_alpha) { + const float* const JXL_RESTRICT alpha_row = + io->Main().alpha().ConstRow(y); + if (alpha_is_premultiplied) { + for (size_t x = 0; x < io->xsize(); ++x) { + row_data[x] = + OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], + input_rows[2][x], alpha_row[x]); + } + } else { + for (size_t x = 0; x < io->xsize(); ++x) { + row_data[x] = OpenEXR::Rgba(alpha_row[x] * input_rows[0][x], + alpha_row[x] * input_rows[1][x], + alpha_row[x] * input_rows[2][x], + alpha_row[x]); + } + } + } else { + for (size_t x = 0; x < io->xsize(); ++x) { + row_data[x] = OpenEXR::Rgba(input_rows[0][x], input_rows[1][x], + input_rows[2][x], 1.f); + } + } + }, + "EncodeImageEXR"); + output.writePixels(/*numScanLines=*/end_y - start_y + 1); + } + } + + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_exr.h b/lib/extras/codec_exr.h new file mode 100644 index 0000000..7a50b56 --- /dev/null +++ b/lib/extras/codec_exr.h @@ -0,0 +1,34 @@ +// 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_EXR_H_ +#define LIB_EXTRAS_CODEC_EXR_H_ + +// Encodes OpenEXR images in memory. + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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`. color_hints are ignored. +Status DecodeImageEXR(Span bytes, const ColorHints& color_hints, + ThreadPool* pool, CodecInOut* io); + +// Transforms from io->c_current to `c_desired` (with the transfer function set +// to linear as that is the OpenEXR convention) and encodes into `bytes`. +Status EncodeImageEXR(const CodecInOut* io, const ColorEncoding& c_desired, + ThreadPool* pool, PaddedBytes* bytes); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_EXR_H_ diff --git a/lib/extras/codec_gif.cc b/lib/extras/codec_gif.cc new file mode 100644 index 0000000..4c6cdb5 --- /dev/null +++ b/lib/extras/codec_gif.cc @@ -0,0 +1,341 @@ +// 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_gif.h" + +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/luminance.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { +namespace extras { + +namespace { + +struct ReadState { + Span bytes; +}; + +struct DGifCloser { + void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); } +}; +using GifUniquePtr = std::unique_ptr; + +// Gif does not support partial transparency, so this considers anything non-0 +// as opaque. +bool AllOpaque(const ImageF& alpha) { + for (size_t y = 0; y < alpha.ysize(); ++y) { + const float* const JXL_RESTRICT row = alpha.ConstRow(y); + for (size_t x = 0; x < alpha.xsize(); ++x) { + if (row[x] == 0.f) { + return false; + } + } + } + return true; +} + +} // namespace + +Status DecodeImageGIF(Span bytes, const ColorHints& color_hints, + ThreadPool* pool, CodecInOut* io) { + int error = GIF_OK; + ReadState state = {bytes}; + const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes, + int n) { + ReadState* const state = reinterpret_cast(gif->UserData); + // giflib API requires the input size `n` to be signed int. + if (static_cast(n) > state->bytes.size()) { + n = state->bytes.size(); + } + memcpy(bytes, state->bytes.data(), n); + state->bytes.remove_prefix(n); + return n; + }; + GifUniquePtr gif(DGifOpen(&state, ReadFromSpan, &error)); + if (gif == nullptr) { + if (error == D_GIF_ERR_NOT_GIF_FILE) { + // Not an error. + return false; + } else { + return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(error)); + } + } + error = DGifSlurp(gif.get()); + if (error != GIF_OK) { + return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(gif->Error)); + } + + msan::UnpoisonMemory(gif.get(), sizeof(*gif)); + if (gif->SColorMap) { + msan::UnpoisonMemory(gif->SColorMap, sizeof(*gif->SColorMap)); + msan::UnpoisonMemory( + gif->SColorMap->Colors, + sizeof(*gif->SColorMap->Colors) * gif->SColorMap->ColorCount); + } + msan::UnpoisonMemory(gif->SavedImages, + sizeof(*gif->SavedImages) * gif->ImageCount); + + const SizeConstraints* constraints = &io->constraints; + + JXL_RETURN_IF_ERROR( + VerifyDimensions(constraints, gif->SWidth, gif->SHeight)); + uint64_t total_pixel_count = + static_cast(gif->SWidth) * gif->SHeight; + for (int i = 0; i < gif->ImageCount; ++i) { + const SavedImage& image = gif->SavedImages[i]; + uint32_t w = image.ImageDesc.Width; + uint32_t h = image.ImageDesc.Height; + JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, w, h)); + uint64_t pixel_count = static_cast(w) * h; + if (total_pixel_count + pixel_count < total_pixel_count) { + return JXL_FAILURE("Image too big"); + } + total_pixel_count += pixel_count; + if (total_pixel_count > constraints->dec_max_pixels) { + return JXL_FAILURE("Image too big"); + } + } + + if (!gif->SColorMap) { + for (int i = 0; i < gif->ImageCount; ++i) { + if (!gif->SavedImages[i].ImageDesc.ColorMap) { + return JXL_FAILURE("Missing GIF color map"); + } + } + } + + if (gif->ImageCount > 1) { + io->metadata.m.have_animation = true; + // Delays in GIF are specified in 100ths of a second. + io->metadata.m.animation.tps_numerator = 100; + } + + io->frames.clear(); + io->frames.reserve(gif->ImageCount); + io->dec_pixels = 0; + + io->metadata.m.SetUintSamples(8); + io->metadata.m.SetAlphaBits(0); + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + /*is_gray=*/false, io)); + + Image3F canvas(gif->SWidth, gif->SHeight); + io->SetSize(gif->SWidth, gif->SHeight); + ImageF alpha(gif->SWidth, gif->SHeight); + GifColorType background_color; + if (gif->SColorMap == nullptr) { + background_color = {0, 0, 0}; + } else { + if (gif->SBackGroundColor >= gif->SColorMap->ColorCount) { + return JXL_FAILURE("GIF specifies out-of-bounds background color"); + } + background_color = gif->SColorMap->Colors[gif->SBackGroundColor]; + } + FillPlane(background_color.Red, &canvas.Plane(0)); + FillPlane(background_color.Green, &canvas.Plane(1)); + FillPlane(background_color.Blue, &canvas.Plane(2)); + ZeroFillImage(&alpha); + + Rect previous_rect_if_restore_to_background; + + bool has_alpha = false; + bool replace = true; + bool last_base_was_none = true; + for (int i = 0; i < gif->ImageCount; ++i) { + const SavedImage& image = gif->SavedImages[i]; + msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) * + image.ImageDesc.Width * + image.ImageDesc.Height); + const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top, + image.ImageDesc.Width, image.ImageDesc.Height); + io->dec_pixels += image_rect.xsize() * image_rect.ysize(); + Rect total_rect; + if (previous_rect_if_restore_to_background.xsize() != 0 || + previous_rect_if_restore_to_background.ysize() != 0) { + const size_t xbegin = std::min( + image_rect.x0(), previous_rect_if_restore_to_background.x0()); + const size_t ybegin = std::min( + image_rect.y0(), previous_rect_if_restore_to_background.y0()); + const size_t xend = + std::max(image_rect.x0() + image_rect.xsize(), + previous_rect_if_restore_to_background.x0() + + previous_rect_if_restore_to_background.xsize()); + const size_t yend = + std::max(image_rect.y0() + image_rect.ysize(), + previous_rect_if_restore_to_background.y0() + + previous_rect_if_restore_to_background.ysize()); + total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin); + previous_rect_if_restore_to_background = Rect(); + replace = true; + } else { + total_rect = image_rect; + replace = false; + } + if (!image_rect.IsInside(canvas)) { + return JXL_FAILURE("GIF frame extends outside of the canvas"); + } + const ColorMapObject* const color_map = + image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap; + JXL_CHECK(color_map); + msan::UnpoisonMemory(color_map, sizeof(*color_map)); + msan::UnpoisonMemory(color_map->Colors, + sizeof(*color_map->Colors) * color_map->ColorCount); + GraphicsControlBlock gcb; + DGifSavedExtensionToGCB(gif.get(), i, &gcb); + msan::UnpoisonMemory(&gcb, sizeof(gcb)); + + ImageBundle bundle(&io->metadata.m); + if (io->metadata.m.have_animation) { + bundle.duration = gcb.DelayTime; + bundle.origin.x0 = total_rect.x0(); + bundle.origin.y0 = total_rect.y0(); + if (last_base_was_none) { + replace = true; + } + bundle.blend = !replace; + // TODO(veluca): this could in principle be implemented. + if (last_base_was_none && + (total_rect.x0() != 0 || total_rect.y0() != 0 || + total_rect.xsize() != canvas.xsize() || + total_rect.ysize() != canvas.ysize() || !replace)) { + return JXL_FAILURE( + "GIF with dispose-to-0 is not supported for non-full or " + "blended frames"); + } + switch (gcb.DisposalMode) { + case DISPOSE_DO_NOT: + case DISPOSE_BACKGROUND: + bundle.use_for_next_frame = true; + last_base_was_none = false; + break; + case DISPOSE_PREVIOUS: + bundle.use_for_next_frame = false; + break; + default: + bundle.use_for_next_frame = false; + last_base_was_none = true; + } + } + Image3F frame = CopyImage(canvas); + ImageF frame_alpha = CopyImage(alpha); + for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) { + float* const JXL_RESTRICT row_r = image_rect.Row(&frame.Plane(0), y); + float* const JXL_RESTRICT row_g = image_rect.Row(&frame.Plane(1), y); + float* const JXL_RESTRICT row_b = image_rect.Row(&frame.Plane(2), y); + float* const JXL_RESTRICT row_alpha = image_rect.Row(&frame_alpha, y); + for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) { + const GifByteType byte = image.RasterBits[byte_index]; + if (byte >= color_map->ColorCount) { + return JXL_FAILURE("GIF color is out of bounds"); + } + if (byte == gcb.TransparentColor) continue; + GifColorType color = color_map->Colors[byte]; + row_alpha[x] = 1.f; + row_r[x] = (1.f / 255) * color.Red; + row_g[x] = (1.f / 255) * color.Green; + row_b[x] = (1.f / 255) * color.Blue; + } + } + Image3F sub_frame(total_rect.xsize(), total_rect.ysize()); + ImageF sub_frame_alpha(total_rect.xsize(), total_rect.ysize()); + bool blend_alpha = false; + if (replace) { + CopyImageTo(total_rect, frame, &sub_frame); + CopyImageTo(total_rect, frame_alpha, &sub_frame_alpha); + } else { + for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) { + float* const JXL_RESTRICT row_r = sub_frame.PlaneRow(0, y); + float* const JXL_RESTRICT row_g = sub_frame.PlaneRow(1, y); + float* const JXL_RESTRICT row_b = sub_frame.PlaneRow(2, y); + float* const JXL_RESTRICT row_alpha = sub_frame_alpha.Row(y); + for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) { + const GifByteType byte = image.RasterBits[byte_index]; + if (byte > color_map->ColorCount) { + return JXL_FAILURE("GIF color is out of bounds"); + } + if (byte == gcb.TransparentColor) { + row_alpha[x] = 0; + row_r[x] = 0; + row_g[x] = 0; + row_b[x] = 0; + blend_alpha = + true; // need to use alpha channel if BlendMode blend is used + continue; + } + GifColorType color = color_map->Colors[byte]; + row_alpha[x] = 1.f; + row_r[x] = (1.f / 255) * color.Red; + row_g[x] = (1.f / 255) * color.Green; + row_b[x] = (1.f / 255) * color.Blue; + } + } + } + bundle.SetFromImage(std::move(sub_frame), ColorEncoding::SRGB()); + if (has_alpha || !AllOpaque(frame_alpha) || blend_alpha) { + if (!has_alpha) { + has_alpha = true; + io->metadata.m.SetAlphaBits(8); + for (ImageBundle& previous_frame : io->frames) { + ImageF previous_alpha(previous_frame.xsize(), previous_frame.ysize()); + FillImage(1.f, &previous_alpha); + previous_frame.SetAlpha(std::move(previous_alpha), + /*alpha_is_premultiplied=*/false); + } + } + bundle.SetAlpha(std::move(sub_frame_alpha), + /*alpha_is_premultiplied=*/false); + } + io->frames.push_back(std::move(bundle)); + switch (gcb.DisposalMode) { + case DISPOSE_DO_NOT: + canvas = std::move(frame); + alpha = std::move(frame_alpha); + break; + + case DISPOSE_BACKGROUND: + FillPlane((1.f / 255) * background_color.Red, &canvas.Plane(0), + image_rect); + FillPlane((1.f / 255) * background_color.Green, &canvas.Plane(1), + image_rect); + FillPlane((1.f / 255) * background_color.Blue, &canvas.Plane(2), + image_rect); + FillPlane(0.f, &alpha, image_rect); + previous_rect_if_restore_to_background = image_rect; + break; + + case DISPOSE_PREVIOUS: + break; + + case DISPOSAL_UNSPECIFIED: + default: + FillPlane((1.f / 255) * background_color.Red, &canvas.Plane(0)); + FillPlane((1.f / 255) * background_color.Green, + &canvas.Plane(1)); + FillPlane((1.f / 255) * background_color.Blue, &canvas.Plane(2)); + ZeroFillImage(&alpha); + } + } + + SetIntensityTarget(io); + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_gif.h b/lib/extras/codec_gif.h new file mode 100644 index 0000000..71a05c6 --- /dev/null +++ b/lib/extras/codec_gif.h @@ -0,0 +1,31 @@ +// 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_GIF_H_ +#define LIB_EXTRAS_CODEC_GIF_H_ + +// Decodes GIF images in memory. + +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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" + +namespace jxl { +namespace extras { + +// Decodes `bytes` into `io`. color_hints are ignored. +Status DecodeImageGIF(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_GIF_H_ diff --git a/lib/extras/codec_jpg.cc b/lib/extras/codec_jpg.cc new file mode 100644 index 0000000..aed32a7 --- /dev/null +++ b/lib/extras/codec_jpg.cc @@ -0,0 +1,520 @@ +// 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_jpg.h" + +#include +#include + +#if JPEGXL_ENABLE_JPEG +// After stddef/stdio +#include +#include +#include +#endif // JPEGXL_ENABLE_JPEG + +#include +#include +#include +#include +#include + +#include "lib/extras/time.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/jpeg/enc_jpeg_data_reader.h" +#include "lib/jxl/luminance.h" +#include "lib/jxl/sanitizers.h" +#if JPEGXL_ENABLE_SJPEG +#include "sjpeg.h" +#endif + +namespace jxl { +namespace extras { + +#if JPEGXL_ENABLE_JPEG +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; +constexpr size_t kMaxBytesInMarker = 65533; + +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; + +bool MarkerIsICC(const jpeg_saved_marker_ptr marker) { + return marker->marker == kICCMarker && + marker->data_length >= sizeof kICCSignature + 2 && + std::equal(std::begin(kICCSignature), std::end(kICCSignature), + marker->data); +} +bool MarkerIsExif(const jpeg_saved_marker_ptr marker) { + return marker->marker == kExifMarker && + marker->data_length >= sizeof kExifSignature + 2 && + std::equal(std::begin(kExifSignature), std::end(kExifSignature), + marker->data); +} + +Status ReadICCProfile(jpeg_decompress_struct* const cinfo, + PaddedBytes* const icc) { + constexpr size_t kICCSignatureSize = sizeof kICCSignature; + // ICC signature + uint8_t index + uint8_t max_index. + constexpr size_t kICCHeadSize = kICCSignatureSize + 2; + // Markers are 1-indexed, and we keep them that way in this vector to get a + // convenient 0 at the front for when we compute the offsets later. + std::vector marker_lengths; + int num_markers = 0; + int seen_markers_count = 0; + bool has_num_markers = false; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + // marker is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(marker, sizeof(*marker)); + msan::UnpoisonMemory(marker->data, marker->data_length); + if (!MarkerIsICC(marker)) continue; + + const int current_marker = marker->data[kICCSignatureSize]; + if (current_marker == 0) { + return JXL_FAILURE("inconsistent JPEG ICC marker numbering"); + } + const int current_num_markers = marker->data[kICCSignatureSize + 1]; + if (current_marker > current_num_markers) { + return JXL_FAILURE("inconsistent JPEG ICC marker numbering"); + } + if (has_num_markers) { + if (current_num_markers != num_markers) { + return JXL_FAILURE("inconsistent numbers of JPEG ICC markers"); + } + } else { + num_markers = current_num_markers; + has_num_markers = true; + marker_lengths.resize(num_markers + 1); + } + + size_t marker_length = marker->data_length - kICCHeadSize; + + if (marker_length == 0) { + // NB: if we allow empty chunks, then the next check is incorrect. + return JXL_FAILURE("Empty ICC chunk"); + } + + if (marker_lengths[current_marker] != 0) { + return JXL_FAILURE("duplicate JPEG ICC marker number"); + } + marker_lengths[current_marker] = marker_length; + seen_markers_count++; + } + + if (marker_lengths.empty()) { + // Not an error. + return false; + } + + if (seen_markers_count != num_markers) { + JXL_DASSERT(has_num_markers); + return JXL_FAILURE("Incomplete set of ICC chunks"); + } + + std::vector offsets = std::move(marker_lengths); + std::partial_sum(offsets.begin(), offsets.end(), offsets.begin()); + icc->resize(offsets.back()); + + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + if (!MarkerIsICC(marker)) continue; + const uint8_t* first = marker->data + kICCHeadSize; + uint8_t current_marker = marker->data[kICCSignatureSize]; + size_t offset = offsets[current_marker - 1]; + size_t marker_length = offsets[current_marker] - offset; + std::copy_n(first, marker_length, icc->data() + offset); + } + + return true; +} + +void ReadExif(jpeg_decompress_struct* const cinfo, PaddedBytes* const exif) { + constexpr size_t kExifSignatureSize = sizeof kExifSignature; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + // marker is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(marker, sizeof(*marker)); + msan::UnpoisonMemory(marker->data, marker->data_length); + if (!MarkerIsExif(marker)) continue; + size_t marker_length = marker->data_length - kExifSignatureSize; + exif->resize(marker_length); + std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data()); + return; + } +} + +// 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 = + kMaxBytesInMarker - sizeof kICCSignature - 2; + const int num_markers = + static_cast(DivCeil(icc.size(), kMaxIccBytesInMarker)); + size_t begin = 0; + for (int current_marker = 0; current_marker < num_markers; ++current_marker) { + const size_t length = std::min(kMaxIccBytesInMarker, icc.size() - begin); + jpeg_write_m_header( + cinfo, kICCMarker, + static_cast(length + sizeof kICCSignature + 2)); + for (const unsigned char c : kICCSignature) { + jpeg_write_m_byte(cinfo, c); + } + jpeg_write_m_byte(cinfo, current_marker + 1); + jpeg_write_m_byte(cinfo, num_markers); + for (size_t i = 0; i < length; ++i) { + jpeg_write_m_byte(cinfo, icc[begin]); + ++begin; + } + } +} +void WriteExif(jpeg_compress_struct* const cinfo, const PaddedBytes& exif) { + if (exif.size() < 4) return; + jpeg_write_m_header( + cinfo, kExifMarker, + static_cast(exif.size() - 4 + sizeof kExifSignature)); + for (const unsigned char c : kExifSignature) { + jpeg_write_m_byte(cinfo, c); + } + for (size_t i = 4; i < exif.size(); ++i) { + jpeg_write_m_byte(cinfo, exif[i]); + } +} + +Status SetChromaSubsampling(const YCbCrChromaSubsampling& chroma_subsampling, + jpeg_compress_struct* const cinfo) { + for (size_t i = 0; i < 3; i++) { + cinfo->comp_info[i].h_samp_factor = + 1 << (chroma_subsampling.MaxHShift() - + chroma_subsampling.HShift(i < 2 ? i ^ 1 : i)); + cinfo->comp_info[i].v_samp_factor = + 1 << (chroma_subsampling.MaxVShift() - + chroma_subsampling.VShift(i < 2 ? i ^ 1 : i)); + } + return true; +} + +void MyErrorExit(j_common_ptr cinfo) { + jmp_buf* env = static_cast(cinfo->client_data); + (*cinfo->err->output_message)(cinfo); + jpeg_destroy_decompress(reinterpret_cast(cinfo)); + longjmp(*env, 1); +} + +void MyOutputMessage(j_common_ptr cinfo) { +#if JXL_DEBUG_WARNING == 1 + char buf[JMSG_LENGTH_MAX]; + (*cinfo->err->format_message)(cinfo, buf); + JXL_WARNING("%s", buf); +#endif +} + +} // namespace +#endif // JPEGXL_ENABLE_JPEG + +Status DecodeImageJPGCoefficients(Span bytes, CodecInOut* io) { + // Use brunsli JPEG decoder to read quantized coefficients. + if (!jpeg::DecodeImageJPG(bytes, io)) { + fprintf(stderr, "Corrupt or CMYK JPEG.\n"); + return false; + } + return true; +} + +Status DecodeImageJPG(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io, double* const elapsed_deinterleave) { + if (elapsed_deinterleave != nullptr) *elapsed_deinterleave = 0; + // Don't do anything for non-JPEG files (no need to report an error) + if (!IsJPG(bytes)) return false; + +#if JPEGXL_ENABLE_JPEG + // TODO(veluca): use JPEGData also for pixels? + + // We need to declare all the non-trivial destructor local variables before + // the call to setjmp(). + ColorEncoding color_encoding; + PaddedBytes icc; + Image3F image; + std::unique_ptr row; + ImageBundle bundle(&io->metadata.m); + + const auto try_catch_block = [&]() -> bool { + jpeg_decompress_struct cinfo; + // cinfo is initialized by libjpeg, which we are not instrumenting with + // msan, therefore we need to initialize cinfo here. + msan::UnpoisonMemory(&cinfo, sizeof(cinfo)); + // Setup error handling in jpeg library so we can deal with broken jpegs in + // the fuzzer. + jpeg_error_mgr jerr; + jmp_buf env; + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = &MyErrorExit; + jerr.output_message = &MyOutputMessage; + if (setjmp(env)) { + return false; + } + cinfo.client_data = static_cast(&env); + + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, reinterpret_cast(bytes.data()), + bytes.size()); + jpeg_save_markers(&cinfo, kICCMarker, 0xFFFF); + jpeg_save_markers(&cinfo, kExifMarker, 0xFFFF); + jpeg_read_header(&cinfo, TRUE); + const auto failure = [&cinfo](const char* str) -> Status { + jpeg_abort_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return JXL_FAILURE("%s", str); + }; + if (!VerifyDimensions(&io->constraints, cinfo.image_width, + cinfo.image_height)) { + return failure("image too big"); + } + // Might cause CPU-zip bomb. + if (cinfo.arith_code) { + return failure("arithmetic code JPEGs are not supported"); + } + if (ReadICCProfile(&cinfo, &icc)) { + if (!color_encoding.SetICC(std::move(icc))) { + return failure("read an invalid ICC profile"); + } + } else { + color_encoding = ColorEncoding::SRGB(cinfo.output_components == 1); + } + ReadExif(&cinfo, &io->blobs.exif); + io->metadata.m.SetUintSamples(BITS_IN_JSAMPLE); + io->metadata.m.color_encoding = color_encoding; + int nbcomp = cinfo.num_components; + if (nbcomp != 1 && nbcomp != 3) { + return failure("unsupported number of components in JPEG"); + } + if (!ApplyColorHints(color_hints, /*color_already_set=*/true, false, io)) { + return failure("ApplyColorHints failed"); + } + + jpeg_start_decompress(&cinfo); + JXL_ASSERT(cinfo.output_components == nbcomp); + image = Image3F(cinfo.image_width, cinfo.image_height); + row.reset(new JSAMPLE[cinfo.output_components * cinfo.image_width]); + for (size_t y = 0; y < image.ysize(); ++y) { + JSAMPROW rows[] = {row.get()}; + jpeg_read_scanlines(&cinfo, rows, 1); + msan::UnpoisonMemory( + row.get(), + sizeof(JSAMPLE) * cinfo.output_components * cinfo.image_width); + auto start = Now(); + float* const JXL_RESTRICT output_row[] = { + image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)}; + if (cinfo.output_components == 1) { + for (size_t x = 0; x < image.xsize(); ++x) { + output_row[0][x] = output_row[1][x] = output_row[2][x] = + row[x] * (1.f / kJPEGSampleMultiplier); + } + } else { // 3 components + for (size_t x = 0; x < image.xsize(); ++x) { + for (size_t c = 0; c < 3; ++c) { + output_row[c][x] = row[3 * x + c] * (1.f / kJPEGSampleMultiplier); + } + } + } + auto end = Now(); + if (elapsed_deinterleave != nullptr) { + *elapsed_deinterleave += end - start; + } + } + io->SetFromImage(std::move(image), color_encoding); + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + io->dec_pixels = io->xsize() * io->ysize(); + return true; + }; + + return try_catch_block(); +#else // JPEGXL_ENABLE_JPEG + return JXL_FAILURE("JPEG decoding not enabled at build time."); +#endif // JPEGXL_ENABLE_JPEG +} + +#if JPEGXL_ENABLE_JPEG +Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io, + size_t quality, + const YCbCrChromaSubsampling& chroma_subsampling, + PaddedBytes* bytes) { + jpeg_compress_struct cinfo; + // cinfo is initialized by libjpeg, which we are not instrumenting with + // msan. + msan::UnpoisonMemory(&cinfo, sizeof(cinfo)); + jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + unsigned char* buffer = nullptr; + unsigned long size = 0; + jpeg_mem_dest(&cinfo, &buffer, &size); + cinfo.image_width = ib->xsize(); + cinfo.image_height = ib->ysize(); + if (ib->IsGray()) { + cinfo.input_components = 1; + cinfo.in_color_space = JCS_GRAYSCALE; + } else { + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + } + jpeg_set_defaults(&cinfo); + cinfo.optimize_coding = TRUE; + if (cinfo.input_components == 3) { + JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo)); + } + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + if (!ib->IsSRGB()) { + WriteICCProfile(&cinfo, ib->c_current().ICC()); + } + WriteExif(&cinfo, io->blobs.exif); + if (cinfo.input_components > 3 || cinfo.input_components < 0) + return JXL_FAILURE("invalid numbers of components"); + + std::unique_ptr 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(cinfo.input_components); ++c) { + JXL_RETURN_IF_ERROR(c < 3); + row[cinfo.input_components * x + c] = static_cast( + std::max(std::min(kJPEGSampleMultiplier * input_row[c][x] + .5f, + kJPEGSampleMax), + kJPEGSampleMin)); + } + } + JSAMPROW rows[] = {row.get()}; + jpeg_write_scanlines(&cinfo, rows, 1); + } + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + bytes->resize(size); + // Compressed image data is initialized by libjpeg, which we are not + // instrumenting with msan. + msan::UnpoisonMemory(buffer, size); + std::copy_n(buffer, size, bytes->data()); + std::free(buffer); + return true; +} + +Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality, + const YCbCrChromaSubsampling& chroma_subsampling, + PaddedBytes* bytes) { +#if !JPEGXL_ENABLE_SJPEG + return JXL_FAILURE("JPEG XL was built without sjpeg support"); +#else + sjpeg::EncoderParam param(quality); + if (!ib->IsSRGB()) { + param.iccp.assign(ib->metadata()->color_encoding.ICC().begin(), + ib->metadata()->color_encoding.ICC().end()); + } + if (chroma_subsampling.Is444()) { + param.yuv_mode = SJPEG_YUV_444; + } else if (chroma_subsampling.Is420()) { + param.yuv_mode = SJPEG_YUV_SHARP; + } else { + return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); + } + std::vector 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( + std::max(0.f, std::min(255.f, roundf(255.f * row[x]))))); + } + } + } + std::string output; + JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->xsize(), ib->ysize(), + ib->xsize() * 3, param, &output)); + bytes->assign( + reinterpret_cast(output.data()), + reinterpret_cast(output.data() + output.size())); + return true; +#endif +} +#endif // JPEGXL_ENABLE_JPEG + +Status EncodeImageJPGCoefficients(const CodecInOut* io, PaddedBytes* bytes) { + auto write = [&bytes](const uint8_t* buf, size_t len) { + bytes->append(buf, buf + len); + return len; + }; + return jpeg::WriteJpeg(*io->Main().jpeg_data, write); +} + +Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, + YCbCrChromaSubsampling chroma_subsampling, + ThreadPool* pool, PaddedBytes* bytes) { + if (io->Main().HasAlpha()) { + return JXL_FAILURE("alpha is not supported"); + } + if (quality > 100) { + return JXL_FAILURE("please specify a 0-100 JPEG quality"); + } + +#if JPEGXL_ENABLE_JPEG + const ImageBundle* ib; + ImageMetadata metadata = io->metadata.m; + ImageBundle ib_store(&metadata); + JXL_RETURN_IF_ERROR(TransformIfNeeded( + io->Main(), io->metadata.m.color_encoding, pool, &ib_store, &ib)); + + switch (encoder) { + case JpegEncoder::kLibJpeg: + JXL_RETURN_IF_ERROR( + EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes)); + break; + case JpegEncoder::kSJpeg: + JXL_RETURN_IF_ERROR( + EncodeWithSJpeg(ib, quality, chroma_subsampling, bytes)); + break; + default: + return JXL_FAILURE("tried to use an unknown JPEG encoder"); + } + + return true; +#else // JPEGXL_ENABLE_JPEG + return JXL_FAILURE("JPEG pixel encoding not enabled at build time"); +#endif // JPEGXL_ENABLE_JPEG +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_jpg.h b/lib/extras/codec_jpg.h new file mode 100644 index 0000000..dbd3833 --- /dev/null +++ b/lib/extras/codec_jpg.h @@ -0,0 +1,60 @@ +// 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_JPG_H_ +#define LIB_EXTRAS_CODEC_JPG_H_ + +// Encodes JPG pixels and metadata in memory. + +#include + +#include "lib/extras/codec.h" +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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" + +namespace jxl { +namespace extras { + +enum class JpegEncoder { + kLibJpeg, + kSJpeg, +}; + +static inline bool IsJPG(const Span bytes) { + if (bytes.size() < 2) return false; + if (bytes[0] != 0xFF || bytes[1] != 0xD8) return false; + return true; +} + +// Decodes `bytes` into `io`. color_hints are ignored. +// `elapsed_deinterleave`, if non-null, will be set to the time (in seconds) +// that it took to deinterleave the raw JSAMPLEs to planar floats. +Status DecodeImageJPG(Span bytes, const ColorHints& color_hints, + ThreadPool* pool, CodecInOut* io, + double* elapsed_deinterleave = nullptr); + +// Encodes into `bytes`. +Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality, + YCbCrChromaSubsampling chroma_subsampling, + ThreadPool* pool, PaddedBytes* bytes); + +// Temporary wrappers to load the JPEG coefficients to a CodecInOut. This should +// be replaced by calling the corresponding JPEG input and output functions on +// the API. + +// Decodes the JPEG image coefficients to a CodecIO for lossless recompression. +Status DecodeImageJPGCoefficients(Span bytes, CodecInOut* io); + +// Reconstructs the JPEG from the coefficients and metadata in CodecInOut. +Status EncodeImageJPGCoefficients(const CodecInOut* io, PaddedBytes* bytes); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_JPG_H_ diff --git a/lib/extras/codec_pgx.cc b/lib/extras/codec_pgx.cc new file mode 100644 index 0000000..604c6c3 --- /dev/null +++ b/lib/extras/codec_pgx.cc @@ -0,0 +1,273 @@ +// 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_pgx.h" + +#include +#include +#include + +#include +#include +#include +#include + +#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/color_management.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/fields.h" // AllDefault +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/luminance.h" + +namespace jxl { +namespace extras { +namespace { + +struct HeaderPGX { + // NOTE: PGX is always grayscale + size_t xsize; + size_t ysize; + size_t bits_per_sample; + bool big_endian; + bool is_signed; +}; + +class Parser { + public: + explicit Parser(const Span bytes) + : pos_(bytes.data()), end_(pos_ + bytes.size()) {} + + // Sets "pos" to the first non-header byte/pixel on success. + Status ParseHeader(HeaderPGX* header, const uint8_t** pos) { + // codec.cc ensures we have at least two bytes => no range check here. + if (pos_[0] != 'P' || pos_[1] != 'G') return false; + pos_ += 2; + return ParseHeaderPGX(header, pos); + } + + // Exposed for testing + Status ParseUnsigned(size_t* number) { + if (pos_ == end_) return JXL_FAILURE("PGX: reached end before number"); + if (!IsDigit(*pos_)) return JXL_FAILURE("PGX: expected unsigned number"); + + *number = 0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + return true; + } + + private: + static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } + static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } + static bool IsWhitespace(const uint8_t c) { + return IsLineBreak(c) || c == '\t' || c == ' '; + } + + Status SkipSpace() { + if (pos_ == end_) return JXL_FAILURE("PGX: reached end before space"); + const uint8_t c = *pos_; + if (c != ' ') return JXL_FAILURE("PGX: expected space"); + ++pos_; + return true; + } + + Status SkipLineBreak() { + if (pos_ == end_) return JXL_FAILURE("PGX: reached end before line break"); + // Line break can be either "\n" (0a) or "\r\n" (0d 0a). + if (*pos_ == '\n') { + pos_++; + return true; + } else if (*pos_ == '\r' && pos_ + 1 != end_ && *(pos_ + 1) == '\n') { + pos_ += 2; + return true; + } + return JXL_FAILURE("PGX: expected line break"); + } + + Status SkipSingleWhitespace() { + if (pos_ == end_) return JXL_FAILURE("PGX: reached end before whitespace"); + if (!IsWhitespace(*pos_)) return JXL_FAILURE("PGX: expected whitespace"); + ++pos_; + return true; + } + + Status ParseHeaderPGX(HeaderPGX* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(SkipSpace()); + if (pos_ + 2 > end_) return JXL_FAILURE("PGX: header too small"); + if (*pos_ == 'M' && *(pos_ + 1) == 'L') { + header->big_endian = true; + } else if (*pos_ == 'L' && *(pos_ + 1) == 'M') { + header->big_endian = false; + } else { + return JXL_FAILURE("PGX: invalid endianness"); + } + pos_ += 2; + JXL_RETURN_IF_ERROR(SkipSpace()); + if (pos_ == end_) return JXL_FAILURE("PGX: header too small"); + if (*pos_ == '+') { + header->is_signed = false; + } else if (*pos_ == '-') { + header->is_signed = true; + } else { + return JXL_FAILURE("PGX: invalid signedness"); + } + pos_++; + // Skip optional space + if (pos_ < end_ && *pos_ == ' ') pos_++; + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->bits_per_sample)); + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + // 0xa, or 0xd 0xa. + JXL_RETURN_IF_ERROR(SkipLineBreak()); + + if (header->bits_per_sample > 16) { + return JXL_FAILURE("PGX: >16 bits not yet supported"); + } + // TODO(lode): support signed integers. This may require changing the way + // external_image works. + if (header->is_signed) { + return JXL_FAILURE("PGX: signed not yet supported"); + } + + 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; + if (pos_ + numpixels * bytes_per_pixel > end_) { + return JXL_FAILURE("PGX: data too small"); + } + + *pos = pos_; + return true; + } + + const uint8_t* pos_; + const uint8_t* const end_; +}; + +constexpr size_t kMaxHeaderSize = 200; + +Status EncodeHeader(const ImageBundle& ib, const size_t bits_per_sample, + char* header, int* JXL_RESTRICT chars_written) { + if (ib.HasAlpha()) return JXL_FAILURE("PGX: can't store alpha"); + if (!ib.IsGray()) return JXL_FAILURE("PGX: must be grayscale"); + // TODO(lode): verify other bit depths: for other bit depths such as 1 or 4 + // bits, have a test case to verify it works correctly. For bits > 16, we may + // need to change the way external_image works. + if (bits_per_sample != 8 && bits_per_sample != 16) { + return JXL_FAILURE("PGX: bits other than 8 or 16 not yet supported"); + } + + // Use ML (Big Endian), LM may not be well supported by all decoders. + *chars_written = snprintf(header, kMaxHeaderSize, "PG ML + %zu %zu %zu\n", + bits_per_sample, ib.xsize(), ib.ysize()); + JXL_RETURN_IF_ERROR(static_cast(*chars_written) < + kMaxHeaderSize); + return true; +} + +template +void ExpectNear(T a, T b, T precision) { + JXL_CHECK(std::abs(a - b) <= precision); +} + +Span MakeSpan(const char* str) { + return Span(reinterpret_cast(str), + strlen(str)); +} + +} // namespace + +Status DecodeImagePGX(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io) { + Parser parser(bytes); + HeaderPGX header = {}; + const uint8_t* pos; + if (!parser.ParseHeader(&header, &pos)) return false; + JXL_RETURN_IF_ERROR( + VerifyDimensions(&io->constraints, header.xsize, header.ysize)); + if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { + return JXL_FAILURE("PGX: bits_per_sample invalid"); + } + + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + /*is_gray=*/true, io)); + io->metadata.m.SetUintSamples(header.bits_per_sample); + io->metadata.m.SetAlphaBits(0); + io->dec_pixels = header.xsize * header.ysize; + io->SetSize(header.xsize, header.ysize); + io->frames.clear(); + io->frames.reserve(1); + ImageBundle ib(&io->metadata.m); + + const bool has_alpha = false; + const bool flipped_y = false; + const Span span(pos, bytes.data() + bytes.size() - pos); + JXL_RETURN_IF_ERROR(ConvertFromExternal( + span, header.xsize, header.ysize, io->metadata.m.color_encoding, + has_alpha, + /*alpha_is_premultiplied=*/false, + io->metadata.m.bit_depth.bits_per_sample, + header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, flipped_y, pool, + &ib, /*float_in=*/false)); + io->frames.push_back(std::move(ib)); + SetIntensityTarget(io); + return true; +} + +Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes) { + if (!Bundle::AllDefault(io->metadata.m)) { + JXL_WARNING("PGX encoder ignoring metadata - use a different codec"); + } + if (!c_desired.IsSRGB()) { + JXL_WARNING( + "PGX encoder cannot store custom ICC profile; decoder\n" + "will need hint key=color_space to get the same values"); + } + + ImageBundle ib = io->Main().Copy(); + + ImageMetadata metadata = io->metadata.m; + ImageBundle store(&metadata); + const ImageBundle* transformed; + JXL_RETURN_IF_ERROR( + TransformIfNeeded(ib, c_desired, pool, &store, &transformed)); + PaddedBytes pixels(ib.xsize() * ib.ysize() * + (bits_per_sample / kBitsPerByte)); + size_t stride = ib.xsize() * (bits_per_sample / kBitsPerByte); + JXL_RETURN_IF_ERROR( + 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())); + + char header[kMaxHeaderSize]; + int header_size = 0; + JXL_RETURN_IF_ERROR(EncodeHeader(ib, bits_per_sample, header, &header_size)); + + bytes->resize(static_cast(header_size) + pixels.size()); + memcpy(bytes->data(), header, static_cast(header_size)); + memcpy(bytes->data() + header_size, pixels.data(), pixels.size()); + + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_pgx.h b/lib/extras/codec_pgx.h new file mode 100644 index 0000000..a5c2c47 --- /dev/null +++ b/lib/extras/codec_pgx.h @@ -0,0 +1,38 @@ +// 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_PGX_H_ +#define LIB_EXTRAS_CODEC_PGX_H_ + +// Encodes/decodes PGX pixels in memory. + +#include +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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 DecodeImagePGX(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io); + +// Transforms from io->c_current to `c_desired` and encodes into `bytes`. +Status EncodeImagePGX(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_PGX_H_ diff --git a/lib/extras/codec_pgx_test.cc b/lib/extras/codec_pgx_test.cc new file mode 100644 index 0000000..457d007 --- /dev/null +++ b/lib/extras/codec_pgx_test.cc @@ -0,0 +1,74 @@ +// 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_pgx.h" + +#include + +#include "gtest/gtest.h" + +namespace jxl { +namespace extras { +namespace { + +Span MakeSpan(const char* str) { + return Span(reinterpret_cast(str), + strlen(str)); +} + +TEST(CodecPGXTest, Test8bits) { + std::string pgx = "PG ML + 8 2 3\npixels"; + + CodecInOut io; + ThreadPool* pool = nullptr; + + EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), pool, &io)); + + ScaleImage(255.f, io.Main().color()); + + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.IsGray()); + EXPECT_EQ(2u, io.xsize()); + EXPECT_EQ(3u, io.ysize()); + + float eps = 1e-5; + EXPECT_NEAR('p', io.Main().color()->Plane(0).Row(0)[0], eps); + EXPECT_NEAR('i', io.Main().color()->Plane(0).Row(0)[1], eps); + EXPECT_NEAR('x', io.Main().color()->Plane(0).Row(1)[0], eps); + EXPECT_NEAR('e', io.Main().color()->Plane(0).Row(1)[1], eps); + EXPECT_NEAR('l', io.Main().color()->Plane(0).Row(2)[0], eps); + EXPECT_NEAR('s', io.Main().color()->Plane(0).Row(2)[1], eps); +} + +TEST(CodecPGXTest, Test16bits) { + std::string pgx = "PG ML + 16 2 3\np_i_x_e_l_s_"; + + CodecInOut io; + ThreadPool* pool = nullptr; + + EXPECT_TRUE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), pool, &io)); + + ScaleImage(255.f, io.Main().color()); + + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.IsGray()); + EXPECT_EQ(2u, io.xsize()); + EXPECT_EQ(3u, io.ysize()); + + float eps = 1e-7; + const auto& plane = io.Main().color()->Plane(0); + EXPECT_NEAR(256.0f * 'p' + '_', plane.Row(0)[0] * 257, eps); + EXPECT_NEAR(256.0f * 'i' + '_', plane.Row(0)[1] * 257, eps); + EXPECT_NEAR(256.0f * 'x' + '_', plane.Row(1)[0] * 257, eps); + EXPECT_NEAR(256.0f * 'e' + '_', plane.Row(1)[1] * 257, eps); + EXPECT_NEAR(256.0f * 'l' + '_', plane.Row(2)[0] * 257, eps); + EXPECT_NEAR(256.0f * 's' + '_', plane.Row(2)[1] * 257, eps); +} + +} // namespace +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_png.cc b/lib/extras/codec_png.cc new file mode 100644 index 0000000..62bcf80 --- /dev/null +++ b/lib/extras/codec_png.cc @@ -0,0 +1,852 @@ +// 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_png.h" + +#include +#include +#include +#include + +// Lodepng library: +#include + +#include +#include +#include +#include +#include + +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/luminance.h" + +namespace jxl { +namespace extras { +namespace { + +#define JXL_PNG_VERBOSE 0 + +// Retrieves XMP and EXIF/IPTC from itext and text. +class BlobsReaderPNG { + public: + static Status Decode(const LodePNGInfo& info, Blobs* blobs) { + for (unsigned idx_itext = 0; idx_itext < info.itext_num; ++idx_itext) { + // We trust these are properly null-terminated by LodePNG. + const char* key = info.itext_keys[idx_itext]; + const char* value = info.itext_strings[idx_itext]; + if (strstr(key, "XML:com.adobe.xmp")) { + blobs->xmp.resize(strlen(value)); // safe, see above + memcpy(blobs->xmp.data(), value, blobs->xmp.size()); + } + } + + for (unsigned idx_text = 0; idx_text < info.text_num; ++idx_text) { + // We trust these are properly null-terminated by LodePNG. + const char* key = info.text_keys[idx_text]; + const char* value = info.text_strings[idx_text]; + std::string type; + PaddedBytes bytes; + + // Handle text chunks annotated with key "Raw profile type ####", with + // #### a type, which may contain metadata. + const char* kKey = "Raw profile type "; + if (strncmp(key, kKey, strlen(kKey)) != 0) continue; + + if (!MaybeDecodeBase16(key, value, &type, &bytes)) { + JXL_WARNING("Couldn't parse 'Raw format type' text chunk"); + continue; + } + if (type == "exif") { + if (!blobs->exif.empty()) { + JXL_WARNING("overwriting EXIF (%zu bytes) with base16 (%zu bytes)", + blobs->exif.size(), bytes.size()); + } + blobs->exif = std::move(bytes); + } else if (type == "iptc") { + // TODO (jon): Deal with IPTC in some way + } else if (type == "8bim") { + // TODO (jon): Deal with 8bim in some way + } else if (type == "xmp") { + if (!blobs->xmp.empty()) { + JXL_WARNING("overwriting XMP (%zu bytes) with base16 (%zu bytes)", + blobs->xmp.size(), bytes.size()); + } + blobs->xmp = std::move(bytes); + } else { + JXL_WARNING( + "Unknown type in 'Raw format type' text chunk: %s: %zu bytes", + type.c_str(), bytes.size()); + } + } + + return true; + } + + private: + // Returns false if invalid. + static JXL_INLINE Status DecodeNibble(const char c, + uint32_t* JXL_RESTRICT nibble) { + if ('a' <= c && c <= 'f') { + *nibble = 10 + c - 'a'; + } else if ('0' <= c && c <= '9') { + *nibble = c - '0'; + } else { + *nibble = 0; + return JXL_FAILURE("Invalid metadata nibble"); + } + JXL_ASSERT(*nibble < 16); + 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. + // We trust key and encoded are null-terminated because they come from + // LodePNG. + static Status MaybeDecodeBase16(const char* key, const char* encoded, + std::string* type, PaddedBytes* bytes) { + const char* encoded_end = encoded + strlen(encoded); + + const char* kKey = "Raw profile type "; + if (strncmp(key, kKey, strlen(kKey)) != 0) return false; + *type = key + strlen(kKey); + const size_t kMaxTypeLen = 20; + if (type->length() > kMaxTypeLen) return false; // Type too long + + // Header: freeform string and number of bytes + // Expected format is: + // \n + // profile name/description\n + // 40\n (the number of bytes after hex-decoding) + // 01234566789abcdef....\n (72 bytes per line max). + // 012345667\n (last line) + const char* pos = encoded; + + if (*(pos++) != '\n') return false; + while (pos < encoded_end && *pos != '\n') { + pos++; + } + if (pos == encoded_end) return false; + // 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 + + // We need 2*bytes for the hex values plus 1 byte every 36 values. + const unsigned long needed_bytes = + bytes_to_decode * 2 + 1 + DivCeil(bytes_to_decode, 36); + if (needed_bytes != static_cast(encoded_end - pos)) { + return JXL_FAILURE("Not enough bytes to parse %lu bytes in hex", + bytes_to_decode); + } + JXL_ASSERT(bytes->empty()); + bytes->reserve(bytes_to_decode); + + // Encoding: base16 with newline after 72 chars. + // pos points to the \n before the first line of hex values. + for (size_t i = 0; i < bytes_to_decode; ++i) { + if (i % 36 == 0) { + if (pos + 1 >= encoded_end) return false; // Truncated base16 1 + if (*pos != '\n') return false; // Expected newline + ++pos; + } + + if (pos + 2 >= encoded_end) return false; // Truncated base16 2; + uint32_t nibble0, nibble1; + JXL_RETURN_IF_ERROR(DecodeNibble(pos[0], &nibble0)); + JXL_RETURN_IF_ERROR(DecodeNibble(pos[1], &nibble1)); + bytes->push_back(static_cast((nibble0 << 4) + nibble1)); + pos += 2; + } + if (pos + 1 != encoded_end) return false; // Too many encoded bytes + if (pos[0] != '\n') return false; // Incorrect metadata terminator + return true; + } +}; + +// Stores XMP and EXIF/IPTC into itext and text. +class BlobsWriterPNG { + public: + static Status Encode(const Blobs& blobs, LodePNGInfo* JXL_RESTRICT info) { + if (!blobs.exif.empty()) { + JXL_RETURN_IF_ERROR(EncodeBase16("exif", blobs.exif, info)); + } + if (!blobs.iptc.empty()) { + JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, info)); + } + + if (!blobs.xmp.empty()) { + JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, info)); + + // Below is the official way, but it does not seem to work in ImageMagick. + // Exiv2 and exiftool are OK with either way of encoding XMP. + if (/* DISABLES CODE */ (0)) { + const char* key = "XML:com.adobe.xmp"; + const std::string text(reinterpret_cast(blobs.xmp.data()), + blobs.xmp.size()); + if (lodepng_add_itext(info, key, "", "", text.c_str()) != 0) { + return JXL_FAILURE("Failed to add itext"); + } + } + } + + 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, + LodePNGInfo* JXL_RESTRICT info) { + // 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%8zu", type.c_str(), bytes.size()); + + const std::string& encoded = std::string(header) + base16; + if (lodepng_add_text(info, key, encoded.c_str()) != 0) { + return JXL_FAILURE("Failed to add text"); + } + + return true; + } +}; + +// Retrieves ColorEncoding from PNG chunks. +class ColorEncodingReaderPNG { + public: + // Fills original->color_encoding or returns false. + Status operator()(const Span bytes, const bool is_gray, + CodecInOut* io) { + ColorEncoding* c_original = &io->metadata.m.color_encoding; + JXL_RETURN_IF_ERROR(Decode(bytes, &io->blobs)); + + const ColorSpace color_space = + is_gray ? ColorSpace::kGray : ColorSpace::kRGB; + + if (have_pq_) { + c_original->SetColorSpace(color_space); + c_original->white_point = WhitePoint::kD65; + c_original->primaries = Primaries::k2100; + c_original->tf.SetTransferFunction(TransferFunction::kPQ); + c_original->rendering_intent = RenderingIntent::kRelative; + if (c_original->CreateICC()) return true; + JXL_WARNING("Failed to synthesize BT.2100 PQ"); + // Else: try the actual ICC profile. + } + + // ICC overrides anything else if present. + if (c_original->SetICC(std::move(icc_))) { + if (have_srgb_) { + JXL_WARNING("Invalid PNG with both sRGB and ICC; ignoring sRGB"); + } + if (is_gray != c_original->IsGray()) { + return JXL_FAILURE("Mismatch between ICC and PNG header grayscale"); + } + return true; // it's fine to ignore gAMA/cHRM. + } + + // PNG requires that sRGB override gAMA/cHRM. + if (have_srgb_) { + return c_original->SetSRGB(color_space, rendering_intent_); + } + + // Try to create a custom profile: + + c_original->SetColorSpace(color_space); + + // Attempt to set whitepoint and primaries if there is a cHRM chunk, or else + // use default sRGB (the PNG then is device-dependent). + // In case of grayscale, do not attempt to set the primaries and ignore the + // ones the PNG image has (but still set the white point). + if (!have_chrm_ || !c_original->SetWhitePoint(white_point_) || + (!is_gray && !c_original->SetPrimaries(primaries_))) { +#if JXL_PNG_VERBOSE >= 1 + JXL_WARNING("No (valid) cHRM, assuming sRGB"); +#endif + c_original->white_point = WhitePoint::kD65; + c_original->primaries = Primaries::kSRGB; + } + + if (!have_gama_ || !c_original->tf.SetGamma(gamma_)) { +#if JXL_PNG_VERBOSE >= 1 + JXL_WARNING("No (valid) gAMA nor sRGB, assuming sRGB"); +#endif + c_original->tf.SetTransferFunction(TransferFunction::kSRGB); + } + + c_original->rendering_intent = RenderingIntent::kRelative; + if (c_original->CreateICC()) return true; + + JXL_WARNING( + "DATA LOSS: unable to create an ICC profile for PNG gAMA/cHRM.\n" + "Image pixels will be interpreted as sRGB. Please add an ICC \n" + "profile to the input image"); + return c_original->SetSRGB(color_space); + } + + // Whether the image has any color profile information (ICC chunk, sRGB + // chunk, cHRM chunk, and so on), or has no color information chunks at all. + bool HaveColorProfile() const { + return have_pq_ || have_srgb_ || have_gama_ || have_chrm_ || have_icc_; + } + + private: + Status DecodeICC(const unsigned char* const payload, + const size_t payload_size) { + if (payload_size == 0) return JXL_FAILURE("Empty ICC payload"); + const unsigned char* pos = payload; + const unsigned char* end = payload + payload_size; + + // Profile name + if (*pos == '\0') return JXL_FAILURE("Expected ICC name"); + for (size_t i = 0;; ++i) { + if (i == 80) return JXL_FAILURE("ICC profile name too long"); + if (pos == end) return JXL_FAILURE("Not enough bytes for ICC name"); + if (*pos++ == '\0') break; + } + + // Special case for BT.2100 PQ (https://w3c.github.io/png-hdr-pq/) - try to + // synthesize the profile because table-based curves are less accurate. + // strcmp is safe because we already verified the string is 0-terminated. + if (!strcmp(reinterpret_cast(payload), "ITUR_2100_PQ_FULL")) { + have_pq_ = true; + } + + // Skip over compression method (only one is allowed) + if (pos == end) return JXL_FAILURE("Not enough bytes for ICC method"); + if (*pos++ != 0) return JXL_FAILURE("Unsupported ICC method"); + + // Decompress + unsigned char* icc_buf = nullptr; + size_t icc_size = 0; + LodePNGDecompressSettings settings; + lodepng_decompress_settings_init(&settings); + const unsigned err = lodepng_zlib_decompress( + &icc_buf, &icc_size, pos, payload_size - (pos - payload), &settings); + if (err == 0) { + icc_.resize(icc_size); + memcpy(icc_.data(), icc_buf, icc_size); + } + free(icc_buf); + have_icc_ = true; + return true; + } + + // Returns floating-point value from the PNG encoding (times 10^5). + static double F64FromU32(const uint32_t x) { + return static_cast(x) * 1E-5; + } + + Status DecodeSRGB(const unsigned char* payload, const size_t payload_size) { + if (payload_size != 1) return JXL_FAILURE("Wrong sRGB size"); + // (PNG uses the same values as ICC.) + if (payload[0] >= 4) return JXL_FAILURE("Invalid Rendering Intent"); + rendering_intent_ = static_cast(payload[0]); + have_srgb_ = true; + return true; + } + + Status DecodeGAMA(const unsigned char* payload, const size_t payload_size) { + if (payload_size != 4) return JXL_FAILURE("Wrong gAMA size"); + gamma_ = F64FromU32(LoadBE32(payload)); + have_gama_ = true; + return true; + } + + Status DecodeCHRM(const unsigned char* payload, const size_t payload_size) { + if (payload_size != 32) return JXL_FAILURE("Wrong cHRM size"); + white_point_.x = F64FromU32(LoadBE32(payload + 0)); + white_point_.y = F64FromU32(LoadBE32(payload + 4)); + primaries_.r.x = F64FromU32(LoadBE32(payload + 8)); + primaries_.r.y = F64FromU32(LoadBE32(payload + 12)); + primaries_.g.x = F64FromU32(LoadBE32(payload + 16)); + primaries_.g.y = F64FromU32(LoadBE32(payload + 20)); + primaries_.b.x = F64FromU32(LoadBE32(payload + 24)); + primaries_.b.y = F64FromU32(LoadBE32(payload + 28)); + have_chrm_ = true; + return true; + } + + Status DecodeEXIF(const unsigned char* payload, const size_t payload_size, + Blobs* blobs) { + // If we already have EXIF, keep the larger one. + if (blobs->exif.size() > payload_size) return true; + blobs->exif.resize(payload_size); + memcpy(blobs->exif.data(), payload, payload_size); + return true; + } + + Status Decode(const Span bytes, Blobs* blobs) { + // Look for colorimetry and text chunks in the PNG image. The PNG chunks + // begin after the PNG magic header of 8 bytes. + const unsigned char* chunk = bytes.data() + 8; + const unsigned char* end = bytes.data() + bytes.size(); + for (;;) { + // chunk points to the first field of a PNG chunk. The chunk has + // respectively 4 bytes of length, 4 bytes type, length bytes of data, + // 4 bytes CRC. + if (chunk + 4 >= end) { + break; // Regular end reached. + } + + char type_char[5]; + if (chunk + 8 >= end) { + JXL_NOTIFY_ERROR("PNG: malformed chunk"); + break; + } + lodepng_chunk_type(type_char, chunk); + std::string type = type_char; + + if (type == "acTL" || type == "fcTL" || type == "fdAT") { + // this is an APNG file, without proper handling we would just return + // the first frame, so for now codec_apng handles animation until the + // animation chunk handling is added here + return false; + } + if (type == "eXIf" || type == "iCCP" || type == "sRGB" || + type == "gAMA" || type == "cHRM") { + const unsigned char* payload = lodepng_chunk_data_const(chunk); + const size_t payload_size = lodepng_chunk_length(chunk); + // The entire chunk needs also 4 bytes of CRC after the payload. + if (payload + payload_size + 4 >= end) { + JXL_NOTIFY_ERROR("PNG: truncated chunk"); + break; + } + if (lodepng_chunk_check_crc(chunk) != 0) { + JXL_NOTIFY_ERROR("CRC mismatch in unknown PNG chunk"); + chunk = lodepng_chunk_next_const(chunk, end); + continue; + } + + if (type == "eXIf") { + JXL_RETURN_IF_ERROR(DecodeEXIF(payload, payload_size, blobs)); + } else if (type == "iCCP") { + JXL_RETURN_IF_ERROR(DecodeICC(payload, payload_size)); + } else if (type == "sRGB") { + JXL_RETURN_IF_ERROR(DecodeSRGB(payload, payload_size)); + } else if (type == "gAMA") { + JXL_RETURN_IF_ERROR(DecodeGAMA(payload, payload_size)); + } else if (type == "cHRM") { + JXL_RETURN_IF_ERROR(DecodeCHRM(payload, payload_size)); + } + } + + chunk = lodepng_chunk_next_const(chunk, end); + } + return true; + } + + PaddedBytes icc_; + + bool have_pq_ = false; + bool have_srgb_ = false; + bool have_gama_ = false; + bool have_chrm_ = false; + bool have_icc_ = false; + + // Only valid if have_srgb_: + RenderingIntent rendering_intent_; + + // Only valid if have_gama_: + double gamma_; + + // Only valid if have_chrm_: + CIExy white_point_; + PrimariesCIExy primaries_; +}; + +// Stores ColorEncoding into PNG chunks. +class ColorEncodingWriterPNG { + public: + static Status Encode(const ColorEncoding& c, LodePNGInfo* JXL_RESTRICT info) { + // Prefer to only write sRGB - smaller. + if (c.IsSRGB()) { + JXL_RETURN_IF_ERROR(AddSRGB(c, info)); + // PNG recommends not including both sRGB and iCCP, so skip the latter. + } else if (!c.HaveFields() || !c.tf.IsGamma()) { + // Having a gamma value means that the source was a PNG with gAMA and + // without iCCP. + JXL_ASSERT(!c.ICC().empty()); + JXL_RETURN_IF_ERROR(AddICC(c.ICC(), info)); + } + + // gAMA and cHRM are always allowed but will be overridden by sRGB/iCCP. + JXL_RETURN_IF_ERROR(MaybeAddGAMA(c, info)); + JXL_RETURN_IF_ERROR(MaybeAddCHRM(c, info)); + return true; + } + + private: + static Status AddChunk(const char* type, const PaddedBytes& payload, + LodePNGInfo* JXL_RESTRICT info) { + // Ignore original location/order of chunks; place them in the first group. + if (lodepng_chunk_create(&info->unknown_chunks_data[0], + &info->unknown_chunks_size[0], payload.size(), + type, payload.data()) != 0) { + return JXL_FAILURE("Failed to add chunk"); + } + return true; + } + + static Status AddICC(const PaddedBytes& icc, LodePNGInfo* JXL_RESTRICT info) { + LodePNGCompressSettings settings; + lodepng_compress_settings_init(&settings); + unsigned char* out = nullptr; + size_t out_size = 0; + if (lodepng_zlib_compress(&out, &out_size, icc.data(), icc.size(), + &settings) != 0) { + return JXL_FAILURE("Failed to compress ICC"); + } + + PaddedBytes payload; + payload.resize(3 + out_size); + // TODO(janwas): use special name if PQ + payload[0] = '1'; // profile name + payload[1] = '\0'; + payload[2] = 0; // compression method (zlib) + memcpy(&payload[3], out, out_size); + free(out); + + return AddChunk("iCCP", payload, info); + } + + static Status AddSRGB(const ColorEncoding& c, + LodePNGInfo* JXL_RESTRICT info) { + PaddedBytes payload; + payload.push_back(static_cast(c.rendering_intent)); + return AddChunk("sRGB", payload, info); + } + + // Returns PNG encoding of floating-point value (times 10^5). + static uint32_t U32FromF64(const double x) { + return static_cast(roundf(x * 1E5)); + } + + static Status MaybeAddGAMA(const ColorEncoding& c, + LodePNGInfo* JXL_RESTRICT info) { + double gamma; + if (c.tf.IsGamma()) { + gamma = c.tf.GetGamma(); + } else if (c.tf.IsLinear()) { + gamma = 1; + } else if (c.tf.IsSRGB()) { + gamma = 0.45455; + } else { + return true; + } + + PaddedBytes payload(4); + StoreBE32(U32FromF64(gamma), payload.data()); + return AddChunk("gAMA", payload, info); + } + + static Status MaybeAddCHRM(const ColorEncoding& c, + LodePNGInfo* JXL_RESTRICT info) { + CIExy white_point = c.GetWhitePoint(); + // A PNG image stores both whitepoint and primaries in the cHRM chunk, but + // for grayscale images we don't have primaries. It does not matter what + // values are stored in the PNG though (all colors are a multiple of the + // whitepoint), so choose default ones. See + // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html section 4.2.2.1. + PrimariesCIExy primaries = + c.IsGray() ? ColorEncoding().GetPrimaries() : c.GetPrimaries(); + + if (c.primaries == Primaries::kSRGB && c.white_point == WhitePoint::kD65) { + // For sRGB, the cHRM chunk is supposed to have very specific values which + // don't quite match the pre-quantized ones we have (red is off by + // 0.00010). Technically, this is only required for full sRGB, but for + // consistency, we might as well use them whenever the primaries and white + // point are sRGB's. + white_point.x = 0.31270; + white_point.y = 0.32900; + primaries.r.x = 0.64000; + primaries.r.y = 0.33000; + primaries.g.x = 0.30000; + primaries.g.y = 0.60000; + primaries.b.x = 0.15000; + primaries.b.y = 0.06000; + } + + PaddedBytes payload(32); + StoreBE32(U32FromF64(white_point.x), &payload[0]); + StoreBE32(U32FromF64(white_point.y), &payload[4]); + StoreBE32(U32FromF64(primaries.r.x), &payload[8]); + StoreBE32(U32FromF64(primaries.r.y), &payload[12]); + StoreBE32(U32FromF64(primaries.g.x), &payload[16]); + StoreBE32(U32FromF64(primaries.g.y), &payload[20]); + StoreBE32(U32FromF64(primaries.b.x), &payload[24]); + StoreBE32(U32FromF64(primaries.b.y), &payload[28]); + return AddChunk("cHRM", payload, info); + } +}; + +// RAII - ensures state is freed even if returning early. +struct PNGState { + PNGState() { lodepng_state_init(&s); } + ~PNGState() { lodepng_state_cleanup(&s); } + + LodePNGState s; +}; + +Status CheckGray(const LodePNGColorMode& mode, bool has_icc, bool* is_gray) { + switch (mode.colortype) { + case LCT_GREY: + case LCT_GREY_ALPHA: + *is_gray = true; + return true; + + case LCT_RGB: + case LCT_RGBA: + *is_gray = false; + return true; + + case LCT_PALETTE: { + if (has_icc) { + // If an ICC profile is present, the PNG specification requires + // palette to be interpreted as RGB colored, not grayscale, so we must + // output color in that case and unfortunately can't optimize it to + // gray if the palette only has gray entries. + *is_gray = false; + return true; + } else { + *is_gray = true; + for (size_t i = 0; i < mode.palettesize; i++) { + if (mode.palette[i * 4] != mode.palette[i * 4 + 1] || + mode.palette[i * 4] != mode.palette[i * 4 + 2]) { + *is_gray = false; + break; + } + } + return true; + } + } + + default: + *is_gray = false; + return JXL_FAILURE("Unexpected PNG color type"); + } +} + +Status CheckAlpha(const LodePNGColorMode& mode, bool* has_alpha) { + if (mode.key_defined) { + // Color key marks a single color as transparent. + *has_alpha = true; + return true; + } + + switch (mode.colortype) { + case LCT_GREY: + case LCT_RGB: + *has_alpha = false; + return true; + + case LCT_GREY_ALPHA: + case LCT_RGBA: + *has_alpha = true; + return true; + + case LCT_PALETTE: { + *has_alpha = false; + for (size_t i = 0; i < mode.palettesize; i++) { + // PNG palettes are always 8-bit. + if (mode.palette[i * 4 + 3] != 255) { + *has_alpha = true; + break; + } + } + return true; + } + + default: + *has_alpha = false; + return JXL_FAILURE("Unexpected PNG color type"); + } +} + +LodePNGColorType MakeType(const bool is_gray, const bool has_alpha) { + if (is_gray) { + return has_alpha ? LCT_GREY_ALPHA : LCT_GREY; + } + return has_alpha ? LCT_RGBA : LCT_RGB; +} + +// Inspects first chunk of the given type and updates state with the information +// when the chunk is relevant and present in the file. +Status InspectChunkType(const Span bytes, + const std::string& type, LodePNGState* state) { + const unsigned char* chunk = lodepng_chunk_find_const( + bytes.data(), bytes.data() + bytes.size(), type.c_str()); + if (chunk && lodepng_inspect_chunk(state, chunk - bytes.data(), bytes.data(), + bytes.size()) != 0) { + return JXL_FAILURE("Invalid chunk \"%s\" in PNG image", type.c_str()); + } + return true; +} + +} // namespace + +Status DecodeImagePNG(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io) { + unsigned w, h; + PNGState state; + if (lodepng_inspect(&w, &h, &state.s, bytes.data(), bytes.size()) != 0) { + return false; // not an error - just wrong format + } + JXL_RETURN_IF_ERROR(VerifyDimensions(&io->constraints, w, h)); + io->SetSize(w, h); + // Palette RGB values + if (!InspectChunkType(bytes, "PLTE", &state.s)) { + return false; + } + // Transparent color key, or palette transparency + if (!InspectChunkType(bytes, "tRNS", &state.s)) { + return false; + } + // ICC profile + if (!InspectChunkType(bytes, "iCCP", &state.s)) { + return false; + } + const LodePNGColorMode& color_mode = state.s.info_png.color; + bool has_icc = state.s.info_png.iccp_defined; + + bool is_gray, has_alpha; + JXL_RETURN_IF_ERROR(CheckGray(color_mode, has_icc, &is_gray)); + JXL_RETURN_IF_ERROR(CheckAlpha(color_mode, &has_alpha)); + // We want LodePNG to promote 1/2/4 bit pixels to 8. + size_t bits_per_sample = std::max(color_mode.bitdepth, 8u); + if (bits_per_sample != 8 && bits_per_sample != 16) { + return JXL_FAILURE("Unexpected PNG bit depth"); + } + io->metadata.m.SetUintSamples(static_cast(bits_per_sample)); + io->metadata.m.SetAlphaBits( + has_alpha ? io->metadata.m.bit_depth.bits_per_sample : 0); + + // Always decode to 8/16-bit RGB/RGBA, not LCT_PALETTE. + state.s.info_raw.bitdepth = static_cast(bits_per_sample); + state.s.info_raw.colortype = MakeType(is_gray, has_alpha); + unsigned char* out = nullptr; + const unsigned err = + lodepng_decode(&out, &w, &h, &state.s, bytes.data(), bytes.size()); + // Automatically call free(out) on return. + std::unique_ptr out_ptr{out, free}; + if (err != 0) { + return JXL_FAILURE("PNG decode failed: %s", lodepng_error_text(err)); + } + + if (!BlobsReaderPNG::Decode(state.s.info_png, &io->blobs)) { + JXL_WARNING("PNG metadata may be incomplete"); + } + ColorEncodingReaderPNG reader; + JXL_RETURN_IF_ERROR(reader(bytes, is_gray, io)); +#if JXL_PNG_VERBOSE >= 1 + printf("PNG read %s\n", Description(io->metadata.m.color_encoding).c_str()); +#endif + + const size_t num_channels = (is_gray ? 1 : 3) + has_alpha; + const size_t out_size = w * h * num_channels * bits_per_sample / kBitsPerByte; + + const JxlEndianness endianness = JXL_BIG_ENDIAN; // PNG requirement + const Span span(out, out_size); + const bool ok = ConvertFromExternal( + span, w, h, io->metadata.m.color_encoding, has_alpha, + /*alpha_is_premultiplied=*/false, + io->metadata.m.bit_depth.bits_per_sample, endianness, + /*flipped_y=*/false, pool, &io->Main(), /*float_in=*/false); + JXL_RETURN_IF_ERROR(ok); + io->dec_pixels = w * h; + io->metadata.m.bit_depth.bits_per_sample = io->Main().DetectRealBitdepth(); + io->metadata.m.xyb_encoded = false; + SetIntensityTarget(io); + JXL_RETURN_IF_ERROR( + ApplyColorHints(color_hints, reader.HaveColorProfile(), is_gray, io)); + return true; +} + +Status EncodeImagePNG(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes) { + if (bits_per_sample > 8) { + bits_per_sample = 16; + } else if (bits_per_sample < 8) { + // PNG can also do 4, 2, and 1 bits per sample, but it isn't implemented + bits_per_sample = 8; + } + ImageBundle ib = io->Main().Copy(); + const size_t alpha_bits = ib.HasAlpha() ? bits_per_sample : 0; + ImageMetadata metadata = io->metadata.m; + ImageBundle store(&metadata); + const ImageBundle* transformed; + JXL_RETURN_IF_ERROR( + TransformIfNeeded(ib, c_desired, pool, &store, &transformed)); + size_t stride = ib.oriented_xsize() * + DivCeil(c_desired.Channels() * bits_per_sample + alpha_bits, + kBitsPerByte); + PaddedBytes 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())); + + PNGState state; + // For maximum compatibility, still store 8-bit even if pixels are all zero. + state.s.encoder.auto_convert = 0; + + LodePNGInfo* info = &state.s.info_png; + info->color.bitdepth = bits_per_sample; + info->color.colortype = MakeType(ib.IsGray(), ib.HasAlpha()); + state.s.info_raw = info->color; + + JXL_RETURN_IF_ERROR(ColorEncodingWriterPNG::Encode(c_desired, info)); + JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(io->blobs, info)); + + unsigned char* out = nullptr; + size_t out_size = 0; + const unsigned err = + lodepng_encode(&out, &out_size, raw_bytes.data(), ib.oriented_xsize(), + ib.oriented_ysize(), &state.s); + // Automatically call free(out) on return. + std::unique_ptr out_ptr{out, free}; + if (err != 0) { + return JXL_FAILURE("Failed to encode PNG: %s", lodepng_error_text(err)); + } + bytes->resize(out_size); + memcpy(bytes->data(), out, out_size); + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_png.h b/lib/extras/codec_png.h new file mode 100644 index 0000000..df755d0 --- /dev/null +++ b/lib/extras/codec_png.h @@ -0,0 +1,41 @@ +// 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_PNG_H_ +#define LIB_EXTRAS_CODEC_PNG_H_ + +// Encodes/decodes PNG pixels and metadata in memory. + +#include +#include + +// TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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 DecodeImagePNG(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io); + +// Transforms from io->c_current to `c_desired` and encodes into `bytes`. +Status EncodeImagePNG(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_PNG_H_ diff --git a/lib/extras/codec_pnm.cc b/lib/extras/codec_pnm.cc new file mode 100644 index 0000000..9539432 --- /dev/null +++ b/lib/extras/codec_pnm.cc @@ -0,0 +1,578 @@ +// 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_pnm.h" + +#include +#include +#include + +#include +#include +#include +#include + +#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/status.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_external_image.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 { + +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_bit; // PBM + bool is_gray; // PGM + int is_yuv; // Y4M: where 1 = 444, 2 = 422, 3 = 420 + size_t bits_per_sample; + bool floating_point; + bool big_endian; +}; + +class Parser { + public: + explicit Parser(const Span bytes) + : pos_(bytes.data()), end_(pos_ + bytes.size()) {} + + // Sets "pos" to the first non-header byte/pixel on success. + Status ParseHeader(HeaderPNM* header, const uint8_t** pos) { + // codec.cc ensures we have at least two bytes => no range check here. + if (pos_[0] == 'Y' && pos_[1] == 'U') return ParseHeaderY4M(header, pos); + if (pos_[0] != 'P') return false; + const uint8_t type = pos_[1]; + pos_ += 2; + + header->is_bit = false; + header->is_yuv = 0; + + switch (type) { + case '4': + header->is_bit = true; + header->is_gray = true; + header->bits_per_sample = 1; + return ParseHeaderPNM(header, pos); + + case '5': + header->is_gray = true; + return ParseHeaderPNM(header, pos); + + case '6': + header->is_gray = false; + return ParseHeaderPNM(header, pos); + + // TODO(jon): P7 (PAM) + + case 'F': + header->is_gray = false; + return ParseHeaderPFM(header, pos); + + case 'f': + header->is_gray = true; + return ParseHeaderPFM(header, pos); + } + return false; + } + + // Exposed for testing + Status ParseUnsigned(size_t* number) { + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before number"); + if (!IsDigit(*pos_)) return JXL_FAILURE("PNM: expected unsigned number"); + + *number = 0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + return true; + } + + Status ParseSigned(double* number) { + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before signed"); + + if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { + return JXL_FAILURE("PNM: expected signed number"); + } + + // Skip sign + const bool is_neg = *pos_ == '-'; + if (is_neg || *pos_ == '+') { + ++pos_; + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before digits"); + } + + // Leading digits + *number = 0.0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + // Decimal places? + if (pos_ < end_ && *pos_ == '.') { + ++pos_; + double place = 0.1; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number += (*pos_ - '0') * place; + place *= 0.1; + ++pos_; + } + } + + if (is_neg) *number = -*number; + return true; + } + + private: + static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } + static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } + static bool IsWhitespace(const uint8_t c) { + return IsLineBreak(c) || c == '\t' || c == ' '; + } + + Status SkipBlank() { + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before blank"); + const uint8_t c = *pos_; + if (c != ' ' && c != '\n') return JXL_FAILURE("PNM: expected blank"); + ++pos_; + return true; + } + + Status SkipSingleWhitespace() { + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_)) return JXL_FAILURE("PNM: expected whitespace"); + ++pos_; + return true; + } + + Status SkipWhitespace() { + if (pos_ == end_) return JXL_FAILURE("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_) && *pos_ != '#') { + return JXL_FAILURE("PNM: expected whitespace/comment"); + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + + // Comment(s) + while (pos_ != end_ && *pos_ == '#') { + while (pos_ != end_ && !IsLineBreak(*pos_)) { + ++pos_; + } + // Newline(s) + while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + return true; + } + + Status ExpectString(const char* str, size_t len) { + // Unlikely to happen. + if (pos_ + len < pos_) return JXL_FAILURE("Y4M: overflow"); + + if (pos_ + len > end_ || strncmp(str, (const char*)pos_, len) != 0) { + return JXL_FAILURE("Y4M: expected %s", str); + } + pos_ += len; + return true; + } + + Status ReadChar(char* out) { + // Unlikely to happen. + if (pos_ + 1 < pos_) return JXL_FAILURE("Y4M: overflow"); + + if (pos_ >= end_) { + return JXL_FAILURE("Y4M: unexpected end of input"); + } + *out = *pos_; + pos_++; + return true; + } + + // TODO(jon): support multi-frame y4m + Status ParseHeaderY4M(HeaderPNM* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(ExpectString("YUV4MPEG2", 9)); + header->is_gray = false; + header->is_yuv = 3; + // TODO(jon): check if 4:2:0 is indeed the default + header->bits_per_sample = 8; + // TODO(jon): check if there's a y4m convention for higher bit depths + while (pos_ < end_) { + char next = 0; + JXL_RETURN_IF_ERROR(ReadChar(&next)); + if (next == 0x0A) break; + if (next != ' ') continue; + char field = 0; + JXL_RETURN_IF_ERROR(ReadChar(&field)); + switch (field) { + case 'W': + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + break; + case 'H': + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + break; + case 'I': + JXL_RETURN_IF_ERROR(ReadChar(&next)); + if (next != 'p') { + return JXL_FAILURE( + "Y4M: only progressive (no frame interlacing) allowed"); + } + break; + case 'C': { + char c1 = 0; + JXL_RETURN_IF_ERROR(ReadChar(&c1)); + char c2 = 0; + JXL_RETURN_IF_ERROR(ReadChar(&c2)); + char c3 = 0; + JXL_RETURN_IF_ERROR(ReadChar(&c3)); + if (c1 != '4') return JXL_FAILURE("Y4M: invalid C param"); + if (c2 == '4') { + if (c3 != '4') return JXL_FAILURE("Y4M: invalid C param"); + header->is_yuv = 1; // 444 + } else if (c2 == '2') { + if (c3 == '2') { + header->is_yuv = 2; // 422 + } else if (c3 == '0') { + header->is_yuv = 3; // 420 + } else { + return JXL_FAILURE("Y4M: invalid C param"); + } + } else { + return JXL_FAILURE("Y4M: invalid C param"); + } + } + [[fallthrough]]; + // no break: fallthrough because this field can have values like + // "C420jpeg" (we are ignoring the chroma sample location and treat + // everything like C420jpeg) + case 'F': // Framerate in fps as numerator:denominator + // TODO(jon): actually read this and set corresponding jxl + // metadata + case 'A': // Pixel aspect ratio (ignoring it, could perhaps adjust + // intrinsic dimensions based on this?) + case 'X': // Comment, ignore + // ignore the field value and go to next one + while (pos_ < end_) { + if (pos_[0] == ' ' || pos_[0] == 0x0A) break; + pos_++; + } + break; + default: + return JXL_FAILURE("Y4M: parse error"); + } + } + JXL_RETURN_IF_ERROR(ExpectString("FRAME", 5)); + while (true) { + char next = 0; + JXL_RETURN_IF_ERROR(ReadChar(&next)); + if (next == 0x0A) { + *pos = pos_; + return true; + } + } + } + + Status ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(SkipWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + + 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); + } + header->floating_point = false; + header->big_endian = true; + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + Status ParseHeaderPFM(HeaderPNM* header, const uint8_t** pos) { + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->xsize)); + + JXL_RETURN_IF_ERROR(SkipBlank()); + JXL_RETURN_IF_ERROR(ParseUnsigned(&header->ysize)); + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + // The scale has no meaning as multiplier, only its sign is used to + // indicate endianness. All software expects nominal range 0..1. + double scale; + JXL_RETURN_IF_ERROR(ParseSigned(&scale)); + header->big_endian = scale >= 0.0; + header->bits_per_sample = 32; + header->floating_point = true; + + JXL_RETURN_IF_ERROR(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + const uint8_t* pos_; + const uint8_t* const end_; +}; + +constexpr size_t kMaxHeaderSize = 200; + +Status EncodeHeader(const ImageBundle& ib, const size_t bits_per_sample, + const bool little_endian, char* header, + int* JXL_RESTRICT chars_written) { + if (ib.HasAlpha()) return JXL_FAILURE("PNM: can't store alpha"); + + if (bits_per_sample == 32) { // PFM + const char type = ib.IsGray() ? 'f' : 'F'; + const double scale = little_endian ? -1.0 : 1.0; + *chars_written = + snprintf(header, kMaxHeaderSize, "P%c\n%zu %zu\n%.1f\n", type, + ib.oriented_xsize(), ib.oriented_ysize(), scale); + JXL_RETURN_IF_ERROR(static_cast(*chars_written) < + kMaxHeaderSize); + } else if (bits_per_sample == 1) { // PBM + if (!ib.IsGray()) { + return JXL_FAILURE("Cannot encode color as PBM"); + } + *chars_written = snprintf(header, kMaxHeaderSize, "P4\n%zu %zu\n", + ib.oriented_xsize(), ib.oriented_ysize()); + JXL_RETURN_IF_ERROR(static_cast(*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"); + const char type = ib.IsGray() ? '5' : '6'; + *chars_written = + snprintf(header, kMaxHeaderSize, "P%c\n%zu %zu\n%u\n", type, + ib.oriented_xsize(), ib.oriented_ysize(), max_val); + JXL_RETURN_IF_ERROR(static_cast(*chars_written) < + kMaxHeaderSize); + } + return true; +} + +Span MakeSpan(const char* str) { + return Span(reinterpret_cast(str), + strlen(str)); +} + +// Flip the image vertically for loading/saving PFM files which have the +// scanlines inverted. +void VerticallyFlipImage(Image3F* const image) { + for (int c = 0; c < 3; c++) { + for (size_t y = 0; y < image->ysize() / 2; y++) { + float* first_row = image->PlaneRow(c, y); + float* other_row = image->PlaneRow(c, image->ysize() - y - 1); + for (size_t x = 0; x < image->xsize(); ++x) { + float tmp = first_row[x]; + first_row[x] = other_row[x]; + other_row[x] = tmp; + } + } + } +} + +} // namespace + +Status DecodeImagePNM(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io) { + Parser parser(bytes); + HeaderPNM header = {}; + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&header, &pos)) return false; + JXL_RETURN_IF_ERROR( + VerifyDimensions(&io->constraints, header.xsize, header.ysize)); + + if (header.bits_per_sample == 0 || header.bits_per_sample > 32) { + return JXL_FAILURE("PNM: bits_per_sample invalid"); + } + + JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false, + header.is_gray, io)); + + if (header.floating_point) { + io->metadata.m.SetFloat32Samples(); + } else { + io->metadata.m.SetUintSamples(header.bits_per_sample); + } + io->metadata.m.SetAlphaBits(0); + io->dec_pixels = header.xsize * header.ysize; + + if (header.is_yuv > 0) { + Image3F yuvdata(header.xsize, header.ysize); + ImageBundle bundle(&io->metadata.m); + const int hshift[3][3] = {{0, 0, 0}, {0, 1, 1}, {0, 1, 1}}; + const int vshift[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 1, 1}}; + + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < header.ysize >> vshift[header.is_yuv - 1][c]; + ++y) { + float* const JXL_RESTRICT row = + yuvdata.PlaneRow((c == 2 ? 2 : 1 - c), y); + if (pos + (header.xsize >> hshift[header.is_yuv - 1][c]) > + bytes.data() + bytes.size()) + return JXL_FAILURE("Not enough image data"); + for (size_t x = 0; x < header.xsize >> hshift[header.is_yuv - 1][c]; + ++x) { + row[x] = (1.f / 255.f) * ((*pos++) - 128.f); + } + } + } + bundle.SetFromImage(std::move(yuvdata), io->metadata.m.color_encoding); + bundle.color_transform = ColorTransform::kYCbCr; + + YCbCrChromaSubsampling subsampling; + uint8_t cssh[3] = { + 2, static_cast(hshift[header.is_yuv - 1][1] ? 1 : 2), + static_cast(hshift[header.is_yuv - 1][2] ? 1 : 2)}; + uint8_t cssv[3] = { + 2, static_cast(vshift[header.is_yuv - 1][1] ? 1 : 2), + static_cast(vshift[header.is_yuv - 1][2] ? 1 : 2)}; + + JXL_RETURN_IF_ERROR(subsampling.Set(cssh, cssv)); + + bundle.chroma_subsampling = subsampling; + + io->Main() = std::move(bundle); + } else { + const bool flipped_y = header.bits_per_sample == 32; // PFMs are flipped + const bool float_in = header.bits_per_sample == 32; + const Span span(pos, bytes.data() + bytes.size() - pos); + JXL_RETURN_IF_ERROR(ConvertFromExternal( + span, header.xsize, header.ysize, io->metadata.m.color_encoding, + /*has_alpha=*/false, /*alpha_is_premultiplied=*/false, + io->metadata.m.bit_depth.bits_per_sample, + header.big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN, flipped_y, pool, + &io->Main(), float_in)); + } + if (!header.floating_point) { + io->metadata.m.bit_depth.bits_per_sample = io->Main().DetectRealBitdepth(); + } + io->SetSize(header.xsize, header.ysize); + SetIntensityTarget(io); + return true; +} + +Status EncodeImagePNM(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes) { + const bool floating_point = bits_per_sample > 16; + // Choose native for PFM; PGM/PPM require big-endian (N/A for PBM) + const JxlEndianness endianness = + floating_point ? JXL_NATIVE_ENDIAN : JXL_BIG_ENDIAN; + + ImageMetadata metadata_copy = io->metadata.m; + // AllDefault sets all_default, which can cause a race condition. + if (!Bundle::AllDefault(metadata_copy)) { + JXL_WARNING("PNM encoder ignoring metadata - use a different codec"); + } + if (!c_desired.IsSRGB()) { + JXL_WARNING( + "PNM encoder cannot store custom ICC profile; decoder\n" + "will need hint key=color_space to get the same values"); + } + + ImageBundle ib = io->Main().Copy(); + // In case of PFM the image must be flipped upside down since that format + // is designed that way. + const ImageBundle* to_color_transform = &ib; + ImageBundle flipped; + if (floating_point) { + flipped = ib.Copy(); + VerticallyFlipImage(flipped.color()); + to_color_transform = &flipped; + } + ImageMetadata metadata = io->metadata.m; + ImageBundle store(&metadata); + const ImageBundle* transformed; + JXL_RETURN_IF_ERROR(TransformIfNeeded(*to_color_transform, c_desired, pool, + &store, &transformed)); + size_t stride = ib.oriented_xsize() * + (c_desired.Channels() * bits_per_sample) / kBitsPerByte; + PaddedBytes pixels(stride * ib.oriented_ysize()); + JXL_RETURN_IF_ERROR(ConvertToExternal( + *transformed, bits_per_sample, floating_point, c_desired.Channels(), + endianness, stride, pool, pixels.data(), pixels.size(), + /*out_callback=*/nullptr, /*out_opaque=*/nullptr, + metadata.GetOrientation())); + + char header[kMaxHeaderSize]; + int header_size = 0; + bool is_little_endian = endianness == JXL_LITTLE_ENDIAN || + (endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()); + JXL_RETURN_IF_ERROR(EncodeHeader(*transformed, bits_per_sample, + is_little_endian, header, &header_size)); + + bytes->resize(static_cast(header_size) + pixels.size()); + memcpy(bytes->data(), header, static_cast(header_size)); + memcpy(bytes->data() + header_size, pixels.data(), pixels.size()); + + return true; +} + +void TestCodecPNM() { + size_t u = 77777; // Initialized to wrong value. + double d = 77.77; +// Failing to parse invalid strings results in a crash if `JXL_CRASH_ON_ERROR` +// is defined and hence the tests fail. Therefore we only run these tests if +// `JXL_CRASH_ON_ERROR` is not defined. +#ifndef JXL_CRASH_ON_ERROR + JXL_CHECK(false == Parser(MakeSpan("")).ParseUnsigned(&u)); + JXL_CHECK(false == Parser(MakeSpan("+")).ParseUnsigned(&u)); + JXL_CHECK(false == Parser(MakeSpan("-")).ParseUnsigned(&u)); + JXL_CHECK(false == Parser(MakeSpan("A")).ParseUnsigned(&u)); + + JXL_CHECK(false == Parser(MakeSpan("")).ParseSigned(&d)); + JXL_CHECK(false == Parser(MakeSpan("+")).ParseSigned(&d)); + JXL_CHECK(false == Parser(MakeSpan("-")).ParseSigned(&d)); + JXL_CHECK(false == Parser(MakeSpan("A")).ParseSigned(&d)); +#endif + JXL_CHECK(true == Parser(MakeSpan("1")).ParseUnsigned(&u)); + JXL_CHECK(u == 1); + + JXL_CHECK(true == Parser(MakeSpan("32")).ParseUnsigned(&u)); + JXL_CHECK(u == 32); + + JXL_CHECK(true == Parser(MakeSpan("1")).ParseSigned(&d)); + JXL_CHECK(d == 1.0); + JXL_CHECK(true == Parser(MakeSpan("+2")).ParseSigned(&d)); + JXL_CHECK(d == 2.0); + JXL_CHECK(true == Parser(MakeSpan("-3")).ParseSigned(&d)); + JXL_CHECK(std::abs(d - -3.0) < 1E-15); + JXL_CHECK(true == Parser(MakeSpan("3.141592")).ParseSigned(&d)); + JXL_CHECK(std::abs(d - 3.141592) < 1E-15); + JXL_CHECK(true == Parser(MakeSpan("-3.141592")).ParseSigned(&d)); + JXL_CHECK(std::abs(d - -3.141592) < 1E-15); +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/codec_pnm.h b/lib/extras/codec_pnm.h new file mode 100644 index 0000000..c9e5560 --- /dev/null +++ b/lib/extras/codec_pnm.h @@ -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_EXTRAS_CODEC_PNM_H_ +#define LIB_EXTRAS_CODEC_PNM_H_ + +// Encodes/decodes PBM/PGM/PPM/PFM pixels in memory. + +#include +#include + +// TODO(janwas): workaround for incorrect Win64 codegen (cause unknown) +#include + +#include "lib/extras/color_hints.h" +#include "lib/jxl/base/data_parallel.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`. color_hints may specify "color_space", which +// defaults to sRGB. +Status DecodeImagePNM(const Span bytes, + const ColorHints& color_hints, ThreadPool* pool, + CodecInOut* io); + +// Transforms from io->c_current to `c_desired` and encodes into `bytes`. +Status EncodeImagePNM(const CodecInOut* io, const ColorEncoding& c_desired, + size_t bits_per_sample, ThreadPool* pool, + PaddedBytes* bytes); + +void TestCodecPNM(); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_CODEC_PNM_H_ diff --git a/lib/extras/codec_psd.cc b/lib/extras/codec_psd.cc new file mode 100644 index 0000000..b51ef82 --- /dev/null +++ b/lib/extras/codec_psd.cc @@ -0,0 +1,617 @@ +// 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 +#include +#include + +#include +#include +#include +#include +#include + +#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/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 chans, + std::vector 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 %zu)", 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(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 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(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> 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: %zu",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. %zu", 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 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 + } + + 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: %zu", layerlength); + int layercount = static_cast(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=%zu", 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(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> layer_chan_id; + std::vector layer_offsets(layercount + 1, 0); + std::vector 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: %zu x %zu 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 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: %zu 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(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 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 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 (%zu)", + (int)(pos - bytes.data()), (size_t)pos); + ImageBundle& layer = io->frames[il++]; + std::vector& 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 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 chan_id(real_nb_channels); + std::iota(chan_id.begin(), chan_id.end(), 0); + std::vector invert(real_nb_channels, false); + if (static_cast(spotcolor.size()) + colormodel + 1 < + real_nb_channels) { + return JXL_FAILURE("Inconsistent layer configuration"); + } + if (!merged_has_alpha) { + if (colormodel >= real_nb_channels) { + return JXL_FAILURE("Inconsistent layer configuration"); + } + chan_id.erase(chan_id.begin() + colormodel); + invert.erase(invert.begin() + colormodel); + } else { + 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 diff --git a/lib/extras/codec_psd.h b/lib/extras/codec_psd.h new file mode 100644 index 0000000..c04ef2d --- /dev/null +++ b/lib/extras/codec_psd.h @@ -0,0 +1,37 @@ +// 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 +#include + +#include "lib/extras/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 bytes, + const 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_ diff --git a/lib/extras/codec_test.cc b/lib/extras/codec_test.cc new file mode 100644 index 0000000..fd167b3 --- /dev/null +++ b/lib/extras/codec_test.cc @@ -0,0 +1,378 @@ +// 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.h" + +#include +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec_pgx.h" +#include "lib/extras/codec_pnm.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/luminance.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace extras { +namespace { + +CodecInOut CreateTestImage(const size_t xsize, const size_t ysize, + const bool is_gray, const bool add_alpha, + const size_t bits_per_sample, + const ColorEncoding& c_native) { + Image3F image(xsize, ysize); + std::mt19937_64 rng(129); + std::uniform_real_distribution dist(0.0f, 1.0f); + if (is_gray) { + for (size_t y = 0; y < ysize; ++y) { + float* JXL_RESTRICT row0 = image.PlaneRow(0, y); + float* JXL_RESTRICT row1 = image.PlaneRow(1, y); + float* JXL_RESTRICT row2 = image.PlaneRow(2, y); + for (size_t x = 0; x < xsize; ++x) { + row0[x] = row1[x] = row2[x] = dist(rng); + } + } + } else { + RandomFillImage(&image, 1.0f); + } + CodecInOut io; + + if (bits_per_sample == 32) { + io.metadata.m.SetFloat32Samples(); + } else { + io.metadata.m.SetUintSamples(bits_per_sample); + } + io.metadata.m.color_encoding = c_native; + io.SetFromImage(std::move(image), c_native); + if (add_alpha) { + ImageF alpha(xsize, ysize); + RandomFillImage(&alpha, 1.f); + io.metadata.m.SetAlphaBits(bits_per_sample <= 8 ? 8 : 16); + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + } + return io; +} + +// Ensures reading a newly written file leads to the same image pixels. +void TestRoundTrip(Codec codec, const size_t xsize, const size_t ysize, + const bool is_gray, const bool add_alpha, + const size_t bits_per_sample, ThreadPool* pool) { + // JPEG encoding is not lossless. + if (codec == Codec::kJPG) return; + if (codec == Codec::kPNM && add_alpha) return; + // Our EXR codec always uses 16-bit premultiplied alpha, does not support + // grayscale, and somehow does not have sufficient precision for this test. + if (codec == Codec::kEXR) return; + printf("Codec %s bps:%zu gr:%d al:%d\n", + ExtensionFromCodec(codec, is_gray, bits_per_sample).c_str(), + bits_per_sample, is_gray, add_alpha); + + ColorEncoding c_native; + c_native.SetColorSpace(is_gray ? ColorSpace::kGray : ColorSpace::kRGB); + // Note: this must not be wider than c_external, otherwise gamut clipping + // will cause large round-trip errors. + c_native.primaries = Primaries::kP3; + c_native.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_CHECK(c_native.CreateICC()); + + // Generally store same color space to reduce round trip errors.. + ColorEncoding c_external = c_native; + // .. unless we have enough precision for some transforms. + if (bits_per_sample >= 16) { + c_external.white_point = WhitePoint::kE; + c_external.primaries = Primaries::k2100; + c_external.tf.SetTransferFunction(TransferFunction::kSRGB); + } + JXL_CHECK(c_external.CreateICC()); + + const CodecInOut io = CreateTestImage(xsize, ysize, is_gray, add_alpha, + bits_per_sample, c_native); + const ImageBundle& ib1 = io.Main(); + + PaddedBytes encoded; + JXL_CHECK(Encode(io, codec, c_external, bits_per_sample, &encoded, pool)); + + CodecInOut io2; + ColorHints color_hints; + io2.target_nits = io.metadata.m.IntensityTarget(); + // Only for PNM because PNG will warn about ignoring them. + if (codec == Codec::kPNM) { + color_hints.Add("color_space", Description(c_external)); + } + JXL_CHECK(SetFromBytes(Span(encoded), color_hints, &io2, pool, + nullptr)); + ImageBundle& ib2 = io2.Main(); + + EXPECT_EQ(Description(c_external), + Description(io2.metadata.m.color_encoding)); + + // See c_external above - for low bits_per_sample the encoded space is + // already the same. + if (bits_per_sample < 16) { + EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current())); + } + + if (add_alpha) { + EXPECT_TRUE(SamePixels(ib1.alpha(), *ib2.alpha())); + } + + JXL_CHECK(ib2.TransformTo(ib1.c_current(), pool)); + + double max_l1, max_rel; + // Round-trip tolerances must be higher than in external_image_test because + // codecs do not support unbounded ranges. +#if JPEGXL_ENABLE_SKCMS + if (bits_per_sample <= 12) { + max_l1 = 0.5; + max_rel = 6E-3; + } else { + max_l1 = 1E-3; + max_rel = 5E-4; + } +#else // JPEGXL_ENABLE_SKCMS + if (bits_per_sample <= 12) { + max_l1 = 0.5; + max_rel = 6E-3; + } else if (bits_per_sample == 16) { + max_l1 = 3E-3; + max_rel = 1E-4; + } else { +#ifdef __ARM_ARCH + // pow() implementation in arm is a bit less precise than in x86 and + // therefore we need a bigger error margin in this case. + max_l1 = 1E-7; + max_rel = 1E-4; +#else + max_l1 = 1E-7; + max_rel = 1E-5; +#endif + } +#endif // JPEGXL_ENABLE_SKCMS + + VerifyRelativeError(ib1.color(), *ib2.color(), max_l1, max_rel); +} + +#if 0 +TEST(CodecTest, TestRoundTrip) { + ThreadPoolInternal pool(12); + + const size_t xsize = 7; + const size_t ysize = 4; + + for (Codec codec : Values()) { + for (int bits_per_sample : {8, 10, 12, 16, 32}) { + for (bool is_gray : {false, true}) { + for (bool add_alpha : {false, true}) { + TestRoundTrip(codec, xsize, ysize, is_gray, add_alpha, + static_cast(bits_per_sample), &pool); + } + } + } + } +} +#endif + +CodecInOut DecodeRoundtrip(const std::string& pathname, Codec expected_codec, + ThreadPool* pool, + const ColorHints& color_hints = ColorHints()) { + CodecInOut io; + const PaddedBytes orig = ReadTestData(pathname); + JXL_CHECK( + SetFromBytes(Span(orig), color_hints, &io, pool, nullptr)); + const ImageBundle& ib1 = io.Main(); + + // Encode/Decode again to make sure Encode carries through all metadata. + PaddedBytes encoded; + JXL_CHECK(Encode(io, expected_codec, io.metadata.m.color_encoding, + io.metadata.m.bit_depth.bits_per_sample, &encoded, pool)); + + CodecInOut io2; + JXL_CHECK(SetFromBytes(Span(encoded), color_hints, &io2, pool, + nullptr)); + const ImageBundle& ib2 = io2.Main(); + EXPECT_EQ(Description(ib1.metadata()->color_encoding), + Description(ib2.metadata()->color_encoding)); + EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current())); + + size_t bits_per_sample = io2.metadata.m.bit_depth.bits_per_sample; + + // "Same" pixels? + double max_l1 = bits_per_sample <= 12 ? 1.3 : 2E-3; + double max_rel = bits_per_sample <= 12 ? 6E-3 : 1E-4; + if (ib1.metadata()->color_encoding.IsGray()) { + max_rel *= 2.0; + } else if (ib1.metadata()->color_encoding.primaries != Primaries::kSRGB) { + // Need more tolerance for large gamuts (anything but sRGB) + max_l1 *= 1.5; + max_rel *= 3.0; + } + VerifyRelativeError(ib1.color(), ib2.color(), max_l1, max_rel); + + // Simulate the encoder removing profile and decoder restoring it. + if (!ib2.metadata()->color_encoding.WantICC()) { + io2.metadata.m.color_encoding.InternalRemoveICC(); + EXPECT_TRUE(io2.metadata.m.color_encoding.CreateICC()); + } + + return io2; +} + +#if 0 +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"}; + for (const char* relative_pathname : paths) { + const CodecInOut io = + DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool); + EXPECT_EQ(8, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample); + + EXPECT_EQ(64, io.xsize()); + EXPECT_EQ(64, io.ysize()); + EXPECT_FALSE(io.metadata.m.HasAlpha()); + + const ColorEncoding& c_original = io.metadata.m.color_encoding; + EXPECT_FALSE(c_original.ICC().empty()); + EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); + EXPECT_EQ(WhitePoint::kD65, c_original.white_point); + EXPECT_EQ(Primaries::kSRGB, c_original.primaries); + EXPECT_TRUE(c_original.tf.IsSRGB()); + } +} + +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", + }; + const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65, + WhitePoint::kD65}; + const Primaries primaries[3] = {Primaries::kCustom, Primaries::kSRGB, + Primaries::k2100}; + + for (size_t i = 0; i < 3; ++i) { + const CodecInOut io = DecodeRoundtrip(paths[i], Codec::kPNG, &pool); + EXPECT_EQ(16, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample); + + EXPECT_EQ(64, io.xsize()); + EXPECT_EQ(64, io.ysize()); + EXPECT_FALSE(io.metadata.m.HasAlpha()); + + const ColorEncoding& c_original = io.metadata.m.color_encoding; + EXPECT_FALSE(c_original.ICC().empty()); + EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); + EXPECT_EQ(white_points[i], c_original.white_point); + EXPECT_EQ(primaries[i], c_original.primaries); + EXPECT_TRUE(c_original.tf.IsLinear()); + } +} + +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", + }; + for (const char* relative_pathname : paths) { + const CodecInOut io = + DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool); + EXPECT_GE(16, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_LE(14, io.metadata.m.bit_depth.bits_per_sample); + + EXPECT_EQ(64, io.xsize()); + EXPECT_EQ(64, io.ysize()); + EXPECT_FALSE(io.metadata.m.HasAlpha()); + + const ColorEncoding& c_original = io.metadata.m.color_encoding; + EXPECT_FALSE(c_original.ICC().empty()); + EXPECT_EQ(RenderingIntent::kPerceptual, c_original.rendering_intent); + EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); + EXPECT_EQ(WhitePoint::kD65, c_original.white_point); + EXPECT_EQ(Primaries::kSRGB, c_original.primaries); + EXPECT_EQ(TransferFunction::k709, c_original.tf.GetTransferFunction()); + } +} + +TEST(CodecTest, TestPNGSuite) { + 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); + + // Extract gAMA + const CodecInOut b1 = + DecodeRoundtrip("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); + 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); + EXPECT_EQ(978, b_exif.blobs.exif.size()); +} +#endif + +void VerifyWideGamutMetadata(const std::string& relative_pathname, + const Primaries primaries, ThreadPool* pool) { + const CodecInOut io = DecodeRoundtrip(relative_pathname, Codec::kPNG, pool); + + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + + const ColorEncoding& c_original = io.metadata.m.color_encoding; + EXPECT_FALSE(c_original.ICC().empty()); + EXPECT_EQ(RenderingIntent::kAbsolute, c_original.rendering_intent); + EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace()); + EXPECT_EQ(WhitePoint::kD65, c_original.white_point); + EXPECT_EQ(primaries, c_original.primaries); +} + +TEST(CodecTest, TestWideGamut) { + ThreadPoolInternal pool(12); + // VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-bars.png", + // Primaries::kP3, &pool); + VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-ring.png", + Primaries::kP3, &pool); + // VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-bars.png", + // Primaries::k2100, &pool); + // VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-ring.png", + // Primaries::k2100, &pool); +} + +TEST(CodecTest, TestPNM) { TestCodecPNM(); } + +} // namespace +} // namespace extras +} // namespace jxl diff --git a/lib/extras/color_description.cc b/lib/extras/color_description.cc new file mode 100644 index 0000000..6db11b0 --- /dev/null +++ b/lib/extras/color_description.cc @@ -0,0 +1,218 @@ +// 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/color_description.h" + +#include +#include + +namespace jxl { + +namespace { + +template +struct EnumName { + const char* name; + T value; +}; + +const EnumName kJxlColorSpaceNames[] = { + {"RGB", JXL_COLOR_SPACE_RGB}, + {"Gra", JXL_COLOR_SPACE_GRAY}, + {"XYB", JXL_COLOR_SPACE_XYB}, + {"CS?", JXL_COLOR_SPACE_UNKNOWN}, +}; + +const EnumName kJxlWhitePointNames[] = { + {"D65", JXL_WHITE_POINT_D65}, + {"Cst", JXL_WHITE_POINT_CUSTOM}, + {"EER", JXL_WHITE_POINT_E}, + {"DCI", JXL_WHITE_POINT_DCI}, +}; + +const EnumName kJxlPrimariesNames[] = { + {"SRG", JXL_PRIMARIES_SRGB}, + {"Cst", JXL_PRIMARIES_CUSTOM}, + {"202", JXL_PRIMARIES_2100}, + {"DCI", JXL_PRIMARIES_P3}, +}; + +const EnumName kJxlTransferFunctionNames[] = { + {"709", JXL_TRANSFER_FUNCTION_709}, + {"TF?", JXL_TRANSFER_FUNCTION_UNKNOWN}, + {"Lin", JXL_TRANSFER_FUNCTION_LINEAR}, + {"SRG", JXL_TRANSFER_FUNCTION_SRGB}, + {"PeQ", JXL_TRANSFER_FUNCTION_PQ}, + {"DCI", JXL_TRANSFER_FUNCTION_DCI}, + {"HLG", JXL_TRANSFER_FUNCTION_HLG}, + {"", JXL_TRANSFER_FUNCTION_GAMMA}, +}; + +const EnumName kJxlRenderingIntentNames[] = { + {"Per", JXL_RENDERING_INTENT_PERCEPTUAL}, + {"Rel", JXL_RENDERING_INTENT_RELATIVE}, + {"Sat", JXL_RENDERING_INTENT_SATURATION}, + {"Abs", JXL_RENDERING_INTENT_ABSOLUTE}, +}; + +template +Status ParseEnum(const std::string& token, const EnumName* enum_values, + size_t enum_len, T* value) { + std::string str; + for (size_t i = 0; i < enum_len; i++) { + if (enum_values[i].name == token) { + *value = enum_values[i].value; + return true; + } + } + return false; +} +#define ARRAYSIZE(X) (sizeof(X) / sizeof((X)[0])) +#define PARSE_ENUM(type, token, value) \ + ParseEnum(token, k##type##Names, ARRAYSIZE(k##type##Names), value) + +class Tokenizer { + public: + Tokenizer(const std::string* input, char separator) + : input_(input), separator_(separator) {} + + Status Next(std::string* next) { + const size_t end = input_->find(separator_, start_); + if (end == std::string::npos) { + *next = input_->substr(start_); // rest of string + } else { + *next = input_->substr(start_, end - start_); + } + if (next->empty()) return JXL_FAILURE("Missing token"); + start_ = end + 1; + return true; + } + + private: + const std::string* const input_; // not owned + const char separator_; + size_t start_ = 0; // of next token +}; + +Status ParseDouble(const std::string& num, double* d) { + char* end; + errno = 0; + *d = strtod(num.c_str(), &end); + if (*d == 0.0 && end == num.c_str()) { + return JXL_FAILURE("Invalid double: %s", num.c_str()); + } + if (std::isnan(*d)) { + return JXL_FAILURE("Invalid double: %s", num.c_str()); + } + if (errno == ERANGE) { + return JXL_FAILURE("Double out of range: %s", num.c_str()); + } + return true; +} + +Status ParseDouble(Tokenizer* tokenizer, double* d) { + std::string num; + JXL_RETURN_IF_ERROR(tokenizer->Next(&num)); + return ParseDouble(num, d); +} + +Status ParseColorSpace(Tokenizer* tokenizer, JxlColorEncoding* c) { + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + JxlColorSpace cs; + if (PARSE_ENUM(JxlColorSpace, str, &cs)) { + c->color_space = cs; + return true; + } + + return JXL_FAILURE("Unknown ColorSpace %s", str.c_str()); +} + +Status ParseWhitePoint(Tokenizer* tokenizer, JxlColorEncoding* c) { + if (c->color_space == JXL_COLOR_SPACE_XYB) { + // Implicit white point. + c->white_point = JXL_WHITE_POINT_D65; + return true; + } + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (PARSE_ENUM(JxlWhitePoint, str, &c->white_point)) return true; + + Tokenizer xy_tokenizer(&str, ';'); + c->white_point = JXL_WHITE_POINT_CUSTOM; + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->white_point_xy + 0)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->white_point_xy + 1)); + return true; +} + +Status ParsePrimaries(Tokenizer* tokenizer, JxlColorEncoding* c) { + if (c->color_space == JXL_COLOR_SPACE_GRAY || + c->color_space == JXL_COLOR_SPACE_XYB) { + // No primaries case. + return true; + } + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (PARSE_ENUM(JxlPrimaries, str, &c->primaries)) return true; + + Tokenizer xy_tokenizer(&str, ';'); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_red_xy + 0)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_red_xy + 1)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_green_xy + 0)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_green_xy + 1)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_blue_xy + 0)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, c->primaries_blue_xy + 1)); + c->primaries = JXL_PRIMARIES_CUSTOM; + + return JXL_FAILURE("Invalid primaries %s", str.c_str()); +} + +Status ParseRenderingIntent(Tokenizer* tokenizer, JxlColorEncoding* c) { + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (PARSE_ENUM(JxlRenderingIntent, str, &c->rendering_intent)) return true; + + return JXL_FAILURE("Invalid RenderingIntent %s\n", str.c_str()); +} + +Status ParseTransferFunction(Tokenizer* tokenizer, JxlColorEncoding* c) { + if (c->color_space == JXL_COLOR_SPACE_XYB) { + // Implicit TF. + c->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + c->gamma = 1 / 3.; + return true; + } + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (PARSE_ENUM(JxlTransferFunction, str, &c->transfer_function)) { + return true; + } + + if (str[0] == 'g') { + JXL_RETURN_IF_ERROR(ParseDouble(str.substr(1), &c->gamma)); + c->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + return true; + } + + return JXL_FAILURE("Invalid gamma %s", str.c_str()); +} + +} // namespace + +Status ParseDescription(const std::string& description, JxlColorEncoding* c) { + *c = {}; + Tokenizer tokenizer(&description, '_'); + JXL_RETURN_IF_ERROR(ParseColorSpace(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseWhitePoint(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParsePrimaries(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseRenderingIntent(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseTransferFunction(&tokenizer, c)); + return true; +} + +} // namespace jxl diff --git a/lib/extras/color_description.h b/lib/extras/color_description.h new file mode 100644 index 0000000..989d591 --- /dev/null +++ b/lib/extras/color_description.h @@ -0,0 +1,22 @@ +// 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_COLOR_DESCRIPTION_H_ +#define LIB_EXTRAS_COLOR_DESCRIPTION_H_ + +#include + +#include "jxl/color_encoding.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Parse the color description into a JxlColorEncoding "RGB_D65_SRG_Rel_Lin". +Status ParseDescription(const std::string& description, + JxlColorEncoding* JXL_RESTRICT c); + +} // namespace jxl + +#endif // LIB_EXTRAS_COLOR_DESCRIPTION_H_ diff --git a/lib/extras/color_description_test.cc b/lib/extras/color_description_test.cc new file mode 100644 index 0000000..e8cc1a2 --- /dev/null +++ b/lib/extras/color_description_test.cc @@ -0,0 +1,38 @@ +// 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/color_description.h" + +#include "gtest/gtest.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/test_utils.h" + +namespace jxl { + +// Verify ParseDescription(Description) yields the same ColorEncoding +TEST(ColorDescriptionTest, RoundTripAll) { + for (const auto& cdesc : test::AllEncodings()) { + const ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc); + const std::string description = Description(c_original); + printf("%s\n", description.c_str()); + + JxlColorEncoding c_external = {}; + EXPECT_TRUE(ParseDescription(description, &c_external)); + ColorEncoding c_internal; + EXPECT_TRUE( + ConvertExternalToInternalColorEncoding(c_external, &c_internal)); + EXPECT_TRUE(c_original.SameColorEncoding(c_internal)) + << "Where c_original=" << c_original + << " and c_internal=" << c_internal; + } +} + +TEST(ColorDescriptionTest, NanGamma) { + const std::string description = "Gra_2_Per_gnan"; + JxlColorEncoding c; + EXPECT_FALSE(ParseDescription(description, &c)); +} + +} // namespace jxl diff --git a/lib/extras/color_hints.cc b/lib/extras/color_hints.cc new file mode 100644 index 0000000..6fff949 --- /dev/null +++ b/lib/extras/color_hints.cc @@ -0,0 +1,67 @@ +// 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/color_hints.h" + +#include "lib/extras/color_description.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/color_encoding_internal.h" + +namespace jxl { +namespace extras { + +Status ApplyColorHints(const ColorHints& color_hints, + const bool color_already_set, const bool is_gray, + CodecInOut* io) { + if (color_already_set) { + return color_hints.Foreach( + [](const std::string& key, const std::string& /*value*/) { + JXL_WARNING("Decoder ignoring %s hint", key.c_str()); + return true; + }); + } + + bool got_color_space = false; + + JXL_RETURN_IF_ERROR(color_hints.Foreach( + [is_gray, io, &got_color_space](const std::string& key, + const std::string& value) -> Status { + ColorEncoding* c_original = &io->metadata.m.color_encoding; + if (key == "color_space") { + JxlColorEncoding c_original_external; + if (!ParseDescription(value, &c_original_external) || + !ConvertExternalToInternalColorEncoding(c_original_external, + c_original) || + !c_original->CreateICC()) { + return JXL_FAILURE("Failed to apply color_space"); + } + + if (is_gray != io->metadata.m.color_encoding.IsGray()) { + return JXL_FAILURE("mismatch between file and color_space hint"); + } + + got_color_space = true; + } else if (key == "icc_pathname") { + PaddedBytes icc; + JXL_RETURN_IF_ERROR(ReadFile(value, &icc)); + JXL_RETURN_IF_ERROR(c_original->SetICC(std::move(icc))); + got_color_space = true; + } else { + JXL_WARNING("Ignoring %s hint", key.c_str()); + } + return true; + })); + + if (!got_color_space) { + JXL_WARNING("No color_space/icc_pathname given, assuming sRGB"); + JXL_RETURN_IF_ERROR(io->metadata.m.color_encoding.SetSRGB( + is_gray ? ColorSpace::kGray : ColorSpace::kRGB)); + } + + return true; +} + +} // namespace extras +} // namespace jxl diff --git a/lib/extras/color_hints.h b/lib/extras/color_hints.h new file mode 100644 index 0000000..8643144 --- /dev/null +++ b/lib/extras/color_hints.h @@ -0,0 +1,73 @@ +// 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_COLOR_HINTS_H_ +#define LIB_EXTRAS_COLOR_HINTS_H_ + +// Not all the formats implemented in the extras lib support bundling color +// information into the file, and those that support it may not have it. +// To allow attaching color information to those file formats the caller can +// define these color hints. + +#include +#include + +#include +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" + +namespace jxl { + +class ColorHints { + public: + // key=color_space, value=Description(c/pp): specify the ColorEncoding of + // the pixels for decoding. Otherwise, if the codec did not obtain an ICC + // profile from the image, assume sRGB. + // + // Strings are taken from the command line, so avoid spaces for convenience. + void Add(const std::string& key, const std::string& value) { + kv_.emplace_back(key, value); + } + + // Calls `func(key, value)` for each key/value in the order they were added, + // returning false immediately if `func` returns false. + template + Status Foreach(const Func& func) const { + for (const KeyValue& kv : kv_) { + Status ok = func(kv.key, kv.value); + if (!ok) { + return JXL_FAILURE("ColorHints::Foreach returned false"); + } + } + return true; + } + + private: + // Splitting into key/value avoids parsing in each codec. + struct KeyValue { + KeyValue(std::string key, std::string value) + : key(std::move(key)), value(std::move(value)) {} + + std::string key; + std::string value; + }; + + std::vector kv_; +}; + +namespace extras { + +// Apply the color hints to the decoded image in CodecInOut if any. +// color_already_set tells whether the color encoding was already set, in which +// case the hints are ignored if any hint is passed. +Status ApplyColorHints(const ColorHints& color_hints, bool color_already_set, + bool is_gray, CodecInOut* io); + +} // namespace extras +} // namespace jxl + +#endif // LIB_EXTRAS_COLOR_HINTS_H_ diff --git a/lib/extras/time.cc b/lib/extras/time.cc new file mode 100644 index 0000000..73d1b8f --- /dev/null +++ b/lib/extras/time.cc @@ -0,0 +1,60 @@ +// 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/time.h" + +#include +#include +#include + +#include + +#include "lib/jxl/base/os_macros.h" // for JXL_OS_* + +#if JXL_OS_WIN +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX +#include +#endif // JXL_OS_WIN + +#if JXL_OS_MAC +#include +#include +#endif // JXL_OS_MAC + +#if JXL_OS_HAIKU +#include +#endif // JXL_OS_HAIKU + +namespace jxl { + +double Now() { +#if JXL_OS_WIN + LARGE_INTEGER counter; + (void)QueryPerformanceCounter(&counter); + LARGE_INTEGER freq; + (void)QueryPerformanceFrequency(&freq); + return double(counter.QuadPart) / freq.QuadPart; +#elif JXL_OS_MAC + const auto t = mach_absolute_time(); + // On OSX/iOS platform the elapsed time is cpu time unit + // We have to query the time base information to convert it back + // See https://developer.apple.com/library/mac/qa/qa1398/_index.html + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) { + (void)mach_timebase_info(&timebase); + } + return double(t) * timebase.numer / timebase.denom * 1E-9; +#elif JXL_OS_HAIKU + return double(system_time_nsecs()) * 1E-9; +#else + timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1E-9; +#endif +} + +} // namespace jxl diff --git a/lib/extras/time.h b/lib/extras/time.h new file mode 100644 index 0000000..c71414b --- /dev/null +++ b/lib/extras/time.h @@ -0,0 +1,19 @@ +// 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_TIME_H_ +#define LIB_EXTRAS_TIME_H_ + +// OS-specific function for timing. + +namespace jxl { + +// Returns current time [seconds] from a monotonic clock with unspecified +// starting point - only suitable for computing elapsed time. +double Now(); + +} // namespace jxl + +#endif // LIB_EXTRAS_TIME_H_ diff --git a/lib/extras/tone_mapping.cc b/lib/extras/tone_mapping.cc new file mode 100644 index 0000000..9bb1c05 --- /dev/null +++ b/lib/extras/tone_mapping.cc @@ -0,0 +1,160 @@ +// 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/tone_mapping.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/extras/tone_mapping.cc" +#include +#include + +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +Status ToneMapFrame(const std::pair display_nits, + ImageBundle* const ib, ThreadPool* const pool) { + // Perform tone mapping as described in Report ITU-R BT.2390-8, section 5.4 + // (pp. 23-25). + // https://www.itu.int/pub/R-REP-BT.2390-8-2020 + + HWY_FULL(float) df; + using V = decltype(Zero(df)); + + ColorEncoding linear_rec2020; + linear_rec2020.SetColorSpace(ColorSpace::kRGB); + linear_rec2020.primaries = Primaries::k2100; + linear_rec2020.white_point = WhitePoint::kD65; + linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC()); + JXL_RETURN_IF_ERROR(ib->TransformTo(linear_rec2020, pool)); + + const auto eotf_inv = [&df](const V luminance) -> V { + return TF_PQ().EncodedFromDisplay(df, luminance * Set(df, 1. / 10000)); + }; + + const V pq_mastering_min = + eotf_inv(Set(df, ib->metadata()->tone_mapping.min_nits)); + const V pq_mastering_max = + eotf_inv(Set(df, ib->metadata()->tone_mapping.intensity_target)); + const V pq_mastering_range = pq_mastering_max - pq_mastering_min; + const V inv_pq_mastering_range = + Set(df, 1) / (pq_mastering_max - pq_mastering_min); + const V min_lum = (eotf_inv(Set(df, display_nits.first)) - pq_mastering_min) * + inv_pq_mastering_range; + const V max_lum = + (eotf_inv(Set(df, display_nits.second)) - pq_mastering_min) * + inv_pq_mastering_range; + const V ks = MulAdd(Set(df, 1.5f), max_lum, Set(df, -0.5f)); + const V b = min_lum; + + const V inv_one_minus_ks = Set(df, 1) / Max(Set(df, 1e-6f), Set(df, 1) - ks); + const auto T = [ks, inv_one_minus_ks](const V a) { + return (a - ks) * inv_one_minus_ks; + }; + const auto P = [&T, &df, ks, max_lum](const V b) { + const V t_b = T(b); + const V t_b_2 = t_b * t_b; + const V t_b_3 = t_b_2 * t_b; + return MulAdd( + MulAdd(Set(df, 2), t_b_3, MulAdd(Set(df, -3), t_b_2, Set(df, 1))), ks, + MulAdd(t_b_3 + MulAdd(Set(df, -2), t_b_2, t_b), Set(df, 1) - ks, + MulAdd(Set(df, -2), t_b_3, Set(df, 3) * t_b_2) * max_lum)); + }; + + const V inv_max_display_nits = Set(df, 1 / display_nits.second); + + JXL_RETURN_IF_ERROR(RunOnPool( + pool, 0, ib->ysize(), ThreadPool::SkipInit(), + [&](const int y, const int thread) { + float* const JXL_RESTRICT row_r = ib->color()->PlaneRow(0, y); + float* const JXL_RESTRICT row_g = ib->color()->PlaneRow(1, y); + float* const JXL_RESTRICT row_b = ib->color()->PlaneRow(2, y); + for (size_t x = 0; x < ib->xsize(); x += Lanes(df)) { + V red = Load(df, row_r + x); + V green = Load(df, row_g + x); + V blue = Load(df, row_b + x); + const V luminance = Set(df, ib->metadata()->IntensityTarget()) * + (MulAdd(Set(df, 0.2627f), red, + MulAdd(Set(df, 0.6780f), green, + Set(df, 0.0593f) * blue))); + const V normalized_pq = + Min(Set(df, 1.f), (eotf_inv(luminance) - pq_mastering_min) * + inv_pq_mastering_range); + const V e2 = + IfThenElse(normalized_pq < ks, normalized_pq, P(normalized_pq)); + const V one_minus_e2 = Set(df, 1) - e2; + const V one_minus_e2_2 = one_minus_e2 * one_minus_e2; + const V one_minus_e2_4 = one_minus_e2_2 * one_minus_e2_2; + const V e3 = MulAdd(b, one_minus_e2_4, e2); + const V e4 = MulAdd(e3, pq_mastering_range, pq_mastering_min); + const V new_luminance = + Min(Set(df, display_nits.second), + ZeroIfNegative(Set(df, 10000) * + TF_PQ().DisplayFromEncoded(df, e4))); + + const V ratio = new_luminance / luminance; + const V multiplier = ratio * + Set(df, ib->metadata()->IntensityTarget()) * + inv_max_display_nits; + + red *= multiplier; + green *= multiplier; + blue *= multiplier; + + const V gray = new_luminance * inv_max_display_nits; + + // Desaturate out-of-gamut pixels. + V gray_mix = Zero(df); + for (const V val : {red, green, blue}) { + const V inv_val_minus_gray = Set(df, 1) / (val - gray); + const V bound1 = val * inv_val_minus_gray; + const V bound2 = bound1 - inv_val_minus_gray; + const V min_bound = Min(bound1, bound2); + const V max_bound = Max(bound1, bound2); + gray_mix = Clamp(gray_mix, min_bound, max_bound); + } + gray_mix = Clamp(gray_mix, Zero(df), Set(df, 1)); + for (V* const val : {&red, &green, &blue}) { + *val = IfThenElse(luminance < Set(df, 1e-6), gray, + MulAdd(gray_mix, gray - *val, *val)); + } + + Store(red, df, row_r + x); + Store(green, df, row_g + x); + Store(blue, df, row_b + x); + } + }, + "ToneMap")); + + return true; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +namespace { +HWY_EXPORT(ToneMapFrame); +} + +Status ToneMapTo(const std::pair display_nits, + CodecInOut* const io, ThreadPool* const pool) { + const auto tone_map_frame = HWY_DYNAMIC_DISPATCH(ToneMapFrame); + for (ImageBundle& ib : io->frames) { + JXL_RETURN_IF_ERROR(tone_map_frame(display_nits, &ib, pool)); + } + io->metadata.m.SetIntensityTarget(display_nits.second); + return true; +} + +} // namespace jxl +#endif diff --git a/lib/extras/tone_mapping.h b/lib/extras/tone_mapping.h new file mode 100644 index 0000000..4f9feec --- /dev/null +++ b/lib/extras/tone_mapping.h @@ -0,0 +1,18 @@ +// 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_TONE_MAPPING_H_ +#define LIB_EXTRAS_TONE_MAPPING_H_ + +#include "lib/jxl/codec_in_out.h" + +namespace jxl { + +Status ToneMapTo(std::pair display_nits, CodecInOut* io, + ThreadPool* pool = nullptr); + +} // namespace jxl + +#endif // LIB_EXTRAS_TONE_MAPPING_H_ diff --git a/lib/extras/tone_mapping_gbench.cc b/lib/extras/tone_mapping_gbench.cc new file mode 100644 index 0000000..c87c9fc --- /dev/null +++ b/lib/extras/tone_mapping_gbench.cc @@ -0,0 +1,45 @@ +// 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 "benchmark/benchmark.h" +#include "lib/extras/codec.h" +#include "lib/extras/tone_mapping.h" +#include "lib/jxl/testdata.h" + +namespace jxl { + +static void BM_ToneMapping(benchmark::State& state) { + CodecInOut image; + const PaddedBytes image_bytes = + ReadTestData("imagecompression.info/flower_foveon.png"); + JXL_CHECK(SetFromBytes(Span(image_bytes), &image)); + + // Convert to linear Rec. 2020 so that `ToneMapTo` doesn't have to and we + // mainly measure the tone mapping itself. + ColorEncoding linear_rec2020; + linear_rec2020.SetColorSpace(ColorSpace::kRGB); + linear_rec2020.primaries = Primaries::k2100; + linear_rec2020.white_point = WhitePoint::kD65; + linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear); + JXL_CHECK(linear_rec2020.CreateICC()); + JXL_CHECK(image.TransformTo(linear_rec2020)); + + for (auto _ : state) { + state.PauseTiming(); + CodecInOut tone_mapping_input; + tone_mapping_input.SetFromImage(CopyImage(*image.Main().color()), + image.Main().c_current()); + tone_mapping_input.metadata.m.SetIntensityTarget( + image.metadata.m.IntensityTarget()); + state.ResumeTiming(); + + JXL_CHECK(ToneMapTo({0.1, 100}, &tone_mapping_input)); + } + + state.SetItemsProcessed(state.iterations() * image.xsize() * image.ysize()); +} +BENCHMARK(BM_ToneMapping); + +} // namespace jxl diff --git a/lib/include/jxl/butteraugli.h b/lib/include/jxl/butteraugli.h new file mode 100644 index 0000000..f543413 --- /dev/null +++ b/lib/include/jxl/butteraugli.h @@ -0,0 +1,156 @@ +/* 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. + */ + +/** @file butteraugli.h + * @brief Butteraugli API for JPEG XL. + */ + +#ifndef JXL_BUTTERAUGLI_H_ +#define JXL_BUTTERAUGLI_H_ + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +#include "jxl/jxl_export.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" +#include "jxl/types.h" + +/** + * Opaque structure that holds a butteraugli API. + * + * Allocated and initialized with JxlButteraugliApiCreate(). + * Cleaned up and deallocated with JxlButteraugliApiDestroy(). + */ +typedef struct JxlButteraugliApiStruct JxlButteraugliApi; + +/** + * Opaque structure that holds intermediary butteraugli results. + * + * Allocated and initialized with JxlButteraugliCompute(). + * Cleaned up and deallocated with JxlButteraugliResultDestroy(). + */ +typedef struct JxlButteraugliResultStruct JxlButteraugliResult; + +/** + * Deinitializes and frees JxlButteraugliResult instance. + * + * @param result instance to be cleaned up and deallocated. + */ +JXL_EXPORT void JxlButteraugliResultDestroy(JxlButteraugliResult* result); + +/** + * Creates an instance of JxlButteraugliApi and initializes it. + * + * @p memory_manager will be used for all the library dynamic allocations made + * from this instance. The parameter may be NULL, in which case the default + * allocator will be used. See jxl/memory_manager.h for details. + * + * @param memory_manager custom allocator function. It may be NULL. The memory + * manager will be copied internally. + * @return @c NULL if the instance can not be allocated or initialized + * @return pointer to initialized JxlEncoder otherwise + */ +JXL_EXPORT JxlButteraugliApi* JxlButteraugliApiCreate( + const JxlMemoryManager* memory_manager); + +/** + * Set the parallel runner for multithreading. + * + * @param api api instance. + * @param parallel_runner function pointer to runner for multithreading. A + * multithreaded runner should be set to reach fast performance. + * @param parallel_runner_opaque opaque pointer for parallel_runner. + */ +JXL_EXPORT void JxlButteraugliApiSetParallelRunner( + JxlButteraugliApi* api, JxlParallelRunner parallel_runner, + void* parallel_runner_opaque); + +/** + * Set the hf_asymmetry option for butteraugli. + * + * @param api api instance. + * @param v new hf_asymmetry value. + */ +JXL_EXPORT void JxlButteraugliApiSetHFAsymmetry(JxlButteraugliApi* api, + float v); + +/** + * Set the intensity_target option for butteraugli. + * + * @param api api instance. + * @param v new intensity_target value. + */ +JXL_EXPORT void JxlButteraugliApiSetIntensityTarget(JxlButteraugliApi* api, + float v); + +/** + * Deinitializes and frees JxlButteraugliApi instance. + * + * @param api instance to be cleaned up and deallocated. + */ +JXL_EXPORT void JxlButteraugliApiDestroy(JxlButteraugliApi* api); + +/** + * Computes intermediary butteraugli result between an original image and a + * distortion. + * + * @param api api instance for this computation. + * @param xsize width of the compared images. + * @param ysize height of the compared images. + * @param pixel_format_orig pixel format for original image. + * @param buffer_orig pixel data for original image. + * @param size_orig size of buffer_orig in bytes. + * @param pixel_format_dist pixel format for distortion. + * @param buffer_dist pixel data for distortion. + * @param size_dist size of buffer_dist in bytes. + * @return @c NULL if the results can not be computed or initialized. + * @return pointer to initialized and computed intermediary result. + */ +JXL_EXPORT JxlButteraugliResult* JxlButteraugliCompute( + const JxlButteraugliApi* api, uint32_t xsize, uint32_t ysize, + const JxlPixelFormat* pixel_format_orig, const void* buffer_orig, + size_t size_orig, const JxlPixelFormat* pixel_format_dist, + const void* buffer_dist, size_t size_dist); + +/** + * Computes butteraugli max distance based on an intermediary butteraugli + * result. + * + * @param result intermediary result instance. + * @return max distance. + */ +JXL_EXPORT float JxlButteraugliResultGetMaxDistance( + const JxlButteraugliResult* result); + +/** + * Computes a butteraugli distance based on an intermediary butteraugli result. + * + * @param result intermediary result instance. + * @param pnorm pnorm to calculate. + * @return distance using the given pnorm. + */ +JXL_EXPORT float JxlButteraugliResultGetDistance( + const JxlButteraugliResult* result, float pnorm); + +/** + * Get a pointer to the distmap in the result. + * + * @param result intermediary result instance. + * @param buffer will be set to the distmap. The distance value for (x,y) will + * be available at buffer + y * row_stride + x. + * @param row_stride will be set to the row stride of the distmap. + */ +JXL_EXPORT void JxlButteraugliResultGetDistmap( + const JxlButteraugliResult* result, const float** buffer, + uint32_t* row_stride); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_BUTTERAUGLI_H_ */ diff --git a/lib/include/jxl/butteraugli_cxx.h b/lib/include/jxl/butteraugli_cxx.h new file mode 100644 index 0000000..c0e93ad --- /dev/null +++ b/lib/include/jxl/butteraugli_cxx.h @@ -0,0 +1,55 @@ +// 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. + +/// @file butteraugli_cxx.h +/// @brief C++ header-only helper for @ref butteraugli.h. +/// +/// There's no binary library associated with the header since this is a header +/// only library. + +#ifndef JXL_BUTTERAUGLI_CXX_H_ +#define JXL_BUTTERAUGLI_CXX_H_ + +#include + +#include "jxl/butteraugli.h" + +#if !(defined(__cplusplus) || defined(c_plusplus)) +#error "This a C++ only header. Use jxl/butteraugli.h from C sources." +#endif + +/// Struct to call JxlButteraugliApiDestroy from the JxlButteraugliApiPtr +/// unique_ptr. +struct JxlButteraugliApiDestroyStruct { + /// Calls @ref JxlButteraugliApiDestroy() on the passed api. + void operator()(JxlButteraugliApi* api) { JxlButteraugliApiDestroy(api); } +}; + +/// std::unique_ptr<> type that calls JxlButteraugliApiDestroy() when releasing +/// the pointer. +/// +/// Use this helper type from C++ sources to ensure the api is destroyed and +/// their internal resources released. +typedef std::unique_ptr + JxlButteraugliApiPtr; + +/// Struct to call JxlButteraugliResultDestroy from the JxlButteraugliResultPtr +/// unique_ptr. +struct JxlButteraugliResultDestroyStruct { + /// Calls @ref JxlButteraugliResultDestroy() on the passed result object. + void operator()(JxlButteraugliResult* result) { + JxlButteraugliResultDestroy(result); + } +}; + +/// std::unique_ptr<> type that calls JxlButteraugliResultDestroy() when +/// releasing the pointer. +/// +/// Use this helper type from C++ sources to ensure the result object is +/// destroyed and their internal resources released. +typedef std::unique_ptr + JxlButteraugliResultPtr; + +#endif // JXL_BUTTERAUGLI_CXX_H_ diff --git a/lib/include/jxl/codestream_header.h b/lib/include/jxl/codestream_header.h new file mode 100644 index 0000000..805c835 --- /dev/null +++ b/lib/include/jxl/codestream_header.h @@ -0,0 +1,321 @@ +/* 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. + */ + +/** @file codestream_header.h + * @brief Definitions of structs and enums for the metadata from the JPEG XL + * codestream headers (signature, metadata, preview dimensions, ...), excluding + * color encoding which is in color_encoding.h. + */ + +#ifndef JXL_CODESTREAM_HEADER_H_ +#define JXL_CODESTREAM_HEADER_H_ + +#include +#include + +#include "jxl/color_encoding.h" +#include "jxl/types.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Image orientation metadata. + * Values 1..8 match the EXIF definitions. + * The name indicates the operation to perform to transform from the encoded + * image to the display image. + */ +typedef enum { + JXL_ORIENT_IDENTITY = 1, + JXL_ORIENT_FLIP_HORIZONTAL = 2, + JXL_ORIENT_ROTATE_180 = 3, + JXL_ORIENT_FLIP_VERTICAL = 4, + JXL_ORIENT_TRANSPOSE = 5, + JXL_ORIENT_ROTATE_90_CW = 6, + JXL_ORIENT_ANTI_TRANSPOSE = 7, + JXL_ORIENT_ROTATE_90_CCW = 8, +} JxlOrientation; + +/** Given type of an extra channel. + */ +typedef enum { + JXL_CHANNEL_ALPHA, + JXL_CHANNEL_DEPTH, + JXL_CHANNEL_SPOT_COLOR, + JXL_CHANNEL_SELECTION_MASK, + JXL_CHANNEL_BLACK, + JXL_CHANNEL_CFA, + JXL_CHANNEL_THERMAL, + JXL_CHANNEL_RESERVED0, + JXL_CHANNEL_RESERVED1, + JXL_CHANNEL_RESERVED2, + JXL_CHANNEL_RESERVED3, + JXL_CHANNEL_RESERVED4, + JXL_CHANNEL_RESERVED5, + JXL_CHANNEL_RESERVED6, + JXL_CHANNEL_RESERVED7, + JXL_CHANNEL_UNKNOWN, + JXL_CHANNEL_OPTIONAL +} JxlExtraChannelType; + +/** The codestream preview header */ +typedef struct { + /** Preview width in pixels */ + uint32_t xsize; + + /** Preview height in pixels */ + uint32_t ysize; +} JxlPreviewHeader; + +/** The codestream animation header, optionally present in the beginning of + * the codestream, and if it is it applies to all animation frames, unlike + * JxlFrameHeader which applies to an individual frame. + */ +typedef struct { + /** Numerator of ticks per second of a single animation frame time unit */ + uint32_t tps_numerator; + + /** Denominator of ticks per second of a single animation frame time unit */ + uint32_t tps_denominator; + + /** Amount of animation loops, or 0 to repeat infinitely */ + uint32_t num_loops; + + /** Whether animation time codes are present at animation frames in the + * codestream */ + JXL_BOOL have_timecodes; +} JxlAnimationHeader; + +/** Basic image information. This information is available from the file + * signature and first part of the codestream header. + */ +typedef struct JxlBasicInfo { + /* TODO(lode): need additional fields for (transcoded) JPEG? For reusable + * fields orientation must be read from Exif APP1. For has_icc_profile: must + * look up where ICC profile is guaranteed to be in a JPEG file to be able to + * indicate this. */ + + /* TODO(lode): make struct packed, and/or make this opaque struct with getter + * functions (still separate struct from opaque decoder) */ + + /** Whether the codestream is embedded in the container format. If true, + * metadata information and extensions may be available in addition to the + * codestream. + */ + JXL_BOOL have_container; + + /** Width of the image in pixels, before applying orientation. + */ + uint32_t xsize; + + /** Height of the image in pixels, before applying orientation. + */ + uint32_t ysize; + + /** Original image color channel bit depth. + */ + uint32_t bits_per_sample; + + /** Original image color channel floating point exponent bits, or 0 if they + * are unsigned integer. For example, if the original data is half-precision + * (binary16) floating point, bits_per_sample is 16 and + * exponent_bits_per_sample is 5, and so on for other floating point + * precisions. + */ + uint32_t exponent_bits_per_sample; + + /** Upper bound on the intensity level present in the image in nits. For + * unsigned integer pixel encodings, this is the brightness of the largest + * representable value. The image does not necessarily contain a pixel + * actually this bright. An encoder is allowed to set 255 for SDR images + * without computing a histogram. + */ + float intensity_target; + + /** Lower bound on the intensity level present in the image. This may be + * loose, i.e. lower than the actual darkest pixel. When tone mapping, a + * decoder will map [min_nits, intensity_target] to the display range. + */ + float min_nits; + + /** See the description of @see linear_below. + */ + JXL_BOOL relative_to_max_display; + + /** The tone mapping will leave unchanged (linear mapping) any pixels whose + * brightness is strictly below this. The interpretation depends on + * relative_to_max_display. If true, this is a ratio [0, 1] of the maximum + * display brightness [nits], otherwise an absolute brightness [nits]. + */ + float linear_below; + + /** Whether the data in the codestream is encoded in the original color + * profile that is attached to the codestream metadata header, or is + * encoded in an internally supported absolute color space (which the decoder + * can always convert to linear or non-linear sRGB or to XYB). If the original + * profile is used, the decoder outputs pixel data in the color space matching + * that profile, but doesn't convert it to any other color space. If the + * original profile is not used, the decoder only outputs the data as sRGB + * (linear if outputting to floating point, nonlinear with standard sRGB + * transfer function if outputting to unsigned integers) but will not convert + * it to to the original color profile. The decoder also does not convert to + * the target display color profile, but instead will always indicate which + * color profile the returned pixel data is encoded in when using @see + * JXL_COLOR_PROFILE_TARGET_DATA so that a CMS can be used to convert the + * data. + */ + JXL_BOOL uses_original_profile; + + /** Indicates a preview image exists near the beginning of the codestream. + * The preview itself or its dimensions are not included in the basic info. + */ + JXL_BOOL have_preview; + + /** Indicates animation frames exist in the codestream. The animation + * information is not included in the basic info. + */ + JXL_BOOL have_animation; + + /** Image orientation, value 1-8 matching the values used by JEITA CP-3451C + * (Exif version 2.3). + */ + JxlOrientation orientation; + + /** Number of color channels encoded in the image, this is either 1 for + * grayscale data, or 3 for colored data. This count does not include + * the alpha channel or other extra channels. To check presence of an alpha + * channel, such as in the case of RGBA color, check alpha_bits != 0. + * If and only if this is 1, the JxlColorSpace in the JxlColorEncoding is + * JXL_COLOR_SPACE_GRAY. + */ + uint32_t num_color_channels; + + /** Number of additional image channels. This includes the main alpha channel, + * but can also include additional channels such as depth, additional alpha + * channels, spot colors, and so on. Information about the extra channels + * can be queried with JxlDecoderGetExtraChannelInfo. The main alpha channel, + * if it exists, also has its information available in the alpha_bits, + * alpha_exponent_bits and alpha_premultiplied fields in this JxlBasicInfo. + */ + uint32_t num_extra_channels; + + /** Bit depth of the encoded alpha channel, or 0 if there is no alpha channel. + * If present, matches the alpha_bits value of the JxlExtraChannelInfo + * associated with this alpha channel. + */ + uint32_t alpha_bits; + + /** Alpha channel floating point exponent bits, or 0 if they are unsigned. If + * present, matches the alpha_bits value of the JxlExtraChannelInfo associated + * with this alpha channel. integer. + */ + uint32_t alpha_exponent_bits; + + /** Whether the alpha channel is premultiplied. Only used if there is a main + * alpha channel. Matches the alpha_premultiplied value of the + * JxlExtraChannelInfo associated with this alpha channel. + */ + JXL_BOOL alpha_premultiplied; + + /** Dimensions of encoded preview image, only used if have_preview is + * JXL_TRUE. + */ + JxlPreviewHeader preview; + + /** Animation header with global animation properties for all frames, only + * used if have_animation is JXL_TRUE. + */ + JxlAnimationHeader animation; + + /** Padding for forwards-compatibility, in case more fields are exposed + * in a future version of the library. + */ + uint8_t padding[108]; +} JxlBasicInfo; + +/** Information for a single extra channel. + */ +typedef struct { + /** Given type of an extra channel. + */ + JxlExtraChannelType type; + + /** Total bits per sample for this channel. + */ + uint32_t bits_per_sample; + + /** Floating point exponent bits per channel, or 0 if they are unsigned + * integer. + */ + uint32_t exponent_bits_per_sample; + + /** The exponent the channel is downsampled by on each axis. + * TODO(lode): expand this comment to match the JPEG XL specification, + * specify how to upscale, how to round the size computation, and to which + * extra channels this field applies. + */ + uint32_t dim_shift; + + /** Length of the extra channel name in bytes, or 0 if no name. + * Excludes null termination character. + */ + uint32_t name_length; + + /** Whether alpha channel uses premultiplied alpha. Only applicable if + * type is JXL_CHANNEL_ALPHA. + */ + JXL_BOOL alpha_premultiplied; + + /** Spot color of the current spot channel in linear RGBA. Only applicable if + * type is JXL_CHANNEL_SPOT_COLOR. + */ + float spot_color[4]; + + /** Only applicable if type is JXL_CHANNEL_CFA. + * TODO(lode): add comment about the meaning of this field. + */ + uint32_t cfa_channel; +} JxlExtraChannelInfo; + +/* TODO(lode): add API to get the codestream header extensions. */ +/** Extensions in the codestream header. */ +typedef struct { + /** Extension bits. */ + uint64_t extensions; +} JxlHeaderExtensions; + +/** The header of one displayed frame. */ +typedef struct { + /** How long to wait after rendering in ticks. The duration in seconds of a + * tick is given by tps_numerator and tps_denominator in JxlAnimationHeader. + */ + uint32_t duration; + + /** SMPTE timecode of the current frame in form 0xHHMMSSFF, or 0. The bits are + * interpreted from most-significant to least-significant as hour, minute, + * second, and frame. If timecode is nonzero, it is strictly larger than that + * of a previous frame with nonzero duration. These values are only available + * if have_timecodes in JxlAnimationHeader is JXL_TRUE. + * This value is only used if have_timecodes in JxlAnimationHeader is + * JXL_TRUE. + */ + uint32_t timecode; + + /** Length of the frame name in bytes, or 0 if no name. + * Excludes null termination character. + */ + uint32_t name_length; + + /** Indicates this is the last animation frame. + */ + JXL_BOOL is_last; +} JxlFrameHeader; + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_CODESTREAM_HEADER_H_ */ diff --git a/lib/include/jxl/color_encoding.h b/lib/include/jxl/color_encoding.h new file mode 100644 index 0000000..e86dae3 --- /dev/null +++ b/lib/include/jxl/color_encoding.h @@ -0,0 +1,145 @@ +/* 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. + */ + +/** @file color_encoding.h + * @brief Color Encoding definitions used by JPEG XL. + * All CIE units are for the standard 1931 2 degree observer. + */ + +#ifndef JXL_COLOR_ENCODING_H_ +#define JXL_COLOR_ENCODING_H_ + +#include + +#include "jxl/types.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Color space of the image data. */ +typedef enum { + /** Tristimulus RGB */ + JXL_COLOR_SPACE_RGB, + /** Luminance based, the primaries in JxlColorEncoding must be ignored. This + * value implies that num_color_channels in JxlBasicInfo is 1, any other value + * implies num_color_channels is 3. */ + JXL_COLOR_SPACE_GRAY, + /** XYB (opsin) color space */ + JXL_COLOR_SPACE_XYB, + /** None of the other table entries describe the color space appropriately */ + JXL_COLOR_SPACE_UNKNOWN, +} JxlColorSpace; + +/** Built-in whitepoints for color encoding. Numeric values match CICP (Rec. + * ITU-T H.273 | ISO/IEC 23091-2:2019(E)). */ +typedef enum { + /** CIE Standard Illuminant D65: 0.3127, 0.3290 */ + JXL_WHITE_POINT_D65 = 1, + /** Custom white point stored in JxlColorEncoding white_point. */ + JXL_WHITE_POINT_CUSTOM = 2, + /** CIE Standard Illuminant E (equal-energy): 1/3, 1/3 */ + JXL_WHITE_POINT_E = 10, + /** DCI-P3 from SMPTE RP 431-2: 0.314, 0.351 */ + JXL_WHITE_POINT_DCI = 11, +} JxlWhitePoint; + +/** Built-in primaries for color encoding. Numeric values match CICP (Rec. ITU-T + * H.273 | ISO/IEC 23091-2:2019(E)). */ +typedef enum { + /** The CIE xy values of the red, green and blue primaries are: 0.639998686, + 0.330010138; 0.300003784, 0.600003357; 0.150002046, 0.059997204 */ + JXL_PRIMARIES_SRGB = 1, + /** Custom white point stored in JxlColorEncoding primaries_red_xy, + primaries_green_xy and primaries_blue_xy. */ + JXL_PRIMARIES_CUSTOM = 2, + /** As specified in Rec. ITU-R BT.2100-1 */ + JXL_PRIMARIES_2100 = 9, + /** As specified in SMPTE RP 431-2 */ + JXL_PRIMARIES_P3 = 11, +} JxlPrimaries; + +/** Built-in transfer functions for color encoding. Numeric values match CICP + * (Rec. ITU-T H.273 | ISO/IEC 23091-2:2019(E)) unless specified otherwise. */ +typedef enum { + /** As specified in SMPTE RP 431-2 */ + JXL_TRANSFER_FUNCTION_709 = 1, + /** None of the other table entries describe the transfer function. */ + JXL_TRANSFER_FUNCTION_UNKNOWN = 2, + /** The gamma exponent is 1 */ + JXL_TRANSFER_FUNCTION_LINEAR = 8, + /** As specified in IEC 61966-2-1 sRGB */ + JXL_TRANSFER_FUNCTION_SRGB = 13, + /** As specified in SMPTE ST 428-1 */ + JXL_TRANSFER_FUNCTION_PQ = 16, + /** As specified in SMPTE ST 428-1 */ + JXL_TRANSFER_FUNCTION_DCI = 17, + /** As specified in Rec. ITU-R BT.2100-1 (HLG) */ + JXL_TRANSFER_FUNCTION_HLG = 18, + /** Transfer function follows power law given by the gamma value in + JxlColorEncoding. Not a CICP value. */ + JXL_TRANSFER_FUNCTION_GAMMA = 65535, +} JxlTransferFunction; + +/** Renderig intent for color encoding, as specified in ISO 15076-1:2010 */ +typedef enum { + /** vendor-specific */ + JXL_RENDERING_INTENT_PERCEPTUAL = 0, + /** media-relative */ + JXL_RENDERING_INTENT_RELATIVE, + /** vendor-specific */ + JXL_RENDERING_INTENT_SATURATION, + /** ICC-absolute */ + JXL_RENDERING_INTENT_ABSOLUTE, +} JxlRenderingIntent; + +/** Color encoding of the image as structured information. + */ +typedef struct { + /** Color space of the image data. + */ + JxlColorSpace color_space; + + /** Built-in white point. If this value is JXL_WHITE_POINT_CUSTOM, must + * use the numerical whitepoint values from white_point_xy. + */ + JxlWhitePoint white_point; + + /** Numerical whitepoint values in CIE xy space. */ + double white_point_xy[2]; + + /** Built-in RGB primaries. If this value is JXL_PRIMARIES_CUSTOM, must + * use the numerical primaries values below. This field and the custom values + * below are unused and must be ignored if the color space is + * JXL_COLOR_SPACE_GRAY or JXL_COLOR_SPACE_XYB. + */ + JxlPrimaries primaries; + + /** Numerical red primary values in CIE xy space. */ + double primaries_red_xy[2]; + + /** Numerical green primary values in CIE xy space. */ + double primaries_green_xy[2]; + + /** Numerical blue primary values in CIE xy space. */ + double primaries_blue_xy[2]; + + /** Transfer function if have_gamma is 0 */ + JxlTransferFunction transfer_function; + + /** Gamma value used when transfer_function is JXL_TRANSFER_FUNCTION_GAMMA + */ + double gamma; + + /** Rendering intent defined for the color profile. */ + JxlRenderingIntent rendering_intent; +} JxlColorEncoding; + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_COLOR_ENCODING_H_ */ diff --git a/lib/include/jxl/decode.h b/lib/include/jxl/decode.h new file mode 100644 index 0000000..d0845c2 --- /dev/null +++ b/lib/include/jxl/decode.h @@ -0,0 +1,938 @@ +/* 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. + */ + +/** @file decode.h + * @brief Decoding API for JPEG XL. + */ + +#ifndef JXL_DECODE_H_ +#define JXL_DECODE_H_ + +#include +#include + +#include "jxl/codestream_header.h" +#include "jxl/color_encoding.h" +#include "jxl/jxl_export.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" +#include "jxl/types.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Decoder library version. + * + * @return the decoder library version as an integer: + * MAJOR_VERSION * 1000000 + MINOR_VERSION * 1000 + PATCH_VERSION. For example, + * version 1.2.3 would return 1002003. + */ +JXL_EXPORT uint32_t JxlDecoderVersion(void); + +/** The result of JxlSignatureCheck. + */ +typedef enum { + /** Not enough bytes were passed to determine if a valid signature was found. + */ + JXL_SIG_NOT_ENOUGH_BYTES = 0, + + /** No valid JPEGXL header was found. */ + JXL_SIG_INVALID = 1, + + /** A valid JPEG XL codestream signature was found, that is a JPEG XL image + * without container. + */ + JXL_SIG_CODESTREAM = 2, + + /** A valid container signature was found, that is a JPEG XL image embedded + * in a box format container. + */ + JXL_SIG_CONTAINER = 3, +} JxlSignature; + +/** + * JPEG XL signature identification. + * + * Checks if the passed buffer contains a valid JPEG XL signature. The passed @p + * buf of size + * @p size doesn't need to be a full image, only the beginning of the file. + * + * @return a flag indicating if a JPEG XL signature was found and what type. + * - JXL_SIG_NOT_ENOUGH_BYTES not enough bytes were passed to determine + * if a valid signature is there. + * - JXL_SIG_INVALID: no valid signature found for JPEG XL decoding. + * - JXL_SIG_CODESTREAM a valid JPEG XL codestream signature was found. + * - JXL_SIG_CONTAINER a valid JPEG XL container signature was found. + */ +JXL_EXPORT JxlSignature JxlSignatureCheck(const uint8_t* buf, size_t len); + +/** + * Opaque structure that holds the JPEGXL decoder. + * + * Allocated and initialized with JxlDecoderCreate(). + * Cleaned up and deallocated with JxlDecoderDestroy(). + */ +typedef struct JxlDecoderStruct JxlDecoder; + +/** + * Creates an instance of JxlDecoder and initializes it. + * + * @p memory_manager will be used for all the library dynamic allocations made + * from this instance. The parameter may be NULL, in which case the default + * allocator will be used. See jpegxl/memory_manager.h for details. + * + * @param memory_manager custom allocator function. It may be NULL. The memory + * manager will be copied internally. + * @return @c NULL if the instance can not be allocated or initialized + * @return pointer to initialized JxlDecoder otherwise + */ +JXL_EXPORT JxlDecoder* JxlDecoderCreate(const JxlMemoryManager* memory_manager); + +/** + * Re-initializes a JxlDecoder instance, so it can be re-used for decoding + * another image. All state and settings are reset as if the object was + * newly created with JxlDecoderCreate, but the memory manager is kept. + * + * @param dec instance to be re-initialized. + */ +JXL_EXPORT void JxlDecoderReset(JxlDecoder* dec); + +/** + * Deinitializes and frees JxlDecoder instance. + * + * @param dec instance to be cleaned up and deallocated. + */ +JXL_EXPORT void JxlDecoderDestroy(JxlDecoder* dec); + +/** + * Return value for JxlDecoderProcessInput. + * The values above 0x40 are optional informal events that can be subscribed to, + * they are never returned if they have not been registered with + * JxlDecoderSubscribeEvents. + */ +typedef enum { + /** Function call finished successfully, or decoding is finished and there is + * nothing more to be done. + */ + JXL_DEC_SUCCESS = 0, + + /** An error occurred, for example invalid input file or out of memory. + * TODO(lode): add function to get error information from decoder. + */ + JXL_DEC_ERROR = 1, + + /** The decoder needs more input bytes to continue. Before the next + * JxlDecoderProcessInput call, more input data must be set, by calling + * JxlDecoderReleaseInput (if input was set previously) and then calling + * JxlDecoderSetInput. JxlDecoderReleaseInput returns how many bytes are + * not yet processed, before a next call to JxlDecoderProcessInput all + * unprocessed bytes must be provided again (the address need not match, but + * the contents must), and more bytes must be concatenated after the + * unprocessed bytes. + */ + JXL_DEC_NEED_MORE_INPUT = 2, + + /** The decoder is able to decode a preview image and requests setting a + * preview output buffer using JxlDecoderSetPreviewOutBuffer. This occurs if + * JXL_DEC_PREVIEW_IMAGE is requested and it is possible to decode a preview + * image from the codestream and the preview out buffer was not yet set. There + * is maximum one preview image in a codestream. + */ + JXL_DEC_NEED_PREVIEW_OUT_BUFFER = 3, + + /** The decoder is able to decode a DC image and requests setting a DC output + * buffer using JxlDecoderSetDCOutBuffer. This occurs if JXL_DEC_DC_IMAGE is + * requested and it is possible to decode a DC image from the codestream and + * the DC out buffer was not yet set. This event re-occurs for new frames + * if there are multiple animation frames. + * DEPRECATED: the DC feature in this form will be removed. You can use + * JxlDecoderFlushImage for progressive rendering. + */ + JXL_DEC_NEED_DC_OUT_BUFFER = 4, + + /** The decoder requests an output buffer to store the full resolution image, + * which can be set with JxlDecoderSetImageOutBuffer or with + * JxlDecoderSetImageOutCallback. This event re-occurs for new frames if there + * are multiple animation frames and requires setting an output again. + */ + JXL_DEC_NEED_IMAGE_OUT_BUFFER = 5, + + /** Informative event by JxlDecoderProcessInput: JPEG reconstruction buffer is + * too small for reconstructed JPEG codestream to fit. + * JxlDecoderSetJPEGBuffer must be called again to make room for remaining + * bytes. This event may occur multiple times after + * JXL_DEC_JPEG_RECONSTRUCTION + */ + JXL_DEC_JPEG_NEED_MORE_OUTPUT = 6, + + /** Informative event by JxlDecoderProcessInput: basic information such as + * image dimensions and extra channels. This event occurs max once per image. + */ + JXL_DEC_BASIC_INFO = 0x40, + + /** Informative event by JxlDecoderProcessInput: user extensions of the + * codestream header. This event occurs max once per image and always later + * than JXL_DEC_BASIC_INFO and earlier than any pixel data. + */ + JXL_DEC_EXTENSIONS = 0x80, + + /** Informative event by JxlDecoderProcessInput: color encoding or ICC + * profile from the codestream header. This event occurs max once per image + * and always later than JXL_DEC_BASIC_INFO and earlier than any pixel + * data. + */ + JXL_DEC_COLOR_ENCODING = 0x100, + + /** Informative event by JxlDecoderProcessInput: Preview image, a small + * frame, decoded. This event can only happen if the image has a preview + * frame encoded. This event occurs max once for the codestream and always + * later than JXL_DEC_COLOR_ENCODING and before JXL_DEC_FRAME. + */ + JXL_DEC_PREVIEW_IMAGE = 0x200, + + /** Informative event by JxlDecoderProcessInput: Beginning of a frame. + * JxlDecoderGetFrameHeader can be used at this point. A note on frames: + * a JPEG XL image can have internal frames that are not intended to be + * displayed (e.g. used for compositing a final frame), but this only returns + * displayed frames. A displayed frame either has an animation duration or is + * the only or last frame in the image. This event occurs max once per + * displayed frame, always later than JXL_DEC_COLOR_ENCODING, and always + * earlier than any pixel data. While JPEG XL supports encoding a single frame + * as the composition of multiple internal sub-frames also called frames, this + * event is not indicated for the internal frames. + */ + JXL_DEC_FRAME = 0x400, + + /** Informative event by JxlDecoderProcessInput: DC image, 8x8 sub-sampled + * frame, decoded. It is not guaranteed that the decoder will always return DC + * separately, but when it does it will do so before outputting the full + * frame. JxlDecoderSetDCOutBuffer must be used after getting the basic + * image information to be able to get the DC pixels, if not this return + * status only indicates we're past this point in the codestream. This event + * occurs max once per frame and always later than JXL_DEC_FRAME_HEADER + * and other header events and earlier than full resolution pixel data. + * DEPRECATED: the DC feature in this form will be removed. You can use + * JxlDecoderFlushImage for progressive rendering. + */ + JXL_DEC_DC_IMAGE = 0x800, + + /** Informative event by JxlDecoderProcessInput: full frame decoded. + * JxlDecoderSetImageOutBuffer must be used after getting the basic image + * information to be able to get the image pixels, if not this return status + * only indicates we're past this point in the codestream. This event occurs + * max once per frame and always later than JXL_DEC_DC_IMAGE. + */ + JXL_DEC_FULL_IMAGE = 0x1000, + + /** Informative event by JxlDecoderProcessInput: JPEG reconstruction data + * decoded. JxlDecoderSetJPEGBuffer may be used to set a JPEG + * reconstruction buffer after getting the JPEG reconstruction data. If a JPEG + * reconstruction buffer is set a byte stream identical to the JPEG codestream + * used to encode the image will be written to the JPEG reconstruction buffer + * instead of pixels to the image out buffer. This event occurs max once per + * image and always before JXL_DEC_FULL_IMAGE. + */ + JXL_DEC_JPEG_RECONSTRUCTION = 0x2000, +} JxlDecoderStatus; + +/** Rewinds decoder to the beginning. The same input must be given again from + * the beginning of the file and the decoder will emit events from the beginning + * again. When rewinding (as opposed to JxlDecoderReset), the decoder can keep + * state about the image, which it can use to skip to a requested frame more + * efficiently with JxlDecoderSkipFrames. After rewind, + * JxlDecoderSubscribeEvents can be used again, and it is feasible to leave out + * events that were already handled before, such as JXL_DEC_BASIC_INFO and + * JXL_DEC_COLOR_ENCODING, since they will provide the same information as + * before. + * @param dec decoder object + */ +JXL_EXPORT void JxlDecoderRewind(JxlDecoder* dec); + +/** Makes the decoder skip the next `amount` frames. It still needs to process + * the input, but will not output the frame events. It can be more efficient + * when skipping frames, and even more so when using this after + * JxlDecoderRewind. If the decoder is already processing a frame (could + * have emitted JXL_DEC_FRAME but not yet JXL_DEC_FULL_IMAGE), it starts + * skipping from the next frame. If the amount is larger than the amount of + * frames remaining in the image, all remaining frames are skipped. Calling this + * function multiple times adds the amount to skip to the already existing + * amount. + * A frame here is defined as a frame that without skipping emits events such as + * JXL_DEC_FRAME and JXL_FULL_IMAGE, frames that are internal to the file format + * but are not rendered as part of an animation, or are not the final still + * frame of a still image, are not counted. + * @param dec decoder object + * @param amount the amount of frames to skip + */ +JXL_EXPORT void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount); + +/** + * Get the default pixel format for this decoder. + * + * Requires that the decoder can produce JxlBasicInfo. + * + * @param dec JxlDecoder to query when creating the recommended pixel format. + * @param format JxlPixelFormat to populate with the recommended settings for + * the data loaded into this decoder. + * @return JXL_DEC_SUCCESS if no error, JXL_DEC_NEED_MORE_INPUT if the + * basic info isn't yet available, and JXL_DEC_ERROR otherwise. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderDefaultPixelFormat(const JxlDecoder* dec, JxlPixelFormat* format); + +/** + * Set the parallel runner for multithreading. May only be set before starting + * decoding. + * + * @param dec decoder object + * @param parallel_runner function pointer to runner for multithreading. It may + * be NULL to use the default, single-threaded, runner. A multithreaded + * runner should be set to reach fast performance. + * @param parallel_runner_opaque opaque pointer for parallel_runner. + * @return JXL_DEC_SUCCESS if the runner was set, JXL_DEC_ERROR + * otherwise (the previous runner remains set). + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, + void* parallel_runner_opaque); + +/** + * Returns a hint indicating how many more bytes the decoder is expected to + * need to make JxlDecoderGetBasicInfo available after the next + * JxlDecoderProcessInput call. This is a suggested large enough value for + * the amount of bytes to provide in the next JxlDecoderSetInput call, but it is + * not guaranteed to be an upper bound nor a lower bound. + * Can be used before the first JxlDecoderProcessInput call, and is correct + * the first time in most cases. If not, JxlDecoderSizeHintBasicInfo can be + * called again to get an updated hint. + * + * @param dec decoder object + * @return the size hint in bytes if the basic info is not yet fully decoded. + * @return 0 when the basic info is already available. + */ +JXL_EXPORT size_t JxlDecoderSizeHintBasicInfo(const JxlDecoder* dec); + +/** Select for which informative events (JXL_DEC_BASIC_INFO, etc...) the + * decoder should return with a status. It is not required to subscribe to any + * events, data can still be requested from the decoder as soon as it available. + * By default, the decoder is subscribed to no events (events_wanted == 0), and + * the decoder will then only return when it cannot continue because it needs + * more input data or more output buffer. This function may only be be called + * before using JxlDecoderProcessInput + * + * @param dec decoder object + * @param events_wanted bitfield of desired events. + * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSubscribeEvents(JxlDecoder* dec, + int events_wanted); + +/** Enables or disables preserving of original orientation. Some images are + * encoded with an orientation tag indicating the image is rotated and/or + * mirrored (here called the original orientation). + * + * *) If keep_orientation is JXL_FALSE (the default): the decoder will perform + * work to undo the transformation. This ensures the decoded pixels will not + * be rotated or mirrored. The decoder will always set the orientation field + * of the JxlBasicInfo to JXL_ORIENT_IDENTITY to match the returned pixel data. + * The decoder may also swap xsize and ysize in the JxlBasicInfo compared to the + * values inside of the codestream, to correctly match the decoded pixel data, + * e.g. when a 90 degree rotation was performed. + * + * *) If this option is JXL_TRUE: then the image is returned as-is, which may be + * rotated or mirrored, and the user must check the orientation field in + * JxlBasicInfo after decoding to correctly interpret the decoded pixel data. + * This may be faster to decode since the decoder doesn't have to apply the + * transformation, but can cause wrong display of the image if the orientation + * tag is not correctly taken into account by the user. + * + * By default, this option is disabled, and the decoder automatically corrects + * the orientation. + * + * This function must be called at the beginning, before decoding is performed. + * + * @see JxlBasicInfo for the orientation field, and @see JxlOrientation for the + * possible values. + * + * @param dec decoder object + * @param keep_orientation JXL_TRUE to enable, JXL_FALSE to disable. + * @return JXL_DEC_SUCCESS if no error, JXL_DEC_ERROR otherwise. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetKeepOrientation(JxlDecoder* dec, JXL_BOOL keep_orientation); + +/** + * Decodes JPEG XL file using the available bytes. Requires input has been + * set with JxlDecoderSetInput. After JxlDecoderProcessInput, input can + * optionally be released with JxlDecoderReleaseInput and then set again to + * next bytes in the stream. JxlDecoderReleaseInput returns how many bytes are + * not yet processed, before a next call to JxlDecoderProcessInput all + * unprocessed bytes must be provided again (the address need not match, but the + * contents must), and more bytes may be concatenated after the unprocessed + * bytes. + * + * The returned status indicates whether the decoder needs more input bytes, or + * more output buffer for a certain type of output data. No matter what the + * returned status is (other than JXL_DEC_ERROR), new information, such as + * JxlDecoderGetBasicInfo, may have become available after this call. When + * the return value is not JXL_DEC_ERROR or JXL_DEC_SUCCESS, the decoding + * requires more JxlDecoderProcessInput calls to continue. + * + * @param dec decoder object + * @return JXL_DEC_SUCCESS when decoding finished and all events handled. + * @return JXL_DEC_ERROR when decoding failed, e.g. invalid codestream. + * TODO(lode) document the input data mechanism + * @return JXL_DEC_NEED_MORE_INPUT more input data is necessary. + * @return JXL_DEC_BASIC_INFO when basic info such as image dimensions is + * available and this informative event is subscribed to. + * @return JXL_DEC_EXTENSIONS when JPEG XL codestream user extensions are + * available and this informative event is subscribed to. + * @return JXL_DEC_COLOR_ENCODING when color profile information is + * available and this informative event is subscribed to. + * @return JXL_DEC_PREVIEW_IMAGE when preview pixel information is available and + * output in the preview buffer. + * @return JXL_DEC_DC_IMAGE when DC pixel information (8x8 downscaled version + * of the image) is available and output in the DC buffer. + * @return JXL_DEC_FULL_IMAGE when all pixel information at highest detail is + * available and has been output in the pixel buffer. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderProcessInput(JxlDecoder* dec); + +/** + * Sets input data for JxlDecoderProcessInput. The data is owned by the caller + * and may be used by the decoder until JxlDecoderReleaseInput is called or + * the decoder is destroyed or reset so must be kept alive until then. + * @param dec decoder object + * @param data pointer to next bytes to read from + * @param size amount of bytes available starting from data + * @return JXL_DEC_ERROR if input was already set without releasing, + * JXL_DEC_SUCCESS otherwise + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetInput(JxlDecoder* dec, + const uint8_t* data, + size_t size); + +/** + * Releases input which was provided with JxlDecoderSetInput. Between + * JxlDecoderProcessInput and JxlDecoderReleaseInput, the user may not alter + * the data in the buffer. Calling JxlDecoderReleaseInput is required whenever + * any input is already set and new input needs to be added with + * JxlDecoderSetInput, but is not required before JxlDecoderDestroy or + * JxlDecoderReset. Calling JxlDecoderReleaseInput when no input is set is + * not an error and returns 0. + * @param dec decoder object + * @return the amount of bytes the decoder has not yet processed that are + * still remaining in the data set by JxlDecoderSetInput, or 0 if no input is + * set or JxlDecoderReleaseInput was already called. For a next call to + * JxlDecoderProcessInput, the buffer must start with these unprocessed bytes. + * This value doesn't provide information about how many bytes the decoder + * truly processed internally or how large the original JPEG XL codestream or + * file are. + */ +JXL_EXPORT size_t JxlDecoderReleaseInput(JxlDecoder* dec); + +/** + * Outputs the basic image information, such as image dimensions, bit depth and + * all other JxlBasicInfo fields, if available. + * + * @param dec decoder object + * @param info struct to copy the information into, or NULL to only check + * whether the information is available through the return value. + * @return JXL_DEC_SUCCESS if the value is available, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * of other error conditions. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, + JxlBasicInfo* info); + +/** + * Outputs information for extra channel at the given index. The index must be + * smaller than num_extra_channels in the associated JxlBasicInfo. + * + * @param dec decoder object + * @param index index of the extra channel to query. + * @param info struct to copy the information into, or NULL to only check + * whether the information is available through the return value. + * @return JXL_DEC_SUCCESS if the value is available, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * of other error conditions. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelInfo( + const JxlDecoder* dec, size_t index, JxlExtraChannelInfo* info); + +/** + * Outputs name for extra channel at the given index in UTF-8. The index must be + * smaller than num_extra_channels in the associated JxlBasicInfo. The buffer + * for name must have at least name_length + 1 bytes allocated, gotten from + * the associated JxlExtraChannelInfo. + * + * @param dec decoder object + * @param index index of the extra channel to query. + * @param name buffer to copy the name into + * @param size size of the name buffer in bytes + * @return JXL_DEC_SUCCESS if the value is available, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * of other error conditions. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetExtraChannelName(const JxlDecoder* dec, + size_t index, + char* name, + size_t size); + +/** Defines which color profile to get: the profile from the codestream + * metadata header, which represents the color profile of the original image, + * or the color profile from the pixel data received by the decoder. Both are + * the same if the basic has uses_original_profile set. + */ +typedef enum { + /** Get the color profile of the original image from the metadata.. + */ + JXL_COLOR_PROFILE_TARGET_ORIGINAL = 0, + + /** Get the color profile of the pixel data the decoder outputs. */ + JXL_COLOR_PROFILE_TARGET_DATA = 1, +} JxlColorProfileTarget; + +/** + * Outputs the color profile as JPEG XL encoded structured data, if available. + * This is an alternative to an ICC Profile, which can represent a more limited + * amount of color spaces, but represents them exactly through enum values. + * + * It is often possible to use JxlDecoderGetColorAsICCProfile as an + * alternative anyway. The following scenarios are possible: + * - The JPEG XL image has an attached ICC Profile, in that case, the encoded + * structured data is not available, this function will return an error status + * and you must use JxlDecoderGetColorAsICCProfile instead. + * - The JPEG XL image has an encoded structured color profile, and it + * represents an RGB or grayscale color space. This function will return it. + * You can still use JxlDecoderGetColorAsICCProfile as well as an + * alternative if desired, though depending on which RGB color space is + * represented, the ICC profile may be a close approximation. It is also not + * always feasible to deduce from an ICC profile which named color space it + * exactly represents, if any, as it can represent any arbitrary space. + * - The JPEG XL image has an encoded structured color profile, and it indicates + * an unknown or xyb color space. In that case, + * JxlDecoderGetColorAsICCProfile is not available. + * + * If you wish to render the image using a system that supports ICC profiles, + * use JxlDecoderGetColorAsICCProfile first. If you're looking for a specific + * color space possibly indicated in the JPEG XL image, use + * JxlDecoderGetColorAsEncodedProfile first. + * + * @param dec decoder object + * @param format pixel format to output the data to. Only used for + * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param target whether to get the original color profile from the metadata + * or the color profile of the decoded pixels. + * @param color_encoding struct to copy the information into, or NULL to only + * check whether the information is available through the return value. + * @return JXL_DEC_SUCCESS if the data is available and returned, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * the encoded structured color profile does not exist in the codestream. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetColorAsEncodedProfile( + const JxlDecoder* dec, const JxlPixelFormat* format, + JxlColorProfileTarget target, JxlColorEncoding* color_encoding); + +/** + * Outputs the size in bytes of the ICC profile returned by + * JxlDecoderGetColorAsICCProfile, if available, or indicates there is none + * available. In most cases, the image will have an ICC profile available, but + * if it does not, JxlDecoderGetColorAsEncodedProfile must be used instead. + * @see JxlDecoderGetColorAsEncodedProfile for more information. The ICC + * profile is either the exact ICC profile attached to the codestream metadata, + * or a close approximation generated from JPEG XL encoded structured data, + * depending of what is encoded in the codestream. + * + * @param dec decoder object + * @param format pixel format to output the data to. Only used for + * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param target whether to get the original color profile from the metadata + * or the color profile of the decoded pixels. + * @param size variable to output the size into, or NULL to only check the + * return status. + * @return JXL_DEC_SUCCESS if the ICC profile is available, + * JXL_DEC_NEED_MORE_INPUT if the decoder has not yet received enough + * input data to determine whether an ICC profile is available or what its + * size is, JXL_DEC_ERROR in case the ICC profile is not available and + * cannot be generated. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderGetICCProfileSize(const JxlDecoder* dec, const JxlPixelFormat* format, + JxlColorProfileTarget target, size_t* size); + +/** + * Outputs ICC profile if available. The profile is only available if + * JxlDecoderGetICCProfileSize returns success. The output buffer must have + * at least as many bytes as given by JxlDecoderGetICCProfileSize. + * + * @param dec decoder object + * @param format pixel format to output the data to. Only used for + * JXL_COLOR_PROFILE_TARGET_DATA, may be nullptr otherwise. + * @param target whether to get the original color profile from the metadata + * or the color profile of the decoded pixels. + * @param icc_profile buffer to copy the ICC profile into + * @param size size of the icc_profile buffer in bytes + * @return JXL_DEC_SUCCESS if the profile was successfully returned is + * available, JXL_DEC_NEED_MORE_INPUT if not yet available, + * JXL_DEC_ERROR if the profile doesn't exist or the output size is not + * large enough. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetColorAsICCProfile( + const JxlDecoder* dec, const JxlPixelFormat* format, + JxlColorProfileTarget target, uint8_t* icc_profile, size_t size); + +/** Sets the color profile to use for JXL_COLOR_PROFILE_TARGET_DATA for the + * special case when the decoder has a choice. This only has effect for a JXL + * image where uses_original_profile is false, and the original color profile is + * encoded as an ICC color profile rather than a JxlColorEncoding with known + * enum values. In most other cases (uses uses_original_profile is true, or the + * color profile is already given as a JxlColorEncoding), this setting is + * ignored and the decoder uses a profile related to the image. + * No matter what, the JXL_COLOR_PROFILE_TARGET_DATA must still be queried to + * know the actual data format of the decoded pixels after decoding. + * + * The intended use case of this function is for cases where you are using + * a color management system to parse the original ICC color profile + * (JXL_COLOR_PROFILE_TARGET_ORIGINAL), from this you know that the ICC + * profile represents one of the color profiles supported by JxlColorEncoding + * (such as sRGB, PQ or HLG): in that case it is beneficial (but not necessary) + * to use JxlDecoderSetPreferredColorProfile to match the parsed profile. The + * JXL decoder has no color management system built in, but can convert XYB + * color to any of the ones supported by JxlColorEncoding. + * + * Can only be set after the JXL_DEC_COLOR_ENCODING event occurred and before + * any other event occurred, and can affect the result of + * JXL_COLOR_PROFILE_TARGET_DATA (but not of JXL_COLOR_PROFILE_TARGET_ORIGINAL), + * so should be used after getting JXL_COLOR_PROFILE_TARGET_ORIGINAL but before + * getting JXL_COLOR_PROFILE_TARGET_DATA. The color_encoding must be grayscale + * if num_color_channels from the basic info is 1, RGB if num_color_channels + * from the basic info is 3. + * + * If JxlDecoderSetPreferredColorProfile is not used, then for images for which + * uses_original_profile is false and with ICC color profile, the decoder will + * choose linear sRGB for color images, linear grayscale for grayscale images. + * This function only sets a preference, since for other images the decoder has + * no choice what color profile to use, it is determined by the image. + * + * @param dec decoder object + * @param color_encoding the default color encoding to set + * @return JXL_DEC_SUCCESS if the preference was set successfully, JXL_DEC_ERROR + * otherwise. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreferredColorProfile( + JxlDecoder* dec, const JxlColorEncoding* color_encoding); + +/** + * Returns the minimum size in bytes of the preview image output pixel buffer + * for the given format. This is the buffer for JxlDecoderSetPreviewOutBuffer. + * Requires the preview header information is available in the decoder. + * + * @param dec decoder object + * @param format format of pixels + * @param size output value, buffer size in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * information not available yet. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderPreviewOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); + +/** + * Sets the buffer to write the small resolution preview image + * to. The size of the buffer must be at least as large as given by + * JxlDecoderPreviewOutBufferSize. The buffer follows the format described by + * JxlPixelFormat. The preview image dimensions are given by the + * JxlPreviewHeader. The buffer is owned by the caller. + * + * @param dec decoder object + * @param format format of pixels. Object owned by user and its contents are + * copied internally. + * @param buffer buffer type to output the pixel data to + * @param size size of buffer in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * size too small. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer( + JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); + +/** + * Outputs the information from the frame, such as duration when have_animation. + * This function can be called when JXL_DEC_FRAME occurred for the current + * frame, even when have_animation in the JxlBasicInfo is JXL_FALSE. + * + * @param dec decoder object + * @param header struct to copy the information into, or NULL to only check + * whether the information is available through the return value. + * @return JXL_DEC_SUCCESS if the value is available, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * of other error conditions. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameHeader(const JxlDecoder* dec, + JxlFrameHeader* header); + +/** + * Outputs name for the current frame. The buffer + * for name must have at least name_length + 1 bytes allocated, gotten from + * the associated JxlFrameHeader. + * + * @param dec decoder object + * @param name buffer to copy the name into + * @param size size of the name buffer in bytes, including zero termination + * character, so this must be at least JxlFrameHeader.name_length + 1. + * @return JXL_DEC_SUCCESS if the value is available, + * JXL_DEC_NEED_MORE_INPUT if not yet available, JXL_DEC_ERROR in case + * of other error conditions. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderGetFrameName(const JxlDecoder* dec, + char* name, size_t size); + +/** + * Returns the minimum size in bytes of the DC image output buffer + * for the given format. This is the buffer for JxlDecoderSetDCOutBuffer. + * Requires the basic image information is available in the decoder. + * + * @param dec decoder object + * @param format format of pixels + * @param size output value, buffer size in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * information not available yet. + * + * DEPRECATED: the DC feature in this form will be removed. You can use + * JxlDecoderFlushImage for progressive rendering. + */ +JXL_EXPORT JXL_DEPRECATED JxlDecoderStatus JxlDecoderDCOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); + +/** + * Sets the buffer to write the lower resolution (8x8 sub-sampled) DC image + * to. The size of the buffer must be at least as large as given by + * JxlDecoderDCOutBufferSize. The buffer follows the format described by + * JxlPixelFormat. The DC image has dimensions ceil(xsize / 8) * ceil(ysize / + * 8). The buffer is owned by the caller. + * + * @param dec decoder object + * @param format format of pixels. Object owned by user and its contents are + * copied internally. + * @param buffer buffer type to output the pixel data to + * @param size size of buffer in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * size too small. + * + * DEPRECATED: the DC feature in this form will be removed. You can use + * JxlDecoderFlushImage for progressive rendering. + */ +JXL_EXPORT JXL_DEPRECATED JxlDecoderStatus JxlDecoderSetDCOutBuffer( + JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); + +/** + * Returns the minimum size in bytes of the image output pixel buffer for the + * given format. This is the buffer for JxlDecoderSetImageOutBuffer. Requires + * the basic image information is available in the decoder. + * + * @param dec decoder object + * @param format format of the pixels. + * @param size output value, buffer size in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * information not available yet. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderImageOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size); + +/** + * Sets the buffer to write the full resolution image to. This can be set when + * the JXL_DEC_FRAME event occurs, must be set when the + * JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and applies only for the current + * frame. The size of the buffer must be at least as large as given by + * JxlDecoderImageOutBufferSize. The buffer follows the format described by + * JxlPixelFormat. The buffer is owned by the caller. + * + * @param dec decoder object + * @param format format of the pixels. Object owned by user and its contents + * are copied internally. + * @param buffer buffer type to output the pixel data to + * @param size size of buffer in bytes + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * size too small. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetImageOutBuffer( + JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size); + +/** + * Callback function type for JxlDecoderSetImageOutCallback. @see + * JxlDecoderSetImageOutCallback for usage. + * + * The callback may be called simultaneously by different threads when using a + * threaded parallel runner, on different pixels. + * + * @param opaque optional user data, as given to JxlDecoderSetImageOutCallback. + * @param x horizontal position of leftmost pixel of the pixel data. + * @param y vertical position of the pixel data. + * @param num_pixels amount of pixels included in the pixel data, horizontally. + * This is not the same as xsize of the full image, it may be smaller. + * @param pixels pixel data as a horizontal stripe, in the format passed to + * JxlDecoderSetImageOutCallback. The memory is not owned by the user, and is + * only valid during the time the callback is running. + */ +typedef void (*JxlImageOutCallback)(void* opaque, size_t x, size_t y, + size_t num_pixels, const void* pixels); + +/** + * Sets pixel output callback. This is an alternative to + * JxlDecoderSetImageOutBuffer. This can be set when the JXL_DEC_FRAME event + * occurs, must be set when the JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and + * applies only for the current frame. Only one of JxlDecoderSetImageOutBuffer + * or JxlDecoderSetImageOutCallback may be used for the same frame, not both at + * the same time. + * + * The callback will be called multiple times, to receive the image + * data in small chunks. The callback receives a horizontal stripe of pixel + * data, 1 pixel high, xsize pixels wide, called a scanline. The xsize here is + * not the same as the full image width, the scanline may be a partial section, + * and xsize may differ between calls. The user can then process and/or copy the + * partial scanline to an image buffer. The callback may be called + * simultaneously by different threads when using a threaded parallel runner, on + * different pixels. + * + * If JxlDecoderFlushImage is not used, then each pixel will be visited exactly + * once by the different callback calls, during processing with one or more + * JxlDecoderProcessInput calls. These pixels are decoded to full detail, they + * are not part of a lower resolution or lower quality progressive pass, but the + * final pass. + * + * If JxlDecoderFlushImage is used, then in addition each pixel will be visited + * zero or one times during the blocking JxlDecoderFlushImage call. Pixels + * visited as a result of JxlDecoderFlushImage may represent a lower resolution + * or lower quality intermediate progressive pass of the image. Any visited + * pixel will be of a quality at least as good or better than previous visits of + * this pixel. A pixel may be visited zero times if it cannot be decoded yet + * or if it was already decoded to full precision (this behavior is not + * guaranteed). + * + * @param dec decoder object + * @param format format of the pixels. Object owned by user and 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, + * may be NULL. + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * JxlDecoderSetImageOutBuffer already set. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetImageOutCallback(JxlDecoder* dec, const JxlPixelFormat* format, + JxlImageOutCallback callback, void* opaque); + +/** + * Returns the minimum size in bytes of an extra channel pixel buffer for the + * given format. This is the buffer for JxlDecoderSetExtraChannelBuffer. + * Requires the basic image information is available in the decoder. + * + * @param dec decoder object + * @param format format of the pixels. The num_channels value is ignored and is + * always treated to be 1. + * @param size output value, buffer size in bytes + * @param index which extra channel to get, matching the index used in @see + * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in the + * associated JxlBasicInfo. + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * information not available yet or invalid index. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderExtraChannelBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size, + uint32_t index); + +/** + * Sets the buffer to write an extra channel to. This can be set when + * the JXL_DEC_FRAME or JXL_DEC_NEED_IMAGE_OUT_BUFFER event occurs, and applies + * only for the current frame. The size of the buffer must be at least as large + * as given by JxlDecoderExtraChannelBufferSize. The buffer follows the format + * described by JxlPixelFormat, but where num_channels is 1. The buffer is owned + * by the caller. The amount of extra channels is given by the + * num_extra_channels field in the associated JxlBasicInfo, and the information + * of individual extra channels can be queried with @see + * JxlDecoderGetExtraChannelInfo. To get multiple extra channels, this function + * must be called multiple times, once for each wanted index. Not all images + * have extra channels. The alpha channel is an extra channel and can be gotten + * as part of the color channels when using an RGBA pixel buffer with + * JxlDecoderSetImageOutBuffer, but additionally also can be gotten separately + * as extra channel. The color channels themselves cannot be gotten this way. + * + * + * @param dec decoder object + * @param format format of the pixels. Object owned by user and its contents + * are copied internally. The num_channels value is ignored and is always + * treated to be 1. + * @param buffer buffer type to output the pixel data to + * @param size size of buffer in bytes + * @param index which extra channel to get, matching the index used in @see + * JxlDecoderGetExtraChannelInfo. Must be smaller than num_extra_channels in the + * associated JxlBasicInfo. + * @return JXL_DEC_SUCCESS on success, JXL_DEC_ERROR on error, such as + * size too small or invalid index. + */ +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetExtraChannelBuffer(JxlDecoder* dec, const JxlPixelFormat* format, + void* buffer, size_t size, uint32_t index); + +/** + * Sets output buffer for reconstructed JPEG codestream. + * + * The data is owned by the caller + * and may be used by the decoder until JxlDecoderReleaseJPEGBuffer is called or + * the decoder is destroyed or reset so must be kept alive until then. + * + * @param dec decoder object + * @param data pointer to next bytes to write to + * @param size amount of bytes available starting from data + * @return JXL_DEC_ERROR if input was already set without releasing, + * JXL_DEC_SUCCESS otherwise + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderSetJPEGBuffer(JxlDecoder* dec, + uint8_t* data, size_t size); + +/** + * Releases buffer which was provided with JxlDecoderSetJPEGBuffer. + * + * Calling JxlDecoderReleaseJPEGBuffer is required whenever + * a buffer is already set and a new buffer needs to be added with + * JxlDecoderSetJPEGBuffer, but is not required before JxlDecoderDestroy or + * JxlDecoderReset. + * + * Calling JxlDecoderReleaseJPEGBuffer when no input is set is + * not an error and returns 0. + * + * @param dec decoder object + * @return the amount of bytes the decoder has not yet written to of the data + * set by JxlDecoderSetJPEGBuffer, or 0 if no buffer is set or + * JxlDecoderReleaseJPEGBuffer was already called. + */ +JXL_EXPORT size_t JxlDecoderReleaseJPEGBuffer(JxlDecoder* dec); + +/** + * Outputs progressive step towards the decoded image so far when only partial + * input was received. If the flush was successful, the buffer set with + * JxlDecoderSetImageOutBuffer will contain partial image data. + * + * Can be called when JxlDecoderProcessInput returns JXL_DEC_NEED_MORE_INPUT, + * after the JXL_DEC_FRAME event already occurred and before the + * JXL_DEC_FULL_IMAGE event occurred for a frame. + * + * @param dec decoder object + * @return JXL_DEC_SUCCESS if image data was flushed to the output buffer, or + * JXL_DEC_ERROR when no flush was done, e.g. if not enough image data was + * available yet even for flush, or no output buffer was set yet. An error is + * not fatal, it only indicates no flushed image is available now, regular, + * decoding can still be performed. + */ +JXL_EXPORT JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_DECODE_H_ */ diff --git a/lib/include/jxl/decode_cxx.h b/lib/include/jxl/decode_cxx.h new file mode 100644 index 0000000..4e73152 --- /dev/null +++ b/lib/include/jxl/decode_cxx.h @@ -0,0 +1,52 @@ +// 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. + +/// @file decode_cxx.h +/// @brief C++ header-only helper for @ref decode.h. +/// +/// There's no binary library associated with the header since this is a header +/// only library. + +#ifndef JXL_DECODE_CXX_H_ +#define JXL_DECODE_CXX_H_ + +#include + +#include "jxl/decode.h" + +#if !(defined(__cplusplus) || defined(c_plusplus)) +#error "This a C++ only header. Use jxl/decode.h from C sources." +#endif + +/// Struct to call JxlDecoderDestroy from the JxlDecoderPtr unique_ptr. +struct JxlDecoderDestroyStruct { + /// Calls @ref JxlDecoderDestroy() on the passed decoder. + void operator()(JxlDecoder* decoder) { JxlDecoderDestroy(decoder); } +}; + +/// std::unique_ptr<> type that calls JxlDecoderDestroy() when releasing the +/// decoder. +/// +/// Use this helper type from C++ sources to ensure the decoder is destroyed and +/// their internal resources released. +typedef std::unique_ptr JxlDecoderPtr; + +/// Creates an instance of JxlDecoder into a JxlDecoderPtr and initializes it. +/// +/// This function returns a unique_ptr that will call JxlDecoderDestroy() when +/// releasing the pointer. See @ref JxlDecoderCreate for details on the +/// instance creation. +/// +/// @param memory_manager custom allocator function. It may be NULL. The memory +/// manager will be copied internally. +/// @return a @c NULL JxlDecoderPtr if the instance can not be allocated or +/// initialized +/// @return initialized JxlDecoderPtr instance otherwise. +static inline JxlDecoderPtr JxlDecoderMake( + const JxlMemoryManager* memory_manager) { + return JxlDecoderPtr(JxlDecoderCreate(memory_manager)); +} + +#endif // JXL_DECODE_CXX_H_ diff --git a/lib/include/jxl/encode.h b/lib/include/jxl/encode.h new file mode 100644 index 0000000..9d87a90 --- /dev/null +++ b/lib/include/jxl/encode.h @@ -0,0 +1,392 @@ +/* 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. + */ + +/** @file encode.h + * @brief Encoding API for JPEG XL. + */ + +#ifndef JXL_ENCODE_H_ +#define JXL_ENCODE_H_ + +#include "jxl/decode.h" +#include "jxl/jxl_export.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Encoder library version. + * + * @return the encoder library version as an integer: + * MAJOR_VERSION * 1000000 + MINOR_VERSION * 1000 + PATCH_VERSION. For example, + * version 1.2.3 would return 1002003. + */ +JXL_EXPORT uint32_t JxlEncoderVersion(void); + +/** + * Opaque structure that holds the JPEG XL encoder. + * + * Allocated and initialized with JxlEncoderCreate(). + * Cleaned up and deallocated with JxlEncoderDestroy(). + */ +typedef struct JxlEncoderStruct JxlEncoder; + +/** + * Opaque structure that holds frame specific encoding options for a JPEG XL + * encoder. + * + * Allocated and initialized with JxlEncoderOptionsCreate(). + * Cleaned up and deallocated when the encoder is destroyed with + * JxlEncoderDestroy(). + */ +typedef struct JxlEncoderOptionsStruct JxlEncoderOptions; + +/** + * Return value for multiple encoder functions. + */ +typedef enum { + /** Function call finished successfully, or encoding is finished and there is + * nothing more to be done. + */ + JXL_ENC_SUCCESS = 0, + + /** An error occurred, for example out of memory. + */ + JXL_ENC_ERROR = 1, + + /** The encoder needs more output buffer to continue encoding. + */ + JXL_ENC_NEED_MORE_OUTPUT = 2, + + /** The encoder doesn't (yet) support this. + */ + JXL_ENC_NOT_SUPPORTED = 3, + +} JxlEncoderStatus; + +/** + * Creates an instance of JxlEncoder and initializes it. + * + * @p memory_manager will be used for all the library dynamic allocations made + * from this instance. The parameter may be NULL, in which case the default + * allocator will be used. See jpegxl/memory_manager.h for details. + * + * @param memory_manager custom allocator function. It may be NULL. The memory + * manager will be copied internally. + * @return @c NULL if the instance can not be allocated or initialized + * @return pointer to initialized JxlEncoder otherwise + */ +JXL_EXPORT JxlEncoder* JxlEncoderCreate(const JxlMemoryManager* memory_manager); + +/** + * Re-initializes a JxlEncoder instance, so it can be re-used for encoding + * another image. All state and settings are reset as if the object was + * newly created with JxlEncoderCreate, but the memory manager is kept. + * + * @param enc instance to be re-initialized. + */ +JXL_EXPORT void JxlEncoderReset(JxlEncoder* enc); + +/** + * Deinitializes and frees JxlEncoder instance. + * + * @param enc instance to be cleaned up and deallocated. + */ +JXL_EXPORT void JxlEncoderDestroy(JxlEncoder* enc); + +/** + * Set the parallel runner for multithreading. May only be set before starting + * encoding. + * + * @param enc encoder object. + * @param parallel_runner function pointer to runner for multithreading. It may + * be NULL to use the default, single-threaded, runner. A multithreaded + * runner should be set to reach fast performance. + * @param parallel_runner_opaque opaque pointer for parallel_runner. + * @return JXL_ENC_SUCCESS if the runner was set, JXL_ENC_ERROR + * otherwise (the previous runner remains set). + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderSetParallelRunner(JxlEncoder* enc, JxlParallelRunner parallel_runner, + void* parallel_runner_opaque); + +/** + * Encodes JPEG XL file using the available bytes. @p *avail_out indicates how + * many output bytes are available, and @p *next_out points to the input bytes. + * *avail_out will be decremented by the amount of bytes that have been + * processed by the encoder and *next_out will be incremented by the same + * amount, so *next_out will now point at the amount of *avail_out unprocessed + * bytes. + * + * The returned status indicates whether the encoder needs more output bytes. + * When the return value is not JXL_ENC_ERROR or JXL_ENC_SUCCESS, the encoding + * requires more JxlEncoderProcessOutput calls to continue. + * + * @param enc encoder object. + * @param next_out pointer to next bytes to write to. + * @param avail_out amount of bytes available starting from *next_out. + * @return JXL_ENC_SUCCESS when encoding finished and all events handled. + * @return JXL_ENC_ERROR when encoding failed, e.g. invalid input. + * @return JXL_ENC_NEED_MORE_OUTPUT more output buffer is necessary. + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderProcessOutput(JxlEncoder* enc, + uint8_t** next_out, + size_t* avail_out); + +/** + * Sets the buffer to read JPEG encoded bytes from for the next frame to encode. + * + * If JxlEncoderSetBasicInfo has not yet been called, calling + * JxlEncoderAddJPEGFrame will implicitly call it with the parameters of the + * added JPEG frame. + * + * If JxlEncoderSetColorEncoding or JxlEncoderSetICCProfile has not yet been + * called, calling JxlEncoderAddJPEGFrame will implicitly call it with the + * parameters of the added JPEG frame. + * + * If the encoder is set to store JPEG reconstruction metadata using @ref + * JxlEncoderStoreJPEGMetadata and a single JPEG frame is added, it will be + * possible to losslessly reconstruct the JPEG codestream. + * + * @param options set of encoder options to use when encoding the frame. + * @param buffer bytes to read JPEG from. Owned by the caller and its contents + * are copied internally. + * @param size size of buffer in bytes. + * @return JXL_ENC_SUCCESS on success, JXL_ENC_ERROR on error + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderAddJPEGFrame( + const JxlEncoderOptions* options, const uint8_t* buffer, size_t size); + +/** + * Sets the buffer to read pixels from for the next image to encode. Must call + * JxlEncoderSetBasicInfo before JxlEncoderAddImageFrame. + * + * Currently only some pixel formats are supported: + * - JXL_TYPE_UINT8 + * - JXL_TYPE_UINT16 + * - JXL_TYPE_FLOAT16, with nominal range 0..1 + * - JXL_TYPE_FLOAT, with nominal range 0..1 + * + * 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 + * original profile that is set with JxlEncoderSetColorEncoding or + * JxlEncoderSetICCProfile. If false, the pixels are assumed to be nonlinear + * sRGB for integer data types (JXL_TYPE_UINT8, JXL_TYPE_UINT16), and linear + * sRGB for floating point data types (JXL_TYPE_FLOAT16, JXL_TYPE_FLOAT). + * + * @param options set of encoder options to use when encoding the frame. + * @param pixel_format format for pixels. Object owned by the caller and its + * contents are copied internally. + * @param buffer buffer type to input the pixel data from. Owned by the caller + * and its contents are copied internally. + * @param size size of buffer in bytes. + * @return JXL_ENC_SUCCESS on success, JXL_ENC_ERROR on error + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderAddImageFrame( + const JxlEncoderOptions* options, const JxlPixelFormat* pixel_format, + const void* buffer, size_t size); + +/** + * Declares that this encoder will not encode anything further. + * + * Must be called between JxlEncoderAddImageFrame/JPEGFrame of the last frame + * and the next call to JxlEncoderProcessOutput, or JxlEncoderProcessOutput + * won't output the last frame correctly. + * + * @param enc encoder object. + */ +JXL_EXPORT void JxlEncoderCloseInput(JxlEncoder* enc); + +/** + * Sets the original color encoding of the image encoded by this encoder. This + * is an alternative to JxlEncoderSetICCProfile and only one of these two must + * be used. This one sets the color encoding as a @ref JxlColorEncoding, while + * the other sets it as ICC binary data. + * + * @param enc encoder object. + * @param color color encoding. Object owned by the caller and its contents are + * copied internally. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR or + * JXL_ENC_NOT_SUPPORTED otherwise + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderSetColorEncoding(JxlEncoder* enc, const JxlColorEncoding* color); + +/** + * Sets the original color encoding of the image encoded by this encoder as an + * ICC color profile. This is an alternative to JxlEncoderSetColorEncoding and + * only one of these two must be used. This one sets the color encoding as ICC + * binary data, while the other defines it as a @ref JxlColorEncoding. + * + * @param enc encoder object. + * @param icc_profile bytes of the original ICC profile + * @param size size of the icc_profile buffer in bytes + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR or + * JXL_ENC_NOT_SUPPORTED otherwise + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderSetICCProfile(JxlEncoder* enc, + const uint8_t* icc_profile, + size_t size); + +/** + * Initializes a JxlBasicInfo struct to default values. + * For forwards-compatibility, this function has to be called before values + * are assigned to the struct fields. + * The default values correspond to an 8-bit RGB image, no alpha or any + * other extra channels. + * + * @param info global image metadata. Object owned by the caller. + */ +JXL_EXPORT void JxlEncoderInitBasicInfo(JxlBasicInfo* info); + +/** + * Sets the global metadata of the image encoded by this encoder. + * + * @param enc encoder object. + * @param info global image metadata. Object owned by the caller and its + * contents are copied internally. + * @return JXL_ENC_SUCCESS if the operation was successful, + * JXL_ENC_ERROR or JXL_ENC_NOT_SUPPORTED otherwise + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, + const JxlBasicInfo* info); + +/** + * Configure the encoder to store JPEG reconstruction metadata in the JPEG XL + * container. + * + * The encoder must be configured to use the JPEG XL container format using @ref + * JxlEncoderUseContainer for this to have any effect. + * + * If this is set to true and a single JPEG frame is added, it will be + * possible to losslessly reconstruct the JPEG codestream. + * + * @param enc encoder object. + * @param store_jpeg_metadata true if the encoder should store JPEG metadata. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, JXL_BOOL store_jpeg_metadata); + +/** + * Configure the encoder to use the JPEG XL container format. + * + * Using the JPEG XL container format allows to store metadata such as JPEG + * reconstruction (@ref JxlEncoderStoreJPEGMetadata) or other metadata like + * EXIF; but it adds a few bytes to the encoded file for container headers even + * if there is no extra metadata. + * + * @param enc encoder object. + * @param use_container true if the encoder should output the JPEG XL container + * format. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus JxlEncoderUseContainer(JxlEncoder* enc, + JXL_BOOL use_container); + +/** + * Sets lossless/lossy mode for the provided options. Default is lossy. + * + * @param options set of encoder options to update with the new mode + * @param lossless whether the options should be lossless + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderOptionsSetLossless(JxlEncoderOptions* options, JXL_BOOL lossless); + +/** + * Set the decoding speed tier for the provided options. Minimum is 0 (highest + * quality), and maximum is 4 (lowest quality). Default is 0. + * + * @param options set of encoder options to update with the new decoding speed + * tier. + * @param tier the decoding speed tier to set. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderOptionsSetDecodingSpeed(JxlEncoderOptions* options, int tier); + +/** + * Sets encoder effort/speed level without affecting decoding speed. Valid + * values are, from faster to slower speed: 1:lightning 2:thunder 3:falcon + * 4:cheetah 5:hare 6:wombat 7:squirrel 8:kitten 9:tortoise. + * Default: squirrel (7). + * + * @param options set of encoder options to update with the new mode. + * @param effort the effort value to set. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderOptionsSetEffort(JxlEncoderOptions* options, int effort); + +/** + * Sets the distance level for lossy compression: target max butteraugli + * distance, lower = higher quality. Range: 0 .. 15. + * 0.0 = mathematically lossless (however, use JxlEncoderOptionsSetLossless to + * use true lossless). + * 1.0 = visually lossless. + * Recommended range: 0.5 .. 3.0. + * Default value: 1.0. + * If JxlEncoderOptionsSetLossless is used, this value is unused and implied + * to be 0. + * + * @param options set of encoder options to update with the new mode. + * @param distance the distance value to set. + * @return JXL_ENC_SUCCESS if the operation was successful, JXL_ENC_ERROR + * otherwise. + */ +JXL_EXPORT JxlEncoderStatus +JxlEncoderOptionsSetDistance(JxlEncoderOptions* options, float distance); + +/** + * Create a new set of encoder options, with all values initially copied from + * the @p source options, or set to default if @p source is NULL. + * + * The returned pointer is an opaque struct tied to the encoder and it will be + * deallocated by the encoder when JxlEncoderDestroy() is called. For functions + * taking both a @ref JxlEncoder and a @ref JxlEncoderOptions, only + * JxlEncoderOptions created with this function for the same encoder instance + * can be used. + * + * @param enc encoder object. + * @param source source options to copy initial values from, or NULL to get + * defaults initialized to defaults. + * @return the opaque struct pointer identifying a new set of encoder options. + */ +JXL_EXPORT JxlEncoderOptions* JxlEncoderOptionsCreate( + JxlEncoder* enc, const JxlEncoderOptions* source); + +/** + * Sets a color encoding to be sRGB. + * + * @param color_encoding color encoding instance. + * @param is_gray whether the color encoding should be gray scale or color. + */ +JXL_EXPORT void JxlColorEncodingSetToSRGB(JxlColorEncoding* color_encoding, + JXL_BOOL is_gray); + +/** + * Sets a color encoding to be linear sRGB. + * + * @param color_encoding color encoding instance. + * @param is_gray whether the color encoding should be gray scale or color. + */ +JXL_EXPORT void JxlColorEncodingSetToLinearSRGB( + JxlColorEncoding* color_encoding, JXL_BOOL is_gray); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_ENCODE_H_ */ diff --git a/lib/include/jxl/encode_cxx.h b/lib/include/jxl/encode_cxx.h new file mode 100644 index 0000000..841528f --- /dev/null +++ b/lib/include/jxl/encode_cxx.h @@ -0,0 +1,52 @@ +// 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. + +/// @file encode_cxx.h +/// @brief C++ header-only helper for @ref encode.h. +/// +/// There's no binary library associated with the header since this is a header +/// only library. + +#ifndef JXL_ENCODE_CXX_H_ +#define JXL_ENCODE_CXX_H_ + +#include + +#include "jxl/encode.h" + +#if !(defined(__cplusplus) || defined(c_plusplus)) +#error "This a C++ only header. Use jxl/encode.h from C sources." +#endif + +/// Struct to call JxlEncoderDestroy from the JxlEncoderPtr unique_ptr. +struct JxlEncoderDestroyStruct { + /// Calls @ref JxlEncoderDestroy() on the passed encoder. + void operator()(JxlEncoder* encoder) { JxlEncoderDestroy(encoder); } +}; + +/// std::unique_ptr<> type that calls JxlEncoderDestroy() when releasing the +/// encoder. +/// +/// Use this helper type from C++ sources to ensure the encoder is destroyed and +/// their internal resources released. +typedef std::unique_ptr JxlEncoderPtr; + +/// Creates an instance of JxlEncoder into a JxlEncoderPtr and initializes it. +/// +/// This function returns a unique_ptr that will call JxlEncoderDestroy() when +/// releasing the pointer. See @ref JxlEncoderCreate for details on the +/// instance creation. +/// +/// @param memory_manager custom allocator function. It may be NULL. The memory +/// manager will be copied internally. +/// @return a @c NULL JxlEncoderPtr if the instance can not be allocated or +/// initialized +/// @return initialized JxlEncoderPtr instance otherwise. +static inline JxlEncoderPtr JxlEncoderMake( + const JxlMemoryManager* memory_manager) { + return JxlEncoderPtr(JxlEncoderCreate(memory_manager)); +} + +#endif // JXL_ENCODE_CXX_H_ diff --git a/lib/include/jxl/memory_manager.h b/lib/include/jxl/memory_manager.h new file mode 100644 index 0000000..30e6f90 --- /dev/null +++ b/lib/include/jxl/memory_manager.h @@ -0,0 +1,67 @@ +/* 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. + */ + +/** @file memory_manager.h + * @brief Abstraction functions used by JPEG XL to allocate memory. + */ + +#ifndef JXL_MEMORY_MANAGER_H_ +#define JXL_MEMORY_MANAGER_H_ + +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Allocating function for a memory region of a given size. + * + * Allocates a contiguous memory region of size @p size bytes. The returned + * memory may not be aligned to a specific size or initialized at all. + * + * @param opaque custom memory manager handle provided by the caller. + * @param size in bytes of the requested memory region. + * @returns @c 0 if the memory can not be allocated, + * @returns pointer to the memory otherwise. + */ +typedef void* (*jpegxl_alloc_func)(void* opaque, size_t size); + +/** + * Deallocating function pointer type. + * + * This function @b MUST do nothing if @p address is @c 0. + * + * @param opaque custom memory manager handle provided by the caller. + * @param address memory region pointer returned by ::jpegxl_alloc_func, or @c 0 + */ +typedef void (*jpegxl_free_func)(void* opaque, void* address); + +/** + * Memory Manager struct. + * These functions, when provided by the caller, will be used to handle memory + * allocations. + */ +typedef struct JxlMemoryManagerStruct { + /** The opaque pointer that will be passed as the first parameter to all the + * functions in this struct. */ + void* opaque; + + /** Memory allocation function. This can be NULL if and only if also the + * free() member in this class is NULL. All dynamic memory will be allocated + * and freed with these functions if they are not NULL. */ + jpegxl_alloc_func alloc; + /** Free function matching the alloc() member. */ + jpegxl_free_func free; + + /* TODO(deymo): Add cache-aligned alloc/free functions here. */ +} JxlMemoryManager; + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_MEMORY_MANAGER_H_ */ diff --git a/lib/include/jxl/parallel_runner.h b/lib/include/jxl/parallel_runner.h new file mode 100644 index 0000000..3411c99 --- /dev/null +++ b/lib/include/jxl/parallel_runner.h @@ -0,0 +1,151 @@ +/* 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. + */ + +/** + * @file parallel_runner.h + */ + +/** API for running data operations in parallel in a multi-threaded environment. + * This module allows the JPEG XL caller to define their own way of creating and + * assigning threads. + * + * The JxlParallelRunner function type defines a parallel data processing + * runner that may be implemented by the caller to allow the library to process + * in multiple threads. The multi-threaded processing in this library only + * requires to run the same function over each number of a range, possibly + * running each call in a different thread. The JPEG XL caller is responsible + * for implementing this logic using the thread APIs available in their system. + * For convenience, a C++ implementation based on std::thread is provided in + * jpegxl/parallel_runner_thread.h (part of the jpegxl_threads library). + * + * Thread pools usually store small numbers of heterogeneous tasks in a queue. + * When tasks are identical or differ only by an integer input parameter, it is + * much faster to store just one function of an integer parameter and call it + * for each value. Conventional vector-of-tasks can be run in parallel using a + * lambda function adapter that simply calls task_funcs[task]. + * + * If no multi-threading is desired, a @c NULL value of JxlParallelRunner + * will use an internal implementation without multi-threading. + */ + +#ifndef JXL_PARALLEL_RUNNER_H_ +#define JXL_PARALLEL_RUNNER_H_ + +#include +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Return code used in the JxlParallel* functions as return value. A value + * of 0 means success and any other value means error. The special value + * JXL_PARALLEL_RET_RUNNER_ERROR can be used by the runner to indicate any + * other error. + */ +typedef int JxlParallelRetCode; + +/** + * General error returned by the JxlParallelRunInit function to indicate + * an error. + */ +#define JXL_PARALLEL_RET_RUNNER_ERROR (-1) + +/** + * Parallel run initialization callback. See JxlParallelRunner for details. + * + * This function MUST be called by the JxlParallelRunner only once, on the + * same thread that called JxlParallelRunner, before any parallel execution. + * The purpose of this call is to provide the maximum number of threads that the + * JxlParallelRunner will use, which can be used by JPEG XL to allocate + * per-thread storage if needed. + * + * @param jpegxl_opaque the @p jpegxl_opaque handle provided to + * JxlParallelRunner() must be passed here. + * @param num_threads the maximum number of threads. This value must be + * positive. + * @returns 0 if the initialization process was successful. + * @returns an error code if there was an error, which should be returned by + * JxlParallelRunner(). + */ +typedef JxlParallelRetCode (*JxlParallelRunInit)(void* jpegxl_opaque, + size_t num_threads); + +/** + * Parallel run data processing callback. See JxlParallelRunner for details. + * + * This function MUST be called once for every number in the range [start_range, + * end_range) (including start_range but not including end_range) passing this + * number as the @p value. Calls for different value may be executed from + * different threads in parallel. + * + * @param jpegxl_opaque the @p jpegxl_opaque handle provided to + * JxlParallelRunner() must be passed here. + * @param value the number in the range [start_range, end_range) of the call. + * @param thread_id the thread number where this function is being called from. + * This must be lower than the @p num_threads value passed to + * JxlParallelRunInit. + */ +typedef void (*JxlParallelRunFunction)(void* jpegxl_opaque, uint32_t value, + size_t thread_id); + +/** + * JxlParallelRunner function type. A parallel runner implementation can be + * provided by a JPEG XL caller to allow running computations in multiple + * threads. This function must call the initialization function @p init in the + * same thread that called it and then call the passed @p func once for every + * number in the range [start_range, end_range) (including start_range but not + * including end_range) possibly from different multiple threads in parallel. + * + * The JxlParallelRunner function does not need to be re-entrant. This means + * that the same JxlParallelRunner function with the same runner_opaque + * provided parameter will not be called from the library from either @p init or + * @p func in the same decoder or encoder instance. However, a single decoding + * or encoding instance may call the provided JxlParallelRunner multiple + * times for different parts of the decoding or encoding process. + * + * @returns 0 if the @p init call succeeded (returned 0) and no other error + * occurred in the runner code. + * @returns JXL_PARALLEL_RET_RUNNER_ERROR if an error occurred in the runner + * code, for example, setting up the threads. + * @return the return value of @p init() if non-zero. + */ +typedef JxlParallelRetCode (*JxlParallelRunner)( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + +/* The following is an example of a JxlParallelRunner that doesn't use any + * multi-threading. Note that this implementation doesn't store any state + * between multiple calls of the ExampleSequentialRunner function, so the + * runner_opaque value is not used. + + JxlParallelRetCode ExampleSequentialRunner(void* runner_opaque, + void* jpegxl_opaque, + JxlParallelRunInit init, + JxlParallelRunFunction func, + uint32_t start_range, + uint32_t end_range) { + // We only use one thread (the currently running thread). + JxlParallelRetCode init_ret = (*init)(jpegxl_opaque, 1); + if (init_ret != 0) return init_ret; + + // In case of other initialization error (for example when initializing the + // threads) one can return JXL_PARALLEL_RET_RUNNER_ERROR. + + for (uint32_t i = start_range; i < end_range; i++) { + // Every call is in the thread number 0. These don't need to be in any + // order. + (*func)(jpegxl_opaque, i, 0); + } + return 0; + } + */ + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_PARALLEL_RUNNER_H_ */ diff --git a/lib/include/jxl/resizable_parallel_runner.h b/lib/include/jxl/resizable_parallel_runner.h new file mode 100644 index 0000000..88a315d --- /dev/null +++ b/lib/include/jxl/resizable_parallel_runner.h @@ -0,0 +1,75 @@ +/* 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. + */ + +/** @file resizable_parallel_runner.h + * @brief implementation using std::thread of a resizeable ::JxlParallelRunner. + */ + +/** Implementation of JxlParallelRunner than can be used to enable + * multithreading when using the JPEG XL library. This uses std::thread + * internally and related synchronization functions. The number of threads + * created can be changed after creation of the thread pool; the threads + * (including the main thread) are re-used for every + * ResizableParallelRunner::Runner call. Only one concurrent + * JxlResizableParallelRunner call per instance is allowed at a time. + * + * This is a scalable, lower-overhead thread pool runner, especially suitable + * for data-parallel computations in the fork-join model, where clients need to + * know when all tasks have completed. + * + * Compared to the implementation in @ref thread_parallel_runner.h, this + * implementation is tuned for execution on lower-powered systems, including + * for example ARM CPUs with big.LITTLE computation models. + */ + +#ifndef JXL_RESIZABLE_PARALLEL_RUNNER_H_ +#define JXL_RESIZABLE_PARALLEL_RUNNER_H_ + +#include +#include +#include +#include + +#include "jxl/jxl_threads_export.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Parallel runner internally using std::thread. Use as JxlParallelRunner. + */ +JXL_THREADS_EXPORT JxlParallelRetCode JxlResizableParallelRunner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + +/** Creates the runner for JxlResizableParallelRunner. Use as the opaque + * runner. The runner will execute tasks on the calling thread until + * @ref JxlResizableParallelRunnerSetThreads is called. + */ +JXL_THREADS_EXPORT void* JxlResizableParallelRunnerCreate( + const JxlMemoryManager* memory_manager); + +/** Changes the number of threads for JxlResizableParallelRunner. + */ +JXL_THREADS_EXPORT void JxlResizableParallelRunnerSetThreads( + void* runner_opaque, size_t num_threads); + +/** Suggests a number of threads to use for an image of given size. + */ +JXL_THREADS_EXPORT uint32_t +JxlResizableParallelRunnerSuggestThreads(uint64_t xsize, uint64_t ysize); + +/** Destroys the runner created by JxlResizableParallelRunnerCreate. + */ +JXL_THREADS_EXPORT void JxlResizableParallelRunnerDestroy(void* runner_opaque); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_RESIZABLE_PARALLEL_RUNNER_H_ */ diff --git a/lib/include/jxl/resizable_parallel_runner_cxx.h b/lib/include/jxl/resizable_parallel_runner_cxx.h new file mode 100644 index 0000000..54b8b95 --- /dev/null +++ b/lib/include/jxl/resizable_parallel_runner_cxx.h @@ -0,0 +1,59 @@ +// 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. + +/// @file resizable_parallel_runner_cxx.h +/// @brief C++ header-only helper for @ref resizable_parallel_runner.h. +/// +/// There's no binary library associated with the header since this is a header +/// only library. + +#ifndef JXL_RESIZABLE_PARALLEL_RUNNER_CXX_H_ +#define JXL_RESIZABLE_PARALLEL_RUNNER_CXX_H_ + +#include + +#include "jxl/resizable_parallel_runner.h" + +#if !(defined(__cplusplus) || defined(c_plusplus)) +#error \ + "This a C++ only header. Use jxl/jxl_resizable_parallel_runner.h from C" \ + "sources." +#endif + +/// Struct to call JxlResizableParallelRunnerDestroy from the +/// JxlResizableParallelRunnerPtr unique_ptr. +struct JxlResizableParallelRunnerDestroyStruct { + /// Calls @ref JxlResizableParallelRunnerDestroy() on the passed runner. + void operator()(void* runner) { JxlResizableParallelRunnerDestroy(runner); } +}; + +/// std::unique_ptr<> type that calls JxlResizableParallelRunnerDestroy() when +/// releasing the runner. +/// +/// Use this helper type from C++ sources to ensure the runner is destroyed and +/// their internal resources released. +typedef std::unique_ptr + JxlResizableParallelRunnerPtr; + +/// Creates an instance of JxlResizableParallelRunner into a +/// JxlResizableParallelRunnerPtr and initializes it. +/// +/// This function returns a unique_ptr that will call +/// JxlResizableParallelRunnerDestroy() when releasing the pointer. See @ref +/// JxlResizableParallelRunnerCreate for details on the instance creation. +/// +/// @param memory_manager custom allocator function. It may be NULL. The memory +/// manager will be copied internally. +/// @param num_worker_threads the number of worker threads to create. +/// @return a @c NULL JxlResizableParallelRunnerPtr if the instance can not be +/// allocated or initialized +/// @return initialized JxlResizableParallelRunnerPtr instance otherwise. +static inline JxlResizableParallelRunnerPtr JxlResizableParallelRunnerMake( + const JxlMemoryManager* memory_manager) { + return JxlResizableParallelRunnerPtr( + JxlResizableParallelRunnerCreate(memory_manager)); +} + +#endif // JXL_RESIZABLE_PARALLEL_RUNNER_CXX_H_ diff --git a/lib/include/jxl/thread_parallel_runner.h b/lib/include/jxl/thread_parallel_runner.h new file mode 100644 index 0000000..c3d8308 --- /dev/null +++ b/lib/include/jxl/thread_parallel_runner.h @@ -0,0 +1,69 @@ +/* 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. + */ + +/** @file thread_parallel_runner.h + * @brief implementation using std::thread of a ::JxlParallelRunner. + */ + +/** Implementation of JxlParallelRunner than can be used to enable + * multithreading when using the JPEG XL library. This uses std::thread + * internally and related synchronization functions. The number of threads + * created is fixed at construction time and the threads are re-used for every + * ThreadParallelRunner::Runner call. Only one concurrent + * JxlThreadParallelRunner call per instance is allowed at a time. + * + * This is a scalable, lower-overhead thread pool runner, especially suitable + * for data-parallel computations in the fork-join model, where clients need to + * know when all tasks have completed. + * + * This thread pool can efficiently load-balance millions of tasks using an + * atomic counter, thus avoiding per-task virtual or system calls. With 48 + * hyperthreads and 1M tasks that add to an atomic counter, overall runtime is + * 10-20x higher when using std::async, and ~200x for a queue-based thread + */ + +#ifndef JXL_THREAD_PARALLEL_RUNNER_H_ +#define JXL_THREAD_PARALLEL_RUNNER_H_ + +#include +#include +#include +#include + +#include "jxl/jxl_threads_export.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Parallel runner internally using std::thread. Use as JxlParallelRunner. + */ +JXL_THREADS_EXPORT JxlParallelRetCode JxlThreadParallelRunner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + +/** Creates the runner for JxlThreadParallelRunner. Use as the opaque + * runner. + */ +JXL_THREADS_EXPORT void* JxlThreadParallelRunnerCreate( + const JxlMemoryManager* memory_manager, size_t num_worker_threads); + +/** Destroys the runner created by JxlThreadParallelRunnerCreate. + */ +JXL_THREADS_EXPORT void JxlThreadParallelRunnerDestroy(void* runner_opaque); + +/** Returns a default num_worker_threads value for + * JxlThreadParallelRunnerCreate. + */ +JXL_THREADS_EXPORT size_t JxlThreadParallelRunnerDefaultNumWorkerThreads(); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_THREAD_PARALLEL_RUNNER_H_ */ diff --git a/lib/include/jxl/thread_parallel_runner_cxx.h b/lib/include/jxl/thread_parallel_runner_cxx.h new file mode 100644 index 0000000..121c556 --- /dev/null +++ b/lib/include/jxl/thread_parallel_runner_cxx.h @@ -0,0 +1,59 @@ +// 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. + +/// @file thread_parallel_runner_cxx.h +/// @brief C++ header-only helper for @ref thread_parallel_runner.h. +/// +/// There's no binary library associated with the header since this is a header +/// only library. + +#ifndef JXL_THREAD_PARALLEL_RUNNER_CXX_H_ +#define JXL_THREAD_PARALLEL_RUNNER_CXX_H_ + +#include + +#include "jxl/thread_parallel_runner.h" + +#if !(defined(__cplusplus) || defined(c_plusplus)) +#error \ + "This a C++ only header. Use jxl/jxl_thread_parallel_runner.h from C" \ + "sources." +#endif + +/// Struct to call JxlThreadParallelRunnerDestroy from the +/// JxlThreadParallelRunnerPtr unique_ptr. +struct JxlThreadParallelRunnerDestroyStruct { + /// Calls @ref JxlThreadParallelRunnerDestroy() on the passed runner. + void operator()(void* runner) { JxlThreadParallelRunnerDestroy(runner); } +}; + +/// std::unique_ptr<> type that calls JxlThreadParallelRunnerDestroy() when +/// releasing the runner. +/// +/// Use this helper type from C++ sources to ensure the runner is destroyed and +/// their internal resources released. +typedef std::unique_ptr + JxlThreadParallelRunnerPtr; + +/// Creates an instance of JxlThreadParallelRunner into a +/// JxlThreadParallelRunnerPtr and initializes it. +/// +/// This function returns a unique_ptr that will call +/// JxlThreadParallelRunnerDestroy() when releasing the pointer. See @ref +/// JxlThreadParallelRunnerCreate for details on the instance creation. +/// +/// @param memory_manager custom allocator function. It may be NULL. The memory +/// manager will be copied internally. +/// @param num_worker_threads the number of worker threads to create. +/// @return a @c NULL JxlThreadParallelRunnerPtr if the instance can not be +/// allocated or initialized +/// @return initialized JxlThreadParallelRunnerPtr instance otherwise. +static inline JxlThreadParallelRunnerPtr JxlThreadParallelRunnerMake( + const JxlMemoryManager* memory_manager, size_t num_worker_threads) { + return JxlThreadParallelRunnerPtr( + JxlThreadParallelRunnerCreate(memory_manager, num_worker_threads)); +} + +#endif // JXL_THREAD_PARALLEL_RUNNER_CXX_H_ diff --git a/lib/include/jxl/types.h b/lib/include/jxl/types.h new file mode 100644 index 0000000..cc32dbe --- /dev/null +++ b/lib/include/jxl/types.h @@ -0,0 +1,116 @@ +/* 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. + */ + +/** @file types.h + * @brief Data types for the JPEG XL API, for both encoding and decoding. + */ + +#ifndef JXL_TYPES_H_ +#define JXL_TYPES_H_ + +#include +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * A portable @c bool replacement. + * + * ::JXL_BOOL is a "documentation" type: actually it is @c int, but in API it + * denotes a type, whose only values are ::JXL_TRUE and ::JXL_FALSE. + */ +#define JXL_BOOL int +/** Portable @c true replacement. */ +#define JXL_TRUE 1 +/** Portable @c false replacement. */ +#define JXL_FALSE 0 + +/** Data type for the sample values per channel per pixel. + */ +typedef enum { + /** Use 32-bit single-precision floating point values, with range 0.0-1.0 + * (within gamut, may go outside this range for wide color gamut). Floating + * point output, either JXL_TYPE_FLOAT or JXL_TYPE_FLOAT16, is recommended + * 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, + + /** 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, + + /** Use 16-bit IEEE 754 half-precision floating point values */ + JXL_TYPE_FLOAT16, +} JxlDataType; + +/** Ordering of multi-byte data. + */ +typedef enum { + /** Use the endianness of the system, either little endian or big endian, + * without forcing either specific endianness. Do not use if pixel data + * should be exported to a well defined format. + */ + JXL_NATIVE_ENDIAN = 0, + /** Force little endian */ + JXL_LITTLE_ENDIAN = 1, + /** Force big endian */ + JXL_BIG_ENDIAN = 2, +} JxlEndianness; + +/** Data type for the sample values per channel per pixel for the output buffer + * for pixels. This is not necessarily the same as the data type encoded in the + * codestream. The channels are interleaved per pixel. The pixels are + * organized row by row, left to right, top to bottom. + * TODO(lode): implement padding / alignment (row stride) + * TODO(lode): support different channel orders if needed (RGB, BGR, ...) + */ +typedef struct { + /** Amount of channels available in a pixel buffer. + * 1: single-channel data, e.g. grayscale or a single extra channel + * 2: single-channel + alpha + * 3: trichromatic, e.g. RGB + * 4: trichromatic + alpha + * TODO(lode): this needs finetuning. It is not yet defined how the user + * chooses output color space. CMYK+alpha needs 5 channels. + */ + uint32_t num_channels; + + /** Data type of each channel. + */ + JxlDataType data_type; + + /** Whether multi-byte data types are represented in big endian or little + * endian format. This applies to JXL_TYPE_UINT16, JXL_TYPE_UINT32 + * and JXL_TYPE_FLOAT. + */ + JxlEndianness endianness; + + /** Align scanlines to a multiple of align bytes, or 0 to require no + * alignment at all (which has the same effect as value 1) + */ + size_t align; +} JxlPixelFormat; + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JXL_TYPES_H_ */ diff --git a/lib/jxl.cmake b/lib/jxl.cmake new file mode 100644 index 0000000..c0290ab --- /dev/null +++ b/lib/jxl.cmake @@ -0,0 +1,567 @@ +# 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. + +# Lists all source files for the JPEG XL decoder library. These are also used +# by the encoder: the encoder uses both dec and enc ourse files, while the +# decoder uses only dec source files. +# TODO(lode): further prune these files and move to JPEGXL_INTERNAL_SOURCES_ENC: +# only those files that the decoder absolutely needs, and or not +# only for encoding, should be listed here. +set(JPEGXL_INTERNAL_SOURCES_DEC + jxl/ac_context.h + jxl/ac_strategy.cc + jxl/ac_strategy.h + jxl/alpha.cc + jxl/alpha.h + jxl/ans_common.cc + jxl/ans_common.h + jxl/ans_params.h + jxl/aux_out.cc + jxl/aux_out.h + jxl/aux_out_fwd.h + jxl/base/arch_macros.h + jxl/base/bits.h + jxl/base/byte_order.h + jxl/base/cache_aligned.cc + jxl/base/cache_aligned.h + jxl/base/compiler_specific.h + jxl/base/data_parallel.cc + jxl/base/data_parallel.h + jxl/base/descriptive_statistics.cc + jxl/base/descriptive_statistics.h + jxl/base/file_io.h + jxl/base/iaca.h + jxl/base/os_macros.h + jxl/base/override.h + jxl/base/padded_bytes.cc + jxl/base/padded_bytes.h + jxl/base/profiler.h + jxl/base/robust_statistics.h + jxl/base/span.h + jxl/base/status.cc + jxl/base/status.h + jxl/base/thread_pool_internal.h + jxl/blending.cc + jxl/blending.h + jxl/chroma_from_luma.cc + jxl/chroma_from_luma.h + jxl/codec_in_out.h + jxl/coeff_order.cc + jxl/coeff_order.h + jxl/coeff_order_fwd.h + jxl/color_encoding_internal.cc + jxl/color_encoding_internal.h + jxl/color_management.cc + jxl/color_management.h + jxl/common.h + jxl/compressed_dc.cc + jxl/compressed_dc.h + jxl/convolve-inl.h + jxl/convolve.cc + jxl/convolve.h + jxl/dct-inl.h + jxl/dct_block-inl.h + jxl/dct_scales.cc + jxl/dct_scales.h + jxl/dct_util.h + jxl/dec_ans.cc + jxl/dec_ans.h + jxl/dec_bit_reader.h + jxl/dec_cache.cc + jxl/dec_cache.h + jxl/dec_context_map.cc + jxl/dec_context_map.h + jxl/dec_external_image.cc + jxl/dec_external_image.h + jxl/dec_frame.cc + jxl/dec_frame.h + jxl/dec_group.cc + jxl/dec_group.h + jxl/dec_group_border.cc + jxl/dec_group_border.h + jxl/dec_huffman.cc + jxl/dec_huffman.h + jxl/dec_modular.cc + jxl/dec_modular.h + jxl/dec_noise.cc + jxl/dec_noise.h + jxl/dec_params.h + jxl/dec_patch_dictionary.cc + jxl/dec_patch_dictionary.h + jxl/dec_reconstruct.cc + jxl/dec_reconstruct.h + jxl/dec_render_pipeline.h + jxl/dec_transforms-inl.h + jxl/dec_upsample.cc + jxl/dec_upsample.h + jxl/dec_xyb-inl.h + jxl/dec_xyb.cc + jxl/dec_xyb.h + jxl/decode.cc + jxl/decode_to_jpeg.cc + jxl/decode_to_jpeg.h + jxl/enc_bit_writer.cc + jxl/enc_bit_writer.h + jxl/entropy_coder.cc + jxl/entropy_coder.h + jxl/epf.cc + jxl/epf.h + jxl/fast_math-inl.h + jxl/field_encodings.h + jxl/fields.cc + jxl/fields.h + jxl/filters.cc + jxl/filters.h + jxl/filters_internal.h + jxl/frame_header.cc + jxl/frame_header.h + jxl/gauss_blur.cc + jxl/gauss_blur.h + jxl/headers.cc + jxl/headers.h + jxl/huffman_table.cc + jxl/huffman_table.h + jxl/icc_codec.cc + jxl/icc_codec.h + jxl/icc_codec_common.cc + jxl/icc_codec_common.h + jxl/image.cc + jxl/image.h + jxl/image_bundle.cc + jxl/image_bundle.h + jxl/image_metadata.cc + jxl/image_metadata.h + jxl/image_ops.h + jxl/jpeg/dec_jpeg_data.cc + jxl/jpeg/dec_jpeg_data.h + jxl/jpeg/dec_jpeg_data_writer.cc + jxl/jpeg/dec_jpeg_data_writer.h + jxl/jpeg/dec_jpeg_output_chunk.h + jxl/jpeg/dec_jpeg_serialization_state.h + jxl/jpeg/jpeg_data.cc + jxl/jpeg/jpeg_data.h + jxl/jxl_inspection.h + jxl/lehmer_code.h + jxl/linalg.h + jxl/loop_filter.cc + jxl/loop_filter.h + jxl/luminance.cc + jxl/luminance.h + jxl/memory_manager_internal.cc + jxl/memory_manager_internal.h + jxl/modular/encoding/context_predict.h + jxl/modular/encoding/dec_ma.cc + jxl/modular/encoding/dec_ma.h + jxl/modular/encoding/encoding.cc + jxl/modular/encoding/encoding.h + jxl/modular/encoding/ma_common.h + jxl/modular/modular_image.cc + jxl/modular/modular_image.h + jxl/modular/options.h + jxl/modular/transform/palette.h + jxl/modular/transform/rct.h + jxl/modular/transform/squeeze.cc + jxl/modular/transform/squeeze.h + jxl/modular/transform/transform.cc + jxl/modular/transform/transform.h + jxl/noise.h + jxl/noise_distributions.h + jxl/opsin_params.cc + jxl/opsin_params.h + jxl/passes_state.cc + jxl/passes_state.h + jxl/patch_dictionary_internal.h + jxl/quant_weights.cc + jxl/quant_weights.h + jxl/quantizer-inl.h + jxl/quantizer.cc + jxl/quantizer.h + jxl/rational_polynomial-inl.h + jxl/sanitizers.h + jxl/splines.cc + jxl/splines.h + jxl/toc.cc + jxl/toc.h + jxl/transfer_functions-inl.h + jxl/transpose-inl.h + jxl/xorshift128plus-inl.h +) + +# List of source files only needed by the encoder or by tools (including +# decoding tools), but not by the decoder library. +set(JPEGXL_INTERNAL_SOURCES_ENC + jxl/butteraugli/butteraugli.cc + jxl/butteraugli/butteraugli.h + jxl/butteraugli_wrapper.cc + jxl/dec_file.cc + jxl/dec_file.h + jxl/enc_ac_strategy.cc + jxl/enc_ac_strategy.h + jxl/enc_adaptive_quantization.cc + jxl/enc_adaptive_quantization.h + jxl/enc_ans.cc + jxl/enc_ans.h + jxl/enc_ans_params.h + jxl/enc_ar_control_field.cc + jxl/enc_ar_control_field.h + jxl/enc_butteraugli_comparator.cc + jxl/enc_butteraugli_comparator.h + jxl/enc_butteraugli_pnorm.cc + jxl/enc_butteraugli_pnorm.h + jxl/enc_cache.cc + jxl/enc_cache.h + jxl/enc_chroma_from_luma.cc + jxl/enc_chroma_from_luma.h + jxl/enc_cluster.cc + jxl/enc_cluster.h + jxl/enc_coeff_order.cc + jxl/enc_coeff_order.h + jxl/enc_color_management.cc + jxl/enc_color_management.h + jxl/enc_comparator.cc + jxl/enc_comparator.h + jxl/enc_context_map.cc + jxl/enc_context_map.h + jxl/enc_detect_dots.cc + jxl/enc_detect_dots.h + jxl/enc_dot_dictionary.cc + jxl/enc_dot_dictionary.h + jxl/enc_entropy_coder.cc + jxl/enc_entropy_coder.h + jxl/enc_external_image.cc + jxl/enc_external_image.h + jxl/enc_fast_heuristics.cc + jxl/enc_file.cc + jxl/enc_file.h + jxl/enc_frame.cc + jxl/enc_frame.h + jxl/enc_gamma_correct.h + jxl/enc_group.cc + jxl/enc_group.h + jxl/enc_heuristics.cc + jxl/enc_heuristics.h + jxl/enc_huffman.cc + jxl/enc_huffman.h + jxl/enc_icc_codec.cc + jxl/enc_icc_codec.h + jxl/enc_image_bundle.cc + jxl/enc_image_bundle.h + jxl/enc_jxl_skcms.h + jxl/enc_modular.cc + jxl/enc_modular.h + jxl/enc_noise.cc + jxl/enc_noise.h + jxl/enc_params.h + jxl/enc_patch_dictionary.cc + jxl/enc_patch_dictionary.h + jxl/enc_photon_noise.cc + jxl/enc_photon_noise.h + jxl/enc_quant_weights.cc + jxl/enc_quant_weights.h + jxl/enc_splines.cc + jxl/enc_splines.h + jxl/enc_toc.cc + jxl/enc_toc.h + jxl/enc_transforms-inl.h + jxl/enc_transforms.cc + jxl/enc_transforms.h + jxl/enc_xyb.cc + jxl/enc_xyb.h + jxl/encode.cc + jxl/encode_internal.h + jxl/gaborish.cc + jxl/gaborish.h + jxl/huffman_tree.cc + jxl/huffman_tree.h + jxl/jpeg/enc_jpeg_data.cc + jxl/jpeg/enc_jpeg_data.h + jxl/jpeg/enc_jpeg_data_reader.cc + jxl/jpeg/enc_jpeg_data_reader.h + jxl/jpeg/enc_jpeg_huffman_decode.cc + jxl/jpeg/enc_jpeg_huffman_decode.h + jxl/linalg.cc + jxl/modular/encoding/enc_encoding.cc + jxl/modular/encoding/enc_encoding.h + jxl/modular/encoding/enc_ma.cc + jxl/modular/encoding/enc_ma.h + jxl/modular/transform/enc_palette.cc + jxl/modular/transform/enc_palette.h + jxl/modular/transform/enc_rct.cc + jxl/modular/transform/enc_rct.h + jxl/modular/transform/enc_squeeze.cc + jxl/modular/transform/enc_squeeze.h + jxl/modular/transform/enc_transform.cc + jxl/modular/transform/enc_transform.h + jxl/optimize.cc + jxl/optimize.h + jxl/progressive_split.cc + jxl/progressive_split.h +) + +set(JPEGXL_DEC_INTERNAL_LIBS + brotlidec-static + brotlicommon-static + hwy +) + +if(JPEGXL_ENABLE_PROFILER) +list(APPEND JPEGXL_DEC_INTERNAL_LIBS jxl_profiler) +endif() + +set(JPEGXL_INTERNAL_LIBS + ${JPEGXL_DEC_INTERNAL_LIBS} + brotlienc-static + Threads::Threads +) + +# strips the -static suffix from all the elements in LIST +function(strip_static OUTPUT_VAR LIB_LIST) + foreach(lib IN LISTS ${LIB_LIST}) + string(REGEX REPLACE "-static$" "" lib "${lib}") + list(APPEND out_list "${lib}") + endforeach() + set(${OUTPUT_VAR} ${out_list} PARENT_SCOPE) +endfunction() + +if (JPEGXL_ENABLE_SKCMS) + list(APPEND JPEGXL_INTERNAL_FLAGS -DJPEGXL_ENABLE_SKCMS=1) + if (JPEGXL_BUNDLE_SKCMS) + list(APPEND JPEGXL_INTERNAL_FLAGS -DJPEGXL_BUNDLE_SKCMS=1) + # skcms objects are later added to JPEGXL_INTERNAL_OBJECTS + else () + list(APPEND JPEGXL_INTERNAL_LIBS skcms) + endif () +else () + list(APPEND JPEGXL_INTERNAL_LIBS lcms2) +endif () + +if (NOT JPEGXL_ENABLE_TRANSCODE_JPEG) + list(APPEND JPEGXL_INTERNAL_FLAGS -DJPEGXL_ENABLE_TRANSCODE_JPEG=0) +endif () + +set(OBJ_COMPILE_DEFINITIONS + JPEGXL_MAJOR_VERSION=${JPEGXL_MAJOR_VERSION} + JPEGXL_MINOR_VERSION=${JPEGXL_MINOR_VERSION} + JPEGXL_PATCH_VERSION=${JPEGXL_PATCH_VERSION} + # Used to determine if we are building the library when defined or just + # including the library when not defined. This is public so libjxl shared + # library gets this define too. + JXL_INTERNAL_LIBRARY_BUILD +) + +# Decoder-only object library +add_library(jxl_dec-obj OBJECT ${JPEGXL_INTERNAL_SOURCES_DEC}) +target_compile_options(jxl_dec-obj PRIVATE ${JPEGXL_INTERNAL_FLAGS}) +target_compile_options(jxl_dec-obj PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +set_property(TARGET jxl_dec-obj PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jxl_dec-obj PUBLIC + ${PROJECT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + $ + $ +) +target_compile_definitions(jxl_dec-obj PUBLIC + ${OBJ_COMPILE_DEFINITIONS} +) +if (JPEGXL_ENABLE_PROFILER) +target_link_libraries(jxl_dec-obj PUBLIC jxl_profiler) +endif() + +# Object library. This is used to hold the set of objects and properties. +add_library(jxl_enc-obj OBJECT ${JPEGXL_INTERNAL_SOURCES_ENC}) +target_compile_options(jxl_enc-obj PRIVATE ${JPEGXL_INTERNAL_FLAGS}) +target_compile_options(jxl_enc-obj PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +set_property(TARGET jxl_enc-obj PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jxl_enc-obj PUBLIC + ${PROJECT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + $ + $ +) +target_compile_definitions(jxl_enc-obj PUBLIC + ${OBJ_COMPILE_DEFINITIONS} +) +if (JPEGXL_ENABLE_PROFILER) +target_link_libraries(jxl_enc-obj PUBLIC jxl_profiler) +endif() + +#TODO(lode): don't depend on CMS for the core library +if (JPEGXL_ENABLE_SKCMS) + target_include_directories(jxl_enc-obj PRIVATE + $ + ) +else () + target_include_directories(jxl_enc-obj PRIVATE + $ + ) +endif () + +# Headers for exporting/importing public headers +include(GenerateExportHeader) +set_target_properties(jxl_dec-obj PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 + DEFINE_SYMBOL JXL_INTERNAL_LIBRARY_BUILD +) +target_include_directories(jxl_dec-obj PUBLIC + ${CMAKE_CURRENT_BINARY_DIR}/include) + +set_target_properties(jxl_enc-obj PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 + DEFINE_SYMBOL JXL_INTERNAL_LIBRARY_BUILD +) +generate_export_header(jxl_enc-obj + BASE_NAME JXL + EXPORT_FILE_NAME include/jxl/jxl_export.h) +target_include_directories(jxl_enc-obj PUBLIC + ${CMAKE_CURRENT_BINARY_DIR}/include) + +# Private static library. This exposes all the internal functions and is used +# for tests. +add_library(jxl_dec-static STATIC + $ +) +target_link_libraries(jxl_dec-static + PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_DEC_INTERNAL_LIBS} hwy) +target_include_directories(jxl_dec-static PUBLIC + "${PROJECT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include") + +# The list of objects in the static and shared libraries. +set(JPEGXL_INTERNAL_OBJECTS + $ + $ +) +if (JPEGXL_ENABLE_SKCMS AND JPEGXL_BUNDLE_SKCMS) + list(APPEND JPEGXL_INTERNAL_OBJECTS $) +endif() + +# Private static library. This exposes all the internal functions and is used +# for tests. +# TODO(lode): once the source files are correctly split so that it is possible +# to do, remove $ 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) +target_include_directories(jxl-static PUBLIC + "${PROJECT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include") + +# JXL_EXPORT is defined to "__declspec(dllimport)" automatically by CMake +# in Windows builds when including headers from the C API and compiling from +# outside the jxl library. This is required when using the shared library, +# however in windows this causes the function to not be found when linking +# against the static library. This define JXL_EXPORT= here forces it to not +# use dllimport in tests and other tools that require the static library. +target_compile_definitions(jxl-static INTERFACE -DJXL_EXPORT=) +target_compile_definitions(jxl_dec-static INTERFACE -DJXL_EXPORT=) + +# TODO(deymo): Move TCMalloc linkage to the tools/ directory since the library +# shouldn't do any allocs anyway. +if(${JPEGXL_ENABLE_TCMALLOC}) + pkg_check_modules(TCMallocMinimal REQUIRED IMPORTED_TARGET + libtcmalloc_minimal) + # tcmalloc 2.8 has concurrency issues that makes it sometimes return nullptr + # for large allocs. See https://github.com/gperftools/gperftools/issues/1204 + # for details. + if(TCMallocMinimal_VERSION VERSION_EQUAL 2.8) + message(FATAL_ERROR + "tcmalloc version 2.8 has a concurrency bug. You have installed " + "version ${TCMallocMinimal_VERSION}, please either downgrade tcmalloc " + "to version 2.7, upgrade to 2.8.1 or newer or pass " + "-DJPEGXL_ENABLE_TCMALLOC=OFF to jpeg-xl cmake line. See the following " + "bug for details:\n" + " https://github.com/gperftools/gperftools/issues/1204\n") + endif() + target_link_libraries(jxl-static PUBLIC PkgConfig::TCMallocMinimal) +endif() # JPEGXL_ENABLE_TCMALLOC + +# Install the static library too, but as jxl.a file without the -static except +# in Windows. +if (NOT WIN32) + set_target_properties(jxl-static PROPERTIES OUTPUT_NAME "jxl") + set_target_properties(jxl_dec-static PROPERTIES OUTPUT_NAME "jxl_dec") +endif() +install(TARGETS jxl-static DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(TARGETS jxl_dec-static DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +if (((NOT DEFINED "${TARGET_SUPPORTS_SHARED_LIBS}") OR + TARGET_SUPPORTS_SHARED_LIBS) AND NOT JPEGXL_STATIC) + +# Public shared library. +add_library(jxl SHARED ${JPEGXL_INTERNAL_OBJECTS}) +strip_static(JPEGXL_INTERNAL_SHARED_LIBS JPEGXL_INTERNAL_LIBS) +target_link_libraries(jxl PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +target_link_libraries(jxl PRIVATE ${JPEGXL_INTERNAL_SHARED_LIBS}) +# Shared library include path contains only the "include/" paths. +target_include_directories(jxl PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include") +set_target_properties(jxl PROPERTIES + VERSION ${JPEGXL_LIBRARY_VERSION} + SOVERSION ${JPEGXL_LIBRARY_SOVERSION} + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") + +# Public shared decoder library. +add_library(jxl_dec SHARED $) +strip_static(JPEGXL_DEC_INTERNAL_SHARED_LIBS JPEGXL_DEC_INTERNAL_LIBS) +target_link_libraries(jxl_dec PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +target_link_libraries(jxl_dec PRIVATE ${JPEGXL_DEC_INTERNAL_SHARED_LIBS}) +# Shared library include path contains only the "include/" paths. +target_include_directories(jxl_dec PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include") +set_target_properties(jxl_dec PROPERTIES + VERSION ${JPEGXL_LIBRARY_VERSION} + SOVERSION ${JPEGXL_LIBRARY_SOVERSION} + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") + +# Add a jxl.version file as a version script to tag symbols with the +# appropriate version number. This script is also used to limit what's exposed +# in the shared library from the static dependencies bundled here. +foreach(target IN ITEMS jxl jxl_dec) + set_target_properties(${target} PROPERTIES + LINK_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/jxl/jxl.version) + if(APPLE) + set_property(TARGET ${target} APPEND_STRING PROPERTY + LINK_FLAGS "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/jxl/jxl_osx.syms") + elseif(WIN32) + # Nothing needed here, we use __declspec(dllexport) (jxl_export.h) + else() + set_property(TARGET ${target} APPEND_STRING PROPERTY + LINK_FLAGS " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/jxl/jxl.version") + endif() # APPLE + # This hides the default visibility symbols from static libraries bundled into + # the shared library. In particular this prevents exposing symbols from hwy + # and skcms in the shared library. + set_property(TARGET ${target} APPEND_STRING PROPERTY + LINK_FLAGS " -Wl,--exclude-libs=ALL") +endforeach() + +# Only install libjxl shared library. The libjxl_dec is not installed since it +# contains symbols also in libjxl which would conflict if programs try to use +# both. +install(TARGETS jxl + DESTINATION ${CMAKE_INSTALL_LIBDIR}) +else() +add_library(jxl ALIAS jxl-static) +add_library(jxl_dec ALIAS jxl_dec-static) +endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC + +# Add a pkg-config file for libjxl. +set(JPEGXL_LIBRARY_REQUIRES + "libhwy libbrotlicommon libbrotlienc libbrotlidec") +if(NOT JPEGXL_ENABLE_SKCMS) + set(JPEGXL_LIBRARY_REQUIRES "${JPEGXL_LIBRARY_REQUIRES} lcms2") +endif() +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/jxl/libjxl.pc.in" + "libjxl.pc" @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libjxl.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") diff --git a/lib/jxl/ac_context.h b/lib/jxl/ac_context.h new file mode 100644 index 0000000..a2b9e04 --- /dev/null +++ b/lib/jxl/ac_context.h @@ -0,0 +1,149 @@ +// 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_AC_CONTEXT_H_ +#define LIB_JXL_AC_CONTEXT_H_ + +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" + +namespace jxl { + +// Block context used for scanning order, number of non-zeros, AC coefficients. +// Equal to the channel. +constexpr uint32_t kDCTOrderContextStart = 0; + +// The number of predicted nonzeros goes from 0 to 1008. We use +// ceil(log2(predicted+1)) as a context for the number of nonzeros, so from 0 to +// 10, inclusive. +constexpr uint32_t kNonZeroBuckets = 37; + +static const uint16_t kCoeffFreqContext[64] = { + 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, + 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, +}; + +static const uint16_t kCoeffNumNonzeroContext[64] = { + 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123, + 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180, + 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, +}; + +// Supremum of ZeroDensityContext(x, y) + 1, when x + y < 64. +constexpr int kZeroDensityContextCount = 458; +// Supremum of ZeroDensityContext(x, y) + 1. +constexpr int kZeroDensityContextLimit = 474; + +/* This function is used for entropy-sources pre-clustering. + * + * Ideally, each combination of |nonzeros_left| and |k| should go to its own + * bucket; but it implies (64 * 63 / 2) == 2016 buckets. If there is other + * dimension (e.g. block context), then number of primary clusters becomes too + * big. + * + * To solve this problem, |nonzeros_left| and |k| values are clustered. It is + * known that their sum is at most 64, consequently, the total number buckets + * is at most A(64) * B(64). + */ +// TODO(user): investigate, why disabling pre-clustering makes entropy code +// less dense. Perhaps we would need to add HQ clustering algorithm that would +// be able to squeeze better by spending more CPU cycles. +static JXL_INLINE size_t ZeroDensityContext(size_t nonzeros_left, size_t k, + size_t covered_blocks, + size_t log2_covered_blocks, + size_t prev) { + JXL_DASSERT((1u << log2_covered_blocks) == covered_blocks); + nonzeros_left = (nonzeros_left + covered_blocks - 1) >> log2_covered_blocks; + k >>= log2_covered_blocks; + JXL_DASSERT(k > 0); + JXL_DASSERT(k < 64); + JXL_DASSERT(nonzeros_left > 0); + // Asserting nonzeros_left + k < 65 here causes crashes in debug mode with + // invalid input, since the (hot) decoding loop does not check this condition. + // As no out-of-bound memory reads are issued even if that condition is + // broken, we check this simpler condition which holds anyway. The decoder + // will still mark a file in which that condition happens as not valid at the + // end of the decoding loop, as `nzeros` will not be `0`. + JXL_DASSERT(nonzeros_left < 64); + return (kCoeffNumNonzeroContext[nonzeros_left] + kCoeffFreqContext[k]) * 2 + + prev; +} + +struct BlockCtxMap { + std::vector dc_thresholds[3]; + std::vector qf_thresholds; + std::vector ctx_map; + size_t num_ctxs, num_dc_ctxs; + + static constexpr uint8_t kDefaultCtxMap[] = { + // Default ctx map clusters all the large transforms together. + 0, 1, 2, 2, 3, 3, 4, 5, 6, 6, 6, 6, 6, // + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, // + 7, 8, 9, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, // + }; + static_assert(3 * kNumOrders == + sizeof(kDefaultCtxMap) / sizeof *kDefaultCtxMap, + "Update default context map"); + + size_t Context(int dc_idx, uint32_t qf, size_t ord, size_t c) const { + size_t qf_idx = 0; + for (uint32_t t : qf_thresholds) { + if (qf > t) qf_idx++; + } + size_t idx = c < 2 ? c ^ 1 : 2; + idx = idx * kNumOrders + ord; + idx = idx * (qf_thresholds.size() + 1) + qf_idx; + idx = idx * num_dc_ctxs + dc_idx; + return ctx_map[idx]; + } + // Non-zero context is based on number of non-zeros and block context. + // For better clustering, contexts with same number of non-zeros are grouped. + constexpr uint32_t ZeroDensityContextsOffset(uint32_t block_ctx) const { + return num_ctxs * kNonZeroBuckets + kZeroDensityContextCount * block_ctx; + } + + // Context map for AC coefficients consists of 2 blocks: + // |num_ctxs x : context for number of non-zeros in the block + // kNonZeroBuckets| computed from block context and predicted + // value (based top and left values) + // |num_ctxs x : context for AC coefficient symbols, + // kZeroDensityContextCount| computed from block context, + // number of non-zeros left and + // index in scan order + constexpr uint32_t NumACContexts() const { + return num_ctxs * (kNonZeroBuckets + kZeroDensityContextCount); + } + + // Non-zero context is based on number of non-zeros and block context. + // For better clustering, contexts with same number of non-zeros are grouped. + inline uint32_t NonZeroContext(uint32_t non_zeros, uint32_t block_ctx) const { + uint32_t ctx; + if (non_zeros >= 64) non_zeros = 64; + if (non_zeros < 8) { + ctx = non_zeros; + } else { + ctx = 4 + non_zeros / 2; + } + return ctx * num_ctxs + block_ctx; + } + + BlockCtxMap() { + ctx_map.assign(std::begin(kDefaultCtxMap), std::end(kDefaultCtxMap)); + num_ctxs = *std::max_element(ctx_map.begin(), ctx_map.end()) + 1; + num_dc_ctxs = 1; + } +}; + +} // namespace jxl + +#endif // LIB_JXL_AC_CONTEXT_H_ diff --git a/lib/jxl/ac_strategy.cc b/lib/jxl/ac_strategy.cc new file mode 100644 index 0000000..f262f33 --- /dev/null +++ b/lib/jxl/ac_strategy.cc @@ -0,0 +1,110 @@ +// 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/ac_strategy.h" + +#include + +#include +#include // iota +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +// Tries to generalize zig-zag order to non-square blocks. Surprisingly, in +// square block frequency along the (i + j == const) diagonals is roughly the +// same. For historical reasons, consecutive diagonals are traversed +// in alternating directions - so called "zig-zag" (or "snake") order. +AcStrategy::CoeffOrderAndLut::CoeffOrderAndLut() { + for (size_t s = 0; s < AcStrategy::kNumValidStrategies; s++) { + const AcStrategy acs = AcStrategy::FromRawStrategy(s); + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + CoefficientLayout(&cy, &cx); + JXL_ASSERT((AcStrategy::CoeffOrderAndLut::kOffset[s + 1] - + AcStrategy::CoeffOrderAndLut::kOffset[s]) == cx * cy); + coeff_order_t* JXL_RESTRICT order_start = + order + AcStrategy::CoeffOrderAndLut::kOffset[s] * kDCTBlockSize; + coeff_order_t* JXL_RESTRICT lut_start = + lut + AcStrategy::CoeffOrderAndLut::kOffset[s] * kDCTBlockSize; + + // CoefficientLayout ensures cx >= cy. + // We compute the zigzag order for a cx x cx block, then discard all the + // lines that are not multiple of the ratio between cx and cy. + size_t xs = cx / cy; + size_t xsm = xs - 1; + size_t xss = CeilLog2Nonzero(xs); + // First half of the block + size_t cur = cx * cy; + for (size_t i = 0; i < cx * kBlockDim; i++) { + for (size_t j = 0; j <= i; j++) { + size_t x = j; + size_t y = i - j; + if (i % 2) std::swap(x, y); + if ((y & xsm) != 0) continue; + y >>= xss; + size_t val = 0; + if (x < cx && y < cy) { + val = y * cx + x; + } else { + val = cur++; + } + lut_start[y * cx * kBlockDim + x] = val; + order_start[val] = y * cx * kBlockDim + x; + } + } + // Second half + for (size_t ip = cx * kBlockDim - 1; ip > 0; ip--) { + size_t i = ip - 1; + for (size_t j = 0; j <= i; j++) { + size_t x = cx * kBlockDim - 1 - (i - j); + size_t y = cx * kBlockDim - 1 - j; + if (i % 2) std::swap(x, y); + if ((y & xsm) != 0) continue; + y >>= xss; + size_t val = cur++; + lut_start[y * cx * kBlockDim + x] = val; + order_start[val] = y * cx * kBlockDim + x; + } + } + } +} + +const AcStrategy::CoeffOrderAndLut* AcStrategy::CoeffOrder() { + static AcStrategy::CoeffOrderAndLut* order = + new AcStrategy::CoeffOrderAndLut(); + return order; +} + +// These definitions are needed before C++17. +constexpr size_t AcStrategy::kMaxCoeffBlocks; +constexpr size_t AcStrategy::kMaxBlockDim; +constexpr size_t AcStrategy::kMaxCoeffArea; +constexpr size_t AcStrategy::CoeffOrderAndLut::kOffset[]; + +AcStrategyImage::AcStrategyImage(size_t xsize, size_t ysize) + : layers_(xsize, ysize) { + row_ = layers_.Row(0); + stride_ = layers_.PixelsPerRow(); +} + +size_t AcStrategyImage::CountBlocks(AcStrategy::Type type) const { + size_t ret = 0; + for (size_t y = 0; y < layers_.ysize(); y++) { + const uint8_t* JXL_RESTRICT row = layers_.ConstRow(y); + for (size_t x = 0; x < layers_.xsize(); x++) { + if (row[x] == ((static_cast(type) << 1) | 1)) ret++; + } + } + return ret; +} + +} // namespace jxl diff --git a/lib/jxl/ac_strategy.h b/lib/jxl/ac_strategy.h new file mode 100644 index 0000000..b515645 --- /dev/null +++ b/lib/jxl/ac_strategy.h @@ -0,0 +1,287 @@ +// 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_AC_STRATEGY_H_ +#define LIB_JXL_AC_STRATEGY_H_ + +#include +#include + +#include // kMaxVectorSize + +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image_ops.h" + +// Defines the different kinds of transforms, and heuristics to choose between +// them. +// `AcStrategy` represents what transform should be used, and which sub-block of +// that transform we are currently in. Note that DCT4x4 is applied on all four +// 4x4 sub-blocks of an 8x8 block. +// `AcStrategyImage` defines which strategy should be used for each 8x8 block +// of the image. The highest 4 bits represent the strategy to be used, the +// lowest 4 represent the index of the block inside that strategy. + +namespace jxl { + +class AcStrategy { + public: + // Extremal values for the number of blocks/coefficients of a single strategy. + static constexpr size_t kMaxCoeffBlocks = 32; + static constexpr size_t kMaxBlockDim = kBlockDim * kMaxCoeffBlocks; + // Maximum number of coefficients in a block. Guaranteed to be a multiple of + // the vector size. + static constexpr size_t kMaxCoeffArea = kMaxBlockDim * kMaxBlockDim; + static_assert((kMaxCoeffArea * sizeof(float)) % hwy::kMaxVectorSize == 0, + "Coefficient area is not a multiple of vector size"); + + // Raw strategy types. + enum Type : uint32_t { + // Regular block size DCT + DCT = 0, + // Encode pixels without transforming + IDENTITY = 1, + // Use 2-by-2 DCT + DCT2X2 = 2, + // Use 4-by-4 DCT + DCT4X4 = 3, + // Use 16-by-16 DCT + DCT16X16 = 4, + // Use 32-by-32 DCT + DCT32X32 = 5, + // Use 16-by-8 DCT + DCT16X8 = 6, + // Use 8-by-16 DCT + DCT8X16 = 7, + // Use 32-by-8 DCT + DCT32X8 = 8, + // Use 8-by-32 DCT + DCT8X32 = 9, + // Use 32-by-16 DCT + DCT32X16 = 10, + // Use 16-by-32 DCT + DCT16X32 = 11, + // 4x8 and 8x4 DCT + DCT4X8 = 12, + DCT8X4 = 13, + // Corner-DCT. + AFV0 = 14, + AFV1 = 15, + AFV2 = 16, + AFV3 = 17, + // Larger DCTs + DCT64X64 = 18, + DCT64X32 = 19, + DCT32X64 = 20, + DCT128X128 = 21, + DCT128X64 = 22, + DCT64X128 = 23, + DCT256X256 = 24, + DCT256X128 = 25, + DCT128X256 = 26, + // Marker for num of valid strategies. + kNumValidStrategies + }; + + static constexpr uint32_t TypeBit(const Type type) { + return 1u << static_cast(type); + } + + // Returns true if this block is the first 8x8 block (i.e. top-left) of a + // possibly multi-block strategy. + JXL_INLINE bool IsFirstBlock() const { return is_first_; } + + JXL_INLINE bool IsMultiblock() const { + constexpr uint32_t bits = + TypeBit(Type::DCT16X16) | TypeBit(Type::DCT32X32) | + TypeBit(Type::DCT16X8) | TypeBit(Type::DCT8X16) | + TypeBit(Type::DCT32X8) | TypeBit(Type::DCT8X32) | + TypeBit(Type::DCT16X32) | TypeBit(Type::DCT32X16) | + TypeBit(Type::DCT32X64) | TypeBit(Type::DCT64X32) | + TypeBit(Type::DCT64X64) | TypeBit(DCT64X128) | TypeBit(DCT128X64) | + TypeBit(DCT128X128) | TypeBit(DCT128X256) | TypeBit(DCT256X128) | + TypeBit(DCT256X256); + JXL_DASSERT(Strategy() < kNumValidStrategies); + return ((1u << static_cast(Strategy())) & bits) != 0; + } + + // Returns the raw strategy value. Should only be used for tokenization. + JXL_INLINE uint8_t RawStrategy() const { + return static_cast(strategy_); + } + + JXL_INLINE Type Strategy() const { return strategy_; } + + // Inverse check + static JXL_INLINE constexpr bool IsRawStrategyValid(int raw_strategy) { + return raw_strategy < static_cast(kNumValidStrategies) && + raw_strategy >= 0; + } + static JXL_INLINE AcStrategy FromRawStrategy(uint8_t raw_strategy) { + return FromRawStrategy(static_cast(raw_strategy)); + } + static JXL_INLINE AcStrategy FromRawStrategy(Type raw_strategy) { + JXL_DASSERT(IsRawStrategyValid(static_cast(raw_strategy))); + return AcStrategy(raw_strategy, /*is_first=*/true); + } + + // "Natural order" means the order of increasing of "anisotropic" frequency of + // continuous version of DCT basis. + // Round-trip, for any given strategy s: + // X = NaturalCoeffOrder(s)[NaturalCoeffOrderLutN(s)[X]] + // X = NaturalCoeffOrderLut(s)[NaturalCoeffOrderN(s)[X]] + JXL_INLINE const coeff_order_t* NaturalCoeffOrder() const { + return CoeffOrder()->order + + CoeffOrderAndLut::kOffset[RawStrategy()] * kDCTBlockSize; + } + + JXL_INLINE const coeff_order_t* NaturalCoeffOrderLut() const { + return CoeffOrder()->lut + + CoeffOrderAndLut::kOffset[RawStrategy()] * kDCTBlockSize; + } + + // Number of 8x8 blocks that this strategy will cover. 0 for non-top-left + // blocks inside a multi-block transform. + JXL_INLINE size_t covered_blocks_x() const { + static constexpr uint8_t kLut[] = {1, 1, 1, 1, 2, 4, 1, 2, 1, + 4, 2, 4, 1, 1, 1, 1, 1, 1, + 8, 4, 8, 16, 8, 16, 32, 16, 32}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + JXL_INLINE size_t covered_blocks_y() const { + static constexpr uint8_t kLut[] = {1, 1, 1, 1, 2, 4, 2, 1, 4, + 1, 4, 2, 1, 1, 1, 1, 1, 1, + 8, 8, 4, 16, 16, 8, 32, 32, 16}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + JXL_INLINE size_t log2_covered_blocks() const { + static constexpr uint8_t kLut[] = {0, 0, 0, 0, 2, 4, 1, 1, 2, + 2, 3, 3, 0, 0, 0, 0, 0, 0, + 6, 5, 5, 8, 7, 7, 10, 9, 9}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + struct CoeffOrderAndLut { + // Those offsets get multiplied by kDCTBlockSize. + // TODO(veluca): reduce this array by merging together the same order type. + static constexpr size_t kOffset[kNumValidStrategies + 1] = { + 0, 1, 2, 3, 4, 8, 24, 26, 28, 32, 36, 44, 52, 53, + 54, 55, 56, 57, 58, 122, 154, 186, 442, 570, 698, 1722, 2234, 2746, + }; + static constexpr size_t kTotalTableSize = + kOffset[kNumValidStrategies] * kDCTBlockSize; + coeff_order_t order[kTotalTableSize]; + coeff_order_t lut[kTotalTableSize]; + + private: + CoeffOrderAndLut(); + friend class AcStrategy; + }; + + private: + friend class AcStrategyRow; + JXL_INLINE AcStrategy(Type strategy, bool is_first) + : strategy_(strategy), is_first_(is_first) { + JXL_DASSERT(IsMultiblock() || is_first == true); + } + + Type strategy_; + bool is_first_; + + static const CoeffOrderAndLut* CoeffOrder(); +}; + +// Class to use a certain row of the AC strategy. +class AcStrategyRow { + public: + explicit AcStrategyRow(const uint8_t* row) : row_(row) {} + AcStrategy operator[](size_t x) const { + return AcStrategy(static_cast(row_[x] >> 1), row_[x] & 1); + } + + private: + const uint8_t* JXL_RESTRICT row_; +}; + +class AcStrategyImage { + public: + AcStrategyImage() = default; + AcStrategyImage(size_t xsize, size_t ysize); + AcStrategyImage(AcStrategyImage&&) = default; + AcStrategyImage& operator=(AcStrategyImage&&) = default; + + void FillDCT8(const Rect& rect) { + FillPlane((static_cast(AcStrategy::Type::DCT) << 1) | 1, + &layers_, rect); + } + void FillDCT8() { FillDCT8(Rect(layers_)); } + + void FillInvalid() { FillImage(INVALID, &layers_); } + + void Set(size_t x, size_t y, AcStrategy::Type type) { +#if JXL_ENABLE_ASSERT + AcStrategy acs = AcStrategy::FromRawStrategy(type); +#endif // JXL_ENABLE_ASSERT + JXL_ASSERT(y + acs.covered_blocks_y() <= layers_.ysize()); + JXL_ASSERT(x + acs.covered_blocks_x() <= layers_.xsize()); + JXL_CHECK(SetNoBoundsCheck(x, y, type, /*check=*/false)); + } + + Status SetNoBoundsCheck(size_t x, size_t y, AcStrategy::Type type, + bool check = true) { + AcStrategy acs = AcStrategy::FromRawStrategy(type); + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + size_t pos = (y + iy) * stride_ + x + ix; + if (check && row_[pos] != INVALID) { + return JXL_FAILURE("Invalid AC strategy: block overlap"); + } + row_[pos] = + (static_cast(type) << 1) | ((iy | ix) == 0 ? 1 : 0); + } + } + return true; + } + + bool IsValid(size_t x, size_t y) { return row_[y * stride_ + x] != INVALID; } + + AcStrategyRow ConstRow(size_t y, size_t x_prefix = 0) const { + return AcStrategyRow(layers_.ConstRow(y) + x_prefix); + } + + AcStrategyRow ConstRow(const Rect& rect, size_t y) const { + return ConstRow(rect.y0() + y, rect.x0()); + } + + size_t PixelsPerRow() const { return layers_.PixelsPerRow(); } + + size_t xsize() const { return layers_.xsize(); } + size_t ysize() const { return layers_.ysize(); } + + // Count the number of blocks of a given type. + size_t CountBlocks(AcStrategy::Type type) const; + + private: + ImageB layers_; + uint8_t* JXL_RESTRICT row_; + size_t stride_; + + // A value that does not represent a valid combined AC strategy + // value. Used as a sentinel. + static constexpr uint8_t INVALID = 0xFF; +}; + +} // namespace jxl + +#endif // LIB_JXL_AC_STRATEGY_H_ diff --git a/lib/jxl/ac_strategy_test.cc b/lib/jxl/ac_strategy_test.cc new file mode 100644 index 0000000..e4ceb88 --- /dev/null +++ b/lib/jxl/ac_strategy_test.cc @@ -0,0 +1,225 @@ +// 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/ac_strategy.h" + +#include + +#include +#include +#include // HWY_ALIGN_MAX +#include +#include + +#include "lib/jxl/common.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/dec_transforms_testonly.h" +#include "lib/jxl/enc_transforms.h" + +namespace jxl { +namespace { + +// Test that DCT -> IDCT is a noop. +class AcStrategyRoundtrip : public ::hwy::TestWithParamTargetAndT { + protected: + void Run() { + const AcStrategy::Type type = static_cast(GetParam()); + const AcStrategy acs = AcStrategy::FromRawStrategy(type); + + auto mem = hwy::AllocateAligned(4 * AcStrategy::kMaxCoeffArea); + float* scratch_space = mem.get(); + float* coeffs = scratch_space + AcStrategy::kMaxCoeffArea; + float* idct = coeffs + AcStrategy::kMaxCoeffArea; + + for (size_t i = 0; i < std::min(1024u, 64u << acs.log2_covered_blocks()); + i++) { + float* input = idct + AcStrategy::kMaxCoeffArea; + std::fill_n(input, AcStrategy::kMaxCoeffArea, 0); + input[i] = 0.2f; + TransformFromPixels(type, input, acs.covered_blocks_x() * 8, coeffs, + scratch_space); + ASSERT_NEAR(coeffs[0], 0.2 / (64 << acs.log2_covered_blocks()), 1e-6) + << " i = " << i; + TransformToPixels(type, coeffs, idct, acs.covered_blocks_x() * 8, + scratch_space); + for (size_t j = 0; j < 64u << acs.log2_covered_blocks(); j++) { + ASSERT_NEAR(idct[j], j == i ? 0.2f : 0, 2e-6) + << "j = " << j << " i = " << i << " acs " << type; + } + } + // Test DC. + std::fill_n(idct, AcStrategy::kMaxCoeffArea, 0); + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + float* dc = idct + AcStrategy::kMaxCoeffArea; + std::fill_n(dc, AcStrategy::kMaxCoeffArea, 0); + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2; + LowestFrequenciesFromDC(type, dc, acs.covered_blocks_x() * 8, coeffs); + DCFromLowestFrequencies(type, coeffs, idct, acs.covered_blocks_x() * 8); + std::fill_n(dc, AcStrategy::kMaxCoeffArea, 0); + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2; + for (size_t j = 0; j < 64u << acs.log2_covered_blocks(); j++) { + ASSERT_NEAR(idct[j], dc[j], 1e-6) + << "j = " << j << " x = " << x << " y = " << y << " acs " << type; + } + } + } + } +}; + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P_T( + AcStrategyRoundtrip, + ::testing::Range(0, int(AcStrategy::Type::kNumValidStrategies))); + +TEST_P(AcStrategyRoundtrip, Test) { Run(); } + +// Test that DC(2x2) -> DCT coefficients -> IDCT -> downsampled IDCT is a noop. +class AcStrategyRoundtripDownsample + : public ::hwy::TestWithParamTargetAndT { + protected: + void Run() { + const AcStrategy::Type type = static_cast(GetParam()); + const AcStrategy acs = AcStrategy::FromRawStrategy(type); + + auto mem = hwy::AllocateAligned(4 * AcStrategy::kMaxCoeffArea); + float* scratch_space = mem.get(); + float* coeffs = scratch_space + AcStrategy::kMaxCoeffArea; + std::fill_n(coeffs, AcStrategy::kMaxCoeffArea, 0.0f); + float* idct = coeffs + AcStrategy::kMaxCoeffArea; + + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + float* dc = idct + AcStrategy::kMaxCoeffArea; + std::fill_n(dc, AcStrategy::kMaxCoeffArea, 0); + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2f; + LowestFrequenciesFromDC(type, dc, acs.covered_blocks_x() * 8, coeffs); + TransformToPixels(type, coeffs, idct, acs.covered_blocks_x() * 8, + scratch_space); + std::fill_n(coeffs, AcStrategy::kMaxCoeffArea, 0.0f); + std::fill_n(dc, AcStrategy::kMaxCoeffArea, 0); + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2f; + // Downsample + for (size_t dy = 0; dy < acs.covered_blocks_y(); dy++) { + for (size_t dx = 0; dx < acs.covered_blocks_x(); dx++) { + float sum = 0; + for (size_t iy = 0; iy < 8; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + sum += idct[(dy * 8 + iy) * 8 * acs.covered_blocks_x() + + dx * 8 + ix]; + } + } + sum /= 64.0f; + ASSERT_NEAR(sum, dc[dy * 8 * acs.covered_blocks_x() + dx], 1e-6) + << "acs " << type; + } + } + } + } + } +}; + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P_T( + AcStrategyRoundtripDownsample, + ::testing::Range(0, int(AcStrategy::Type::kNumValidStrategies))); + +TEST_P(AcStrategyRoundtripDownsample, Test) { Run(); } + +// Test that IDCT(block with zeros in the non-topleft corner) -> downsampled +// IDCT is the same as IDCT -> DC(2x2) of the same block. +class AcStrategyDownsample : public ::hwy::TestWithParamTargetAndT { + protected: + void Run() { + const AcStrategy::Type type = static_cast(GetParam()); + const AcStrategy acs = AcStrategy::FromRawStrategy(type); + size_t cx = acs.covered_blocks_y(); + size_t cy = acs.covered_blocks_x(); + CoefficientLayout(&cy, &cx); + + auto mem = hwy::AllocateAligned(4 * AcStrategy::kMaxCoeffArea); + float* scratch_space = mem.get(); + float* idct = scratch_space + AcStrategy::kMaxCoeffArea; + float* idct_acs_downsampled = idct + AcStrategy::kMaxCoeffArea; + + for (size_t y = 0; y < cy; y++) { + for (size_t x = 0; x < cx; x++) { + float* coeffs = idct + AcStrategy::kMaxCoeffArea; + std::fill_n(coeffs, AcStrategy::kMaxCoeffArea, 0); + coeffs[y * cx * 8 + x] = 0.2f; + TransformToPixels(type, coeffs, idct, acs.covered_blocks_x() * 8, + scratch_space); + std::fill_n(coeffs, AcStrategy::kMaxCoeffArea, 0); + coeffs[y * cx * 8 + x] = 0.2f; + DCFromLowestFrequencies(type, coeffs, idct_acs_downsampled, + acs.covered_blocks_x() * 8); + // Downsample + for (size_t dy = 0; dy < acs.covered_blocks_y(); dy++) { + for (size_t dx = 0; dx < acs.covered_blocks_x(); dx++) { + float sum = 0; + for (size_t iy = 0; iy < 8; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + sum += idct[(dy * 8 + iy) * 8 * acs.covered_blocks_x() + + dx * 8 + ix]; + } + } + sum /= 64; + ASSERT_NEAR( + sum, idct_acs_downsampled[dy * 8 * acs.covered_blocks_x() + dx], + 1e-6) + << " acs " << type; + } + } + } + } + } +}; + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P_T( + AcStrategyDownsample, + ::testing::Range(0, int(AcStrategy::Type::kNumValidStrategies))); + +TEST_P(AcStrategyDownsample, Test) { Run(); } + +class AcStrategyTargetTest : public ::hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(AcStrategyTargetTest); + +TEST_P(AcStrategyTargetTest, RoundtripAFVDCT) { + HWY_ALIGN_MAX float idct[16]; + for (size_t i = 0; i < 16; i++) { + HWY_ALIGN_MAX float pixels[16] = {}; + pixels[i] = 1; + HWY_ALIGN_MAX float coeffs[16] = {}; + + AFVDCT4x4(pixels, coeffs); + AFVIDCT4x4(coeffs, idct); + for (size_t j = 0; j < 16; j++) { + EXPECT_NEAR(idct[j], pixels[j], 1e-6); + } + } +} + +TEST_P(AcStrategyTargetTest, BenchmarkAFV) { + const AcStrategy::Type type = AcStrategy::Type::AFV0; + HWY_ALIGN_MAX float pixels[64] = {1}; + HWY_ALIGN_MAX float coeffs[64] = {}; + HWY_ALIGN_MAX float scratch_space[64] = {}; + for (size_t i = 0; i < 1 << 14; i++) { + TransformToPixels(type, coeffs, pixels, 8, scratch_space); + TransformFromPixels(type, pixels, 8, coeffs, scratch_space); + } + EXPECT_NEAR(pixels[0], 0.0, 1E-6); +} + +TEST_P(AcStrategyTargetTest, BenchmarkAFVDCT) { + HWY_ALIGN_MAX float pixels[64] = {1}; + HWY_ALIGN_MAX float coeffs[64] = {}; + for (size_t i = 0; i < 1 << 14; i++) { + AFVDCT4x4(pixels, coeffs); + AFVIDCT4x4(coeffs, pixels); + } + EXPECT_NEAR(pixels[0], 1.0, 1E-6); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/adaptive_reconstruction_test.cc b/lib/jxl/adaptive_reconstruction_test.cc new file mode 100644 index 0000000..788bb7c --- /dev/null +++ b/lib/jxl/adaptive_reconstruction_test.cc @@ -0,0 +1,184 @@ +// 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 +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_reconstruct.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/test_utils.h" + +namespace jxl { +namespace { + +const size_t xsize = 16; +const size_t ysize = 8; + +void GenerateFlat(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + Image3F in(xsize, ysize); + // Plane c = foreground, all others = background. + for (size_t y = 0; y < ysize; ++y) { + float* rows[3] = {in.PlaneRow(0, y), in.PlaneRow(1, y), + in.PlaneRow(2, y)}; + for (size_t x = 0; x < xsize; ++x) { + rows[0][x] = rows[1][x] = rows[2][x] = background; + rows[c][x] = foreground; + } + } + images->push_back(std::move(in)); + } +} + +// Single foreground point at any position in any channel +void GeneratePoints(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + Image3F in(xsize, ysize); + FillImage(background, &in); + in.PlaneRow(c, y)[x] = foreground; + images->push_back(std::move(in)); + } + } + } +} + +void GenerateHorzEdges(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + // Begin of foreground rows + for (size_t y = 1; y < ysize; ++y) { + Image3F in(xsize, ysize); + FillImage(background, &in); + for (size_t iy = y; iy < ysize; ++iy) { + std::fill(in.PlaneRow(c, iy), in.PlaneRow(c, iy) + xsize, foreground); + } + images->push_back(std::move(in)); + } + } +} + +void GenerateVertEdges(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + // Begin of foreground columns + for (size_t x = 1; x < xsize; ++x) { + Image3F in(xsize, ysize); + FillImage(background, &in); + for (size_t iy = 0; iy < ysize; ++iy) { + float* JXL_RESTRICT row = in.PlaneRow(c, iy); + for (size_t ix = x; ix < xsize; ++ix) { + row[ix] = foreground; + } + } + images->push_back(std::move(in)); + } + } +} + +void DumpTestImage(const char* name, const Image3F& img) { + fprintf(stderr, "Image %s:\n", name); + for (size_t y = 0; y < img.ysize(); ++y) { + const float* row_x = img.ConstPlaneRow(0, y); + const float* row_y = img.ConstPlaneRow(1, y); + const float* row_b = img.ConstPlaneRow(2, y); + for (size_t x = 0; x < img.xsize(); ++x) { + fprintf(stderr, "%5.1f|%5.1f|%5.1f ", row_x[x], row_y[x], row_b[x]); + } + fprintf(stderr, "\n"); + } + fprintf(stderr, "\n"); +} + +// Ensures input remains unchanged by filter - verifies the edge-preserving +// nature of the filter because inputs are piecewise constant. +void EnsureUnchanged(const float background, const float foreground, + uint32_t epf_iters) { + std::vector images; + GenerateFlat(background, foreground, &images); + GeneratePoints(background, foreground, &images); + GenerateHorzEdges(background, foreground, &images); + GenerateVertEdges(background, foreground, &images); + + CodecMetadata metadata; + JXL_CHECK(metadata.size.Set(xsize, ysize)); + metadata.m.xyb_encoded = false; + FrameHeader frame_header(&metadata); + // Ensure no CT is applied + frame_header.color_transform = ColorTransform::kNone; + LoopFilter& lf = frame_header.loop_filter; + lf.gab = false; + lf.epf_iters = epf_iters; + FrameDimensions frame_dim = frame_header.ToFrameDimensions(); + + jxl::PassesDecoderState state; + JXL_CHECK( + jxl::InitializePassesSharedState(frame_header, &state.shared_storage)); + JXL_CHECK(state.Init()); + state.InitForAC(/*pool=*/nullptr); + + JXL_CHECK(state.filter_weights.Init(lf, frame_dim)); + FillImage(-0.5f, &state.filter_weights.sigma); + + for (size_t idx_image = 0; idx_image < images.size(); ++idx_image) { + const Image3F& in = images[idx_image]; + state.decoded = CopyImage(in); + + ImageBundle out(&metadata.m); + out.SetFromImage(CopyImage(in), ColorEncoding::LinearSRGB()); + FillImage(-99.f, out.color()); // Initialized with garbage. + Image3F padded = PadImageMirror(in, 2 * kBlockDim, 0); + // Call with `force_fir` set to true to force to apply filters to all of the + // input image. + JXL_CHECK(FinalizeFrameDecoding(&out, &state, /*pool=*/nullptr, + /*force_fir=*/true, + /*skip_blending=*/true)); + +#if JXL_HIGH_PRECISION + VerifyRelativeError(in, *out.color(), 1E-3, 1E-4); +#else + VerifyRelativeError(in, *out.color(), 1E-2, 1E-2); +#endif + if (testing::Test::HasFatalFailure()) { + DumpTestImage("in", in); + DumpTestImage("out", *out.color()); + } + } +} + +} // namespace + +class AdaptiveReconstructionTest : public testing::TestWithParam {}; + +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(EPFItersGroup, AdaptiveReconstructionTest, + testing::Values(1, 2, 3), + testing::PrintToStringParamName()); + +TEST_P(AdaptiveReconstructionTest, TestBright) { + EnsureUnchanged(1.0f, 128.0f, GetParam()); +} +TEST_P(AdaptiveReconstructionTest, TestDark) { + EnsureUnchanged(128.0f, 1.0f, GetParam()); +} + +} // namespace jxl diff --git a/lib/jxl/alpha.cc b/lib/jxl/alpha.cc new file mode 100644 index 0000000..77ac902 --- /dev/null +++ b/lib/jxl/alpha.cc @@ -0,0 +1,111 @@ +// 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/alpha.h" + +#include + +#include + +namespace jxl { + +static float Clamp(float x) { return std::max(std::min(1.0f, x), 0.0f); } + +void PerformAlphaBlending(const AlphaBlendingInputLayer& bg, + const AlphaBlendingInputLayer& fg, + const AlphaBlendingOutput& out, size_t num_pixels, + bool alpha_is_premultiplied, bool clamp) { + if (alpha_is_premultiplied) { + for (size_t x = 0; x < num_pixels; ++x) { + float fga = clamp ? Clamp(fg.a[x]) : fg.a[x]; + out.r[x] = (fg.r[x] + bg.r[x] * (1.f - fga)); + out.g[x] = (fg.g[x] + bg.g[x] * (1.f - fga)); + out.b[x] = (fg.b[x] + bg.b[x] * (1.f - fga)); + out.a[x] = (1.f - (1.f - fga) * (1.f - bg.a[x])); + } + } else { + for (size_t x = 0; x < num_pixels; ++x) { + float fga = clamp ? Clamp(fg.a[x]) : fg.a[x]; + const float new_a = 1.f - (1.f - fga) * (1.f - bg.a[x]); + const float rnew_a = (new_a > 0 ? 1.f / new_a : 0.f); + out.r[x] = (fg.r[x] * fga + bg.r[x] * bg.a[x] * (1.f - fga)) * rnew_a; + out.g[x] = (fg.g[x] * fga + bg.g[x] * bg.a[x] * (1.f - fga)) * rnew_a; + out.b[x] = (fg.b[x] * fga + bg.b[x] * bg.a[x] * (1.f - fga)) * rnew_a; + out.a[x] = new_a; + } + } +} +void PerformAlphaBlending(const float* bg, const float* bga, const float* fg, + const float* fga, float* out, size_t num_pixels, + bool alpha_is_premultiplied, bool clamp) { + if (bg == bga && fg == fga) { + for (size_t x = 0; x < num_pixels; ++x) { + float fa = clamp ? fga[x] : std::min(std::max(0.0f, fga[x]), 1.0f); + out[x] = (1.f - (1.f - fa) * (1.f - bga[x])); + } + } else { + if (alpha_is_premultiplied) { + for (size_t x = 0; x < num_pixels; ++x) { + float fa = clamp ? fga[x] : Clamp(fga[x]); + out[x] = (fg[x] + bg[x] * (1.f - fa)); + } + } else { + for (size_t x = 0; x < num_pixels; ++x) { + float fa = clamp ? fga[x] : Clamp(fga[x]); + const float new_a = 1.f - (1.f - fa) * (1.f - bga[x]); + const float rnew_a = (new_a > 0 ? 1.f / new_a : 0.f); + out[x] = (fg[x] * fa + bg[x] * bga[x] * (1.f - fa)) * rnew_a; + } + } + } +} + +void PerformAlphaWeightedAdd(const float* bg, const float* fg, const float* fga, + float* out, size_t num_pixels, bool clamp) { + if (fg == fga) { + memcpy(out, bg, num_pixels * sizeof(*out)); + } else { + for (size_t x = 0; x < num_pixels; ++x) { + out[x] = bg[x] + fg[x] * Clamp(fga[x]); + } + } +} + +void PerformMulBlending(const float* bg, const float* fg, float* out, + size_t num_pixels, bool clamp) { + if (clamp) { + for (size_t x = 0; x < num_pixels; ++x) { + out[x] = bg[x] * Clamp(fg[x]); + } + } else { + for (size_t x = 0; x < num_pixels; ++x) { + out[x] = bg[x] * fg[x]; + } + } +} + +void PremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, + float* JXL_RESTRICT b, const float* JXL_RESTRICT a, + size_t num_pixels) { + for (size_t x = 0; x < num_pixels; ++x) { + const float multiplier = std::max(kSmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } +} + +void UnpremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, + float* JXL_RESTRICT b, const float* JXL_RESTRICT a, + size_t num_pixels) { + for (size_t x = 0; x < num_pixels; ++x) { + const float multiplier = 1.f / std::max(kSmallAlpha, a[x]); + r[x] *= multiplier; + g[x] *= multiplier; + b[x] *= multiplier; + } +} + +} // namespace jxl diff --git a/lib/jxl/alpha.h b/lib/jxl/alpha.h new file mode 100644 index 0000000..efb76c8 --- /dev/null +++ b/lib/jxl/alpha.h @@ -0,0 +1,66 @@ +// 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_ALPHA_H_ +#define LIB_JXL_ALPHA_H_ + +#include +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +// A very small value to avoid divisions by zero when converting to +// unpremultiplied alpha. Page 21 of the technical introduction to OpenEXR +// (https://www.openexr.com/documentation/TechnicalIntroduction.pdf) recommends +// "a power of two" that is "less than half of the smallest positive 16-bit +// floating-point value". That smallest value happens to be the denormal number +// 2^-24, so 2^-26 should be a good choice. +static constexpr float kSmallAlpha = 1.f / (1u << 26u); + +struct AlphaBlendingInputLayer { + const float* r; + const float* g; + const float* b; + const float* a; +}; + +struct AlphaBlendingOutput { + float* r; + float* g; + float* b; + float* a; +}; + +// Note: The pointers in `out` are allowed to alias those in `bg` or `fg`. +// No pointer shall be null. +void PerformAlphaBlending(const AlphaBlendingInputLayer& bg, + const AlphaBlendingInputLayer& fg, + const AlphaBlendingOutput& out, size_t num_pixels, + bool alpha_is_premultiplied, bool clamp); +// Single plane alpha blending +void PerformAlphaBlending(const float* bg, const float* bga, const float* fg, + const float* fga, float* out, size_t num_pixels, + bool alpha_is_premultiplied, bool clamp); + +void PerformAlphaWeightedAdd(const float* bg, const float* fg, const float* fga, + float* out, size_t num_pixels, bool clamp); + +void PerformMulBlending(const float* bg, const float* fg, float* out, + size_t num_pixels, bool clamp); + +void PremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, + float* JXL_RESTRICT b, const float* JXL_RESTRICT a, + size_t num_pixels); +void UnpremultiplyAlpha(float* JXL_RESTRICT r, float* JXL_RESTRICT g, + float* JXL_RESTRICT b, const float* JXL_RESTRICT a, + size_t num_pixels); + +} // namespace jxl + +#endif // LIB_JXL_ALPHA_H_ diff --git a/lib/jxl/alpha_test.cc b/lib/jxl/alpha_test.cc new file mode 100644 index 0000000..d90bbd3 --- /dev/null +++ b/lib/jxl/alpha_test.cc @@ -0,0 +1,134 @@ +// 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/alpha.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +using ::testing::_; +using ::testing::ElementsAre; +using ::testing::FloatNear; + +TEST(AlphaTest, BlendingWithNonPremultiplied) { + const float bg_rgb[3] = {100, 110, 120}; + const float bg_a = 180.f / 255; + const float fg_rgb[3] = {25, 21, 23}; + const float fg_a = 15420.f / 65535; + const float fg_a2 = 2.0f; + float out_rgb[3]; + float out_a; + PerformAlphaBlending( + /*bg=*/{&bg_rgb[0], &bg_rgb[1], &bg_rgb[2], &bg_a}, + /*fg=*/{&fg_rgb[0], &fg_rgb[1], &fg_rgb[2], &fg_a}, + /*out=*/{&out_rgb[0], &out_rgb[1], &out_rgb[2], &out_a}, 1, + /*alpha_is_premultiplied=*/false, /*clamp=*/false); + EXPECT_THAT(out_rgb, + ElementsAre(FloatNear(77.2f, .05f), FloatNear(83.0f, .05f), + FloatNear(90.6f, .05f))); + EXPECT_NEAR(out_a, 3174.f / 4095, 1e-5); + PerformAlphaBlending( + /*bg=*/{&bg_rgb[0], &bg_rgb[1], &bg_rgb[2], &bg_a}, + /*fg=*/{&fg_rgb[0], &fg_rgb[1], &fg_rgb[2], &fg_a2}, + /*out=*/{&out_rgb[0], &out_rgb[1], &out_rgb[2], &out_a}, 1, + /*alpha_is_premultiplied=*/false, /*clamp=*/true); + EXPECT_THAT(out_rgb, ElementsAre(FloatNear(fg_rgb[0], .05f), + FloatNear(fg_rgb[1], .05f), + FloatNear(fg_rgb[2], .05f))); + EXPECT_NEAR(out_a, 1.0f, 1e-5); +} + +TEST(AlphaTest, BlendingWithPremultiplied) { + const float bg_rgb[3] = {100, 110, 120}; + const float bg_a = 180.f / 255; + const float fg_rgb[3] = {25, 21, 23}; + const float fg_a = 15420.f / 65535; + const float fg_a2 = 2.0f; + float out_rgb[3]; + float out_a; + PerformAlphaBlending( + /*bg=*/{&bg_rgb[0], &bg_rgb[1], &bg_rgb[2], &bg_a}, + /*fg=*/{&fg_rgb[0], &fg_rgb[1], &fg_rgb[2], &fg_a}, + /*out=*/{&out_rgb[0], &out_rgb[1], &out_rgb[2], &out_a}, 1, + /*alpha_is_premultiplied=*/true, /*clamp=*/false); + EXPECT_THAT(out_rgb, + ElementsAre(FloatNear(101.5f, .05f), FloatNear(105.1f, .05f), + FloatNear(114.8f, .05f))); + EXPECT_NEAR(out_a, 3174.f / 4095, 1e-5); + PerformAlphaBlending( + /*bg=*/{&bg_rgb[0], &bg_rgb[1], &bg_rgb[2], &bg_a}, + /*fg=*/{&fg_rgb[0], &fg_rgb[1], &fg_rgb[2], &fg_a2}, + /*out=*/{&out_rgb[0], &out_rgb[1], &out_rgb[2], &out_a}, 1, + /*alpha_is_premultiplied=*/true, /*clamp=*/true); + EXPECT_THAT(out_rgb, ElementsAre(FloatNear(fg_rgb[0], .05f), + FloatNear(fg_rgb[1], .05f), + FloatNear(fg_rgb[2], .05f))); + EXPECT_NEAR(out_a, 1.0f, 1e-5); +} + +TEST(AlphaTest, Mul) { + const float bg = 100; + const float fg = 25; + float out; + PerformMulBlending(&bg, &fg, &out, 1, /*clamp=*/false); + EXPECT_THAT(out, FloatNear(fg * bg, .05f)); + PerformMulBlending(&bg, &fg, &out, 1, /*clamp=*/true); + EXPECT_THAT(out, FloatNear(bg, .05f)); +} + +TEST(AlphaTest, PremultiplyAndUnpremultiply) { + const float alpha[] = {0.f, 63.f / 255, 127.f / 255, 1.f}; + float r[] = {120, 130, 140, 150}; + float g[] = {124, 134, 144, 154}; + float b[] = {127, 137, 147, 157}; + + PremultiplyAlpha(r, g, b, alpha, 4); + EXPECT_THAT( + r, ElementsAre(FloatNear(0.f, 1e-5f), FloatNear(130 * 63.f / 255, 1e-5f), + FloatNear(140 * 127.f / 255, 1e-5f), 150)); + EXPECT_THAT( + g, ElementsAre(FloatNear(0.f, 1e-5f), FloatNear(134 * 63.f / 255, 1e-5f), + FloatNear(144 * 127.f / 255, 1e-5f), 154)); + EXPECT_THAT( + b, ElementsAre(FloatNear(0.f, 1e-5f), FloatNear(137 * 63.f / 255, 1e-5f), + FloatNear(147 * 127.f / 255, 1e-5f), 157)); + + UnpremultiplyAlpha(r, g, b, alpha, 4); + EXPECT_THAT(r, ElementsAre(FloatNear(120, 1e-4f), FloatNear(130, 1e-4f), + FloatNear(140, 1e-4f), FloatNear(150, 1e-4f))); + EXPECT_THAT(g, ElementsAre(FloatNear(124, 1e-4f), FloatNear(134, 1e-4f), + FloatNear(144, 1e-4f), FloatNear(154, 1e-4f))); + EXPECT_THAT(b, ElementsAre(FloatNear(127, 1e-4f), FloatNear(137, 1e-4f), + FloatNear(147, 1e-4f), FloatNear(157, 1e-4f))); +} + +TEST(AlphaTest, UnpremultiplyAndPremultiply) { + const float alpha[] = {0.f, 63.f / 255, 127.f / 255, 1.f}; + float r[] = {50, 60, 70, 80}; + float g[] = {54, 64, 74, 84}; + float b[] = {57, 67, 77, 87}; + + UnpremultiplyAlpha(r, g, b, alpha, 4); + EXPECT_THAT(r, ElementsAre(_, FloatNear(60 * 255.f / 63, 1e-4f), + FloatNear(70 * 255.f / 127, 1e-4f), 80)); + EXPECT_THAT(g, ElementsAre(_, FloatNear(64 * 255.f / 63, 1e-4f), + FloatNear(74 * 255.f / 127, 1e-4f), 84)); + EXPECT_THAT(b, ElementsAre(_, FloatNear(67 * 255.f / 63, 1e-4f), + FloatNear(77 * 255.f / 127, 1e-4f), 87)); + + PremultiplyAlpha(r, g, b, alpha, 4); + EXPECT_THAT(r, ElementsAre(FloatNear(50, 1e-4f), FloatNear(60, 1e-4f), + FloatNear(70, 1e-4f), FloatNear(80, 1e-4f))); + EXPECT_THAT(g, ElementsAre(FloatNear(54, 1e-4f), FloatNear(64, 1e-4f), + FloatNear(74, 1e-4f), FloatNear(84, 1e-4f))); + EXPECT_THAT(b, ElementsAre(FloatNear(57, 1e-4f), FloatNear(67, 1e-4f), + FloatNear(77, 1e-4f), FloatNear(87, 1e-4f))); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/ans_common.cc b/lib/jxl/ans_common.cc new file mode 100644 index 0000000..cc0d58b --- /dev/null +++ b/lib/jxl/ans_common.cc @@ -0,0 +1,148 @@ +// 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/ans_common.h" + +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +std::vector CreateFlatHistogram(int length, int total_count) { + JXL_ASSERT(length > 0); + JXL_ASSERT(length <= total_count); + const int count = total_count / length; + std::vector result(length, count); + const int rem_counts = total_count % length; + for (int i = 0; i < rem_counts; ++i) { + ++result[i]; + } + return result; +} + +// First, all trailing non-occuring symbols are removed from the distribution; +// if this leaves the distribution empty, a dummy symbol with max weight is +// added. This ensures that the resulting distribution sums to total table size. +// Then, `entry_size` is chosen to be the largest power of two so that +// `table_size` = ANS_TAB_SIZE/`entry_size` is at least as big as the +// distribution size. +// Note that each entry will only ever contain two different symbols, and +// consecutive ranges of offsets, which allows us to use a compact +// representation. +// Each entry is initialized with only the (symbol=i, offset) pairs; then +// positions for which the entry overflows (i.e. distribution[i] > entry_size) +// or is not full are computed, and put into a stack in increasing order. +// Missing symbols in the distribution are padded with 0 (because `table_size` +// >= number of symbols). The `cutoff` value for each entry is initialized to +// the number of occupied slots in that entry (i.e. `distributions[i]`). While +// the overflowing-symbol stack is not empty (which implies that the +// underflowing-symbol stack also is not), the top overfull and underfull +// positions are popped from the stack; the empty slots in the underfull entry +// are then filled with as many slots as needed from the overfull entry; such +// slots are placed after the slots in the overfull entry, and `offsets[1]` is +// computed accordingly. The formerly underfull entry is thus now neither +// underfull nor overfull, and represents exactly two symbols. The overfull +// entry might be either overfull or underfull, and is pushed into the +// corresponding stack. +void InitAliasTable(std::vector distribution, uint32_t range, + size_t log_alpha_size, AliasTable::Entry* JXL_RESTRICT a) { + while (!distribution.empty() && distribution.back() == 0) { + distribution.pop_back(); + } + // Ensure that a valid table is always returned, even for an empty + // alphabet. Otherwise, a specially-crafted stream might crash the + // decoder. + if (distribution.empty()) { + distribution.emplace_back(range); + } + const size_t table_size = 1 << log_alpha_size; +#if JXL_ENABLE_ASSERT + int sum = std::accumulate(distribution.begin(), distribution.end(), 0); +#endif // JXL_ENABLE_ASSERT + JXL_ASSERT(static_cast(sum) == range); + // range must be a power of two + JXL_ASSERT((range & (range - 1)) == 0); + JXL_ASSERT(distribution.size() <= table_size); + JXL_ASSERT(table_size <= range); + const uint32_t entry_size = range >> log_alpha_size; // this is exact + // Special case for single-symbol distributions, that ensures that the state + // does not change when decoding from such a distribution. Note that, since we + // hardcode offset0 == 0, it is not straightforward (if at all possible) to + // fix the general case to produce this result. + for (size_t sym = 0; sym < distribution.size(); sym++) { + if (distribution[sym] == ANS_TAB_SIZE) { + for (size_t i = 0; i < table_size; i++) { + a[i].right_value = sym; + a[i].cutoff = 0; + a[i].offsets1 = entry_size * i; + a[i].freq0 = 0; + a[i].freq1_xor_freq0 = ANS_TAB_SIZE; + } + return; + } + } + std::vector underfull_posn; + std::vector overfull_posn; + std::vector cutoffs(1 << log_alpha_size); + // Initialize entries. + for (size_t i = 0; i < distribution.size(); i++) { + cutoffs[i] = distribution[i]; + if (cutoffs[i] > entry_size) { + overfull_posn.push_back(i); + } else if (cutoffs[i] < entry_size) { + underfull_posn.push_back(i); + } + } + for (uint32_t i = distribution.size(); i < table_size; i++) { + cutoffs[i] = 0; + underfull_posn.push_back(i); + } + // Reassign overflow/underflow values. + while (!overfull_posn.empty()) { + uint32_t overfull_i = overfull_posn.back(); + overfull_posn.pop_back(); + JXL_ASSERT(!underfull_posn.empty()); + uint32_t underfull_i = underfull_posn.back(); + underfull_posn.pop_back(); + uint32_t underfull_by = entry_size - cutoffs[underfull_i]; + cutoffs[overfull_i] -= underfull_by; + // overfull positions have their original symbols + a[underfull_i].right_value = overfull_i; + a[underfull_i].offsets1 = cutoffs[overfull_i]; + // Slots in the right part of entry underfull_i were taken from the end + // of the symbols in entry overfull_i. + if (cutoffs[overfull_i] < entry_size) { + underfull_posn.push_back(overfull_i); + } else if (cutoffs[overfull_i] > entry_size) { + overfull_posn.push_back(overfull_i); + } + } + for (uint32_t i = 0; i < table_size; i++) { + // cutoffs[i] is properly initialized but the clang-analyzer doesn't infer + // it since it is partially initialized across two for-loops. + // NOLINTNEXTLINE(clang-analyzer-core.UndefinedBinaryOperatorResult) + if (cutoffs[i] == entry_size) { + a[i].right_value = i; + a[i].offsets1 = 0; + a[i].cutoff = 0; + } else { + // Note that, if cutoff is not equal to entry_size, + // a[i].offsets1 was initialized with (overfull cutoff) - + // (entry_size - a[i].cutoff). Thus, subtracting + // a[i].cutoff cannot make it negative. + a[i].offsets1 -= cutoffs[i]; + a[i].cutoff = cutoffs[i]; + } + const size_t freq0 = i < distribution.size() ? distribution[i] : 0; + const size_t i1 = a[i].right_value; + const size_t freq1 = i1 < distribution.size() ? distribution[i1] : 0; + a[i].freq0 = static_cast(freq0); + a[i].freq1_xor_freq0 = static_cast(freq1 ^ freq0); + } +} + +} // namespace jxl diff --git a/lib/jxl/ans_common.h b/lib/jxl/ans_common.h new file mode 100644 index 0000000..12ce1ef --- /dev/null +++ b/lib/jxl/ans_common.h @@ -0,0 +1,143 @@ +// 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_ANS_COMMON_H_ +#define LIB_JXL_ANS_COMMON_H_ + +#include + +#include +#include // Prefetch +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Returns the precision (number of bits) that should be used to store +// a histogram count such that Log2Floor(count) == logcount. +static JXL_INLINE uint32_t GetPopulationCountPrecision(uint32_t logcount, + uint32_t shift) { + int32_t r = std::min( + logcount, int(shift) - int((ANS_LOG_TAB_SIZE - logcount) >> 1)); + if (r < 0) return 0; + return r; +} + +// Returns a histogram where the counts are positive, differ by at most 1, +// and add up to total_count. The bigger counts (if any) are at the beginning +// of the histogram. +std::vector CreateFlatHistogram(int length, int total_count); + +// An alias table implements a mapping from the [0, ANS_TAB_SIZE) range into +// the [0, ANS_MAX_ALPHABET_SIZE) range, satisfying the following conditions: +// - each symbol occurs as many times as specified by any valid distribution +// of frequencies of the symbols. A valid distribution here is an array of +// ANS_MAX_ALPHABET_SIZE that contains numbers in the range [0, ANS_TAB_SIZE], +// and whose sum is ANS_TAB_SIZE. +// - lookups can be done in constant time, and also return how many smaller +// input values map into the same symbol, according to some well-defined order +// of input values. +// - the space used by the alias table is given by a small constant times the +// index of the largest symbol with nonzero probability in the distribution. +// Each of the entries in the table covers a range of `entry_size` values in the +// [0, ANS_TAB_SIZE) range; consecutive entries represent consecutive +// sub-ranges. In the range covered by entry `i`, the first `cutoff` values map +// to symbol `i`, while the others map to symbol `right_value`. +// +// TODO(veluca): consider making the order used for computing offsets easier to +// define - it is currently defined by the algorithm to compute the alias table. +// Beware of breaking the implicit assumption that symbols that come after the +// cutoff value should have an offset at least as big as the cutoff. + +struct AliasTable { + struct Symbol { + size_t value; + size_t offset; + size_t freq; + }; + +// Working set size matters here (~64 tables x 256 entries). +// offsets0 is always zero (beginning of [0] side among the same symbol). +// offsets1 is an offset of (pos >= cutoff) side decremented by cutoff. +#pragma pack(push, 1) + struct Entry { + uint8_t cutoff; // < kEntrySizeMinus1 when used by ANS. + uint8_t right_value; // < alphabet size. + uint16_t freq0; + + // Only used if `greater` (see Lookup) + uint16_t offsets1; // <= ANS_TAB_SIZE + uint16_t freq1_xor_freq0; // for branchless ternary in Lookup + }; +#pragma pack(pop) + + // Dividing `value` by `entry_size` determines `i`, the entry which is + // responsible for the input. If the remainder is below `cutoff`, then the + // mapped symbol is `i`; since `offsets[0]` stores the number of occurrences + // of `i` "before" the start of this entry, the offset of the input will be + // `offsets[0] + remainder`. If the remainder is above cutoff, the mapped + // symbol is `right_value`; since `offsets[1]` stores the number of + // occurrences of `right_value` "before" this entry, minus the `cutoff` value, + // the input offset is then `remainder + offsets[1]`. + static JXL_INLINE Symbol Lookup(const Entry* JXL_RESTRICT table, size_t value, + size_t log_entry_size, + size_t entry_size_minus_1) { + const size_t i = value >> log_entry_size; + const size_t pos = value & entry_size_minus_1; + +#if JXL_BYTE_ORDER_LITTLE + uint64_t entry; + memcpy(&entry, &table[i].cutoff, sizeof(entry)); + const size_t cutoff = entry & 0xFF; // = MOVZX + const size_t right_value = (entry >> 8) & 0xFF; // = MOVZX + const size_t freq0 = (entry >> 16) & 0xFFFF; +#else + // Generates multiple loads with complex addressing. + const size_t cutoff = table[i].cutoff; + const size_t right_value = table[i].right_value; + const size_t freq0 = table[i].freq0; +#endif + + const bool greater = pos >= cutoff; + +#if JXL_BYTE_ORDER_LITTLE + const uint64_t conditional = greater ? entry : 0; // = CMOV + const size_t offsets1_or_0 = (conditional >> 32) & 0xFFFF; + const size_t freq1_xor_freq0_or_0 = conditional >> 48; +#else + const size_t offsets1_or_0 = greater ? table[i].offsets1 : 0; + const size_t freq1_xor_freq0_or_0 = greater ? table[i].freq1_xor_freq0 : 0; +#endif + + // WARNING: moving this code may interfere with CMOV heuristics. + Symbol s; + s.value = greater ? right_value : i; + s.offset = offsets1_or_0 + pos; + s.freq = freq0 ^ freq1_xor_freq0_or_0; // = greater ? freq1 : freq0 + // XOR avoids implementation-defined conversion from unsigned to signed. + // Alternatives considered: BEXTR is 2 cycles on HSW, SET+shift causes + // spills, simple ternary has a long dependency chain. + + return s; + } + + static HWY_INLINE void Prefetch(const Entry* JXL_RESTRICT table, size_t value, + size_t log_entry_size) { + const size_t i = value >> log_entry_size; + hwy::Prefetch(table + i); + } +}; + +// Computes an alias table for a given distribution. +void InitAliasTable(std::vector distribution, uint32_t range, + size_t log_alpha_size, AliasTable::Entry* JXL_RESTRICT a); + +} // namespace jxl + +#endif // LIB_JXL_ANS_COMMON_H_ diff --git a/lib/jxl/ans_common_test.cc b/lib/jxl/ans_common_test.cc new file mode 100644 index 0000000..2c4ea8e --- /dev/null +++ b/lib/jxl/ans_common_test.cc @@ -0,0 +1,43 @@ +// 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/ans_common.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/ans_params.h" + +namespace jxl { +namespace { + +void VerifyAliasDistribution(const std::vector& distribution, + uint32_t range) { + constexpr size_t log_alpha_size = 8; + AliasTable::Entry table[1 << log_alpha_size]; + InitAliasTable(distribution, range, log_alpha_size, table); + std::vector> offsets(distribution.size()); + for (uint32_t i = 0; i < range; i++) { + AliasTable::Symbol s = AliasTable::Lookup( + table, i, ANS_LOG_TAB_SIZE - 8, (1 << (ANS_LOG_TAB_SIZE - 8)) - 1); + offsets[s.value].push_back(s.offset); + } + for (uint32_t i = 0; i < distribution.size(); i++) { + ASSERT_EQ(static_cast(distribution[i]), offsets[i].size()); + std::sort(offsets[i].begin(), offsets[i].end()); + for (uint32_t j = 0; j < offsets[i].size(); j++) { + ASSERT_EQ(offsets[i][j], j); + } + } +} + +TEST(ANSCommonTest, AliasDistributionSmoke) { + VerifyAliasDistribution({ANS_TAB_SIZE / 2, ANS_TAB_SIZE / 2}, ANS_TAB_SIZE); + VerifyAliasDistribution({ANS_TAB_SIZE}, ANS_TAB_SIZE); + VerifyAliasDistribution({0, 0, 0, ANS_TAB_SIZE, 0}, ANS_TAB_SIZE); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/ans_params.h b/lib/jxl/ans_params.h new file mode 100644 index 0000000..4bbc284 --- /dev/null +++ b/lib/jxl/ans_params.h @@ -0,0 +1,36 @@ +// 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_ANS_PARAMS_H_ +#define LIB_JXL_ANS_PARAMS_H_ + +// Common parameters that are needed for both the ANS entropy encoding and +// decoding methods. + +#include +#include + +namespace jxl { + +// TODO(veluca): decide if 12 is the best constant here (valid range is up to +// 16). This requires recomputing the Huffman tables in {enc,dec}_ans.cc +// 14 gives a 0.2% improvement at d1 and makes d8 slightly worse. This is +// likely not worth the increase in encoder complexity. +#define ANS_LOG_TAB_SIZE 12u +#define ANS_TAB_SIZE (1 << ANS_LOG_TAB_SIZE) +#define ANS_TAB_MASK (ANS_TAB_SIZE - 1) + +// Largest possible symbol to be encoded by either ANS or prefix coding. +#define PREFIX_MAX_ALPHABET_SIZE 4096 +#define ANS_MAX_ALPHABET_SIZE 256 + +// Max number of bits for prefix coding. +#define PREFIX_MAX_BITS 15 + +#define ANS_SIGNATURE 0x13 // Initial state, used as CRC. + +} // namespace jxl + +#endif // LIB_JXL_ANS_PARAMS_H_ diff --git a/lib/jxl/ans_test.cc b/lib/jxl/ans_test.cc new file mode 100644 index 0000000..808c5f3 --- /dev/null +++ b/lib/jxl/ans_test.cc @@ -0,0 +1,280 @@ +// 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 +#include + +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { +namespace { + +void RoundtripTestcase(int n_histograms, int alphabet_size, + const std::vector& input_values) { + constexpr uint16_t kMagic1 = 0x9e33; + constexpr uint16_t kMagic2 = 0x8b04; + + BitWriter writer; + // Space for magic bytes. + BitWriter::Allotment allotment_magic1(&writer, 16); + writer.Write(16, kMagic1); + ReclaimAndCharge(&writer, &allotment_magic1, 0, nullptr); + + std::vector context_map; + EntropyEncodingData codes; + std::vector> input_values_vec; + input_values_vec.push_back(input_values); + + BuildAndEncodeHistograms(HistogramParams(), n_histograms, input_values_vec, + &codes, &context_map, &writer, 0, nullptr); + WriteTokens(input_values_vec[0], codes, context_map, &writer, 0, nullptr); + + // Magic bytes + padding + BitWriter::Allotment allotment_magic2(&writer, 24); + writer.Write(16, kMagic2); + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment_magic2, 0, nullptr); + + // We do not truncate the output. Reading past the end reads out zeroes + // anyway. + BitReader br(writer.GetSpan()); + + ASSERT_EQ(br.ReadBits(16), kMagic1); + + std::vector dec_context_map; + ANSCode decoded_codes; + ASSERT_TRUE( + DecodeHistograms(&br, n_histograms, &decoded_codes, &dec_context_map)); + ASSERT_EQ(dec_context_map, context_map); + ANSSymbolReader reader(&decoded_codes, &br); + + for (const Token& symbol : input_values) { + uint32_t read_symbol = + reader.ReadHybridUint(symbol.context, &br, dec_context_map); + ASSERT_EQ(read_symbol, symbol.value); + } + ASSERT_TRUE(reader.CheckANSFinalState()); + + ASSERT_EQ(br.ReadBits(16), kMagic2); + EXPECT_TRUE(br.Close()); +} + +TEST(ANSTest, EmptyRoundtrip) { + RoundtripTestcase(2, ANS_MAX_ALPHABET_SIZE, std::vector()); +} + +TEST(ANSTest, SingleSymbolRoundtrip) { + for (uint32_t i = 0; i < ANS_MAX_ALPHABET_SIZE; i++) { + RoundtripTestcase(2, ANS_MAX_ALPHABET_SIZE, {{0, i}}); + } + for (uint32_t i = 0; i < ANS_MAX_ALPHABET_SIZE; i++) { + RoundtripTestcase(2, ANS_MAX_ALPHABET_SIZE, + std::vector(1024, {0, i})); + } +} + +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) +constexpr size_t kReps = 10; +#else +constexpr size_t kReps = 100; +#endif + +void RoundtripRandomStream(int alphabet_size, size_t reps = kReps, + size_t num = 1 << 18) { + constexpr int kNumHistograms = 3; + std::mt19937_64 rng; + for (size_t i = 0; i < reps; i++) { + std::vector symbols; + for (size_t j = 0; j < num; j++) { + int context = std::uniform_int_distribution<>(0, kNumHistograms - 1)(rng); + int value = std::uniform_int_distribution<>(0, alphabet_size - 1)(rng); + symbols.emplace_back(context, value); + } + RoundtripTestcase(kNumHistograms, alphabet_size, symbols); + } +} + +void RoundtripRandomUnbalancedStream(int alphabet_size) { + constexpr int kNumHistograms = 3; + constexpr int kPrecision = 1 << 10; + std::mt19937_64 rng; + for (int i = 0; i < 100; i++) { + std::vector distributions[kNumHistograms]; + for (int j = 0; j < kNumHistograms; j++) { + distributions[j].resize(kPrecision); + int symbol = 0; + int remaining = 1; + for (int k = 0; k < kPrecision; k++) { + if (remaining == 0) { + if (symbol < alphabet_size - 1) symbol++; + // There is no meaning behind this distribution: it's anything that + // will create a nonuniform distribution and won't have too few + // symbols usually. Also we want different distributions we get to be + // sufficiently dissimilar. + remaining = + std::uniform_int_distribution<>(0, (kPrecision - k) / 1)(rng); + } + distributions[j][k] = symbol; + remaining--; + } + } + std::vector symbols; + for (int j = 0; j < 1 << 18; j++) { + int context = std::uniform_int_distribution<>(0, kNumHistograms - 1)(rng); + int value = distributions[context][std::uniform_int_distribution<>( + 0, kPrecision - 1)(rng)]; + symbols.emplace_back(context, value); + } + RoundtripTestcase(kNumHistograms + 1, alphabet_size, symbols); + } +} + +TEST(ANSTest, RandomStreamRoundtrip3Small) { RoundtripRandomStream(3, 1, 16); } + +TEST(ANSTest, RandomStreamRoundtrip3) { RoundtripRandomStream(3); } + +TEST(ANSTest, RandomStreamRoundtripBig) { + RoundtripRandomStream(ANS_MAX_ALPHABET_SIZE); +} + +TEST(ANSTest, RandomUnbalancedStreamRoundtrip3) { + RoundtripRandomUnbalancedStream(3); +} + +TEST(ANSTest, RandomUnbalancedStreamRoundtripBig) { + RoundtripRandomUnbalancedStream(ANS_MAX_ALPHABET_SIZE); +} + +TEST(ANSTest, UintConfigRoundtrip) { + for (size_t log_alpha_size = 5; log_alpha_size <= 8; log_alpha_size++) { + std::vector uint_config, uint_config_dec; + for (size_t i = 0; i < log_alpha_size; i++) { + for (size_t j = 0; j <= i; j++) { + for (size_t k = 0; k <= i - j; k++) { + uint_config.emplace_back(i, j, k); + } + } + } + uint_config.emplace_back(log_alpha_size, 0, 0); + uint_config_dec.resize(uint_config.size()); + BitWriter writer; + BitWriter::Allotment allotment(&writer, 10 * uint_config.size()); + EncodeUintConfigs(uint_config, &writer, log_alpha_size); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + writer.ZeroPadToByte(); + BitReader br(writer.GetSpan()); + EXPECT_TRUE(DecodeUintConfigs(log_alpha_size, &uint_config_dec, &br)); + EXPECT_TRUE(br.Close()); + for (size_t i = 0; i < uint_config.size(); i++) { + EXPECT_EQ(uint_config[i].split_token, uint_config_dec[i].split_token); + EXPECT_EQ(uint_config[i].msb_in_token, uint_config_dec[i].msb_in_token); + EXPECT_EQ(uint_config[i].lsb_in_token, uint_config_dec[i].lsb_in_token); + } + } +} + +void TestCheckpointing(bool ans, bool lz77) { + std::vector> input_values(1); + for (size_t i = 0; i < 1024; i++) { + input_values[0].push_back(Token(0, i % 4)); + } + // up to lz77 window size. + for (size_t i = 0; i < (1 << 20) - 1022; i++) { + input_values[0].push_back(Token(0, (i % 5) + 4)); + } + // Ensure that when the window wraps around, new values are different. + input_values[0].push_back(Token(0, 0)); + for (size_t i = 0; i < 1024; i++) { + input_values[0].push_back(Token(0, i % 4)); + } + + std::vector context_map; + EntropyEncodingData codes; + HistogramParams params; + params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77 + : HistogramParams::LZ77Method::kNone; + params.force_huffman = !ans; + + BitWriter writer; + { + auto input_values_copy = input_values; + BuildAndEncodeHistograms(params, 1, input_values_copy, &codes, &context_map, + &writer, 0, nullptr); + WriteTokens(input_values_copy[0], codes, context_map, &writer, 0, nullptr); + writer.ZeroPadToByte(); + } + + // We do not truncate the output. Reading past the end reads out zeroes + // anyway. + BitReader br(writer.GetSpan()); + Status status = true; + { + BitReaderScopedCloser bc(&br, &status); + + std::vector dec_context_map; + ANSCode decoded_codes; + ASSERT_TRUE(DecodeHistograms(&br, 1, &decoded_codes, &dec_context_map)); + ASSERT_EQ(dec_context_map, context_map); + ANSSymbolReader reader(&decoded_codes, &br); + + ANSSymbolReader::Checkpoint checkpoint; + size_t br_pos; + constexpr size_t kInterval = ANSSymbolReader::kMaxCheckpointInterval - 2; + for (size_t i = 0; i < input_values[0].size(); i++) { + if (i % kInterval == 0 && i > 0) { + reader.Restore(checkpoint); + ASSERT_TRUE(br.Close()); + br = BitReader(writer.GetSpan()); + br.SkipBits(br_pos); + for (size_t j = i - kInterval; j < i; j++) { + Token symbol = input_values[0][j]; + uint32_t read_symbol = + reader.ReadHybridUint(symbol.context, &br, dec_context_map); + ASSERT_EQ(read_symbol, symbol.value) << "j = " << j; + } + } + if (i % kInterval == 0) { + reader.Save(&checkpoint); + br_pos = br.TotalBitsConsumed(); + } + Token symbol = input_values[0][i]; + uint32_t read_symbol = + reader.ReadHybridUint(symbol.context, &br, dec_context_map); + ASSERT_EQ(read_symbol, symbol.value) << "i = " << i; + } + ASSERT_TRUE(reader.CheckANSFinalState()); + } + EXPECT_TRUE(status); +} + +TEST(ANSTest, TestCheckpointingANS) { + TestCheckpointing(/*ans=*/true, /*lz77=*/false); +} + +TEST(ANSTest, TestCheckpointingPrefix) { + TestCheckpointing(/*ans=*/false, /*lz77=*/false); +} + +TEST(ANSTest, TestCheckpointingANSLZ77) { + TestCheckpointing(/*ans=*/true, /*lz77=*/true); +} + +TEST(ANSTest, TestCheckpointingPrefixLZ77) { + TestCheckpointing(/*ans=*/false, /*lz77=*/true); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/aux_out.cc b/lib/jxl/aux_out.cc new file mode 100644 index 0000000..e83140d --- /dev/null +++ b/lib/jxl/aux_out.cc @@ -0,0 +1,96 @@ +// 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/aux_out.h" + +#include + +#include // accumulate + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +void AuxOut::Print(size_t num_inputs) const { + if (num_inputs == 0) return; + + LayerTotals all_layers; + for (size_t i = 0; i < layers.size(); ++i) { + all_layers.Assimilate(layers[i]); + } + + printf("Average butteraugli iters: %10.2f\n", + num_butteraugli_iters * 1.0 / num_inputs); + + for (size_t i = 0; i < layers.size(); ++i) { + if (layers[i].total_bits != 0) { + printf("Total layer bits %-10s\t", LayerName(i)); + printf("%10f%%", 100.0 * layers[i].total_bits / all_layers.total_bits); + layers[i].Print(num_inputs); + } + } + printf("Total image size "); + all_layers.Print(num_inputs); + + const uint32_t dc_pred_total = + std::accumulate(dc_pred_usage.begin(), dc_pred_usage.end(), 0u); + const uint32_t dc_pred_total_xb = + std::accumulate(dc_pred_usage_xb.begin(), dc_pred_usage_xb.end(), 0u); + if (dc_pred_total + dc_pred_total_xb != 0) { + printf("\nDC pred Y XB:\n"); + for (size_t i = 0; i < dc_pred_usage.size(); ++i) { + printf(" %6u (%5.2f%%) %6u (%5.2f%%)\n", dc_pred_usage[i], + 100.0 * dc_pred_usage[i] / dc_pred_total, dc_pred_usage_xb[i], + 100.0 * dc_pred_usage_xb[i] / dc_pred_total_xb); + } + } + + size_t total_blocks = 0; + size_t total_positions = 0; + if (total_blocks != 0 && total_positions != 0) { + printf("\n\t\t Blocks\t\tPositions\t\t\tBlocks/Position\n"); + printf(" Total:\t\t %7zu\t\t %7zu \t\t\t%10f%%\n\n", total_blocks, + total_positions, 100.0 * total_blocks / total_positions); + } +} + +void AuxOut::DumpCoeffImage(const char* label, + const Image3S& coeff_image) const { + JXL_ASSERT(coeff_image.xsize() % 64 == 0); + Image3S reshuffled(coeff_image.xsize() / 8, coeff_image.ysize() * 8); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < coeff_image.ysize(); y++) { + for (size_t x = 0; x < coeff_image.xsize(); x += 64) { + for (size_t i = 0; i < 64; i++) { + reshuffled.PlaneRow(c, 8 * y + i / 8)[x / 8 + i % 8] = + coeff_image.PlaneRow(c, y)[x + i]; + } + } + } + } + DumpImage(label, reshuffled); +} + +void ReclaimAndCharge(BitWriter* JXL_RESTRICT writer, + BitWriter::Allotment* JXL_RESTRICT allotment, + size_t layer, AuxOut* JXL_RESTRICT aux_out) { + size_t used_bits, unused_bits; + allotment->PrivateReclaim(writer, &used_bits, &unused_bits); + +#if 0 + printf("Layer %s bits: max %zu used %zu unused %zu\n", LayerName(layer), + allotment->MaxBits(), used_bits, unused_bits); +#endif + + // This may be a nested call with aux_out == null. Whenever we know that + // aux_out is null, we can call ReclaimUnused directly. + if (aux_out != nullptr) { + aux_out->layers[layer].total_bits += used_bits; + aux_out->layers[layer].histogram_bits += allotment->HistogramBits(); + } +} + +} // namespace jxl diff --git a/lib/jxl/aux_out.h b/lib/jxl/aux_out.h new file mode 100644 index 0000000..562df81 --- /dev/null +++ b/lib/jxl/aux_out.h @@ -0,0 +1,313 @@ +// 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_AUX_OUT_H_ +#define LIB_JXL_AUX_OUT_H_ + +// Optional output information for debugging and analyzing size usage. + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jxl_inspection.h" + +namespace jxl { + +// For LayerName and AuxOut::layers[] index. Order does not matter. +enum { + kLayerHeader = 0, + kLayerTOC, + kLayerNoise, + kLayerQuant, + kLayerDequantTables, + kLayerOrder, + kLayerDC, + kLayerControlFields, + kLayerAC, + kLayerACTokens, + kLayerDictionary, + kLayerDots, + kLayerSplines, + kLayerLossless, + kLayerModularGlobal, + kLayerModularDcGroup, + kLayerModularAcGroup, + kLayerModularTree, + kLayerAlpha, + kLayerDepth, + kLayerExtraChannels, + kNumImageLayers +}; + +static inline const char* LayerName(size_t layer) { + switch (layer) { + case kLayerHeader: + return "headers"; + case kLayerTOC: + return "TOC"; + case kLayerNoise: + return "noise"; + case kLayerQuant: + return "quantizer"; + case kLayerDequantTables: + return "quant tables"; + case kLayerOrder: + return "order"; + case kLayerDC: + return "DC"; + case kLayerControlFields: + return "ControlFields"; + case kLayerAC: + return "AC"; + case kLayerACTokens: + return "ACTokens"; + case kLayerDictionary: + return "dictionary"; + case kLayerDots: + return "dots"; + case kLayerSplines: + return "splines"; + case kLayerLossless: + return "lossless"; + case kLayerModularGlobal: + return "modularGlobal"; + case kLayerModularDcGroup: + return "modularDcGroup"; + case kLayerModularAcGroup: + return "modularAcGroup"; + case kLayerModularTree: + return "modularTree"; + case kLayerAlpha: + return "alpha"; + case kLayerDepth: + return "depth"; + case kLayerExtraChannels: + return "extra channels"; + default: + JXL_ABORT("Invalid layer %zu\n", layer); + } +} + +// Statistics gathered during compression or decompression. +struct AuxOut { + private: + struct LayerTotals { + void Assimilate(const LayerTotals& victim) { + num_clustered_histograms += victim.num_clustered_histograms; + histogram_bits += victim.histogram_bits; + extra_bits += victim.extra_bits; + total_bits += victim.total_bits; + clustered_entropy += victim.clustered_entropy; + } + void Print(size_t num_inputs) const { + printf("%10zd", total_bits); + if (histogram_bits != 0) { + printf(" [c/i:%6.2f | hst:%8zd | ex:%8zd | h+c+e:%12.3f", + num_clustered_histograms * 1.0 / num_inputs, histogram_bits >> 3, + extra_bits >> 3, + (histogram_bits + clustered_entropy + extra_bits) / 8.0); + printf("]"); + } + printf("\n"); + } + size_t num_clustered_histograms = 0; + size_t extra_bits = 0; + + // Set via BitsWritten below + size_t histogram_bits = 0; + size_t total_bits = 0; + + double clustered_entropy = 0.0; + }; + + public: + AuxOut() = default; + AuxOut(const AuxOut&) = default; + + void Assimilate(const AuxOut& victim) { + for (size_t i = 0; i < layers.size(); ++i) { + layers[i].Assimilate(victim.layers[i]); + } + num_blocks += victim.num_blocks; + num_small_blocks += victim.num_small_blocks; + num_dct4x8_blocks += victim.num_dct4x8_blocks; + num_afv_blocks += victim.num_afv_blocks; + num_dct8_blocks += victim.num_dct8_blocks; + num_dct8x16_blocks += victim.num_dct8x16_blocks; + num_dct8x32_blocks += victim.num_dct8x32_blocks; + num_dct16_blocks += victim.num_dct16_blocks; + num_dct16x32_blocks += victim.num_dct16x32_blocks; + num_dct32_blocks += victim.num_dct32_blocks; + num_dct32x64_blocks += victim.num_dct32x64_blocks; + num_dct64_blocks += victim.num_dct64_blocks; + num_butteraugli_iters += victim.num_butteraugli_iters; + for (size_t i = 0; i < dc_pred_usage.size(); ++i) { + dc_pred_usage[i] += victim.dc_pred_usage[i]; + dc_pred_usage_xb[i] += victim.dc_pred_usage_xb[i]; + } + } + + void Print(size_t num_inputs) const; + + template + void DumpImage(const char* label, const Image3& image) const { + if (!dump_image) return; + if (debug_prefix.empty()) return; + std::ostringstream pathname; + pathname << debug_prefix << label << ".png"; + CodecInOut io; + // Always save to 16-bit png. + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = ColorEncoding::SRGB(); + io.SetFromImage(ConvertToFloat(image), io.metadata.m.color_encoding); + (void)dump_image(io, pathname.str()); + } + template + void DumpImage(const char* label, const Plane& image) { + DumpImage(label, + Image3(CopyImage(image), CopyImage(image), CopyImage(image))); + } + + template + void DumpXybImage(const char* label, const Image3& image) const { + if (!dump_image) return; + if (debug_prefix.empty()) return; + std::ostringstream pathname; + pathname << debug_prefix << label << ".png"; + + Image3F linear(image.xsize(), image.ysize()); + OpsinParams opsin_params; + opsin_params.Init(kDefaultIntensityTarget); + OpsinToLinear(image, Rect(linear), nullptr, &linear, opsin_params); + + CodecInOut io; + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + io.SetFromImage(std::move(linear), io.metadata.m.color_encoding); + + (void)dump_image(io, pathname.str()); + } + + // Normalizes all the channels to range 0-1, creating a false-color image + // which allows seeing the information from non-RGB channels in an RGB debug + // image. + template + void DumpImageNormalized(const char* label, const Image3& image) const { + std::array min; + std::array max; + 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])); + 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); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = static_cast((row_in[x] - min[c]) * mul); + } + } + } + DumpImage(label, normalized); + } + + template + void DumpPlaneNormalized(const char* label, const Plane& image) const { + T min; + T max; + ImageMinMax(image, &min, &max); + Image3B normalized(image.xsize(), image.ysize()); + for (size_t c = 0; c < 3; ++c) { + float mul = min == max ? 0 : (255.0f / (max - min)); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* JXL_RESTRICT row_in = image.ConstRow(y); + uint8_t* JXL_RESTRICT row_out = normalized.PlaneRow(c, y); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = static_cast((row_in[x] - min) * mul); + } + } + } + DumpImage(label, normalized); + } + + // This dumps coefficients as a 16-bit PNG with coefficients of a block placed + // in the area that would contain that block in a normal image. To view the + // resulting image manually, rescale intensities by using: + // $ convert -auto-level IMAGE.PNG - | display - + void DumpCoeffImage(const char* label, const Image3S& coeff_image) const; + + void SetInspectorImage3F(const jxl::InspectorImage3F& inspector) { + inspector_image3f_ = inspector; + } + + // Allows hooking intermediate data inspection into various places of the + // processing pipeline. Returns true iff processing should proceed. + bool InspectImage3F(const char* label, const Image3F& image) { + if (inspector_image3f_ != nullptr) { + return inspector_image3f_(label, image); + } + return true; + } + + std::array layers; + size_t num_blocks = 0; + + // Number of blocks that use larger DCT (set by ac_strategy). + size_t num_small_blocks = 0; + size_t num_dct4x8_blocks = 0; + size_t num_afv_blocks = 0; + size_t num_dct8_blocks = 0; + size_t num_dct8x16_blocks = 0; + size_t num_dct8x32_blocks = 0; + size_t num_dct16_blocks = 0; + size_t num_dct16x32_blocks = 0; + size_t num_dct32_blocks = 0; + size_t num_dct32x64_blocks = 0; + size_t num_dct64_blocks = 0; + + std::array dc_pred_usage = {{0}}; + std::array dc_pred_usage_xb = {{0}}; + + int num_butteraugli_iters = 0; + + // If not empty, additional debugging information (e.g. debug images) is + // saved in files with this prefix. + std::string debug_prefix; + + // By how much the decoded image was downsampled relative to the encoded + // image. + size_t downsampling = 1; + + jxl::InspectorImage3F inspector_image3f_; + + std::function dump_image = + nullptr; +}; + +// Used to skip image creation if they won't be written to debug directory. +static inline bool WantDebugOutput(const AuxOut* aux_out) { + // Need valid pointer and filename. + return aux_out != nullptr && !aux_out->debug_prefix.empty(); +} + +} // namespace jxl + +#endif // LIB_JXL_AUX_OUT_H_ diff --git a/lib/jxl/aux_out_fwd.h b/lib/jxl/aux_out_fwd.h new file mode 100644 index 0000000..29b31ad --- /dev/null +++ b/lib/jxl/aux_out_fwd.h @@ -0,0 +1,28 @@ +// 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_AUX_OUT_FWD_H_ +#define LIB_JXL_AUX_OUT_FWD_H_ + +#include + +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +struct AuxOut; + +// Helper function that ensures the `bits_written` are charged to `layer` in +// `aux_out`. Example usage: +// BitWriter::Allotment allotment(&writer, max_bits); +// writer.Write(..); writer.Write(..); +// ReclaimAndCharge(&writer, &allotment, layer, aux_out); +void ReclaimAndCharge(BitWriter* JXL_RESTRICT writer, + BitWriter::Allotment* JXL_RESTRICT allotment, + size_t layer, AuxOut* JXL_RESTRICT aux_out); + +} // namespace jxl + +#endif // LIB_JXL_AUX_OUT_FWD_H_ diff --git a/lib/jxl/base/arch_macros.h b/lib/jxl/base/arch_macros.h new file mode 100644 index 0000000..a983019 --- /dev/null +++ b/lib/jxl/base/arch_macros.h @@ -0,0 +1,33 @@ +// 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_ARCH_MACROS_H_ +#define LIB_JXL_BASE_ARCH_MACROS_H_ + +// Defines the JXL_ARCH_* macros. + +namespace jxl { + +#if defined(__x86_64__) || defined(_M_X64) +#define JXL_ARCH_X64 1 +#else +#define JXL_ARCH_X64 0 +#endif + +#if defined(__powerpc64__) || defined(_M_PPC) +#define JXL_ARCH_PPC 1 +#else +#define JXL_ARCH_PPC 0 +#endif + +#if defined(__aarch64__) || defined(__arm__) +#define JXL_ARCH_ARM 1 +#else +#define JXL_ARCH_ARM 0 +#endif + +} // namespace jxl + +#endif // LIB_JXL_BASE_ARCH_MACROS_H_ diff --git a/lib/jxl/base/bits.h b/lib/jxl/base/bits.h new file mode 100644 index 0000000..9f86118 --- /dev/null +++ b/lib/jxl/base/bits.h @@ -0,0 +1,147 @@ +// 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_BITS_H_ +#define LIB_JXL_BASE_BITS_H_ + +// Specialized instructions for processing register-sized bit arrays. + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +#if JXL_COMPILER_MSVC +#include +#endif + +#include +#include + +namespace jxl { + +// Empty struct used as a size tag type. +template +struct SizeTag {}; + +template +constexpr bool IsSigned() { + return T(0) > T(-1); +} + +// Undefined results for x == 0. +static JXL_INLINE JXL_MAYBE_UNUSED size_t +Num0BitsAboveMS1Bit_Nonzero(SizeTag<4> /* tag */, const uint32_t x) { + JXL_DASSERT(x != 0); +#if JXL_COMPILER_MSVC + unsigned long index; + _BitScanReverse(&index, x); + return 31 - index; +#else + return static_cast(__builtin_clz(x)); +#endif +} +static JXL_INLINE JXL_MAYBE_UNUSED size_t +Num0BitsAboveMS1Bit_Nonzero(SizeTag<8> /* tag */, const uint64_t x) { + JXL_DASSERT(x != 0); +#if JXL_COMPILER_MSVC +#if JXL_ARCH_X64 + unsigned long index; + _BitScanReverse64(&index, x); + return 63 - index; +#else // JXL_ARCH_X64 + // _BitScanReverse64 not available + uint32_t msb = static_cast(x >> 32u); + unsigned long index; + if (msb == 0) { + uint32_t lsb = static_cast(x & 0xFFFFFFFF); + _BitScanReverse(&index, lsb); + return 63 - index; + } else { + _BitScanReverse(&index, msb); + return 31 - index; + } +#endif // JXL_ARCH_X64 +#else + return static_cast(__builtin_clzll(x)); +#endif +} +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t +Num0BitsAboveMS1Bit_Nonzero(const T x) { + static_assert(!IsSigned(), "Num0BitsAboveMS1Bit_Nonzero: use unsigned"); + return Num0BitsAboveMS1Bit_Nonzero(SizeTag(), x); +} + +// Undefined results for x == 0. +static JXL_INLINE JXL_MAYBE_UNUSED size_t +Num0BitsBelowLS1Bit_Nonzero(SizeTag<4> /* tag */, const uint32_t x) { + JXL_DASSERT(x != 0); +#if JXL_COMPILER_MSVC + unsigned long index; + _BitScanForward(&index, x); + return index; +#else + return static_cast(__builtin_ctz(x)); +#endif +} +static JXL_INLINE JXL_MAYBE_UNUSED size_t +Num0BitsBelowLS1Bit_Nonzero(SizeTag<8> /* tag */, const uint64_t x) { + JXL_DASSERT(x != 0); +#if JXL_COMPILER_MSVC +#if JXL_ARCH_X64 + unsigned long index; + _BitScanForward64(&index, x); + return index; +#else // JXL_ARCH_64 + // _BitScanForward64 not available + uint32_t lsb = static_cast(x & 0xFFFFFFFF); + unsigned long index; + if (lsb == 0) { + uint32_t msb = static_cast(x >> 32u); + _BitScanForward(&index, msb); + return 32 + index; + } else { + _BitScanForward(&index, lsb); + return index; + } +#endif // JXL_ARCH_X64 +#else + return static_cast(__builtin_ctzll(x)); +#endif +} +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t Num0BitsBelowLS1Bit_Nonzero(T x) { + static_assert(!IsSigned(), "Num0BitsBelowLS1Bit_Nonzero: use unsigned"); + return Num0BitsBelowLS1Bit_Nonzero(SizeTag(), x); +} + +// Returns bit width for x == 0. +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t Num0BitsAboveMS1Bit(const T x) { + return (x == 0) ? sizeof(T) * 8 : Num0BitsAboveMS1Bit_Nonzero(x); +} + +// Returns bit width for x == 0. +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t Num0BitsBelowLS1Bit(const T x) { + return (x == 0) ? sizeof(T) * 8 : Num0BitsBelowLS1Bit_Nonzero(x); +} + +// Returns base-2 logarithm, rounded down. +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t FloorLog2Nonzero(const T x) { + return (sizeof(T) * 8 - 1) ^ Num0BitsAboveMS1Bit_Nonzero(x); +} + +// Returns base-2 logarithm, rounded up. +template +static JXL_INLINE JXL_MAYBE_UNUSED size_t CeilLog2Nonzero(const T x) { + const size_t floor_log2 = FloorLog2Nonzero(x); + if ((x & (x - 1)) == 0) return floor_log2; // power of two + return floor_log2 + 1; +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_BITS_H_ diff --git a/lib/jxl/base/byte_order.h b/lib/jxl/base/byte_order.h new file mode 100644 index 0000000..f27017d --- /dev/null +++ b/lib/jxl/base/byte_order.h @@ -0,0 +1,283 @@ +// 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_BYTE_ORDER_H_ +#define LIB_JXL_BASE_BYTE_ORDER_H_ + +#include +#include // memcpy + +#include "lib/jxl/base/compiler_specific.h" + +#if JXL_COMPILER_MSVC +#include // _byteswap_* +#endif + +#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) +#define JXL_BYTE_ORDER_LITTLE 1 +#else +// This means that we don't know that the byte order is little endian, in +// this case we use endian-neutral code that works for both little- and +// big-endian. +#define JXL_BYTE_ORDER_LITTLE 0 +#endif + +// Returns whether the system is little-endian (least-significant byte first). +#if JXL_BYTE_ORDER_LITTLE +static constexpr bool IsLittleEndian() { return true; } +#else +static inline bool IsLittleEndian() { + const uint32_t multibyte = 1; + uint8_t byte; + memcpy(&byte, &multibyte, 1); + return byte == 1; +} +#endif + +#if JXL_COMPILER_MSVC +#define JXL_BSWAP32(x) _byteswap_ulong(x) +#define JXL_BSWAP64(x) _byteswap_uint64(x) +#else +#define JXL_BSWAP32(x) __builtin_bswap32(x) +#define JXL_BSWAP64(x) __builtin_bswap64(x) +#endif + +static JXL_INLINE uint32_t LoadBE16(const uint8_t* p) { + const uint32_t byte1 = p[0]; + const uint32_t byte0 = p[1]; + return (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadLE16(const uint8_t* p) { + const uint32_t byte0 = p[0]; + const uint32_t byte1 = p[1]; + return (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadBE24(const uint8_t* p) { + const uint32_t byte2 = p[0]; + const uint32_t byte1 = p[1]; + const uint32_t byte0 = p[2]; + return (byte2 << 16) | (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadLE24(const uint8_t* p) { + const uint32_t byte0 = p[0]; + const uint32_t byte1 = p[1]; + const uint32_t byte2 = p[2]; + return (byte2 << 16) | (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadBE32(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint32_t big; + memcpy(&big, p, 4); + return JXL_BSWAP32(big); +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint32_t byte3 = p[0]; + const uint32_t byte2 = p[1]; + const uint32_t byte1 = p[2]; + const uint32_t byte0 = p[3]; + return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE uint64_t LoadBE64(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint64_t big; + memcpy(&big, p, 8); + return JXL_BSWAP64(big); +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint64_t byte7 = p[0]; + const uint64_t byte6 = p[1]; + const uint64_t byte5 = p[2]; + const uint64_t byte4 = p[3]; + const uint64_t byte3 = p[4]; + const uint64_t byte2 = p[5]; + const uint64_t byte1 = p[6]; + const uint64_t byte0 = p[7]; + return (byte7 << 56ull) | (byte6 << 48ull) | (byte5 << 40ull) | + (byte4 << 32ull) | (byte3 << 24ull) | (byte2 << 16ull) | + (byte1 << 8ull) | byte0; +#endif +} + +static JXL_INLINE uint32_t LoadLE32(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint32_t little; + memcpy(&little, p, 4); + return little; +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint32_t byte0 = p[0]; + const uint32_t byte1 = p[1]; + const uint32_t byte2 = p[2]; + const uint32_t byte3 = p[3]; + return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE uint64_t LoadLE64(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint64_t little; + memcpy(&little, p, 8); + return little; +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint64_t byte0 = p[0]; + const uint64_t byte1 = p[1]; + const uint64_t byte2 = p[2]; + const uint64_t byte3 = p[3]; + const uint64_t byte4 = p[4]; + const uint64_t byte5 = p[5]; + const uint64_t byte6 = p[6]; + const uint64_t byte7 = p[7]; + return (byte7 << 56) | (byte6 << 48) | (byte5 << 40) | (byte4 << 32) | + (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE void StoreBE16(const uint32_t native, uint8_t* p) { + p[0] = (native >> 8) & 0xFF; + p[1] = native & 0xFF; +} + +static JXL_INLINE void StoreLE16(const uint32_t native, uint8_t* p) { + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +} + +static JXL_INLINE void StoreBE24(const uint32_t native, uint8_t* p) { + p[0] = (native >> 16) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[2] = native & 0xFF; +} + +static JXL_INLINE void StoreLE24(const uint32_t native, uint8_t* p) { + p[2] = (native >> 24) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +} + +static JXL_INLINE void StoreBE32(const uint32_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint32_t big = JXL_BSWAP32(native); + memcpy(p, &big, 4); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[0] = native >> 24; + p[1] = (native >> 16) & 0xFF; + p[2] = (native >> 8) & 0xFF; + p[3] = native & 0xFF; +#endif +} + +static JXL_INLINE void StoreBE64(const uint64_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint64_t big = JXL_BSWAP64(native); + memcpy(p, &big, 8); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[0] = native >> 56ull; + p[1] = (native >> 48ull) & 0xFF; + p[2] = (native >> 40ull) & 0xFF; + p[3] = (native >> 32ull) & 0xFF; + p[4] = (native >> 24ull) & 0xFF; + p[5] = (native >> 16ull) & 0xFF; + p[6] = (native >> 8ull) & 0xFF; + p[7] = native & 0xFF; +#endif +} + +static JXL_INLINE void StoreLE32(const uint32_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint32_t little = native; + memcpy(p, &little, 4); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[3] = native >> 24; + p[2] = (native >> 16) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +#endif +} + +static JXL_INLINE void StoreLE64(const uint64_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint64_t little = native; + memcpy(p, &little, 8); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[7] = native >> 56; + p[6] = (native >> 48) & 0xFF; + p[5] = (native >> 40) & 0xFF; + p[4] = (native >> 32) & 0xFF; + p[3] = (native >> 24) & 0xFF; + p[2] = (native >> 16) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +#endif +} + +// Big/Little Endian order. +struct OrderBE {}; +struct OrderLE {}; + +// Wrappers for calling from generic code. +static JXL_INLINE void Store16(OrderBE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreBE16(native, p); +} + +static JXL_INLINE void Store16(OrderLE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreLE16(native, p); +} + +static JXL_INLINE void Store24(OrderBE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreBE24(native, p); +} + +static JXL_INLINE void Store24(OrderLE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreLE24(native, p); +} +static JXL_INLINE void Store32(OrderBE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreBE32(native, p); +} + +static JXL_INLINE void Store32(OrderLE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreLE32(native, p); +} + +static JXL_INLINE uint32_t Load16(OrderBE /*tag*/, const uint8_t* p) { + return LoadBE16(p); +} + +static JXL_INLINE uint32_t Load16(OrderLE /*tag*/, const uint8_t* p) { + return LoadLE16(p); +} + +static JXL_INLINE uint32_t Load24(OrderBE /*tag*/, const uint8_t* p) { + return LoadBE24(p); +} + +static JXL_INLINE uint32_t Load24(OrderLE /*tag*/, const uint8_t* p) { + return LoadLE24(p); +} +static JXL_INLINE uint32_t Load32(OrderBE /*tag*/, const uint8_t* p) { + return LoadBE32(p); +} + +static JXL_INLINE uint32_t Load32(OrderLE /*tag*/, const uint8_t* p) { + return LoadLE32(p); +} + +#endif // LIB_JXL_BASE_BYTE_ORDER_H_ diff --git a/lib/jxl/base/cache_aligned.cc b/lib/jxl/base/cache_aligned.cc new file mode 100644 index 0000000..54936d7 --- /dev/null +++ b/lib/jxl/base/cache_aligned.cc @@ -0,0 +1,154 @@ +// 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/cache_aligned.h" + +#include +#include + +// Disabled: slower than malloc + alignment. +#define JXL_USE_MMAP 0 + +#if JXL_USE_MMAP +#include +#endif + +#include // std::max +#include +#include // kMaxVectorSize +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace { + +#pragma pack(push, 1) +struct AllocationHeader { + void* allocated; + size_t allocated_size; + uint8_t left_padding[hwy::kMaxVectorSize]; +}; +#pragma pack(pop) + +std::atomic num_allocations{0}; +std::atomic bytes_in_use{0}; +std::atomic max_bytes_in_use{0}; + +} // namespace + +// Avoids linker errors in pre-C++17 builds. +constexpr size_t CacheAligned::kPointerSize; +constexpr size_t CacheAligned::kCacheLineSize; +constexpr size_t CacheAligned::kAlignment; +constexpr size_t CacheAligned::kAlias; + +void CacheAligned::PrintStats() { + fprintf(stderr, "Allocations: %zu (max bytes in use: %E)\n", + size_t(num_allocations.load(std::memory_order_relaxed)), + double(max_bytes_in_use.load(std::memory_order_relaxed))); +} + +size_t CacheAligned::NextOffset() { + static std::atomic next{0}; + constexpr uint32_t kGroups = CacheAligned::kAlias / CacheAligned::kAlignment; + const uint32_t group = next.fetch_add(1, std::memory_order_relaxed) % kGroups; + return CacheAligned::kAlignment * group; +} + +void* CacheAligned::Allocate(const size_t payload_size, size_t offset) { + JXL_ASSERT(payload_size <= std::numeric_limits::max() / 2); + JXL_ASSERT((offset % kAlignment == 0) && offset <= kAlias); + + // What: | misalign | unused | AllocationHeader |payload + // Size: |<= kAlias | offset | |payload_size + // ^allocated.^aligned.^header............^payload + // The header must immediately precede payload, which must remain aligned. + // To avoid wasting space, the header resides at the end of `unused`, + // which therefore cannot be empty (offset == 0). + if (offset == 0) { + offset = kAlignment; // = round_up(sizeof(AllocationHeader), kAlignment) + static_assert(sizeof(AllocationHeader) <= kAlignment, "Else: round up"); + } + +#if JXL_USE_MMAP + const size_t allocated_size = offset + payload_size; + const int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE; + void* allocated = + mmap(nullptr, allocated_size, PROT_READ | PROT_WRITE, flags, -1, 0); + if (allocated == MAP_FAILED) return nullptr; + const uintptr_t aligned = reinterpret_cast(allocated); +#else + const size_t allocated_size = kAlias + offset + payload_size; + void* allocated = malloc(allocated_size); + if (allocated == nullptr) return nullptr; + // Always round up even if already aligned - we already asked for kAlias + // extra bytes and there's no way to give them back. + uintptr_t aligned = reinterpret_cast(allocated) + kAlias; + static_assert((kAlias & (kAlias - 1)) == 0, "kAlias must be a power of 2"); + static_assert(kAlias >= kAlignment, "Cannot align to more than kAlias"); + aligned &= ~(kAlias - 1); +#endif + +#if 0 + // No effect. + uintptr_t page_aligned = reinterpret_cast(allocated); + page_aligned &= ~(4096 - 1); + if (madvise(reinterpret_cast(page_aligned), allocated_size, + MADV_WILLNEED) != 0) { + JXL_NOTIFY_ERROR("madvise failed"); + } +#elif 0 + // INCREASES both first and subsequent decode times. + if (mlock(allocated, allocated_size) != 0) { + JXL_NOTIFY_ERROR("mlock failed"); + } +#endif + + // Update statistics (#allocations and max bytes in use) + num_allocations.fetch_add(1, std::memory_order_relaxed); + const uint64_t prev_bytes = + bytes_in_use.fetch_add(allocated_size, std::memory_order_acq_rel); + uint64_t expected_max = max_bytes_in_use.load(std::memory_order_acquire); + for (;;) { + const uint64_t desired = + std::max(expected_max, prev_bytes + allocated_size); + if (max_bytes_in_use.compare_exchange_strong(expected_max, desired, + std::memory_order_acq_rel)) { + break; + } + } + + const uintptr_t payload = aligned + offset; // still aligned + + // Stash `allocated` and payload_size inside header for use by Free(). + AllocationHeader* header = reinterpret_cast(payload) - 1; + header->allocated = allocated; + header->allocated_size = allocated_size; + + return JXL_ASSUME_ALIGNED(reinterpret_cast(payload), 64); +} + +void CacheAligned::Free(const void* aligned_pointer) { + if (aligned_pointer == nullptr) { + return; + } + const uintptr_t payload = reinterpret_cast(aligned_pointer); + JXL_ASSERT(payload % kAlignment == 0); + const AllocationHeader* header = + reinterpret_cast(payload) - 1; + + // Subtract (2's complement negation). + bytes_in_use.fetch_add(~header->allocated_size + 1, + std::memory_order_acq_rel); + +#if JXL_USE_MMAP + munmap(header->allocated, header->allocated_size); +#else + free(header->allocated); +#endif +} + +} // namespace jxl diff --git a/lib/jxl/base/cache_aligned.h b/lib/jxl/base/cache_aligned.h new file mode 100644 index 0000000..e57df14 --- /dev/null +++ b/lib/jxl/base/cache_aligned.h @@ -0,0 +1,74 @@ +// 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_CACHE_ALIGNED_H_ +#define LIB_JXL_BASE_CACHE_ALIGNED_H_ + +// Memory allocator with support for alignment + misalignment. + +#include +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +// Functions that depend on the cache line size. +class CacheAligned { + public: + static void PrintStats(); + + static constexpr size_t kPointerSize = sizeof(void*); + static constexpr size_t kCacheLineSize = 64; + // To avoid RFOs, match L2 fill size (pairs of lines). + static constexpr size_t kAlignment = 2 * kCacheLineSize; + // Minimum multiple for which cache set conflicts and/or loads blocked by + // preceding stores can occur. + static constexpr size_t kAlias = 2048; + + // Returns a 'random' (cyclical) offset suitable for Allocate. + static size_t NextOffset(); + + // Returns null or memory whose address is congruent to `offset` (mod kAlias). + // This reduces cache conflicts and load/store stalls, especially with large + // allocations that would otherwise have similar alignments. At least + // `payload_size` (which can be zero) bytes will be accessible. + static void* Allocate(size_t payload_size, size_t offset); + + static void* Allocate(const size_t payload_size) { + return Allocate(payload_size, NextOffset()); + } + + static void Free(const void* aligned_pointer); +}; + +// Avoids the need for a function pointer (deleter) in CacheAlignedUniquePtr. +struct CacheAlignedDeleter { + void operator()(uint8_t* aligned_pointer) const { + return CacheAligned::Free(aligned_pointer); + } +}; + +using CacheAlignedUniquePtr = std::unique_ptr; + +// Does not invoke constructors. +static inline CacheAlignedUniquePtr AllocateArray(const size_t bytes) { + return CacheAlignedUniquePtr( + static_cast(CacheAligned::Allocate(bytes)), + CacheAlignedDeleter()); +} + +static inline CacheAlignedUniquePtr AllocateArray(const size_t bytes, + const size_t offset) { + return CacheAlignedUniquePtr( + static_cast(CacheAligned::Allocate(bytes, offset)), + CacheAlignedDeleter()); +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_CACHE_ALIGNED_H_ diff --git a/lib/jxl/base/compiler_specific.h b/lib/jxl/base/compiler_specific.h new file mode 100644 index 0000000..b279fa0 --- /dev/null +++ b/lib/jxl/base/compiler_specific.h @@ -0,0 +1,153 @@ +// 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_COMPILER_SPECIFIC_H_ +#define LIB_JXL_BASE_COMPILER_SPECIFIC_H_ + +// Macros for compiler version + nonstandard keywords, e.g. __builtin_expect. + +#include + +// #if is shorter and safer than #ifdef. *_VERSION are zero if not detected, +// otherwise 100 * major + minor version. Note that other packages check for +// #ifdef COMPILER_MSVC, so we cannot use that same name. + +#ifdef _MSC_VER +#define JXL_COMPILER_MSVC _MSC_VER +#else +#define JXL_COMPILER_MSVC 0 +#endif + +#ifdef __GNUC__ +#define JXL_COMPILER_GCC (__GNUC__ * 100 + __GNUC_MINOR__) +#else +#define JXL_COMPILER_GCC 0 +#endif + +#ifdef __clang__ +#define JXL_COMPILER_CLANG (__clang_major__ * 100 + __clang_minor__) +// Clang pretends to be GCC for compatibility. +#undef JXL_COMPILER_GCC +#define JXL_COMPILER_GCC 0 +#else +#define JXL_COMPILER_CLANG 0 +#endif + +#if JXL_COMPILER_MSVC +#define JXL_RESTRICT __restrict +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_RESTRICT __restrict__ +#else +#define JXL_RESTRICT +#endif + +#if JXL_COMPILER_MSVC +#define JXL_INLINE __forceinline +#define JXL_NOINLINE __declspec(noinline) +#else +#define JXL_INLINE inline __attribute__((always_inline)) +#define JXL_NOINLINE __attribute__((noinline)) +#endif + +#if JXL_COMPILER_MSVC +#define JXL_NORETURN __declspec(noreturn) +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_NORETURN __attribute__((noreturn)) +#endif + +#if JXL_COMPILER_MSVC +#define JXL_UNREACHABLE __assume(false) +#elif JXL_COMPILER_CLANG || JXL_COMPILER_GCC >= 405 +#define JXL_UNREACHABLE __builtin_unreachable() +#else +#define JXL_UNREACHABLE +#endif + +#if JXL_COMPILER_MSVC +#define JXL_MAYBE_UNUSED +#else +// Encountered "attribute list cannot appear here" when using the C++17 +// [[maybe_unused]], so only use the old style attribute for now. +#define JXL_MAYBE_UNUSED __attribute__((unused)) +#endif + +#if JXL_COMPILER_MSVC +// Unsupported, __assume is not the same. +#define JXL_LIKELY(expr) expr +#define JXL_UNLIKELY(expr) expr +#else +#define JXL_LIKELY(expr) __builtin_expect(!!(expr), 1) +#define JXL_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#endif + +#if JXL_COMPILER_MSVC +#include + +#pragma intrinsic(_ReadWriteBarrier) +#define JXL_COMPILER_FENCE _ReadWriteBarrier() +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_COMPILER_FENCE asm volatile("" : : : "memory") +#else +#define JXL_COMPILER_FENCE +#endif + +// Returns a void* pointer which the compiler then assumes is N-byte aligned. +// Example: float* JXL_RESTRICT aligned = (float*)JXL_ASSUME_ALIGNED(in, 32); +// +// The assignment semantics are required by GCC/Clang. ICC provides an in-place +// __assume_aligned, whereas MSVC's __assume appears unsuitable. +#if JXL_COMPILER_CLANG +// Early versions of Clang did not support __builtin_assume_aligned. +#define JXL_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) +#elif JXL_COMPILER_GCC +#define JXL_HAS_ASSUME_ALIGNED 1 +#else +#define JXL_HAS_ASSUME_ALIGNED 0 +#endif + +#if JXL_HAS_ASSUME_ALIGNED +#define JXL_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) +#else +#define JXL_ASSUME_ALIGNED(ptr, align) (ptr) /* not supported */ +#endif + +#ifdef __has_attribute +#define JXL_HAVE_ATTRIBUTE(x) __has_attribute(x) +#else +#define JXL_HAVE_ATTRIBUTE(x) 0 +#endif + +// Raises warnings if the function return value is unused. Should appear as the +// first part of a function definition/declaration. +#if JXL_HAVE_ATTRIBUTE(nodiscard) +#define JXL_MUST_USE_RESULT [[nodiscard]] +#elif JXL_COMPILER_CLANG && JXL_HAVE_ATTRIBUTE(warn_unused_result) +#define JXL_MUST_USE_RESULT __attribute__((warn_unused_result)) +#else +#define JXL_MUST_USE_RESULT +#endif + +// Disable certain -fsanitize flags for functions that are expected to include +// things like unsigned integer overflow. For example use in the function +// declaration JXL_NO_SANITIZE("unsigned-integer-overflow") to silence unsigned +// integer overflow ubsan messages. +#if JXL_COMPILER_CLANG && JXL_HAVE_ATTRIBUTE(no_sanitize) +#define JXL_NO_SANITIZE(X) __attribute__((no_sanitize(X))) +#else +#define JXL_NO_SANITIZE(X) +#endif + +#if JXL_HAVE_ATTRIBUTE(__format__) +#define JXL_FORMAT(idx_fmt, idx_arg) \ + __attribute__((__format__(__printf__, idx_fmt, idx_arg))) +#else +#define JXL_FORMAT(idx_fmt, idx_arg) +#endif + +#if JXL_COMPILER_MSVC +using ssize_t = intptr_t; +#endif + +#endif // LIB_JXL_BASE_COMPILER_SPECIFIC_H_ diff --git a/lib/jxl/base/data_parallel.cc b/lib/jxl/base/data_parallel.cc new file mode 100644 index 0000000..20a9112 --- /dev/null +++ b/lib/jxl/base/data_parallel.cc @@ -0,0 +1,23 @@ +// 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/data_parallel.h" + +namespace jxl { + +// static +JxlParallelRetCode ThreadPool::SequentialRunnerStatic( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + JxlParallelRetCode init_ret = (*init)(jpegxl_opaque, 1); + if (init_ret != 0) return init_ret; + + for (uint32_t i = start_range; i < end_range; i++) { + (*func)(jpegxl_opaque, i, 0); + } + return 0; +} + +} // namespace jxl diff --git a/lib/jxl/base/data_parallel.h b/lib/jxl/base/data_parallel.h new file mode 100644 index 0000000..8982974 --- /dev/null +++ b/lib/jxl/base/data_parallel.h @@ -0,0 +1,155 @@ +// 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_DATA_PARALLEL_H_ +#define LIB_JXL_BASE_DATA_PARALLEL_H_ + +// Portable, low-overhead C++11 ThreadPool alternative to OpenMP for +// data-parallel computations. + +#include +#include + +#include "jxl/parallel_runner.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +class ThreadPool { + public: + // Use this type as an InitFunc to skip the initialization step in Run(). + // When this is used the return value of Run() is always true and does not + // need to be checked. + struct SkipInit {}; + + ThreadPool(JxlParallelRunner runner, void* runner_opaque) + : runner_(runner ? runner : &ThreadPool::SequentialRunnerStatic), + runner_opaque_(runner ? runner_opaque : static_cast(this)) {} + + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator&(const ThreadPool&) = delete; + + // Runs init_func(num_threads) followed by data_func(task, thread) on worker + // thread(s) for every task in [begin, end). init_func() must return a Status + // indicating whether the initialization succeeded. + // "thread" is an integer smaller than num_threads. + // Not thread-safe - no two calls to Run may overlap. + // Subsequent calls will reuse the same threads. + // + // Precondition: begin <= end. + template + Status Run(uint32_t begin, uint32_t end, const InitFunc& init_func, + const DataFunc& data_func, const char* caller = "") { + JXL_ASSERT(begin <= end); + if (begin == end) return true; + RunCallState call_state(init_func, data_func); + // The runner_ uses the C convention and returns 0 in case of error, so we + // convert it to an Status. + return (*runner_)(runner_opaque_, static_cast(&call_state), + &call_state.CallInitFunc, &call_state.CallDataFunc, begin, + end) == 0; + } + + // Specialization that returns bool when SkipInit is used. + template + bool Run(uint32_t begin, uint32_t end, const SkipInit /* tag */, + const DataFunc& data_func, const char* caller = "") { + return Run(begin, end, ReturnTrueInit, data_func, caller); + } + + private: + static Status ReturnTrueInit(size_t num_threads) { return true; } + + // class holding the state of a Run() call to pass to the runner_ as an + // opaque_jpegxl pointer. + template + class RunCallState final { + public: + RunCallState(const InitFunc& init_func, const DataFunc& data_func) + : init_func_(init_func), data_func_(data_func) {} + + // JxlParallelRunInit interface. + static int CallInitFunc(void* jpegxl_opaque, size_t num_threads) { + const auto* self = + static_cast*>(jpegxl_opaque); + // Returns -1 when the internal init function returns false Status to + // indicate an error. + return self->init_func_(num_threads) ? 0 : -1; + } + + // JxlParallelRunFunction interface. + static void CallDataFunc(void* jpegxl_opaque, uint32_t value, + size_t thread_id) { + const auto* self = + static_cast*>(jpegxl_opaque); + return self->data_func_(value, thread_id); + } + + private: + const InitFunc& init_func_; + const DataFunc& data_func_; + }; + + // Default JxlParallelRunner used when no runner is provided by the + // caller. This runner doesn't use any threading and thread_id is always 0. + static JxlParallelRetCode SequentialRunnerStatic( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + + // The caller supplied runner function and its opaque void*. + const JxlParallelRunner runner_; + void* const runner_opaque_; +}; + +// TODO(deymo): Convert the return value to a Status when not using SkipInit. +template +bool RunOnPool(ThreadPool* pool, const uint32_t begin, const uint32_t end, + const InitFunc& init_func, const DataFunc& data_func, + const char* caller) { + Status ret = true; + if (pool == nullptr) { + ThreadPool default_pool(nullptr, nullptr); + ret = default_pool.Run(begin, end, init_func, data_func, caller); + } else { + ret = pool->Run(begin, end, init_func, data_func, caller); + } + return ret; +} + +// Accelerates multiple unsigned 32-bit divisions with the same divisor by +// precomputing a multiplier. This is useful for splitting a contiguous range of +// indices (the task index) into 2D indices. Exhaustively tested on dividends +// up to 4M with non-power of two divisors up to 2K. +class Divider { + public: + // "d" is the divisor (what to divide by). + explicit Divider(const uint32_t d) : shift_(FloorLog2Nonzero(d)) { + // Power of two divisors (including 1) are not supported because it is more + // efficient to special-case them at a higher level. + JXL_ASSERT((d & (d - 1)) != 0); + + // ceil_log2 = floor_log2 + 1 because we ruled out powers of two above. + const uint64_t next_pow2 = 1ULL << (shift_ + 1); + + mul_ = ((next_pow2 - d) << 32) / d + 1; + } + + // "n" is the numerator (what is being divided). + inline uint32_t operator()(const uint32_t n) const { + // Algorithm from "Division by Invariant Integers using Multiplication". + // Its "sh1" is hardcoded to 1 because we don't need to handle d=1. + const uint32_t hi = (uint64_t(mul_) * n) >> 32; + return (hi + ((n - hi) >> 1)) >> shift_; + } + + private: + uint32_t mul_; + const int shift_; +}; + +} // namespace jxl + +#endif // LIB_JXL_BASE_DATA_PARALLEL_H_ diff --git a/lib/jxl/base/descriptive_statistics.cc b/lib/jxl/base/descriptive_statistics.cc new file mode 100644 index 0000000..9303f2c --- /dev/null +++ b/lib/jxl/base/descriptive_statistics.cc @@ -0,0 +1,102 @@ +// 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/descriptive_statistics.h" + +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { + +void Stats::Assimilate(const Stats& other) { + const int64_t total_n = n_ + other.n_; + if (total_n == 0) return; // Nothing to do; prevents div by zero. + + min_ = std::min(min_, other.min_); + max_ = std::max(max_, other.max_); + + product_ *= other.product_; + + const double product_n = n_ * other.n_; + const double n2 = n_ * n_; + const double other_n2 = other.n_ * other.n_; + // Warning: multiplying int64 can overflow here. + const double total_n2 = static_cast(total_n) * total_n; + const double total_n3 = static_cast(total_n2) * total_n; + // Precompute reciprocal for speed - used at least twice. + const double inv_total_n = 1.0 / total_n; + const double inv_total_n2 = 1.0 / total_n2; + + const double delta = other.m1_ - m1_; + const double delta2 = delta * delta; + const double delta3 = delta * delta2; + const double delta4 = delta2 * delta2; + + m1_ = (n_ * m1_ + other.n_ * other.m1_) * inv_total_n; + + const double new_m2 = m2_ + other.m2_ + delta2 * product_n * inv_total_n; + + const double new_m3 = + m3_ + other.m3_ + delta3 * product_n * (n_ - other.n_) * inv_total_n2 + + 3.0 * delta * (n_ * other.m2_ - other.n_ * m2_) * inv_total_n; + + m4_ += other.m4_ + + delta4 * product_n * (n2 - product_n + other_n2) / total_n3 + + 6.0 * delta2 * (n2 * other.m2_ + other_n2 * m2_) * inv_total_n2 + + 4.0 * delta * (n_ * other.m3_ - other.n_ * m3_) * inv_total_n; + + m2_ = new_m2; + m3_ = new_m3; + n_ = total_n; +} + +std::string Stats::ToString(int exclude) const { + if (Count() == 0) return std::string("(none)"); + + char buf[300]; + size_t pos = 0; + int ret; // snprintf - bytes written or negative for error. + + if ((exclude & kNoCount) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Count=%6zu ", + static_cast(Count())); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoMeanSD) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Mean=%9.6f SD=%8.5f ", Mean(), + StandardDeviation()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoMinMax) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Min=%8.5f Max=%8.5f ", Min(), + Max()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoSkewKurt) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Skew=%5.2f Kurt=%7.2f ", + Skewness(), Kurtosis()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoGeomean) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "GeoMean=%9.6f ", + GeometricMean()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + JXL_ASSERT(pos < sizeof(buf)); + return buf; +} + +} // namespace jxl diff --git a/lib/jxl/base/descriptive_statistics.h b/lib/jxl/base/descriptive_statistics.h new file mode 100644 index 0000000..0d1e485 --- /dev/null +++ b/lib/jxl/base/descriptive_statistics.h @@ -0,0 +1,126 @@ +// 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_DESCRIPTIVE_STATISTICS_H_ +#define LIB_JXL_BASE_DESCRIPTIVE_STATISTICS_H_ + +// For analyzing the range/distribution of scalars. + +#include + +#include +#include +#include + +namespace jxl { + +// Descriptive statistics of a variable (4 moments). +class Stats { + public: + void Notify(const float x) { + ++n_; + + min_ = std::min(min_, x); + max_ = std::max(max_, x); + + product_ *= x; + + // Online moments. Reference: https://goo.gl/9ha694 + const double d = x - m1_; + const double d_div_n = d / n_; + const double d2n1_div_n = d * (n_ - 1) * d_div_n; + const int64_t n_poly = n_ * n_ - 3 * n_ + 3; + m1_ += d_div_n; + m4_ += d_div_n * (d_div_n * (d2n1_div_n * n_poly + 6.0 * m2_) - 4.0 * m3_); + m3_ += d_div_n * (d2n1_div_n * (n_ - 2) - 3.0 * m2_); + m2_ += d2n1_div_n; + } + + void Assimilate(const Stats& other); + + int64_t Count() const { return n_; } + + float Min() const { return min_; } + float Max() const { return max_; } + + double GeometricMean() const { + return n_ == 0 ? 0.0 : pow(product_, 1.0 / n_); + } + + double Mean() const { return m1_; } + // Same as Mu2. Assumes n_ is large. + double SampleVariance() const { + return n_ == 0 ? 0.0 : m2_ / static_cast(n_); + } + // Unbiased estimator for population variance even for smaller n_. + double Variance() const { + if (n_ == 0) return 0.0; + if (n_ == 1) return m2_; + return m2_ / static_cast(n_ - 1); + } + double StandardDeviation() const { return std::sqrt(Variance()); } + // Near zero for normal distributions; if positive on a unimodal distribution, + // the right tail is fatter. Assumes n_ is large. + double SampleSkewness() const { + if (std::abs(m2_) < 1E-7) return 0.0; + return m3_ * std::sqrt(static_cast(n_)) / std::pow(m2_, 1.5); + } + // Corrected for bias (same as Wikipedia and Minitab but not Excel). + double Skewness() const { + if (n_ == 0) return 0.0; + const double biased = SampleSkewness(); + const double r = (n_ - 1.0) / n_; + return biased * std::pow(r, 1.5); + } + // Near zero for normal distributions; smaller values indicate fewer/smaller + // outliers and larger indicates more/larger outliers. Assumes n_ is large. + double SampleKurtosis() const { + if (std::abs(m2_) < 1E-7) return 0.0; + return m4_ * n_ / (m2_ * m2_); + } + // Corrected for bias (same as Wikipedia and Minitab but not Excel). + double Kurtosis() const { + if (n_ == 0) return 0.0; + const double biased = SampleKurtosis(); + const double r = (n_ - 1.0) / n_; + return biased * r * r; + } + + // Central moments, useful for "method of moments"-based parameter estimation + // of a mixture of two Gaussians. Assumes Count() != 0. + double Mu1() const { return m1_; } + double Mu2() const { return m2_ / static_cast(n_); } + double Mu3() const { return m3_ / static_cast(n_); } + double Mu4() const { return m4_ / static_cast(n_); } + + // Which statistics to EXCLUDE in ToString + enum { + kNoCount = 1, + kNoMeanSD = 2, + kNoMinMax = 4, + kNoSkewKurt = 8, + kNoGeomean = 16 + }; + + std::string ToString(int exclude = 0) const; + + private: + int64_t n_ = 0; // signed for faster conversion + safe subtraction + + float min_ = 1E30f; + float max_ = -1E30f; + + double product_ = 1.0; + + // Moments + double m1_ = 0.0; + double m2_ = 0.0; + double m3_ = 0.0; + double m4_ = 0.0; +}; + +} // namespace jxl + +#endif // LIB_JXL_BASE_DESCRIPTIVE_STATISTICS_H_ diff --git a/lib/jxl/base/file_io.h b/lib/jxl/base/file_io.h new file mode 100644 index 0000000..8a57d4a --- /dev/null +++ b/lib/jxl/base/file_io.h @@ -0,0 +1,124 @@ +// 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_FILE_IO_H_ +#define LIB_JXL_BASE_FILE_IO_H_ + +// Helper functions for reading/writing files. + +#include +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Returns extension including the dot, or empty string if none. Assumes +// filename is not a hidden file (e.g. ".bashrc"). May be called with a pathname +// if the filename contains a dot and/or no other path component does. +static inline std::string Extension(const std::string& filename) { + const size_t pos = filename.rfind('.'); + if (pos == std::string::npos) return std::string(); + return filename.substr(pos); +} + +// RAII, ensures files are closed even when returning early. +class FileWrapper { + public: + FileWrapper(const FileWrapper& other) = delete; + FileWrapper& operator=(const FileWrapper& other) = delete; + + explicit FileWrapper(const std::string& pathname, const char* mode) + : file_(pathname == "-" ? (mode[0] == 'r' ? stdin : stdout) + : fopen(pathname.c_str(), mode)) {} + + ~FileWrapper() { + if (file_ != nullptr) { + const int err = fclose(file_); + JXL_CHECK(err == 0); + } + } + + // We intend to use FileWrapper as a replacement of FILE. + // NOLINTNEXTLINE(google-explicit-constructor) + operator FILE*() const { return file_; } + + private: + FILE* const file_; +}; + +template +static inline Status ReadFile(const std::string& pathname, + ContainerType* JXL_RESTRICT bytes) { + // Special case for stdin + if (pathname == "-") { + int byte; + bytes->clear(); + while (true) { + byte = getchar(); + if (byte == EOF) break; + bytes->push_back(byte); + } + return true; + } + FileWrapper f(pathname, "rb"); + if (f == nullptr) return JXL_FAILURE("Failed to open file for reading"); + + // Ensure it is a regular file +#ifdef _WIN32 + struct __stat64 s = {}; + const int err = _stat64(pathname.c_str(), &s); + const bool is_file = (s.st_mode & S_IFREG) != 0; +#else + struct stat s = {}; + const int err = stat(pathname.c_str(), &s); + const bool is_file = S_ISREG(s.st_mode); +#endif + if (err != 0) return JXL_FAILURE("Failed to obtain file status"); + if (!is_file) return JXL_FAILURE("Not a file"); + + // Get size of file in bytes + const int64_t size = s.st_size; + if (size <= 0) return JXL_FAILURE("Empty or invalid file size"); + bytes->resize(static_cast(size)); + + size_t pos = 0; + while (pos < bytes->size()) { + // Needed in case ContainerType is std::string, whose data() is const. + char* bytes_writable = reinterpret_cast(&(*bytes)[0]); + const size_t bytes_read = + fread(bytes_writable + pos, 1, bytes->size() - pos, f); + if (bytes_read == 0) return JXL_FAILURE("Failed to read"); + pos += bytes_read; + } + JXL_ASSERT(pos == bytes->size()); + return true; +} + +template +static inline Status WriteFile(const ContainerType& bytes, + const std::string& pathname) { + FileWrapper f(pathname, "wb"); + if (f == nullptr) return JXL_FAILURE("Failed to open file for writing"); + + size_t pos = 0; + while (pos < bytes.size()) { + const size_t bytes_written = + fwrite(bytes.data() + pos, 1, bytes.size() - pos, f); + if (bytes_written == 0) return JXL_FAILURE("Failed to write"); + pos += bytes_written; + } + JXL_ASSERT(pos == bytes.size()); + + return true; +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_FILE_IO_H_ diff --git a/lib/jxl/base/iaca.h b/lib/jxl/base/iaca.h new file mode 100644 index 0000000..e5732da --- /dev/null +++ b/lib/jxl/base/iaca.h @@ -0,0 +1,65 @@ +// 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_IACA_H_ +#define LIB_JXL_BASE_IACA_H_ + +#include "lib/jxl/base/compiler_specific.h" + +// IACA (Intel's Code Analyzer) analyzes instruction latencies, but only for +// code between special markers. These functions embed such markers in an +// executable, but only for reading via IACA - they deliberately trigger a +// crash if executed to ensure they are removed in normal builds. + +#ifndef JXL_IACA_ENABLED +#define JXL_IACA_ENABLED 0 +#endif + +namespace jxl { + +// Call before the region of interest. +static JXL_INLINE void BeginIACA() { +#if JXL_IACA_ENABLED && (JXL_COMPILER_GCC || JXL_COMPILER_CLANG) + asm volatile( + // UD2 "instruction" raises an invalid opcode exception. + ".byte 0x0F, 0x0B\n\t" + // Magic sequence recognized by IACA (MOV + addr32 fs:NOP). This actually + // clobbers EBX, but we don't care because the code won't be run, and we + // want IACA to observe the same code the compiler would have generated + // without this marker. + "movl $111, %%ebx\n\t" + ".byte 0x64, 0x67, 0x90\n\t" + : + : + // (Allegedly) clobbering memory may prevent reordering. + : "memory"); +#endif +} + +// Call after the region of interest. +static JXL_INLINE void EndIACA() { +#if JXL_IACA_ENABLED && (JXL_COMPILER_GCC || JXL_COMPILER_CLANG) + asm volatile( + // See above. + "movl $222, %%ebx\n\t" + ".byte 0x64, 0x67, 0x90\n\t" + // UD2 + ".byte 0x0F, 0x0B\n\t" + : + : + // (Allegedly) clobbering memory may prevent reordering. + : "memory"); +#endif +} + +// Add to a scope to mark a region. +struct ScopeIACA { + JXL_INLINE ScopeIACA() { BeginIACA(); } + JXL_INLINE ~ScopeIACA() { EndIACA(); } +}; + +} // namespace jxl + +#endif // LIB_JXL_BASE_IACA_H_ diff --git a/lib/jxl/base/os_macros.h b/lib/jxl/base/os_macros.h new file mode 100644 index 0000000..b230f26 --- /dev/null +++ b/lib/jxl/base/os_macros.h @@ -0,0 +1,50 @@ +// 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_OS_MACROS_H_ +#define LIB_JXL_BASE_OS_MACROS_H_ + +// Defines the JXL_OS_* macros. + +#if defined(_WIN32) || defined(_WIN64) +#define JXL_OS_WIN 1 +#else +#define JXL_OS_WIN 0 +#endif + +#ifdef __linux__ +#define JXL_OS_LINUX 1 +#else +#define JXL_OS_LINUX 0 +#endif + +#ifdef __MACH__ +#define JXL_OS_MAC 1 +#else +#define JXL_OS_MAC 0 +#endif + +#define JXL_OS_IOS 0 +#ifdef __APPLE__ +#include +#if TARGET_OS_IPHONE +#undef JXL_OS_IOS +#define JXL_OS_IOS 1 +#endif +#endif + +#ifdef __FreeBSD__ +#define JXL_OS_FREEBSD 1 +#else +#define JXL_OS_FREEBSD 0 +#endif + +#ifdef __HAIKU__ +#define JXL_OS_HAIKU 1 +#else +#define JXL_OS_HAIKU 0 +#endif + +#endif // LIB_JXL_BASE_OS_MACROS_H_ diff --git a/lib/jxl/base/override.h b/lib/jxl/base/override.h new file mode 100644 index 0000000..1f8b657 --- /dev/null +++ b/lib/jxl/base/override.h @@ -0,0 +1,29 @@ +// 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_OVERRIDE_H_ +#define LIB_JXL_BASE_OVERRIDE_H_ + +// 'Trool' for command line arguments: force enable/disable, or use default. + +namespace jxl { + +// No effect if kDefault, otherwise forces a feature (typically a FrameHeader +// flag) on or off. +enum class Override : int { kOn = 1, kOff = 0, kDefault = -1 }; + +static inline Override OverrideFromBool(bool flag) { + return flag ? Override::kOn : Override::kOff; +} + +static inline bool ApplyOverride(Override o, bool default_condition) { + if (o == Override::kOn) return true; + if (o == Override::kOff) return false; + return default_condition; +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_OVERRIDE_H_ diff --git a/lib/jxl/base/padded_bytes.cc b/lib/jxl/base/padded_bytes.cc new file mode 100644 index 0000000..11e4bff --- /dev/null +++ b/lib/jxl/base/padded_bytes.cc @@ -0,0 +1,63 @@ +// 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/padded_bytes.h" + +namespace jxl { + +void PaddedBytes::IncreaseCapacityTo(size_t capacity) { + JXL_ASSERT(capacity > capacity_); + + size_t new_capacity = std::max(capacity, 3 * capacity_ / 2); + new_capacity = std::max(64, new_capacity); + + // BitWriter writes up to 7 bytes past the end. + CacheAlignedUniquePtr new_data = AllocateArray(new_capacity + 8); + if (new_data == nullptr) { + // Allocation failed, discard all data to ensure this is noticed. + size_ = capacity_ = 0; + return; + } + + if (data_ == nullptr) { + // First allocation: ensure first byte is initialized (won't be copied). + new_data[0] = 0; + } else { + // Subsequent resize: copy existing data to new location. + memcpy(new_data.get(), data_.get(), size_); + // Ensure that the first new byte is initialized, to allow write_bits to + // safely append to the newly-resized PaddedBytes. + new_data[size_] = 0; + } + + capacity_ = new_capacity; + std::swap(new_data, data_); +} + +void PaddedBytes::assign(const uint8_t* new_begin, const uint8_t* new_end) { + JXL_DASSERT(new_begin <= new_end); + const size_t new_size = static_cast(new_end - new_begin); + + // memcpy requires non-overlapping ranges, and resizing might invalidate the + // new range. Neither happens if the new range is completely to the left or + // right of the _allocated_ range (irrespective of size_). + const uint8_t* allocated_end = begin() + capacity_; + const bool outside = new_end <= begin() || new_begin >= allocated_end; + if (outside) { + resize(new_size); // grow or shrink + memcpy(data(), new_begin, new_size); + return; + } + + // There is overlap. The new size cannot be larger because we own the memory + // and the new range cannot include anything outside the allocated range. + JXL_ASSERT(new_size <= capacity_); + + // memmove allows overlap and capacity_ is sufficient. + memmove(data(), new_begin, new_size); + size_ = new_size; // shrink +} + +} // namespace jxl diff --git a/lib/jxl/base/padded_bytes.h b/lib/jxl/base/padded_bytes.h new file mode 100644 index 0000000..1840a6c --- /dev/null +++ b/lib/jxl/base/padded_bytes.h @@ -0,0 +1,195 @@ +// 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_PADDED_BYTES_H_ +#define LIB_JXL_BASE_PADDED_BYTES_H_ + +// std::vector replacement with padding to reduce bounds checks in WriteBits + +#include +#include +#include // memcpy + +#include // max +#include +#include // swap + +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Provides a subset of the std::vector interface with some differences: +// - allows BitWriter to write 64 bits at a time without bounds checking; +// - ONLY zero-initializes the first byte (required by BitWriter); +// - ensures cache-line alignment. +class PaddedBytes { + public: + // Required for output params. + PaddedBytes() : size_(0), capacity_(0) {} + + explicit PaddedBytes(size_t size) : size_(size), capacity_(0) { + if (size != 0) IncreaseCapacityTo(size); + } + + PaddedBytes(size_t size, uint8_t value) : size_(size), capacity_(0) { + if (size != 0) { + IncreaseCapacityTo(size); + } + if (size_ != 0) { + memset(data(), value, size); + } + } + + PaddedBytes(const PaddedBytes& other) : size_(other.size_), capacity_(0) { + if (size_ != 0) IncreaseCapacityTo(size_); + if (data() != nullptr) memcpy(data(), other.data(), size_); + } + PaddedBytes& operator=(const PaddedBytes& other) { + // Self-assignment is safe. + resize(other.size()); + if (data() != nullptr) memmove(data(), other.data(), size_); + return *this; + } + + // default is not OK - need to set other.size_ to 0! + PaddedBytes(PaddedBytes&& other) noexcept + : size_(other.size_), + capacity_(other.capacity_), + data_(std::move(other.data_)) { + other.size_ = other.capacity_ = 0; + } + PaddedBytes& operator=(PaddedBytes&& other) noexcept { + size_ = other.size_; + capacity_ = other.capacity_; + data_ = std::move(other.data_); + + if (&other != this) { + other.size_ = other.capacity_ = 0; + } + return *this; + } + + void swap(PaddedBytes& other) { + std::swap(size_, other.size_); + std::swap(capacity_, other.capacity_); + std::swap(data_, other.data_); + } + + void reserve(size_t capacity) { + if (capacity > capacity_) IncreaseCapacityTo(capacity); + } + + // NOTE: unlike vector, this does not initialize the new data! + // However, we guarantee that write_bits can safely append after + // the resize, as we zero-initialize the first new byte of data. + // If size < capacity(), does not invalidate the memory. + void resize(size_t size) { + if (size > capacity_) IncreaseCapacityTo(size); + size_ = (data() == nullptr) ? 0 : size; + } + + // resize(size) plus explicit initialization of the new data with `value`. + void resize(size_t size, uint8_t value) { + size_t old_size = size_; + resize(size); + if (size_ > old_size) { + memset(data() + old_size, value, size_ - old_size); + } + } + + // Amortized constant complexity due to exponential growth. + void push_back(uint8_t x) { + if (size_ == capacity_) { + IncreaseCapacityTo(capacity_ + 1); + if (data() == nullptr) return; + } + + data_[size_++] = x; + } + + size_t size() const { return size_; } + size_t capacity() const { return capacity_; } + + uint8_t* data() { return data_.get(); } + const uint8_t* data() const { return data_.get(); } + + // std::vector operations implemented in terms of the public interface above. + + void clear() { resize(0); } + bool empty() const { return size() == 0; } + + void assign(std::initializer_list il) { + resize(il.size()); + memcpy(data(), il.begin(), il.size()); + } + + // Replaces data() with [new_begin, new_end); potentially reallocates. + void assign(const uint8_t* new_begin, const uint8_t* new_end); + + uint8_t* begin() { return data(); } + const uint8_t* begin() const { return data(); } + uint8_t* end() { return begin() + size(); } + const uint8_t* end() const { return begin() + size(); } + + uint8_t& operator[](const size_t i) { + BoundsCheck(i); + return data()[i]; + } + const uint8_t& operator[](const size_t i) const { + BoundsCheck(i); + return data()[i]; + } + + uint8_t& back() { + JXL_ASSERT(size() != 0); + return data()[size() - 1]; + } + const uint8_t& back() const { + JXL_ASSERT(size() != 0); + return data()[size() - 1]; + } + + template + void append(const T& other) { + append(reinterpret_cast(other.data()), + reinterpret_cast(other.data()) + other.size()); + } + + void append(const uint8_t* begin, const uint8_t* end) { + size_t old_size = size(); + resize(size() + (end - begin)); + memcpy(data() + old_size, begin, end - begin); + } + + private: + void BoundsCheck(size_t i) const { + // <= is safe due to padding and required by BitWriter. + JXL_ASSERT(i <= size()); + } + + // Copies existing data to newly allocated "data_". If allocation fails, + // data() == nullptr and size_ = capacity_ = 0. + // The new capacity will be at least 1.5 times the old capacity. This ensures + // that we avoid quadratic behaviour. + void IncreaseCapacityTo(size_t capacity); + + size_t size_; + size_t capacity_; + CacheAlignedUniquePtr data_; +}; + +template +static inline void Append(const T& s, PaddedBytes* out, + size_t* JXL_RESTRICT byte_pos) { + memcpy(out->data() + *byte_pos, s.data(), s.size()); + *byte_pos += s.size(); + JXL_CHECK(*byte_pos <= out->size()); +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_PADDED_BYTES_H_ diff --git a/lib/jxl/base/profiler.h b/lib/jxl/base/profiler.h new file mode 100644 index 0000000..13f95d2 --- /dev/null +++ b/lib/jxl/base/profiler.h @@ -0,0 +1,32 @@ +// 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_PROFILER_H_ +#define LIB_JXL_BASE_PROFILER_H_ + +// High precision, low overhead time measurements. Returns exact call counts and +// total elapsed time for user-defined 'zones' (code regions, i.e. C++ scopes). +// +// To use the profiler you must set the JPEGXL_ENABLE_PROFILER CMake flag, which +// defines PROFILER_ENABLED and links against the libjxl_profiler library. + +// If zero, this file has no effect and no measurements will be recorded. +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif // PROFILER_ENABLED + +#if PROFILER_ENABLED + +#include "lib/profiler/profiler.h" + +#else // !PROFILER_ENABLED + +#define PROFILER_ZONE(name) +#define PROFILER_FUNC +#define PROFILER_PRINT_RESULTS() + +#endif // PROFILER_ENABLED + +#endif // LIB_JXL_BASE_PROFILER_H_ diff --git a/lib/jxl/base/robust_statistics.h b/lib/jxl/base/robust_statistics.h new file mode 100644 index 0000000..4e6445b --- /dev/null +++ b/lib/jxl/base/robust_statistics.h @@ -0,0 +1,357 @@ +// 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_ROBUST_STATISTICS_H_ +#define LIB_JXL_BASE_ROBUST_STATISTICS_H_ + +// Robust statistics: Mode, Median, MedianAbsoluteDeviation. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +namespace jxl { + +template +T Geomean(const T* items, size_t count) { + double product = 1.0; + for (size_t i = 0; i < count; ++i) { + product *= items[i]; + } + return static_cast(std::pow(product, 1.0 / count)); +} + +// Round up for integers +template ::is_integer>::type* = nullptr> +inline T Half(T x) { + return (x + 1) / 2; +} + +// Mul is faster than div. +template ::is_integer>::type* = nullptr> +inline T Half(T x) { + return x * T(0.5); +} + +// Returns the median value. Side effect: values <= median will appear before, +// values >= median after the middle index. +// Guarantees average speed O(num_values). +template +T Median(T* samples, const size_t num_samples) { + HWY_ASSERT(num_samples != 0); + std::nth_element(samples, samples + num_samples / 2, samples + num_samples); + T result = samples[num_samples / 2]; + // If even size, find largest element in the partially sorted vector to + // use as second element to average with + if ((num_samples & 1) == 0) { + T biggest = *std::max_element(samples, samples + num_samples / 2); + result = Half(result + biggest); + } + return result; +} + +template +T Median(std::vector* samples) { + return Median(samples->data(), samples->size()); +} + +template +static inline T Median3(const T a, const T b, const T c) { + return std::max(std::min(a, b), std::min(c, std::max(a, b))); +} + +template +static inline T Median5(const T a, const T b, const T c, const T d, const T e) { + return Median3(e, std::max(std::min(a, b), std::min(c, d)), + std::min(std::max(a, b), std::max(c, d))); +} + +// Returns a robust measure of variability. +template +T MedianAbsoluteDeviation(const T* samples, const size_t num_samples, + const T median) { + HWY_ASSERT(num_samples != 0); + std::vector abs_deviations; + abs_deviations.reserve(num_samples); + for (size_t i = 0; i < num_samples; ++i) { + abs_deviations.push_back(std::abs(samples[i] - median)); + } + return Median(&abs_deviations); +} + +template +T MedianAbsoluteDeviation(const std::vector& samples, const T median) { + return MedianAbsoluteDeviation(samples.data(), samples.size(), median); +} + +// Half{Range/Sample}Mode are implementations of "Robust estimators of the mode +// and skewness of continuous data". The mode is less affected by outliers in +// highly-skewed distributions than the median. + +// Robust estimator of the mode for data given as sorted values. +// O(N*logN), N=num_values. +class HalfSampleMode { + public: + // Returns mode. "sorted" must be in ascending order. + template + T operator()(const T* const HWY_RESTRICT sorted, + const size_t num_values) const { + int64_t center = num_values / 2; + int64_t width = num_values; + + // Zoom in on modal intervals of decreasing width. Stop before we reach + // width=1, i.e. single values, for which there is no "slope". + while (width > 2) { + // Round up so we can still reach the outer edges of odd widths. + width = Half(width); + + center = CenterOfIntervalWithMinSlope(sorted, num_values, center, width); + } + + return sorted[center]; // mode := middle value in modal interval. + } + + private: + // Returns center of the densest region [c-radius, c+radius]. + template + static HWY_INLINE int64_t CenterOfIntervalWithMinSlope( + const T* HWY_RESTRICT sorted, const int64_t total_values, + const int64_t center, const int64_t width) { + const int64_t radius = Half(width); + + auto compute_slope = [radius, total_values, sorted]( + int64_t c, int64_t* actual_center = nullptr) { + // For symmetry, check 2*radius+1 values, i.e. [min, max]. + const int64_t min = std::max(c - radius, int64_t(0)); + const int64_t max = std::min(c + radius, total_values - 1); + HWY_ASSERT(min < max); + HWY_ASSERT(sorted[min] <= + sorted[max] + std::numeric_limits::epsilon()); + const float dx = max - min + 1; + const float slope = (sorted[max] - sorted[min]) / dx; + + if (actual_center != nullptr) { + // c may be out of bounds, so return center of the clamped bounds. + *actual_center = Half(min + max); + } + return slope; + }; + + // First find min_slope for all centers. + float min_slope = std::numeric_limits::max(); + for (int64_t c = center - radius; c <= center + radius; ++c) { + min_slope = std::min(min_slope, compute_slope(c)); + } + + // Candidates := centers with slope ~= min_slope. + std::vector candidates; + for (int64_t c = center - radius; c <= center + radius; ++c) { + int64_t actual_center; + const float slope = compute_slope(c, &actual_center); + if (slope <= min_slope * 1.001f) { + candidates.push_back(actual_center); + } + } + + // Keep the median. + HWY_ASSERT(!candidates.empty()); + if (candidates.size() == 1) return candidates[0]; + return Median(&candidates); + } +}; + +// Robust estimator of the mode for data given as a CDF. +// O(N*logN), N=num_bins. +class HalfRangeMode { + public: + // Returns mode expressed as a histogram bin index. "cdf" must be weakly + // monotonically increasing, e.g. from std::partial_sum. + int operator()(const uint32_t* HWY_RESTRICT cdf, + const size_t num_bins) const { + int center = num_bins / 2; + int width = num_bins; + + // Zoom in on modal intervals of decreasing width. Stop before we reach + // width=1, i.e. original bins, because those are noisy. + while (width > 2) { + // Round up so we can still reach the outer edges of odd widths. + width = Half(width); + + center = CenterOfIntervalWithMaxDensity(cdf, num_bins, center, width); + } + + return center; // mode := midpoint of modal interval. + } + + private: + // Returns center of the densest interval [c-radius, c+radius]. + static HWY_INLINE int CenterOfIntervalWithMaxDensity( + const uint32_t* HWY_RESTRICT cdf, const int total_bins, const int center, + const int width) { + const int radius = Half(width); + + auto compute_density = [radius, total_bins, cdf]( + int c, int* actual_center = nullptr) { + // For symmetry, check 2*radius+1 bins, i.e. [min, max]. + const int min = std::max(c - radius, 1); // for -1 below + const int max = std::min(c + radius, total_bins - 1); + HWY_ASSERT(min < max); + HWY_ASSERT(cdf[min] <= cdf[max - 1]); + const int num_bins = max - min + 1; + // Sum over [min, max] == CDF(max) - CDF(min-1). + const float density = float(cdf[max] - cdf[min - 1]) / num_bins; + + if (actual_center != nullptr) { + // c may be out of bounds, so take center of the clamped bounds. + *actual_center = Half(min + max); + } + return density; + }; + + // First find max_density for all centers. + float max_density = 0.0f; + for (int c = center - radius; c <= center + radius; ++c) { + max_density = std::max(max_density, compute_density(c)); + } + + // Candidates := centers with density ~= max_density. + std::vector candidates; + for (int c = center - radius; c <= center + radius; ++c) { + int actual_center; + const float density = compute_density(c, &actual_center); + if (density >= max_density * 0.999f) { + candidates.push_back(actual_center); + } + } + + // Keep the median. + HWY_ASSERT(!candidates.empty()); + if (candidates.size() == 1) return candidates[0]; + return Median(&candidates); + } +}; + +// Sorts integral values in ascending order. About 3x faster than std::sort for +// input distributions with very few unique values. +template +void CountingSort(T* begin, T* end) { + // Unique values and their frequency (similar to flat_map). + using Unique = std::pair; + std::vector unique; + for (const T* p = begin; p != end; ++p) { + const T value = *p; + const auto pos = + std::find_if(unique.begin(), unique.end(), + [value](const Unique& u) { return u.first == value; }); + if (pos == unique.end()) { + unique.push_back(std::make_pair(*p, 1)); + } else { + ++pos->second; + } + } + + // Sort in ascending order of value (pair.first). + std::sort(unique.begin(), unique.end()); + + // Write that many copies of each unique value to the array. + T* HWY_RESTRICT p = begin; + for (const auto& value_count : unique) { + std::fill(p, p + value_count.second, value_count.first); + p += value_count.second; + } + HWY_ASSERT(p == end); +} + +struct Bivariate { + Bivariate(float x, float y) : x(x), y(y) {} + float x; + float y; +}; + +class Line { + public: + constexpr Line(const float slope, const float intercept) + : slope_(slope), intercept_(intercept) {} + + constexpr float slope() const { return slope_; } + constexpr float intercept() const { return intercept_; } + + // Robust line fit using Siegel's repeated-median algorithm. + explicit Line(const std::vector& points) { + const size_t N = points.size(); + // This straightforward N^2 implementation is OK for small N. + HWY_ASSERT(N < 10 * 1000); + + // One for every point i. + std::vector medians; + medians.reserve(N); + + // One for every j != i. Never cleared to avoid reallocation. + std::vector slopes(N - 1); + + for (size_t i = 0; i < N; ++i) { + // Index within slopes[] (avoids the hole where j == i). + size_t idx_slope = 0; + + for (size_t j = 0; j < N; ++j) { + if (j == i) continue; + + const float dy = points[j].y - points[i].y; + const float dx = points[j].x - points[i].x; + HWY_ASSERT(std::abs(dx) > 1E-7f); // x must be distinct + slopes[idx_slope++] = dy / dx; + } + HWY_ASSERT(idx_slope == N - 1); + + const float median = Median(&slopes); + medians.push_back(median); + } + + slope_ = Median(&medians); + + // Solve for intercept, overwriting medians[]. + for (size_t i = 0; i < N; ++i) { + medians[i] = points[i].y - slope_ * points[i].x; + } + intercept_ = Median(&medians); + } + + constexpr float operator()(float x) const { return x * slope_ + intercept_; } + + private: + float slope_; + float intercept_; +}; + +static inline void EvaluateQuality(const Line& line, + const std::vector& points, + float* HWY_RESTRICT max_l1, + float* HWY_RESTRICT median_abs_deviation) { + // For computing median_abs_deviation. + std::vector abs_deviations; + abs_deviations.reserve(points.size()); + + *max_l1 = 0.0f; + for (const Bivariate& point : points) { + const float l1 = std::abs(line(point.x) - point.y); + *max_l1 = std::max(*max_l1, l1); + abs_deviations.push_back(l1); + } + + *median_abs_deviation = Median(&abs_deviations); +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_ROBUST_STATISTICS_H_ diff --git a/lib/jxl/base/span.h b/lib/jxl/base/span.h new file mode 100644 index 0000000..f9e59b3 --- /dev/null +++ b/lib/jxl/base/span.h @@ -0,0 +1,58 @@ +// 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_SPAN_H_ +#define LIB_JXL_BASE_SPAN_H_ + +// Span (array view) is a non-owning container that provides cheap "cut" +// operations and could be used as "ArrayLike" data source for PaddedBytes. + +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { + +template +class Span { + public: + constexpr Span() noexcept : Span(nullptr, 0) {} + + constexpr Span(T* array, size_t length) noexcept + : ptr_(array), len_(length) {} + + template + explicit constexpr Span(T (&a)[N]) noexcept : Span(a, N) {} + + template + explicit constexpr Span(const ArrayLike& other) noexcept + : Span(reinterpret_cast(other.data()), other.size()) { + static_assert(sizeof(*other.data()) == sizeof(T), + "Incompatible type of source."); + } + + constexpr T* data() const noexcept { return ptr_; } + + constexpr size_t size() const noexcept { return len_; } + + constexpr T& operator[](size_t i) const noexcept { + // MSVC 2015 accepts this as constexpr, but not ptr_[i] + return *(data() + i); + } + + void remove_prefix(size_t n) noexcept { + JXL_ASSERT(size() >= n); + ptr_ += n; + len_ -= n; + } + + private: + T* ptr_; + size_t len_; +}; + +} // namespace jxl + +#endif // LIB_JXL_BASE_SPAN_H_ diff --git a/lib/jxl/base/status.cc b/lib/jxl/base/status.cc new file mode 100644 index 0000000..d4e6d0e --- /dev/null +++ b/lib/jxl/base/status.cc @@ -0,0 +1,46 @@ +// 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 +#include +#include + +#include + +#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 diff --git a/lib/jxl/base/status.h b/lib/jxl/base/status.h new file mode 100644 index 0000000..e57e6b0 --- /dev/null +++ b/lib/jxl/base/status.h @@ -0,0 +1,299 @@ +// 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_STATUS_H_ +#define LIB_JXL_BASE_STATUS_H_ + +// Error handling: Status return type + helper macros. + +#include +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +// Uncomment to abort when JXL_FAILURE or JXL_STATUS with a fatal error is +// reached: +// #define JXL_CRASH_ON_ERROR + +#ifndef JXL_ENABLE_ASSERT +#define JXL_ENABLE_ASSERT 1 +#endif + +#ifndef JXL_ENABLE_CHECK +#define JXL_ENABLE_CHECK 1 +#endif + +// Pass -DJXL_DEBUG_ON_ERROR at compile time to print debug messages when a +// function returns JXL_FAILURE or calls JXL_NOTIFY_ERROR. Note that this is +// irrelevant if you also pass -DJXL_CRASH_ON_ERROR. +#if defined(JXL_DEBUG_ON_ERROR) || defined(JXL_CRASH_ON_ERROR) +#undef JXL_DEBUG_ON_ERROR +#define JXL_DEBUG_ON_ERROR 1 +#else // JXL_DEBUG_ON_ERROR || JXL_CRASH_ON_ERROR +#ifdef NDEBUG +#define JXL_DEBUG_ON_ERROR 0 +#else // NDEBUG +#define JXL_DEBUG_ON_ERROR 1 +#endif // NDEBUG +#endif // JXL_DEBUG_ON_ERROR || JXL_CRASH_ON_ERROR + +// Pass -DJXL_DEBUG_ON_ALL_ERROR at compile time to print debug messages on +// all error (fatal and non-fatal) status. This implies JXL_DEBUG_ON_ERROR. +#if defined(JXL_DEBUG_ON_ALL_ERROR) +#undef JXL_DEBUG_ON_ALL_ERROR +#define JXL_DEBUG_ON_ALL_ERROR 1 +// JXL_DEBUG_ON_ALL_ERROR implies JXL_DEBUG_ON_ERROR too. +#undef JXL_DEBUG_ON_ERROR +#define JXL_DEBUG_ON_ERROR 1 +#else // JXL_DEBUG_ON_ALL_ERROR +#define JXL_DEBUG_ON_ALL_ERROR 0 +#endif // JXL_DEBUG_ON_ALL_ERROR + +// The Verbose level for the library +#ifndef JXL_DEBUG_V_LEVEL +#define JXL_DEBUG_V_LEVEL 0 +#endif // JXL_DEBUG_V_LEVEL + +// Pass -DJXL_DEBUG_ON_ABORT=0 to disable the debug messages on JXL_ASSERT, +// JXL_CHECK and JXL_ABORT. +#ifndef JXL_DEBUG_ON_ABORT +#define JXL_DEBUG_ON_ABORT 1 +#endif // JXL_DEBUG_ON_ABORT + +// Print a debug message on standard error. You should use the JXL_DEBUG macro +// 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, ...); + +// 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 +// function is never called and optimized out in release builds. Note that the +// arguments are compiled but not evaluated when enabled is false. The format +// string must be a explicit string in the call, for example: +// JXL_DEBUG(JXL_DEBUG_MYMODULE, "my module message: %d", some_var); +// Add a header at the top of your module's .cc or .h file (depending on whether +// you have JXL_DEBUG calls from the .h as well) like this: +// #ifndef JXL_DEBUG_MYMODULE +// #define JXL_DEBUG_MYMODULE 0 +// #endif JXL_DEBUG_MYMODULE +#define JXL_DEBUG(enabled, format, ...) \ + do { \ + if (enabled) { \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, \ + ##__VA_ARGS__); \ + } \ + } while (0) + +// JXL_DEBUG version that prints the debug message if the global verbose level +// defined at compile time by JXL_DEBUG_V_LEVEL is greater or equal than the +// passed level. +#define JXL_DEBUG_V(level, format, ...) \ + JXL_DEBUG(level <= JXL_DEBUG_V_LEVEL, format, ##__VA_ARGS__) + +// Warnings (via JXL_WARNING) are enabled by default in debug builds (opt and +// debug). +#ifdef JXL_DEBUG_WARNING +#undef JXL_DEBUG_WARNING +#define JXL_DEBUG_WARNING 1 +#else // JXL_DEBUG_WARNING +#ifdef NDEBUG +#define JXL_DEBUG_WARNING 0 +#else // JXL_DEBUG_WARNING +#define JXL_DEBUG_WARNING 1 +#endif // NDEBUG +#endif // JXL_DEBUG_WARNING +#define JXL_WARNING(format, ...) \ + JXL_DEBUG(JXL_DEBUG_WARNING, format, ##__VA_ARGS__) + +// Exits the program after printing a stack trace when possible. +JXL_NORETURN bool Abort(); + +// Exits the program after printing file/line plus a formatted string. +#define JXL_ABORT(format, ...) \ + ((JXL_DEBUG_ON_ABORT) && ::jxl::Debug(("%s:%d: JXL_ABORT: " format "\n"), \ + __FILE__, __LINE__, ##__VA_ARGS__), \ + ::jxl::Abort()) + +// Does not guarantee running the code, use only for debug mode checks. +#if JXL_ENABLE_ASSERT +#define JXL_ASSERT(condition) \ + do { \ + if (!(condition)) { \ + JXL_DEBUG(JXL_DEBUG_ON_ABORT, "JXL_ASSERT: %s", #condition); \ + ::jxl::Abort(); \ + } \ + } while (0) +#else +#define JXL_ASSERT(condition) \ + do { \ + } while (0) +#endif + +// Define JXL_IS_DEBUG_BUILD that denotes asan, msan and other debug builds, +// but not opt or release. +#ifndef JXL_IS_DEBUG_BUILD +#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ + defined(MEMORY_SANITIZER) || defined(THREAD_SANITIZER) || \ + defined(__clang_analyzer__) +#define JXL_IS_DEBUG_BUILD 1 +#else +#define JXL_IS_DEBUG_BUILD 0 +#endif +#endif // JXL_IS_DEBUG_BUILD + +// Same as above, but only runs in debug builds (builds where NDEBUG is not +// defined). This is useful for slower asserts that we want to run more rarely +// than usual. These will run on asan, msan and other debug builds, but not in +// opt or release. +#if JXL_IS_DEBUG_BUILD +#define JXL_DASSERT(condition) \ + do { \ + if (!(condition)) { \ + JXL_DEBUG(JXL_DEBUG_ON_ABORT, "JXL_DASSERT: %s", #condition); \ + ::jxl::Abort(); \ + } \ + } while (0) +#else +#define JXL_DASSERT(condition) \ + do { \ + } while (0) +#endif + +// Always runs the condition, so can be used for non-debug calls. +#if JXL_ENABLE_CHECK +#define JXL_CHECK(condition) \ + do { \ + if (!(condition)) { \ + JXL_DEBUG(JXL_DEBUG_ON_ABORT, "JXL_CHECK: %s", #condition); \ + ::jxl::Abort(); \ + } \ + } while (0) +#else +#define JXL_CHECK(condition) \ + do { \ + (void)(condition); \ + } while (0) +#endif + +// A jxl::Status value from a StatusCode or Status which prints a debug message +// when enabled. +#define JXL_STATUS(status, format, ...) \ + ::jxl::StatusMessage(::jxl::Status(status), "%s:%d: " format "\n", __FILE__, \ + __LINE__, ##__VA_ARGS__) + +// Notify of an error but discard the resulting Status value. This is only +// useful for debug builds or when building with JXL_CRASH_ON_ERROR. +#define JXL_NOTIFY_ERROR(format, ...) \ + (void)JXL_STATUS(::jxl::StatusCode::kGenericError, "JXL_ERROR: " format, \ + ##__VA_ARGS__) + +// An error Status with a message. The JXL_STATUS() macro will return a Status +// object with a kGenericError code, but the comma operator helps with +// clang-tidy inference and potentially with optimizations. +#define JXL_FAILURE(format, ...) \ + ((void)JXL_STATUS(::jxl::StatusCode::kGenericError, "JXL_FAILURE: " format, \ + ##__VA_ARGS__), \ + ::jxl::Status(::jxl::StatusCode::kGenericError)) + +// Always evaluates the status exactly once, so can be used for non-debug calls. +// Returns from the current context if the passed Status expression is an error +// (fatal or non-fatal). The return value is the passed Status. +#define JXL_RETURN_IF_ERROR(status) \ + do { \ + ::jxl::Status jxl_return_if_error_status = (status); \ + if (!jxl_return_if_error_status) { \ + (void)::jxl::StatusMessage( \ + jxl_return_if_error_status, \ + "%s:%d: JXL_RETURN_IF_ERROR code=%d: %s\n", __FILE__, __LINE__, \ + static_cast(jxl_return_if_error_status.code()), #status); \ + return jxl_return_if_error_status; \ + } \ + } while (0) + +// As above, but without calling StatusMessage. Intended for bundles (see +// fields.h), which have numerous call sites (-> relevant for code size) and do +// not want to generate excessive messages when decoding partial headers. +#define JXL_QUIET_RETURN_IF_ERROR(status) \ + do { \ + ::jxl::Status jxl_return_if_error_status = (status); \ + if (!jxl_return_if_error_status) { \ + return jxl_return_if_error_status; \ + } \ + } while (0) + +enum class StatusCode : int32_t { + // Non-fatal errors (negative values). + kNotEnoughBytes = -1, + + // The only non-error status code. + kOk = 0, + + // Fatal-errors (positive values) + kGenericError = 1, +}; + +// Drop-in replacement for bool that raises compiler warnings if not used +// after being returned from a function. Example: +// Status LoadFile(...) { return true; } is more compact than +// bool JXL_MUST_USE_RESULT LoadFile(...) { return true; } +// In case of error, the status can carry an extra error code in its value which +// is split between fatal and non-fatal error codes. +class JXL_MUST_USE_RESULT Status { + public: + // We want implicit constructor from bool to allow returning "true" or "false" + // on a function when using Status. "true" means kOk while "false" means a + // generic fatal error. + // NOLINTNEXTLINE(google-explicit-constructor) + constexpr Status(bool ok) + : code_(ok ? StatusCode::kOk : StatusCode::kGenericError) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + constexpr Status(StatusCode code) : code_(code) {} + + // We also want implicit cast to bool to check for return values of functions. + // NOLINTNEXTLINE(google-explicit-constructor) + constexpr operator bool() const { return code_ == StatusCode::kOk; } + + constexpr StatusCode code() const { return code_; } + + // Returns whether the status code is a fatal error. + constexpr bool IsFatalError() const { + return static_cast(code_) > 0; + } + + private: + StatusCode code_; +}; + +// Helper function to create a Status and print the debug message or abort when +// needed. +inline JXL_FORMAT(2, 3) Status + StatusMessage(const Status status, const char* format, ...) { + // This block will be optimized out when JXL_DEBUG_ON_ERROR and + // JXL_DEBUG_ON_ALL_ERROR are both disabled. + if ((JXL_DEBUG_ON_ERROR && status.IsFatalError()) || + (JXL_DEBUG_ON_ALL_ERROR && !status)) { + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + } +#ifdef JXL_CRASH_ON_ERROR + // JXL_CRASH_ON_ERROR means to Abort() only on non-fatal errors. + if (status.IsFatalError()) { + Abort(); + } +#endif // JXL_CRASH_ON_ERROR + return status; +} + +} // namespace jxl + +#endif // LIB_JXL_BASE_STATUS_H_ diff --git a/lib/jxl/base/thread_pool_internal.h b/lib/jxl/base/thread_pool_internal.h new file mode 100644 index 0000000..6e23a33 --- /dev/null +++ b/lib/jxl/base/thread_pool_internal.h @@ -0,0 +1,52 @@ +// 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_THREAD_POOL_INTERNAL_H_ +#define LIB_JXL_BASE_THREAD_POOL_INTERNAL_H_ + +#include + +#include + +#include "jxl/parallel_runner.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/threads/thread_parallel_runner_internal.h" + +namespace jxl { + +// Helper class to pass an internal ThreadPool-like object using threads. This +// is only suitable for tests or tools that access the internal API of JPEG XL. +// In other cases the caller will provide a JxlParallelRunner() for handling +// this. This class uses jpegxl::ThreadParallelRunner (from jpegxl_threads +// library). For interface details check jpegxl::ThreadParallelRunner. +class ThreadPoolInternal : public ThreadPool { + public: + // Starts the given number of worker threads and blocks until they are ready. + // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks + // run on the main thread. + explicit ThreadPoolInternal( + int num_worker_threads = std::thread::hardware_concurrency()) + : ThreadPool(&jpegxl::ThreadParallelRunner::Runner, + static_cast(&runner_)), + runner_(num_worker_threads) {} + + ThreadPoolInternal(const ThreadPoolInternal&) = delete; + ThreadPoolInternal& operator&(const ThreadPoolInternal&) = delete; + + size_t NumThreads() const { return runner_.NumThreads(); } + size_t NumWorkerThreads() const { return runner_.NumWorkerThreads(); } + + template + void RunOnEachThread(const Func& func) { + runner_.RunOnEachThread(func); + } + + private: + jpegxl::ThreadParallelRunner runner_; +}; + +} // namespace jxl + +#endif // LIB_JXL_BASE_THREAD_POOL_INTERNAL_H_ diff --git a/lib/jxl/bit_reader_test.cc b/lib/jxl/bit_reader_test.cc new file mode 100644 index 0000000..f8123a4 --- /dev/null +++ b/lib/jxl/bit_reader_test.cc @@ -0,0 +1,261 @@ +// 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 +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { +namespace { + +TEST(BitReaderTest, ExtendsWithZeroes) { + for (size_t size = 4; size < 32; ++size) { + std::vector data(size, 0xff); + + for (size_t n_bytes = 0; n_bytes < size; n_bytes++) { + BitReader br(Span(data.data(), n_bytes)); + // Read all the bits + for (size_t i = 0; i < n_bytes * kBitsPerByte; i++) { + ASSERT_EQ(br.ReadBits(1), 1u) << "n_bytes=" << n_bytes << " i=" << i; + } + + // PEEK more than the declared size - all will be zero. Cannot consume. + for (size_t i = 0; i < BitReader::kMaxBitsPerCall; i++) { + ASSERT_EQ(br.PeekBits(i), 0u) + << "size=" << size << "n_bytes=" << n_bytes << " i=" << i; + } + + EXPECT_TRUE(br.Close()); + } + } +} + +struct Symbol { + uint32_t num_bits; + uint32_t value; +}; + +// Reading from output gives the same values. +TEST(BitReaderTest, TestRoundTrip) { + ThreadPoolInternal pool(8); + pool.Run(0, 1000, ThreadPool::SkipInit(), + [](const int task, const int /* thread */) { + constexpr size_t kMaxBits = 8000; + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + + std::vector symbols; + symbols.reserve(1000); + + std::mt19937 rng(55537 + 129 * task); + std::uniform_int_distribution<> dist(1, 32); // closed interval + + for (;;) { + const uint32_t num_bits = dist(rng); + if (writer.BitsWritten() + num_bits > kMaxBits) break; + const uint32_t value = rng() >> (32 - num_bits); + symbols.push_back({num_bits, value}); + writer.Write(num_bits, value); + } + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + for (const Symbol& s : symbols) { + EXPECT_EQ(s.value, reader.ReadBits(s.num_bits)); + } + EXPECT_TRUE(reader.Close()); + }); +} + +// SkipBits is the same as reading that many bits. +TEST(BitReaderTest, TestSkip) { + ThreadPoolInternal pool(8); + pool.Run( + 0, 96, ThreadPool::SkipInit(), + [](const int task, const int /* thread */) { + constexpr size_t kSize = 100; + + for (size_t skip = 0; skip < 128; ++skip) { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kSize * kBitsPerByte); + // Start with "task" 1-bits. + for (int i = 0; i < task; ++i) { + writer.Write(1, 1); + } + + // Write 0-bits that we will skip over + for (size_t i = 0; i < skip; ++i) { + writer.Write(1, 0); + } + + // Write terminator bits '101' + writer.Write(3, 5); + EXPECT_EQ(task + skip + 3, writer.BitsWritten()); + writer.ZeroPadToByte(); + AuxOut aux_out; + ReclaimAndCharge(&writer, &allotment, 0, &aux_out); + EXPECT_LT(aux_out.layers[0].total_bits, kSize * 8); + + BitReader reader1(writer.GetSpan()); + BitReader reader2(writer.GetSpan()); + // Verify initial 1-bits + for (int i = 0; i < task; ++i) { + EXPECT_EQ(1u, reader1.ReadBits(1)); + EXPECT_EQ(1u, reader2.ReadBits(1)); + } + + // SkipBits or manually read "skip" bits + reader1.SkipBits(skip); + for (size_t i = 0; i < skip; ++i) { + EXPECT_EQ(0u, reader2.ReadBits(1)) + << " skip=" << skip << " i=" << i; + } + EXPECT_EQ(reader1.TotalBitsConsumed(), reader2.TotalBitsConsumed()); + + // Ensure both readers see the terminator bits. + EXPECT_EQ(5u, reader1.ReadBits(3)); + EXPECT_EQ(5u, reader2.ReadBits(3)); + + EXPECT_TRUE(reader1.Close()); + EXPECT_TRUE(reader2.Close()); + } + }); +} + +// Verifies byte order and different groupings of bits. +TEST(BitReaderTest, TestOrder) { + constexpr size_t kMaxBits = 16; + + // u(1) - bits written into LSBs of first byte + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + for (size_t i = 0; i < 5; ++i) { + writer.Write(1, 1); + } + for (size_t i = 0; i < 5; ++i) { + writer.Write(1, 0); + } + for (size_t i = 0; i < 6; ++i) { + writer.Write(1, 1); + } + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0x1Fu, reader.ReadFixedBits<8>()); + EXPECT_EQ(0xFCu, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // u(8) - get bytes in the same order + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(8, 0xF8); + writer.Write(8, 0x3F); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0xF8u, reader.ReadFixedBits<8>()); + EXPECT_EQ(0x3Fu, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // u(16) - little-endian bytes + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(16, 0xF83F); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0x3Fu, reader.ReadFixedBits<8>()); + EXPECT_EQ(0xF8u, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // Non-byte-aligned, mixed sizes + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(1, 1); + writer.Write(3, 6); + writer.Write(8, 0xDB); + writer.Write(4, 8); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0xBDu, reader.ReadFixedBits<8>()); + EXPECT_EQ(0x8Du, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } +} + +TEST(BitReaderTest, TotalCountersTest) { + uint8_t buf[8] = {1, 2, 3, 4}; + BitReader reader(Span(buf, sizeof(buf))); + + EXPECT_EQ(sizeof(buf), reader.TotalBytes()); + EXPECT_EQ(0u, reader.TotalBitsConsumed()); + reader.ReadFixedBits<1>(); + EXPECT_EQ(1u, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<10>(); + EXPECT_EQ(11u, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<4>(); + EXPECT_EQ(15u, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<1>(); + EXPECT_EQ(16u, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<16>(); + EXPECT_EQ(32u, reader.TotalBitsConsumed()); + + EXPECT_TRUE(reader.Close()); +} + +TEST(BitReaderTest, MoveTest) { + uint8_t buf[8] = {1, 2, 3, 4}; + BitReader reader2; + { + BitReader reader1(Span(buf, sizeof(buf))); + + EXPECT_EQ(0u, reader1.TotalBitsConsumed()); + reader1.ReadFixedBits<16>(); + EXPECT_EQ(16u, reader1.TotalBitsConsumed()); + + reader2 = std::move(reader1); + // From this point reader1 is invalid, but can continue to access reader2 + // and we don't need to call Close() on reader1. + } + + EXPECT_EQ(16u, reader2.TotalBitsConsumed()); + EXPECT_EQ(3U, reader2.ReadFixedBits<8>()); + EXPECT_EQ(24u, reader2.TotalBitsConsumed()); + + EXPECT_TRUE(reader2.Close()); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/bits_test.cc b/lib/jxl/bits_test.cc new file mode 100644 index 0000000..bf5fa62 --- /dev/null +++ b/lib/jxl/bits_test.cc @@ -0,0 +1,79 @@ +// 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/bits.h" + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(BitsTest, TestNumZeroBits) { + // Zero input is well-defined. + EXPECT_EQ(32u, Num0BitsAboveMS1Bit(0u)); + EXPECT_EQ(64u, Num0BitsAboveMS1Bit(0ull)); + EXPECT_EQ(32u, Num0BitsBelowLS1Bit(0u)); + EXPECT_EQ(64u, Num0BitsBelowLS1Bit(0ull)); + + EXPECT_EQ(31u, Num0BitsAboveMS1Bit(1u)); + EXPECT_EQ(30u, Num0BitsAboveMS1Bit(2u)); + EXPECT_EQ(63u, Num0BitsAboveMS1Bit(1ull)); + EXPECT_EQ(62u, Num0BitsAboveMS1Bit(2ull)); + + EXPECT_EQ(0u, Num0BitsBelowLS1Bit(1u)); + EXPECT_EQ(0u, Num0BitsBelowLS1Bit(1ull)); + EXPECT_EQ(1u, Num0BitsBelowLS1Bit(2u)); + EXPECT_EQ(1u, Num0BitsBelowLS1Bit(2ull)); + + EXPECT_EQ(0u, Num0BitsAboveMS1Bit(0x80000000u)); + EXPECT_EQ(0u, Num0BitsAboveMS1Bit(0x8000000000000000ull)); + EXPECT_EQ(31u, Num0BitsBelowLS1Bit(0x80000000u)); + EXPECT_EQ(63u, Num0BitsBelowLS1Bit(0x8000000000000000ull)); +} + +TEST(BitsTest, TestFloorLog2) { + // for input = [1, 7] + const size_t expected[7] = {0, 1, 1, 2, 2, 2, 2}; + for (uint32_t i = 1; i <= 7; ++i) { + EXPECT_EQ(expected[i - 1], FloorLog2Nonzero(i)) << " " << i; + EXPECT_EQ(expected[i - 1], FloorLog2Nonzero(uint64_t(i))) << " " << i; + } + + EXPECT_EQ(31u, FloorLog2Nonzero(0x80000000u)); + EXPECT_EQ(31u, FloorLog2Nonzero(0x80000001u)); + EXPECT_EQ(31u, FloorLog2Nonzero(0xFFFFFFFFu)); + + EXPECT_EQ(31u, FloorLog2Nonzero(0x80000000ull)); + EXPECT_EQ(31u, FloorLog2Nonzero(0x80000001ull)); + EXPECT_EQ(31u, FloorLog2Nonzero(0xFFFFFFFFull)); + + EXPECT_EQ(63u, FloorLog2Nonzero(0x8000000000000000ull)); + EXPECT_EQ(63u, FloorLog2Nonzero(0x8000000000000001ull)); + EXPECT_EQ(63u, FloorLog2Nonzero(0xFFFFFFFFFFFFFFFFull)); +} + +TEST(BitsTest, TestCeilLog2) { + // for input = [1, 7] + const size_t expected[7] = {0, 1, 2, 2, 3, 3, 3}; + for (uint32_t i = 1; i <= 7; ++i) { + EXPECT_EQ(expected[i - 1], CeilLog2Nonzero(i)) << " " << i; + EXPECT_EQ(expected[i - 1], CeilLog2Nonzero(uint64_t(i))) << " " << i; + } + + EXPECT_EQ(31u, CeilLog2Nonzero(0x80000000u)); + EXPECT_EQ(32u, CeilLog2Nonzero(0x80000001u)); + EXPECT_EQ(32u, CeilLog2Nonzero(0xFFFFFFFFu)); + + EXPECT_EQ(31u, CeilLog2Nonzero(0x80000000ull)); + EXPECT_EQ(32u, CeilLog2Nonzero(0x80000001ull)); + EXPECT_EQ(32u, CeilLog2Nonzero(0xFFFFFFFFull)); + + EXPECT_EQ(63u, CeilLog2Nonzero(0x8000000000000000ull)); + EXPECT_EQ(64u, CeilLog2Nonzero(0x8000000000000001ull)); + EXPECT_EQ(64u, CeilLog2Nonzero(0xFFFFFFFFFFFFFFFFull)); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/blending.cc b/lib/jxl/blending.cc new file mode 100644 index 0000000..5125ccf --- /dev/null +++ b/lib/jxl/blending.cc @@ -0,0 +1,472 @@ +// 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/blending.h" + +#include "lib/jxl/alpha.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +namespace { + +// Given two rects A and B, returns a set of rects whose union is A \ B. This +// may require from 0 to 4 rects, one for each non-empty side of B. `storage` +// must have room to accommodate that many rects. The order is consistent when +// called successively with "parallel" rects. +// +// +----------------------+ +// | top | +// +------+-------+-------+ +// | left | inner | right | +// +------+-------+-------+ +// | bottom | +// +----------------------+ +Span SubtractRect(const Rect& outer, const Rect& inner, + Rect* storage) { + size_t num_rects = 0; + + const Rect intersection = inner.Intersection(outer); + if (intersection.xsize() == 0 && intersection.ysize() == 0) { + storage[num_rects++] = outer; + return Span(storage, num_rects); + } + + // Left, same height as inner + if (outer.x0() < inner.x0()) { + storage[num_rects++] = + Rect(outer.x0(), inner.y0(), + std::min(outer.xsize(), inner.x0() - outer.x0()), inner.ysize()); + } + + // Right, same height as inner + if (outer.x0() + outer.xsize() > inner.x0() + inner.xsize()) { + storage[num_rects++] = + Rect(inner.x0() + inner.xsize(), inner.y0(), + std::min(outer.xsize(), outer.x0() + outer.xsize() - + (inner.x0() + inner.xsize())), + inner.ysize()); + } + + // Top, full width + if (outer.y0() < inner.y0()) { + storage[num_rects++] = + Rect(outer.x0(), outer.y0(), outer.xsize(), + std::min(outer.ysize(), inner.y0() - outer.y0())); + } + + // Bottom, full width + if (outer.y0() + outer.ysize() > inner.y0() + inner.ysize()) { + storage[num_rects++] = + Rect(outer.x0(), inner.y0() + inner.ysize(), outer.xsize(), + std::min(outer.ysize(), outer.y0() + outer.ysize() - + (inner.y0() + inner.ysize()))); + } + + return Span(storage, num_rects); +} + +} // namespace + +bool ImageBlender::NeedsBlending(PassesDecoderState* dec_state) { + const PassesSharedState& state = *dec_state->shared; + if (!(state.frame_header.frame_type == FrameType::kRegularFrame || + state.frame_header.frame_type == FrameType::kSkipProgressive)) { + return false; + } + const auto& info = state.frame_header.blending_info; + bool replace_all = (info.mode == BlendMode::kReplace); + for (const auto& ec_i : state.frame_header.extra_channel_blending_info) { + if (ec_i.mode != BlendMode::kReplace) { + replace_all = false; + } + } + // Replace the full frame: nothing to do. + if (!state.frame_header.custom_size_or_origin && replace_all) { + return false; + } + return true; +} + +Status ImageBlender::PrepareBlending( + PassesDecoderState* dec_state, FrameOrigin foreground_origin, + size_t foreground_xsize, size_t foreground_ysize, + const std::vector* extra_channel_info, + const ColorEncoding& frame_color_encoding, const Rect& frame_rect, + Image3F* output, const Rect& output_rect, + std::vector* output_extra_channels, + std::vector output_extra_channels_rects) { + const PassesSharedState& state = *dec_state->shared; + info_ = state.frame_header.blending_info; + + ec_info_ = &state.frame_header.extra_channel_blending_info; + + frame_rect_ = frame_rect; + extra_channel_info_ = extra_channel_info; + output_ = output; + output_rect_ = output_rect; + output_extra_channels_ = output_extra_channels; + output_extra_channels_rects_ = std::move(output_extra_channels_rects); + + size_t image_xsize = state.frame_header.nonserialized_metadata->xsize(); + size_t image_ysize = state.frame_header.nonserialized_metadata->ysize(); + + // the rect in the canvas that needs to be updated + cropbox_ = frame_rect; + // the rect of the foreground that overlaps with the canvas + overlap_ = cropbox_; + o_ = foreground_origin; + o_.x0 -= frame_rect.x0(); + o_.y0 -= frame_rect.y0(); + int x0 = (o_.x0 >= 0 ? o_.x0 : 0); + int y0 = (o_.y0 >= 0 ? o_.y0 : 0); + int xsize = foreground_xsize; + if (o_.x0 < 0) xsize += o_.x0; + int ysize = foreground_ysize; + if (o_.y0 < 0) ysize += o_.y0; + xsize = Clamp1(xsize, 0, (int)cropbox_.xsize() - x0); + ysize = Clamp1(ysize, 0, (int)cropbox_.ysize() - y0); + cropbox_ = Rect(x0, y0, xsize, ysize); + x0 = (o_.x0 < 0 ? -o_.x0 : 0); + y0 = (o_.y0 < 0 ? -o_.y0 : 0); + overlap_ = Rect(x0, y0, xsize, ysize); + + // Image to write to. + ImageBundle& bg = *state.reference_frames[info_.source].frame; + bg_ = &bg; + if (bg.xsize() == 0 && bg.ysize() == 0) { + // there is no background, assume it to be all zeroes + ImageBundle empty(&state.metadata->m); + Image3F color(image_xsize, image_ysize); + ZeroFillImage(&color); + empty.SetFromImage(std::move(color), frame_color_encoding); + if (!output_extra_channels_->empty()) { + std::vector ec; + for (size_t i = 0; i < output_extra_channels_->size(); ++i) { + ImageF eci(image_xsize, image_ysize); + ZeroFillImage(&eci); + ec.push_back(std::move(eci)); + } + empty.SetExtraChannels(std::move(ec)); + } + bg = std::move(empty); + } else if (state.reference_frames[info_.source].ib_is_in_xyb) { + return JXL_FAILURE( + "Trying to blend XYB reference frame %i and non-XYB frame", + info_.source); + } + + if (bg.xsize() < image_xsize || bg.ysize() < image_ysize || + bg.origin.x0 != 0 || bg.origin.y0 != 0) { + return JXL_FAILURE("Trying to use a %zux%zu crop as a background", + bg.xsize(), bg.ysize()); + } + if (state.metadata->m.xyb_encoded) { + if (!dec_state->output_encoding_info.color_encoding_is_original) { + return JXL_FAILURE("Blending in unsupported color space"); + } + } + + if (!overlap_.IsInside(Rect(0, 0, foreground_xsize, foreground_ysize))) { + return JXL_FAILURE("Trying to use a %zux%zu crop as a foreground", + foreground_xsize, foreground_ysize); + } + + if (!cropbox_.IsInside(bg)) { + return JXL_FAILURE( + "Trying blend %zux%zu to (%zu,%zu), but background is %zux%zu", + cropbox_.xsize(), cropbox_.ysize(), cropbox_.x0(), cropbox_.y0(), + bg.xsize(), bg.ysize()); + } + + Rect frame_rects_storage[4], output_rects_storage[4]; + Span frame_rects = SubtractRect( + frame_rect, cropbox_.Translate(frame_rect.x0(), frame_rect.y0()), + frame_rects_storage); + Span output_rects = SubtractRect( + output_rect, cropbox_.Translate(output_rect.x0(), output_rect.y0()), + output_rects_storage); + JXL_ASSERT(frame_rects.size() == output_rects.size()); + for (size_t i = 0; i < frame_rects.size(); ++i) { + CopyImageTo(frame_rects[i], *bg.color(), output_rects[i], output); + } + for (size_t i = 0; i < ec_info_->size(); ++i) { + const auto& eci = (*ec_info_)[i]; + const auto& src = *state.reference_frames[eci.source].frame; + output_rects = + SubtractRect(output_extra_channels_rects_[i], + cropbox_.Translate(output_extra_channels_rects_[i].x0(), + output_extra_channels_rects_[i].y0()), + output_rects_storage); + if (src.xsize() == 0 && src.ysize() == 0) { + for (size_t j = 0; j < output_rects.size(); ++j) { + ZeroFillPlane(&(*output_extra_channels_)[i], output_rects[j]); + } + } else { + if (src.extra_channels()[i].xsize() < image_xsize || + src.extra_channels()[i].ysize() < image_ysize || src.origin.x0 != 0 || + src.origin.y0 != 0) { + return JXL_FAILURE( + "Invalid size %zux%zu or origin %+d%+d for extra channel %zu of " + "reference frame %zu, expected at least %zux%zu+0+0", + src.extra_channels()[i].xsize(), src.extra_channels()[i].ysize(), + static_cast(src.origin.x0), static_cast(src.origin.y0), i, + static_cast(eci.source), image_xsize, image_ysize); + } + for (size_t j = 0; j < frame_rects.size(); ++j) { + CopyImageTo(frame_rects[j], src.extra_channels()[i], output_rects[j], + &(*output_extra_channels_)[i]); + } + } + } + + return true; +} + +ImageBlender::RectBlender ImageBlender::PrepareRect( + const Rect& rect, const Image3F& foreground, + const std::vector& extra_channels, const Rect& input_rect) const { + JXL_DASSERT(rect.xsize() == input_rect.xsize()); + JXL_DASSERT(rect.ysize() == input_rect.ysize()); + JXL_DASSERT(input_rect.IsInside(foreground)); + + RectBlender blender(false); + blender.extra_channel_info_ = extra_channel_info_; + + blender.current_overlap_ = rect.Intersection(overlap_); + if (blender.current_overlap_.xsize() == 0 || + blender.current_overlap_.ysize() == 0) { + blender.done_ = true; + return blender; + } + + blender.current_cropbox_ = + Rect(o_.x0 + blender.current_overlap_.x0(), + o_.y0 + blender.current_overlap_.y0(), + blender.current_overlap_.xsize(), blender.current_overlap_.ysize()); + + // Turn current_overlap_ from being relative to the full foreground to being + // relative to the rect or input_rect. + blender.current_overlap_ = + Rect(blender.current_overlap_.x0() - rect.x0(), + blender.current_overlap_.y0() - rect.y0(), + blender.current_overlap_.xsize(), blender.current_overlap_.ysize()); + + // And this one is relative to the `foreground` subimage. + const Rect input_overlap(blender.current_overlap_.x0() + input_rect.x0(), + blender.current_overlap_.y0() + input_rect.y0(), + blender.current_overlap_.xsize(), + blender.current_overlap_.ysize()); + + blender.blending_info_.resize(extra_channels.size() + 1); + auto make_blending = [&](const BlendingInfo& info, PatchBlending* pb) { + pb->alpha_channel = info.alpha_channel; + pb->clamp = info.clamp; + switch (info.mode) { + case BlendMode::kReplace: { + pb->mode = PatchBlendMode::kReplace; + break; + } + case BlendMode::kAdd: { + pb->mode = PatchBlendMode::kAdd; + break; + } + case BlendMode::kMul: { + pb->mode = PatchBlendMode::kMul; + break; + } + case BlendMode::kBlend: { + pb->mode = PatchBlendMode::kBlendAbove; + break; + } + case BlendMode::kAlphaWeightedAdd: { + pb->mode = PatchBlendMode::kAlphaWeightedAddAbove; + break; + } + default: { + JXL_ABORT("Invalid blend mode"); // should have failed to decode + } + } + }; + make_blending(info_, &blender.blending_info_[0]); + for (size_t i = 0; i < extra_channels.size(); i++) { + make_blending((*ec_info_)[i], &blender.blending_info_[1 + i]); + } + + Rect cropbox_row = blender.current_cropbox_.Line(0); + Rect overlap_row = input_overlap.Line(0); + const auto num_ptrs = 3 + extra_channels.size(); + blender.fg_ptrs_.reserve(num_ptrs); + blender.fg_strides_.reserve(num_ptrs); + blender.bg_ptrs_.reserve(num_ptrs); + blender.bg_strides_.reserve(num_ptrs); + for (size_t c = 0; c < 3; c++) { + blender.fg_ptrs_.push_back(overlap_row.ConstPlaneRow(foreground, c, 0)); + blender.fg_strides_.push_back(foreground.PixelsPerRow()); + blender.bg_ptrs_.push_back( + cropbox_row.Translate(frame_rect_.x0(), frame_rect_.y0()) + .PlaneRow(bg_->color(), c, 0)); + blender.bg_strides_.push_back(bg_->color()->PixelsPerRow()); + blender.out_ptrs_.push_back( + cropbox_row.Translate(output_rect_.x0(), output_rect_.y0()) + .PlaneRow(output_, c, 0)); + blender.out_strides_.push_back(output_->PixelsPerRow()); + } + for (size_t c = 0; c < extra_channels.size(); c++) { + blender.fg_ptrs_.push_back(overlap_row.ConstRow(extra_channels[c], 0)); + blender.fg_strides_.push_back(extra_channels[c].PixelsPerRow()); + blender.bg_ptrs_.push_back( + cropbox_row.Translate(frame_rect_.x0(), frame_rect_.y0()) + .Row(&bg_->extra_channels()[c], 0)); + blender.bg_strides_.push_back(bg_->extra_channels()[c].PixelsPerRow()); + blender.out_ptrs_.push_back( + cropbox_row + .Translate(output_extra_channels_rects_[c].x0(), + output_extra_channels_rects_[c].y0()) + .Row(&(*output_extra_channels_)[c], 0)); + blender.out_strides_.push_back((*output_extra_channels_)[c].PixelsPerRow()); + } + + return blender; +} + +Status PerformBlending( + const float* const* bg, const float* const* fg, float* const* out, + size_t xsize, const PatchBlending& color_blending, + const PatchBlending* ec_blending, + const std::vector& extra_channel_info) { + bool has_alpha = false; + size_t num_ec = extra_channel_info.size(); + for (size_t i = 0; i < num_ec; i++) { + if (extra_channel_info[i].type == jxl::ExtraChannel::kAlpha) { + has_alpha = true; + break; + } + } + ImageF tmp(xsize, 3 + num_ec); + // Blend extra channels first so that we use the pre-blending alpha. + for (size_t i = 0; i < num_ec; i++) { + if (ec_blending[i].mode == PatchBlendMode::kAdd) { + for (size_t x = 0; x < xsize; x++) { + tmp.Row(3 + i)[x] = bg[3 + i][x] + fg[3 + i][x]; + } + } else if (ec_blending[i].mode == PatchBlendMode::kBlendAbove) { + size_t alpha = ec_blending[i].alpha_channel; + bool is_premultiplied = extra_channel_info[alpha].alpha_associated; + PerformAlphaBlending(bg[3 + i], bg[3 + alpha], fg[3 + i], fg[3 + alpha], + tmp.Row(3 + i), xsize, is_premultiplied, + ec_blending[i].clamp); + } else if (ec_blending[i].mode == PatchBlendMode::kBlendBelow) { + size_t alpha = ec_blending[i].alpha_channel; + bool is_premultiplied = extra_channel_info[alpha].alpha_associated; + PerformAlphaBlending(fg[3 + i], fg[3 + alpha], bg[3 + i], bg[3 + alpha], + tmp.Row(3 + i), xsize, is_premultiplied, + ec_blending[i].clamp); + } else if (ec_blending[i].mode == PatchBlendMode::kAlphaWeightedAddAbove) { + size_t alpha = ec_blending[i].alpha_channel; + PerformAlphaWeightedAdd(bg[3 + i], fg[3 + i], fg[3 + alpha], + tmp.Row(3 + i), xsize, ec_blending[i].clamp); + } else if (ec_blending[i].mode == PatchBlendMode::kAlphaWeightedAddBelow) { + size_t alpha = ec_blending[i].alpha_channel; + PerformAlphaWeightedAdd(fg[3 + i], bg[3 + i], bg[3 + alpha], + tmp.Row(3 + i), xsize, ec_blending[i].clamp); + } else if (ec_blending[i].mode == PatchBlendMode::kMul) { + PerformMulBlending(bg[3 + i], fg[3 + i], tmp.Row(3 + i), xsize, + ec_blending[i].clamp); + } else if (ec_blending[i].mode == PatchBlendMode::kReplace) { + memcpy(tmp.Row(3 + i), fg[3 + i], xsize * sizeof(**fg)); + } else if (ec_blending[i].mode == PatchBlendMode::kNone) { + memcpy(tmp.Row(3 + i), bg[3 + i], xsize * sizeof(**fg)); + } else { + JXL_ABORT("Unreachable"); + } + } + size_t alpha = color_blending.alpha_channel; + + if (color_blending.mode == PatchBlendMode::kAdd || + (color_blending.mode == PatchBlendMode::kAlphaWeightedAddAbove && + !has_alpha) || + (color_blending.mode == PatchBlendMode::kAlphaWeightedAddBelow && + !has_alpha)) { + for (int p = 0; p < 3; p++) { + float* out = tmp.Row(p); + for (size_t x = 0; x < xsize; x++) { + out[x] = bg[p][x] + fg[p][x]; + } + } + } else if (color_blending.mode == PatchBlendMode::kBlendAbove + // blend without alpha is just replace + && has_alpha) { + bool is_premultiplied = extra_channel_info[alpha].alpha_associated; + PerformAlphaBlending( + {bg[0], bg[1], bg[2], bg[3 + alpha]}, + {fg[0], fg[1], fg[2], fg[3 + alpha]}, + {tmp.Row(0), tmp.Row(1), tmp.Row(2), tmp.Row(3 + alpha)}, xsize, + is_premultiplied, color_blending.clamp); + } else if (color_blending.mode == PatchBlendMode::kBlendBelow + // blend without alpha is just replace + && has_alpha) { + bool is_premultiplied = extra_channel_info[alpha].alpha_associated; + PerformAlphaBlending( + {fg[0], fg[1], fg[2], fg[3 + alpha]}, + {bg[0], bg[1], bg[2], bg[3 + alpha]}, + {tmp.Row(0), tmp.Row(1), tmp.Row(2), tmp.Row(3 + alpha)}, xsize, + is_premultiplied, color_blending.clamp); + } else if (color_blending.mode == PatchBlendMode::kAlphaWeightedAddAbove) { + JXL_DASSERT(has_alpha); + for (size_t c = 0; c < 3; c++) { + PerformAlphaWeightedAdd(bg[c], fg[c], fg[3 + alpha], tmp.Row(c), xsize, + color_blending.clamp); + } + } else if (color_blending.mode == PatchBlendMode::kAlphaWeightedAddBelow) { + JXL_DASSERT(has_alpha); + for (size_t c = 0; c < 3; c++) { + PerformAlphaWeightedAdd(fg[c], bg[c], bg[3 + alpha], tmp.Row(c), xsize, + color_blending.clamp); + } + } else if (color_blending.mode == PatchBlendMode::kMul) { + for (int p = 0; p < 3; p++) { + PerformMulBlending(bg[p], fg[p], tmp.Row(p), xsize, color_blending.clamp); + } + } else if (color_blending.mode == PatchBlendMode::kReplace || + color_blending.mode == PatchBlendMode::kBlendAbove || + color_blending.mode == PatchBlendMode::kBlendBelow) { // kReplace + for (size_t p = 0; p < 3; p++) { + memcpy(tmp.Row(p), fg[p], xsize * sizeof(**fg)); + } + } else if (color_blending.mode == PatchBlendMode::kNone) { + for (size_t p = 0; p < 3; p++) { + memcpy(tmp.Row(p), bg[p], xsize * sizeof(**fg)); + } + } else { + JXL_ABORT("Unreachable"); + } + for (size_t i = 0; i < 3 + num_ec; i++) { + memcpy(out[i], tmp.Row(i), xsize * sizeof(**out)); + } + return true; +} + +Status ImageBlender::RectBlender::DoBlending(size_t y) { + if (done_ || y < current_overlap_.y0() || + y >= current_overlap_.y0() + current_overlap_.ysize()) { + return true; + } + y -= current_overlap_.y0(); + fg_row_ptrs_.resize(fg_ptrs_.size()); + bg_row_ptrs_.resize(bg_ptrs_.size()); + out_row_ptrs_.resize(out_ptrs_.size()); + for (size_t c = 0; c < fg_row_ptrs_.size(); c++) { + fg_row_ptrs_[c] = fg_ptrs_[c] + y * fg_strides_[c]; + bg_row_ptrs_[c] = bg_ptrs_[c] + y * bg_strides_[c]; + out_row_ptrs_[c] = out_ptrs_[c] + y * out_strides_[c]; + } + return PerformBlending(bg_row_ptrs_.data(), fg_row_ptrs_.data(), + out_row_ptrs_.data(), current_overlap_.xsize(), + blending_info_[0], blending_info_.data() + 1, + *extra_channel_info_); +} + +} // namespace jxl diff --git a/lib/jxl/blending.h b/lib/jxl/blending.h new file mode 100644 index 0000000..e453609 --- /dev/null +++ b/lib/jxl/blending.h @@ -0,0 +1,96 @@ +// 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_BLENDING_H_ +#define LIB_JXL_BLENDING_H_ +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +Status PerformBlending(const float* const* bg, const float* const* fg, + float* const* out, size_t xsize, + const PatchBlending& color_blending, + const PatchBlending* ec_blending, + const std::vector& extra_channel_info); + +class ImageBlender { + public: + class RectBlender { + public: + // Does the blending for a given row of the rect passed to + // ImageBlender::PrepareRect. + Status DoBlending(size_t y); + + // If this returns true, then nothing needs to be done for this rect and + // DoBlending can be skipped (but does not have to). + bool done() const { return done_; } + + private: + friend class ImageBlender; + explicit RectBlender(bool done) : done_(done) {} + + bool done_; + Rect current_overlap_; + Rect current_cropbox_; + const std::vector* extra_channel_info_; + std::vector fg_ptrs_; + std::vector fg_strides_; + std::vector bg_ptrs_; + std::vector bg_strides_; + std::vector out_ptrs_; + std::vector out_strides_; + std::vector fg_row_ptrs_; + std::vector bg_row_ptrs_; + std::vector out_row_ptrs_; + std::vector blending_info_; + }; + + static bool NeedsBlending(PassesDecoderState* dec_state); + + Status PrepareBlending( + PassesDecoderState* dec_state, FrameOrigin foreground_origin, + size_t foreground_xsize, size_t foreground_ysize, + const std::vector* extra_channel_info, + const ColorEncoding& frame_color_encoding, const Rect& frame_rect, + Image3F* output, const Rect& output_rect, + std::vector* output_extra_channels, + std::vector output_extra_channels_rects); + // rect is relative to the full decoded foreground. + // But foreground here can be a subset of the full foreground, and input_rect + // indicates where that rect is in that subset. For example, if rect = + // Rect(10, 10, 20, 20), and foreground is subrect (7, 7, 30, 30) of the full + // foreground, then input_rect should be (3, 3, 20, 20), because that is where + // rect is relative to the foreground crop. + ImageBlender::RectBlender PrepareRect( + const Rect& rect, const Image3F& foreground, + const std::vector& extra_channels, const Rect& input_rect) const; + + // If this returns true, then it is not necessary to call further methods on + // this ImageBlender to achieve blending, although it is not forbidden either + // (those methods will just return immediately in that case). + bool done() const { return done_; } + + private: + BlendingInfo info_; + const std::vector* extra_channel_info_; + Rect frame_rect_; + // Destination, as well as background before DoBlending is called. + Image3F* output_; + ImageBundle* bg_; + Rect output_rect_; + std::vector* output_extra_channels_; + std::vector output_extra_channels_rects_; + Rect cropbox_; + Rect overlap_; + bool done_ = false; + const std::vector* ec_info_; + FrameOrigin o_{}; +}; + +} // namespace jxl + +#endif // LIB_JXL_BLENDING_H_ diff --git a/lib/jxl/blending_test.cc b/lib/jxl/blending_test.cc new file mode 100644 index 0000000..4ce66c2 --- /dev/null +++ b/lib/jxl/blending_test.cc @@ -0,0 +1,98 @@ +// 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/blending.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { + +using ::testing::SizeIs; + +TEST(BlendingTest, Crops) { + ThreadPool* pool = nullptr; + + const PaddedBytes compressed = + ReadTestData("jxl/blending/cropped_traffic_light.jxl"); + DecompressParams dparams; + CodecInOut decoded; + ASSERT_TRUE(DecodeFile(dparams, compressed, &decoded, pool)); + ASSERT_THAT(decoded.frames, SizeIs(4)); + + int i = 0; + for (const ImageBundle& ib : decoded.frames) { + std::ostringstream filename; + filename << "jxl/blending/cropped_traffic_light_frame-" << i << ".png"; + const PaddedBytes compressed_frame = ReadTestData(filename.str()); + CodecInOut frame; + ASSERT_TRUE(SetFromBytes(Span(compressed_frame), &frame)); + EXPECT_TRUE(SamePixels(ib.color(), *frame.Main().color())); + ++i; + } +} + +TEST(BlendingTest, Offset) { + const PaddedBytes background_bytes = ReadTestData("jxl/splines.png"); + CodecInOut background; + ASSERT_TRUE(SetFromBytes(Span(background_bytes), &background)); + const PaddedBytes foreground_bytes = + ReadTestData("jxl/grayscale_patches.png"); + CodecInOut foreground; + ASSERT_TRUE(SetFromBytes(Span(foreground_bytes), &foreground)); + + ImageBlender blender; + CodecMetadata nonserialized_metadata; + ASSERT_TRUE( + nonserialized_metadata.size.Set(background.xsize(), background.ysize())); + PassesSharedState state; + state.frame_header.blending_info.mode = BlendMode::kReplace; + state.frame_header.blending_info.source = 0; + state.frame_header.nonserialized_metadata = &nonserialized_metadata; + state.metadata = &background.metadata; + state.reference_frames[0].frame = &background.Main(); + PassesDecoderState dec_state; + dec_state.shared = &state; + const FrameOrigin foreground_origin = {-50, -50}; + ImageBundle output(&background.metadata.m); + output.SetFromImage(Image3F(background.xsize(), background.ysize()), + background.Main().c_current()); + ASSERT_TRUE(blender.PrepareBlending( + &dec_state, foreground_origin, foreground.xsize(), foreground.ysize(), + &nonserialized_metadata.m.extra_channel_info, + background.Main().c_current(), Rect(background), output.color(), + Rect(*output.color()), {}, {})); + + static constexpr int kStep = 20; + for (size_t x0 = 0; x0 < foreground.xsize(); x0 += kStep) { + for (size_t y0 = 0; y0 < foreground.ysize(); y0 += kStep) { + const Rect rect = + Rect(x0, y0, kStep, kStep).Intersection(Rect(foreground.Main())); + Image3F foreground_crop(rect.xsize(), rect.ysize()); + CopyImageTo(rect, *foreground.Main().color(), Rect(foreground_crop), + &foreground_crop); + auto rect_blender = + blender.PrepareRect(rect, foreground_crop, {}, Rect(foreground_crop)); + for (size_t y = 0; y < rect.ysize(); ++y) { + ASSERT_TRUE(rect_blender.DoBlending(y)); + } + } + } + + const PaddedBytes expected_bytes = + ReadTestData("jxl/blending/grayscale_patches_on_splines.png"); + CodecInOut expected; + ASSERT_TRUE(SetFromBytes(Span(expected_bytes), &expected)); + VerifyRelativeError(*expected.Main().color(), *output.color(), 1. / (2 * 255), + 0); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/butteraugli/butteraugli.cc b/lib/jxl/butteraugli/butteraugli.cc new file mode 100644 index 0000000..92a5ce9 --- /dev/null +++ b/lib/jxl/butteraugli/butteraugli.cc @@ -0,0 +1,2139 @@ +// 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. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) +// +// The physical architecture of butteraugli is based on the following naming +// convention: +// * Opsin - dynamics of the photosensitive chemicals in the retina +// with their immediate electrical processing +// * Xyb - hybrid opponent/trichromatic color space +// x is roughly red-subtract-green. +// y is yellow. +// b is blue. +// Xyb values are computed from Opsin mixing, not directly from rgb. +// * Mask - for visual masking +// * Hf - color modeling for spatially high-frequency features +// * Lf - color modeling for spatially low-frequency features +// * Diffmap - to cluster and build an image of error between the images +// * Blur - to hold the smoothing code + +#include "lib/jxl/butteraugli/butteraugli.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if PROFILER_ENABLED +#include +#endif // PROFILER_ENABLED + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/butteraugli/butteraugli.cc" +#include + +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/gauss_blur.h" +#include "lib/jxl/image_ops.h" + +#ifndef JXL_BUTTERAUGLI_ONCE +#define JXL_BUTTERAUGLI_ONCE + +namespace jxl { + +std::vector ComputeKernel(float sigma) { + const float m = 2.25; // Accuracy increases when m is increased. + const double scaler = -1.0 / (2.0 * sigma * sigma); + const int diff = std::max(1, m * std::fabs(sigma)); + std::vector kernel(2 * diff + 1); + for (int i = -diff; i <= diff; ++i) { + kernel[i + diff] = std::exp(scaler * i * i); + } + return kernel; +} + +void ConvolveBorderColumn(const ImageF& in, const std::vector& kernel, + const size_t x, float* BUTTERAUGLI_RESTRICT row_out) { + const size_t offset = kernel.size() / 2; + int minx = x < offset ? 0 : x - offset; + int maxx = std::min(in.xsize() - 1, x + offset); + float weight = 0.0f; + for (int j = minx; j <= maxx; ++j) { + weight += kernel[j - x + offset]; + } + float scale = 1.0f / weight; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y); + float sum = 0.0f; + for (int j = minx; j <= maxx; ++j) { + sum += row_in[j] * kernel[j - x + offset]; + } + row_out[y] = sum * scale; + } +} + +// Computes a horizontal convolution and transposes the result. +void ConvolutionWithTranspose(const ImageF& in, + const std::vector& kernel, + ImageF* BUTTERAUGLI_RESTRICT out) { + PROFILER_FUNC; + JXL_CHECK(out->xsize() == in.ysize()); + JXL_CHECK(out->ysize() == in.xsize()); + const size_t len = kernel.size(); + const size_t offset = len / 2; + float weight_no_border = 0.0f; + for (size_t j = 0; j < len; ++j) { + weight_no_border += kernel[j]; + } + const float scale_no_border = 1.0f / weight_no_border; + const size_t border1 = std::min(in.xsize(), offset); + const size_t border2 = in.xsize() > offset ? in.xsize() - offset : 0; + std::vector scaled_kernel(len / 2 + 1); + for (size_t i = 0; i <= len / 2; ++i) { + scaled_kernel[i] = kernel[i] * scale_no_border; + } + + // middle + switch (len) { +#if 1 // speed-optimized version + case 7: { + PROFILER_ZONE("conv7"); + const float sk0 = scaled_kernel[0]; + const float sk1 = scaled_kernel[1]; + const float sk2 = scaled_kernel[2]; + const float sk3 = scaled_kernel[3]; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + const float sum0 = (row_in[0] + row_in[6]) * sk0; + const float sum1 = (row_in[1] + row_in[5]) * sk1; + const float sum2 = (row_in[2] + row_in[4]) * sk2; + const float sum = (row_in[3]) * sk3 + sum0 + sum1 + sum2; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + } break; + case 13: { + PROFILER_ZONE("conv15"); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum0 = (row_in[0] + row_in[12]) * scaled_kernel[0]; + float sum1 = (row_in[1] + row_in[11]) * scaled_kernel[1]; + float sum2 = (row_in[2] + row_in[10]) * scaled_kernel[2]; + float sum3 = (row_in[3] + row_in[9]) * scaled_kernel[3]; + sum0 += (row_in[4] + row_in[8]) * scaled_kernel[4]; + sum1 += (row_in[5] + row_in[7]) * scaled_kernel[5]; + const float sum = (row_in[6]) * scaled_kernel[6]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + break; + } + case 15: { + PROFILER_ZONE("conv15"); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum0 = (row_in[0] + row_in[14]) * scaled_kernel[0]; + float sum1 = (row_in[1] + row_in[13]) * scaled_kernel[1]; + float sum2 = (row_in[2] + row_in[12]) * scaled_kernel[2]; + float sum3 = (row_in[3] + row_in[11]) * scaled_kernel[3]; + sum0 += (row_in[4] + row_in[10]) * scaled_kernel[4]; + sum1 += (row_in[5] + row_in[9]) * scaled_kernel[5]; + sum2 += (row_in[6] + row_in[8]) * scaled_kernel[6]; + const float sum = (row_in[7]) * scaled_kernel[7]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + break; + } + case 25: { + PROFILER_ZONE("conv25"); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum0 = (row_in[0] + row_in[24]) * scaled_kernel[0]; + float sum1 = (row_in[1] + row_in[23]) * scaled_kernel[1]; + float sum2 = (row_in[2] + row_in[22]) * scaled_kernel[2]; + float sum3 = (row_in[3] + row_in[21]) * scaled_kernel[3]; + sum0 += (row_in[4] + row_in[20]) * scaled_kernel[4]; + sum1 += (row_in[5] + row_in[19]) * scaled_kernel[5]; + sum2 += (row_in[6] + row_in[18]) * scaled_kernel[6]; + sum3 += (row_in[7] + row_in[17]) * scaled_kernel[7]; + sum0 += (row_in[8] + row_in[16]) * scaled_kernel[8]; + sum1 += (row_in[9] + row_in[15]) * scaled_kernel[9]; + sum2 += (row_in[10] + row_in[14]) * scaled_kernel[10]; + sum3 += (row_in[11] + row_in[13]) * scaled_kernel[11]; + const float sum = (row_in[12]) * scaled_kernel[12]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + break; + } + case 33: { + PROFILER_ZONE("conv33"); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum0 = (row_in[0] + row_in[32]) * scaled_kernel[0]; + float sum1 = (row_in[1] + row_in[31]) * scaled_kernel[1]; + float sum2 = (row_in[2] + row_in[30]) * scaled_kernel[2]; + float sum3 = (row_in[3] + row_in[29]) * scaled_kernel[3]; + sum0 += (row_in[4] + row_in[28]) * scaled_kernel[4]; + sum1 += (row_in[5] + row_in[27]) * scaled_kernel[5]; + sum2 += (row_in[6] + row_in[26]) * scaled_kernel[6]; + sum3 += (row_in[7] + row_in[25]) * scaled_kernel[7]; + sum0 += (row_in[8] + row_in[24]) * scaled_kernel[8]; + sum1 += (row_in[9] + row_in[23]) * scaled_kernel[9]; + sum2 += (row_in[10] + row_in[22]) * scaled_kernel[10]; + sum3 += (row_in[11] + row_in[21]) * scaled_kernel[11]; + sum0 += (row_in[12] + row_in[20]) * scaled_kernel[12]; + sum1 += (row_in[13] + row_in[19]) * scaled_kernel[13]; + sum2 += (row_in[14] + row_in[18]) * scaled_kernel[14]; + sum3 += (row_in[15] + row_in[17]) * scaled_kernel[15]; + const float sum = (row_in[16]) * scaled_kernel[16]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + break; + } + case 37: { + PROFILER_ZONE("conv37"); + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum0 = (row_in[0] + row_in[36]) * scaled_kernel[0]; + float sum1 = (row_in[1] + row_in[35]) * scaled_kernel[1]; + float sum2 = (row_in[2] + row_in[34]) * scaled_kernel[2]; + float sum3 = (row_in[3] + row_in[33]) * scaled_kernel[3]; + sum0 += (row_in[4] + row_in[32]) * scaled_kernel[4]; + sum0 += (row_in[5] + row_in[31]) * scaled_kernel[5]; + sum0 += (row_in[6] + row_in[30]) * scaled_kernel[6]; + sum0 += (row_in[7] + row_in[29]) * scaled_kernel[7]; + sum0 += (row_in[8] + row_in[28]) * scaled_kernel[8]; + sum1 += (row_in[9] + row_in[27]) * scaled_kernel[9]; + sum2 += (row_in[10] + row_in[26]) * scaled_kernel[10]; + sum3 += (row_in[11] + row_in[25]) * scaled_kernel[11]; + sum0 += (row_in[12] + row_in[24]) * scaled_kernel[12]; + sum1 += (row_in[13] + row_in[23]) * scaled_kernel[13]; + sum2 += (row_in[14] + row_in[22]) * scaled_kernel[14]; + sum3 += (row_in[15] + row_in[21]) * scaled_kernel[15]; + sum0 += (row_in[16] + row_in[20]) * scaled_kernel[16]; + sum1 += (row_in[17] + row_in[19]) * scaled_kernel[17]; + const float sum = (row_in[18]) * scaled_kernel[18]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum + sum0 + sum1 + sum2 + sum3; + } + } + break; + } + default: + printf("Warning: Unexpected kernel size! %zu\n", len); +#else + default: +#endif + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y); + for (size_t x = border1; x < border2; ++x) { + const int d = x - offset; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + float sum = 0.0f; + size_t j; + for (j = 0; j <= len / 2; ++j) { + sum += row_in[d + j] * scaled_kernel[j]; + } + for (; j < len; ++j) { + sum += row_in[d + j] * scaled_kernel[len - 1 - j]; + } + row_out[y] = sum; + } + } + } + // left border + for (size_t x = 0; x < border1; ++x) { + ConvolveBorderColumn(in, kernel, x, out->Row(x)); + } + + // right border + for (size_t x = border2; x < in.xsize(); ++x) { + ConvolveBorderColumn(in, kernel, x, out->Row(x)); + } +} + +// Separate horizontal and vertical (next function) convolution passes. +void BlurHorizontalConv(const ImageF& in, const intptr_t xbegin, + const intptr_t xend, const intptr_t ybegin, + const intptr_t yend, const std::vector& kernel, + ImageF* out) { + if (xbegin >= xend || ybegin >= yend) return; + const intptr_t xsize = in.xsize(); + const intptr_t ysize = in.ysize(); + JXL_ASSERT(0 <= xbegin && xend <= xsize); + JXL_ASSERT(0 <= ybegin && yend <= ysize); + (void)xsize; + (void)ysize; + const intptr_t radius = kernel.size() / 2; + + for (intptr_t y = ybegin; y < yend; ++y) { + float* JXL_RESTRICT row_out = out->Row(y); + for (intptr_t x = xbegin; x < xend; ++x) { + float sum = 0.0f; + float sum_weights = 0.0f; + const float* JXL_RESTRICT row_in = in.Row(y); + for (intptr_t ix = -radius; ix <= radius; ++ix) { + const intptr_t in_x = x + ix; + if (in_x < 0 || in_x >= xsize) continue; + const float weight_x = kernel[ix + radius]; + sum += row_in[in_x] * weight_x; + sum_weights += weight_x; + } + row_out[x] = sum / sum_weights; + } + } +} + +void BlurVerticalConv(const ImageF& in, const intptr_t xbegin, + const intptr_t xend, const intptr_t ybegin, + const intptr_t yend, const std::vector& kernel, + ImageF* out) { + if (xbegin >= xend || ybegin >= yend) return; + const intptr_t xsize = in.xsize(); + const intptr_t ysize = in.ysize(); + JXL_ASSERT(0 <= xbegin && xend <= xsize); + JXL_ASSERT(0 <= ybegin && yend <= ysize); + (void)xsize; + const intptr_t radius = kernel.size() / 2; + for (intptr_t y = ybegin; y < yend; ++y) { + float* JXL_RESTRICT row_out = out->Row(y); + for (intptr_t x = xbegin; x < xend; ++x) { + float sum = 0.0f; + float sum_weights = 0.0f; + for (intptr_t iy = -radius; iy <= radius; ++iy) { + const intptr_t in_y = y + iy; + if (in_y < 0 || in_y >= ysize) continue; + const float weight_y = kernel[iy + radius]; + sum += in.ConstRow(in_y)[x] * weight_y; + sum_weights += weight_y; + } + row_out[x] = sum / sum_weights; + } + } +} + +// A blur somewhat similar to a 2D Gaussian blur. +// See: https://en.wikipedia.org/wiki/Gaussian_blur +// +// This is a bottleneck because the sigma can be quite large (>7). We can use +// gauss_blur.cc (runtime independent of sigma, closer to a 4*sigma truncated +// Gaussian and our 2.25 in ComputeKernel), but its boundary conditions are +// zero-valued. This leads to noticeable differences at the edges of diffmaps. +// We retain a special case for 5x5 kernels (even faster than gauss_blur), +// optionally use gauss_blur followed by fixup of the borders for large images, +// or fall back to the previous truncated FIR followed by a transpose. +void Blur(const ImageF& in, float sigma, const ButteraugliParams& params, + BlurTemp* temp, ImageF* out) { + std::vector kernel = ComputeKernel(sigma); + // Separable5 does an in-place convolution, so this fast path is not safe if + // in aliases out. + if (kernel.size() == 5 && &in != out) { + float sum_weights = 0.0f; + for (const float w : kernel) { + sum_weights += w; + } + const float scale = 1.0f / sum_weights; + const float w0 = kernel[2] * scale; + const float w1 = kernel[1] * scale; + const float w2 = kernel[0] * scale; + const WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + }; + Separable5(in, Rect(in), weights, /*pool=*/nullptr, out); + return; + } + + const bool fast_gauss = params.approximate_border; + const bool kBorderFixup = fast_gauss && false; + // Fast+fixup is actually slower for small images that are all border. + const bool too_small_for_fast_gauss = + kBorderFixup && + in.xsize() * in.ysize() < 9 * kernel.size() * kernel.size(); + // If fast gaussian is disabled, use previous transposed convolution. + if (!fast_gauss || too_small_for_fast_gauss) { + ImageF* JXL_RESTRICT temp_t = temp->GetTransposed(in); + ConvolutionWithTranspose(in, kernel, temp_t); + ConvolutionWithTranspose(*temp_t, kernel, out); + return; + } + auto rg = CreateRecursiveGaussian(sigma); + ImageF* JXL_RESTRICT temp_ = temp->Get(in); + ThreadPool* null_pool = nullptr; + FastGaussian(rg, in, null_pool, temp_, out); + + if (kBorderFixup) { + // Produce rg_radius extra pixels around each border + const intptr_t rg_radius = rg->radius; + const intptr_t radius = kernel.size() / 2; + const intptr_t xsize = in.xsize(); + const intptr_t ysize = in.ysize(); + const intptr_t yend_top = std::min(rg_radius + radius, ysize); + const intptr_t ybegin_bottom = + std::max(intptr_t(0), ysize - rg_radius - radius); + // Top (requires radius extra for the vertical pass) + BlurHorizontalConv(in, 0, xsize, 0, yend_top, kernel, temp_); + // Bottom + BlurHorizontalConv(in, 0, xsize, ybegin_bottom, ysize, kernel, temp_); + // Left/right columns between top and bottom + const intptr_t xbegin_right = std::max(intptr_t(0), xsize - rg_radius); + const intptr_t xend_left = std::min(rg_radius, xsize); + BlurHorizontalConv(in, 0, xend_left, yend_top, ybegin_bottom, kernel, + temp_); + BlurHorizontalConv(in, xbegin_right, xsize, yend_top, ybegin_bottom, kernel, + temp_); + + // Entire left/right columns + BlurVerticalConv(*temp_, 0, xend_left, 0, ysize, kernel, out); + BlurVerticalConv(*temp_, xbegin_right, xsize, 0, ysize, kernel, out); + // Top/bottom between left/right + const intptr_t ybegin_bottom2 = std::max(intptr_t(0), ysize - rg_radius); + const intptr_t yend_top2 = std::min(rg_radius, ysize); + BlurVerticalConv(*temp_, xend_left, xbegin_right, 0, yend_top2, kernel, + out); + BlurVerticalConv(*temp_, xend_left, xbegin_right, ybegin_bottom2, ysize, + kernel, out); + } +} + +// Allows PaddedMaltaUnit to call either function via overloading. +struct MaltaTagLF {}; +struct MaltaTag {}; + +} // namespace jxl + +#endif // JXL_BUTTERAUGLI_ONCE + +#include +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Vec; + +template +HWY_INLINE V MaximumClamp(D d, V v, double kMaxVal) { + static const double kMul = 0.724216145665; + const V mul = Set(d, kMul); + const V maxval = Set(d, kMaxVal); + // If greater than maxval or less than -maxval, replace with if_*. + const V if_pos = MulAdd(v - maxval, mul, maxval); + const V if_neg = MulSub(v + maxval, mul, maxval); + const V pos_or_v = IfThenElse(v >= maxval, if_pos, v); + return IfThenElse(v < Neg(maxval), if_neg, pos_or_v); +} + +// Make area around zero less important (remove it). +template +HWY_INLINE V RemoveRangeAroundZero(const D d, const double kw, const V x) { + const auto w = Set(d, kw); + return IfThenElse(x > w, x - w, IfThenElseZero(x < Neg(w), x + w)); +} + +// Make area around zero more important (2x it until the limit). +template +HWY_INLINE V AmplifyRangeAroundZero(const D d, const double kw, const V x) { + const auto w = Set(d, kw); + return IfThenElse(x > w, x + w, IfThenElse(x < Neg(w), x - w, x + x)); +} + +// XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. +// Vals space can be converted to L2-norm space (Euclidean and normalized) +// through visual masking. +template +HWY_INLINE void XybLowFreqToVals(const D d, const V& x, const V& y, + const V& b_arg, V* HWY_RESTRICT valx, + V* HWY_RESTRICT valy, V* HWY_RESTRICT valb) { + static const double xmuli = 32.2217497012; + static const double ymuli = 13.7697791434; + static const double bmuli = 47.504615728; + static const double y_to_b_muli = -0.362267051518; + const V xmul = Set(d, xmuli); + const V ymul = Set(d, ymuli); + const V bmul = Set(d, bmuli); + const V y_to_b_mul = Set(d, y_to_b_muli); + const V b = MulAdd(y_to_b_mul, y, b_arg); + *valb = b * bmul; + *valx = x * xmul; + *valy = y * ymul; +} + +void SuppressXByY(const ImageF& in_x, const ImageF& in_y, const double yw, + ImageF* HWY_RESTRICT out) { + JXL_DASSERT(SameSize(in_x, in_y) && SameSize(in_x, *out)); + const size_t xsize = in_x.xsize(); + const size_t ysize = in_x.ysize(); + + const HWY_FULL(float) d; + static const double s = 0.653020556257; + const auto sv = Set(d, s); + const auto one_minus_s = Set(d, 1.0 - s); + const auto ywv = Set(d, yw); + + for (size_t y = 0; y < ysize; ++y) { + const float* HWY_RESTRICT row_x = in_x.ConstRow(y); + const float* HWY_RESTRICT row_y = in_y.ConstRow(y); + float* HWY_RESTRICT row_out = out->Row(y); + + 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 scaler = MulAdd(ywv / MulAdd(vy, vy, ywv), one_minus_s, sv); + Store(scaler * vx, d, row_out + x); + } + } +} + +static void SeparateFrequencies(size_t xsize, size_t ysize, + const ButteraugliParams& params, + BlurTemp* blur_temp, const Image3F& xyb, + PsychoImage& ps) { + PROFILER_FUNC; + const HWY_FULL(float) d; + + // Extract lf ... + static const double kSigmaLf = 7.15593339443; + static const double kSigmaHf = 3.22489901262; + static const double kSigmaUhf = 1.56416327805; + ps.mf = Image3F(xsize, ysize); + ps.hf[0] = ImageF(xsize, ysize); + ps.hf[1] = ImageF(xsize, ysize); + ps.lf = Image3F(xyb.xsize(), xyb.ysize()); + ps.mf = Image3F(xyb.xsize(), xyb.ysize()); + for (int i = 0; i < 3; ++i) { + Blur(xyb.Plane(i), kSigmaLf, params, blur_temp, &ps.lf.Plane(i)); + + // ... and keep everything else in mf. + for (size_t y = 0; y < ysize; ++y) { + const float* BUTTERAUGLI_RESTRICT row_xyb = xyb.PlaneRow(i, y); + const float* BUTTERAUGLI_RESTRICT row_lf = ps.lf.ConstPlaneRow(i, y); + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(i, y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + const auto mf = Load(d, row_xyb + x) - Load(d, row_lf + x); + Store(mf, d, row_mf + x); + } + } + if (i == 2) { + Blur(ps.mf.Plane(i), kSigmaHf, params, blur_temp, &ps.mf.Plane(i)); + break; + } + // Divide mf into mf and hf. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(i, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + Store(Load(d, row_mf + x), d, row_hf + x); + } + } + Blur(ps.mf.Plane(i), kSigmaHf, params, blur_temp, &ps.mf.Plane(i)); + static const double kRemoveMfRange = 0.29; + static const double kAddMfRange = 0.1; + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto mf = Load(d, row_mf + x); + auto hf = Load(d, row_hf + x) - mf; + mf = RemoveRangeAroundZero(d, kRemoveMfRange, mf); + Store(mf, d, row_mf + x); + Store(hf, d, row_hf + x); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[1].Row(y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto mf = Load(d, row_mf + x); + auto hf = Load(d, row_hf + x) - mf; + + mf = AmplifyRangeAroundZero(d, kAddMfRange, mf); + Store(mf, d, row_mf + x); + Store(hf, d, row_hf + x); + } + } + } + } + + // Temporarily used as output of SuppressXByY + ps.uhf[0] = ImageF(xsize, ysize); + ps.uhf[1] = ImageF(xsize, ysize); + + // Suppress red-green by intensity change in the high freq channels. + static const double suppress = 46.0; + SuppressXByY(ps.hf[0], ps.hf[1], suppress, &ps.uhf[0]); + // hf is the SuppressXByY output, uhf will be written below. + ps.hf[0].Swap(ps.uhf[0]); + + for (int i = 0; i < 2; ++i) { + // Divide hf into hf and uhf. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[i].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] = row_hf[x]; + } + } + Blur(ps.hf[i], kSigmaUhf, params, blur_temp, &ps.hf[i]); + static const double kRemoveHfRange = 1.5; + static const double kAddHfRange = 0.132; + static const double kRemoveUhfRange = 0.04; + static const double kMaxclampHf = 28.4691806922; + static const double kMaxclampUhf = 5.19175294647; + static double kMulYHf = 2.155; + static double kMulYUhf = 2.69313763794; + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[0].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto hf = Load(d, row_hf + x); + auto uhf = Load(d, row_uhf + x) - hf; + hf = RemoveRangeAroundZero(d, kRemoveHfRange, hf); + uhf = RemoveRangeAroundZero(d, kRemoveUhfRange, uhf); + Store(hf, d, row_hf + x); + Store(uhf, d, row_uhf + x); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[1].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[1].Row(y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto hf = Load(d, row_hf + x); + hf = MaximumClamp(d, hf, kMaxclampHf); + + auto uhf = Load(d, row_uhf + x) - hf; + uhf = MaximumClamp(d, uhf, kMaxclampUhf); + uhf *= Set(d, kMulYUhf); + Store(uhf, d, row_uhf + x); + + hf *= Set(d, kMulYHf); + hf = AmplifyRangeAroundZero(d, kAddHfRange, hf); + Store(hf, d, row_hf + x); + } + } + } + } + // Modify range around zero code only concerns the high frequency + // planes and only the X and Y channels. + // Convert low freq xyb to vals space so that we can do a simple squared sum + // diff on the low frequencies later. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_x = ps.lf.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_y = ps.lf.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_b = ps.lf.PlaneRow(2, y); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto valx = Undefined(d); + auto valy = Undefined(d); + auto valb = Undefined(d); + XybLowFreqToVals(d, Load(d, row_x + x), Load(d, row_y + x), + Load(d, row_b + x), &valx, &valy, &valb); + Store(valx, d, row_x + x); + Store(valy, d, row_y + x); + Store(valb, d, row_b + x); + } + } +} + +template +Vec MaltaUnit(MaltaTagLF /*tag*/, const D df, + const float* BUTTERAUGLI_RESTRICT d, const intptr_t xs) { + const intptr_t xs3 = 3 * xs; + + const auto center = LoadU(df, d); + + // x grows, y constant + const auto sum_yconst = LoadU(df, d - 4) + LoadU(df, d - 2) + center + + LoadU(df, d + 2) + LoadU(df, d + 4); + // Will return this, sum of all line kernels + auto retval = sum_yconst * sum_yconst; + { + // y grows, x constant + auto sum = LoadU(df, d - xs3 - xs) + LoadU(df, d - xs - xs) + center + + LoadU(df, d + xs + xs) + LoadU(df, d + xs3 + xs); + retval = MulAdd(sum, sum, retval); + } + { + // both grow + auto sum = LoadU(df, d - xs3 - 3) + LoadU(df, d - xs - xs - 2) + center + + LoadU(df, d + xs + xs + 2) + LoadU(df, d + xs3 + 3); + retval = MulAdd(sum, sum, retval); + } + { + // y grows, x shrinks + auto sum = LoadU(df, d - xs3 + 3) + LoadU(df, d - xs - xs + 2) + center + + LoadU(df, d + xs + xs - 2) + LoadU(df, d + xs3 - 3); + retval = MulAdd(sum, sum, retval); + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs - xs + 1) + + center + LoadU(df, d + xs + xs - 1) + + LoadU(df, d + xs3 + xs - 1); + retval = MulAdd(sum, sum, retval); + } + { + // y grows -4 to 4, x grows -1 -> 1 + auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs - xs - 1) + + center + LoadU(df, d + xs + xs + 1) + + LoadU(df, d + xs3 + xs + 1); + retval = MulAdd(sum, sum, retval); + } + { + // x grows -4 to 4, y grows -1 to 1 + auto sum = LoadU(df, d - 4 - xs) + LoadU(df, d - 2 - xs) + center + + LoadU(df, d + 2 + xs) + LoadU(df, d + 4 + xs); + retval = MulAdd(sum, sum, retval); + } + { + // x grows -4 to 4, y shrinks 1 to -1 + auto sum = LoadU(df, d - 4 + xs) + LoadU(df, d - 2 + xs) + center + + LoadU(df, d + 2 - xs) + LoadU(df, d + 4 - xs); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7______*__ + 8_________ */ + auto sum = LoadU(df, d - xs3 - 2) + LoadU(df, d - xs - xs - 1) + center + + LoadU(df, d + xs + xs + 1) + LoadU(df, d + xs3 + 2); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7__*______ + 8_________ */ + auto sum = LoadU(df, d - xs3 + 2) + LoadU(df, d - xs - xs + 1) + center + + LoadU(df, d + xs + xs - 1) + LoadU(df, d + xs3 - 2); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__*______ + 4____0____ + 5______*__ + 6_______*_ + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - xs - 3) + LoadU(df, d - xs - 2) + center + + LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 3); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_______*_ + 3______*__ + 4____0____ + 5__*______ + 6_*_______ + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - xs + 3) + LoadU(df, d - xs + 2) + center + + LoadU(df, d + xs - 2) + LoadU(df, d + xs + xs - 3); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2________* + 3______*__ + 4____0____ + 5__*______ + 6*________ + 7_________ + 8_________ */ + + auto sum = LoadU(df, d + xs + xs - 4) + LoadU(df, d + xs - 2) + center + + LoadU(df, d - xs + 2) + LoadU(df, d - xs - xs + 4); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2*________ + 3__*______ + 4____0____ + 5______*__ + 6________* + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - xs - 4) + LoadU(df, d - xs - 2) + center + + LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 4); + retval = MulAdd(sum, sum, retval); + } + { + /* 0__*______ + 1_________ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7_________ + 8______*__ */ + auto sum = LoadU(df, d - xs3 - xs - 2) + LoadU(df, d - xs - xs - 1) + + center + LoadU(df, d + xs + xs + 1) + + LoadU(df, d + xs3 + xs + 2); + retval = MulAdd(sum, sum, retval); + } + { + /* 0______*__ + 1_________ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7_________ + 8__*______ */ + auto sum = LoadU(df, d - xs3 - xs + 2) + LoadU(df, d - xs - xs + 1) + + center + LoadU(df, d + xs + xs - 1) + + LoadU(df, d + xs3 + xs - 2); + retval = MulAdd(sum, sum, retval); + } + return retval; +} + +template +Vec MaltaUnit(MaltaTag /*tag*/, const D df, + const float* BUTTERAUGLI_RESTRICT d, const intptr_t xs) { + const intptr_t xs3 = 3 * xs; + + const auto center = LoadU(df, d); + + // x grows, y constant + const auto sum_yconst = LoadU(df, d - 4) + LoadU(df, d - 3) + + LoadU(df, d - 2) + LoadU(df, d - 1) + center + + LoadU(df, d + 1) + LoadU(df, d + 2) + + LoadU(df, d + 3) + LoadU(df, d + 4); + // Will return this, sum of all line kernels + auto retval = sum_yconst * sum_yconst; + + { + // y grows, x constant + auto sum = LoadU(df, d - xs3 - xs) + LoadU(df, d - xs3) + + LoadU(df, d - xs - xs) + LoadU(df, d - xs) + center + + LoadU(df, d + xs) + LoadU(df, d + xs + xs) + LoadU(df, d + xs3) + + LoadU(df, d + xs3 + xs); + retval = MulAdd(sum, sum, retval); + } + { + // both grow + auto sum = LoadU(df, d - xs3 - 3) + LoadU(df, d - xs - xs - 2) + + LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + + LoadU(df, d + xs + xs + 2) + LoadU(df, d + xs3 + 3); + retval = MulAdd(sum, sum, retval); + } + { + // y grows, x shrinks + auto sum = LoadU(df, d - xs3 + 3) + LoadU(df, d - xs - xs + 2) + + LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + + LoadU(df, d + xs + xs - 2) + LoadU(df, d + xs3 - 3); + retval = MulAdd(sum, sum, retval); + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs3 + 1) + + LoadU(df, d - xs - xs + 1) + LoadU(df, d - xs) + center + + LoadU(df, d + xs) + LoadU(df, d + xs + xs - 1) + + LoadU(df, d + xs3 - 1) + LoadU(df, d + xs3 + xs - 1); + retval = MulAdd(sum, sum, retval); + } + { + // y grows -4 to 4, x grows -1 -> 1 + auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs3 - 1) + + LoadU(df, d - xs - xs - 1) + LoadU(df, d - xs) + center + + LoadU(df, d + xs) + LoadU(df, d + xs + xs + 1) + + LoadU(df, d + xs3 + 1) + LoadU(df, d + xs3 + xs + 1); + retval = MulAdd(sum, sum, retval); + } + { + // x grows -4 to 4, y grows -1 to 1 + auto sum = LoadU(df, d - 4 - xs) + LoadU(df, d - 3 - xs) + + LoadU(df, d - 2 - xs) + LoadU(df, d - 1) + center + + LoadU(df, d + 1) + LoadU(df, d + 2 + xs) + + LoadU(df, d + 3 + xs) + LoadU(df, d + 4 + xs); + retval = MulAdd(sum, sum, retval); + } + { + // x grows -4 to 4, y shrinks 1 to -1 + auto sum = LoadU(df, d - 4 + xs) + LoadU(df, d - 3 + xs) + + LoadU(df, d - 2 + xs) + LoadU(df, d - 1) + center + + LoadU(df, d + 1) + LoadU(df, d + 2 - xs) + + LoadU(df, d + 3 - xs) + LoadU(df, d + 4 - xs); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3___*_____ + 4____0____ + 5_____*___ + 6_____*___ + 7______*__ + 8_________ */ + auto sum = LoadU(df, d - xs3 - 2) + LoadU(df, d - xs - xs - 1) + + LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + + LoadU(df, d + xs + xs + 1) + LoadU(df, d + xs3 + 2); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_____*___ + 4____0____ + 5___*_____ + 6___*_____ + 7__*______ + 8_________ */ + auto sum = LoadU(df, d - xs3 + 2) + LoadU(df, d - xs - xs + 1) + + LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + + LoadU(df, d + xs + xs - 1) + LoadU(df, d + xs3 - 2); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__**_____ + 4____0____ + 5_____**__ + 6_______*_ + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - xs - 3) + LoadU(df, d - xs - 2) + + LoadU(df, d - xs - 1) + center + LoadU(df, d + xs + 1) + + LoadU(df, d + xs + 2) + LoadU(df, d + xs + xs + 3); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_______*_ + 3_____**__ + 4____0____ + 5__**_____ + 6_*_______ + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - xs + 3) + LoadU(df, d - xs + 2) + + LoadU(df, d - xs + 1) + center + LoadU(df, d + xs - 1) + + LoadU(df, d + xs - 2) + LoadU(df, d + xs + xs - 3); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_________ + 3______*** + 4___*0*___ + 5***______ + 6_________ + 7_________ + 8_________ */ + + auto sum = LoadU(df, d + xs - 4) + LoadU(df, d + xs - 3) + + LoadU(df, d + xs - 2) + LoadU(df, d - 1) + center + + LoadU(df, d + 1) + LoadU(df, d - xs + 2) + + LoadU(df, d - xs + 3) + LoadU(df, d - xs + 4); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_________ + 1_________ + 2_________ + 3***______ + 4___*0*___ + 5______*** + 6_________ + 7_________ + 8_________ */ + auto sum = LoadU(df, d - xs - 4) + LoadU(df, d - xs - 3) + + LoadU(df, d - xs - 2) + LoadU(df, d - 1) + center + + LoadU(df, d + 1) + LoadU(df, d + xs + 2) + + LoadU(df, d + xs + 3) + LoadU(df, d + xs + 4); + retval = MulAdd(sum, sum, retval); + } + { + /* 0___*_____ + 1___*_____ + 2___*_____ + 3____*____ + 4____0____ + 5____*____ + 6_____*___ + 7_____*___ + 8_____*___ */ + auto sum = LoadU(df, d - xs3 - xs - 1) + LoadU(df, d - xs3 - 1) + + LoadU(df, d - xs - xs - 1) + LoadU(df, d - xs) + center + + LoadU(df, d + xs) + LoadU(df, d + xs + xs + 1) + + LoadU(df, d + xs3 + 1) + LoadU(df, d + xs3 + xs + 1); + retval = MulAdd(sum, sum, retval); + } + { + /* 0_____*___ + 1_____*___ + 2____ *___ + 3____*____ + 4____0____ + 5____*____ + 6___*_____ + 7___*_____ + 8___*_____ */ + auto sum = LoadU(df, d - xs3 - xs + 1) + LoadU(df, d - xs3 + 1) + + LoadU(df, d - xs - xs + 1) + LoadU(df, d - xs) + center + + LoadU(df, d + xs) + LoadU(df, d + xs + xs - 1) + + LoadU(df, d + xs3 - 1) + LoadU(df, d + xs3 + xs - 1); + retval = MulAdd(sum, sum, retval); + } + return retval; +} + +// Returns MaltaUnit. Avoids bounds-checks when x0 and y0 are known +// to be far enough from the image borders. "diffs" is a packed image. +template +static BUTTERAUGLI_INLINE float PaddedMaltaUnit(const ImageF& diffs, + const size_t x0, + const size_t y0) { + const float* BUTTERAUGLI_RESTRICT d = diffs.ConstRow(y0) + x0; + const HWY_CAPPED(float, 1) df; + if ((x0 >= 4 && y0 >= 4 && x0 < (diffs.xsize() - 4) && + y0 < (diffs.ysize() - 4))) { + return GetLane(MaltaUnit(Tag(), df, d, diffs.PixelsPerRow())); + } + + PROFILER_ZONE("Padded Malta"); + float borderimage[12 * 9]; // round up to 4 + for (int dy = 0; dy < 9; ++dy) { + int y = y0 + dy - 4; + if (y < 0 || static_cast(y) >= diffs.ysize()) { + for (int dx = 0; dx < 12; ++dx) { + borderimage[dy * 12 + dx] = 0.0f; + } + continue; + } + + const float* row_diffs = diffs.ConstRow(y); + for (int dx = 0; dx < 9; ++dx) { + int x = x0 + dx - 4; + if (x < 0 || static_cast(x) >= diffs.xsize()) { + borderimage[dy * 12 + dx] = 0.0f; + } else { + borderimage[dy * 12 + dx] = row_diffs[x]; + } + } + std::fill(borderimage + dy * 12 + 9, borderimage + dy * 12 + 12, 0.0f); + } + return GetLane(MaltaUnit(Tag(), df, &borderimage[4 * 12 + 4], 12)); +} + +template +static void MaltaDiffMapT(const Tag tag, const ImageF& lum0, const ImageF& lum1, + const double w_0gt1, const double w_0lt1, + const double norm1, const double len, + const double mulli, ImageF* HWY_RESTRICT diffs, + Image3F* HWY_RESTRICT block_diff_ac, size_t c) { + JXL_DASSERT(SameSize(lum0, lum1) && SameSize(lum0, *diffs)); + const size_t xsize_ = lum0.xsize(); + const size_t ysize_ = lum0.ysize(); + + const float kWeight0 = 0.5; + const float kWeight1 = 0.33; + + const double w_pre0gt1 = mulli * std::sqrt(kWeight0 * w_0gt1) / (len * 2 + 1); + const double w_pre0lt1 = mulli * std::sqrt(kWeight1 * w_0lt1) / (len * 2 + 1); + const float norm2_0gt1 = w_pre0gt1 * norm1; + const float norm2_0lt1 = w_pre0lt1 * norm1; + + for (size_t y = 0; y < ysize_; ++y) { + const float* HWY_RESTRICT row0 = lum0.ConstRow(y); + const float* HWY_RESTRICT row1 = lum1.ConstRow(y); + float* HWY_RESTRICT row_diffs = diffs->Row(y); + for (size_t x = 0; x < xsize_; ++x) { + const float absval = 0.5f * (std::abs(row0[x]) + std::abs(row1[x])); + const float diff = row0[x] - row1[x]; + const float scaler = norm2_0gt1 / (static_cast(norm1) + absval); + + // Primary symmetric quadratic objective. + row_diffs[x] = scaler * diff; + + const float scaler2 = norm2_0lt1 / (static_cast(norm1) + absval); + const double fabs0 = std::fabs(row0[x]); + + // Secondary half-open quadratic objectives. + const double too_small = 0.55 * fabs0; + const double too_big = 1.05 * fabs0; + + if (row0[x] < 0) { + if (row1[x] > -too_small) { + double impact = scaler2 * (row1[x] + too_small); + if (diff < 0) { + row_diffs[x] -= impact; + } else { + row_diffs[x] += impact; + } + } else if (row1[x] < -too_big) { + double impact = scaler2 * (-row1[x] - too_big); + if (diff < 0) { + row_diffs[x] -= impact; + } else { + row_diffs[x] += impact; + } + } + } else { + if (row1[x] < too_small) { + double impact = scaler2 * (too_small - row1[x]); + if (diff < 0) { + row_diffs[x] -= impact; + } else { + row_diffs[x] += impact; + } + } else if (row1[x] > too_big) { + double impact = scaler2 * (row1[x] - too_big); + if (diff < 0) { + row_diffs[x] -= impact; + } else { + row_diffs[x] += impact; + } + } + } + } + } + + size_t y0 = 0; + // Top + for (; y0 < 4; ++y0) { + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += PaddedMaltaUnit(*diffs, x0, y0); + } + } + + const HWY_FULL(float) df; + const size_t aligned_x = std::max(size_t(4), Lanes(df)); + const intptr_t stride = diffs->PixelsPerRow(); + + // Middle + for (; y0 < ysize_ - 4; ++y0) { + const float* BUTTERAUGLI_RESTRICT row_in = diffs->ConstRow(y0); + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + size_t x0 = 0; + for (; x0 < aligned_x; ++x0) { + row_diff[x0] += PaddedMaltaUnit(*diffs, x0, y0); + } + for (; x0 + Lanes(df) + 4 <= xsize_; x0 += Lanes(df)) { + auto diff = Load(df, row_diff + x0); + diff += MaltaUnit(Tag(), df, row_in + x0, stride); + Store(diff, df, row_diff + x0); + } + + for (; x0 < xsize_; ++x0) { + row_diff[x0] += PaddedMaltaUnit(*diffs, x0, y0); + } + } + + // Bottom + for (; y0 < ysize_; ++y0) { + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += PaddedMaltaUnit(*diffs, x0, y0); + } + } +} + +// Need non-template wrapper functions for HWY_EXPORT. +void MaltaDiffMap(const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, const double len, + const double mulli, ImageF* HWY_RESTRICT diffs, + Image3F* HWY_RESTRICT block_diff_ac, size_t c) { + MaltaDiffMapT(MaltaTag(), lum0, lum1, w_0gt1, w_0lt1, norm1, len, mulli, + diffs, block_diff_ac, c); +} + +void MaltaDiffMapLF(const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, const double len, + const double mulli, ImageF* HWY_RESTRICT diffs, + Image3F* HWY_RESTRICT block_diff_ac, size_t c) { + MaltaDiffMapT(MaltaTagLF(), lum0, lum1, w_0gt1, w_0lt1, norm1, len, mulli, + diffs, block_diff_ac, c); +} + +void DiffPrecompute(const ImageF& xyb, float mul, float bias_arg, ImageF* out) { + PROFILER_FUNC; + const size_t xsize = xyb.xsize(); + const size_t ysize = xyb.ysize(); + const float bias = mul * bias_arg; + const float sqrt_bias = sqrt(bias); + for (size_t y = 0; y < ysize; ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = xyb.Row(y); + float* BUTTERAUGLI_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < xsize; ++x) { + // kBias makes sqrt behave more linearly. + row_out[x] = sqrt(mul * std::abs(row_in[x]) + bias) - sqrt_bias; + } + } +} + +// std::log(80.0) / std::log(255.0); +constexpr float kIntensityTargetNormalizationHack = 0.79079917404f; +static const float kInternalGoodQualityThreshold = + 17.8f * kIntensityTargetNormalizationHack; +static const float kGlobalScale = 1.0 / kInternalGoodQualityThreshold; + +void StoreMin3(const float v, float& min0, float& min1, float& min2) { + if (v < min2) { + if (v < min0) { + min2 = min1; + min1 = min0; + min0 = v; + } else if (v < min1) { + min2 = min1; + min1 = v; + } else { + min2 = v; + } + } +} + +// Look for smooth areas near the area of degradation. +// If the areas area generally smooth, don't do masking. +void FuzzyErosion(const ImageF& from, ImageF* to) { + const size_t xsize = from.xsize(); + const size_t ysize = from.ysize(); + static const int kStep = 3; + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + float min0 = from.Row(y)[x]; + float min1 = 2 * min0; + float min2 = min1; + if (x >= kStep) { + float v = from.Row(y)[x - kStep]; + StoreMin3(v, min0, min1, min2); + if (y >= kStep) { + float v = from.Row(y - kStep)[x - kStep]; + StoreMin3(v, min0, min1, min2); + } + if (y < ysize - kStep) { + float v = from.Row(y + kStep)[x - kStep]; + StoreMin3(v, min0, min1, min2); + } + } + if (x < xsize - kStep) { + float v = from.Row(y)[x + kStep]; + StoreMin3(v, min0, min1, min2); + if (y >= kStep) { + float v = from.Row(y - kStep)[x + kStep]; + StoreMin3(v, min0, min1, min2); + } + if (y < ysize - kStep) { + float v = from.Row(y + kStep)[x + kStep]; + StoreMin3(v, min0, min1, min2); + } + } + if (y >= kStep) { + float v = from.Row(y - kStep)[x]; + StoreMin3(v, min0, min1, min2); + } + if (y < ysize - kStep) { + float v = from.Row(y + kStep)[x]; + StoreMin3(v, min0, min1, min2); + } + to->Row(y)[x] = (0.45f * min0 + 0.3f * min1 + 0.25f * min2); + } + } +} + +// Compute values of local frequency and dc masking based on the activity +// in the two images. img_diff_ac may be null. +void Mask(const ImageF& mask0, const ImageF& mask1, + const ButteraugliParams& params, BlurTemp* blur_temp, + ImageF* BUTTERAUGLI_RESTRICT mask, + ImageF* BUTTERAUGLI_RESTRICT diff_ac) { + // Only X and Y components are involved in masking. B's influence + // is considered less important in the high frequency area, and we + // don't model masking from lower frequency signals. + PROFILER_FUNC; + const size_t xsize = mask0.xsize(); + const size_t ysize = mask0.ysize(); + *mask = ImageF(xsize, ysize); + static const float kMul = 6.19424080439; + static const float kBias = 12.61050594197; + static const float kRadius = 2.7; + ImageF diff0(xsize, ysize); + ImageF diff1(xsize, ysize); + ImageF blurred0(xsize, ysize); + ImageF blurred1(xsize, ysize); + DiffPrecompute(mask0, kMul, kBias, &diff0); + DiffPrecompute(mask1, kMul, kBias, &diff1); + Blur(diff0, kRadius, params, blur_temp, &blurred0); + FuzzyErosion(blurred0, &diff0); + Blur(diff1, kRadius, params, blur_temp, &blurred1); + FuzzyErosion(blurred1, &diff1); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + mask->Row(y)[x] = diff1.Row(y)[x]; + if (diff_ac != nullptr) { + static const float kMaskToErrorMul = 10.0; + float diff = blurred0.Row(y)[x] - blurred1.Row(y)[x]; + diff_ac->Row(y)[x] += kMaskToErrorMul * diff * diff; + } + } + } +} + +// `diff_ac` may be null. +void MaskPsychoImage(const PsychoImage& pi0, const PsychoImage& pi1, + const size_t xsize, const size_t ysize, + const ButteraugliParams& params, Image3F* temp, + BlurTemp* blur_temp, ImageF* BUTTERAUGLI_RESTRICT mask, + ImageF* BUTTERAUGLI_RESTRICT diff_ac) { + ImageF mask0(xsize, ysize); + ImageF mask1(xsize, ysize); + static const float muls[3] = { + 2.5f, + 0.4f, + 0.4f, + }; + // Silly and unoptimized approach here. TODO(jyrki): rework this. + for (size_t y = 0; y < ysize; ++y) { + const float* BUTTERAUGLI_RESTRICT row_y_hf0 = pi0.hf[1].Row(y); + const float* BUTTERAUGLI_RESTRICT row_y_hf1 = pi1.hf[1].Row(y); + const float* BUTTERAUGLI_RESTRICT row_y_uhf0 = pi0.uhf[1].Row(y); + const float* BUTTERAUGLI_RESTRICT row_y_uhf1 = pi1.uhf[1].Row(y); + const float* BUTTERAUGLI_RESTRICT row_x_hf0 = pi0.hf[0].Row(y); + const float* BUTTERAUGLI_RESTRICT row_x_hf1 = pi1.hf[0].Row(y); + const float* BUTTERAUGLI_RESTRICT row_x_uhf0 = pi0.uhf[0].Row(y); + const float* BUTTERAUGLI_RESTRICT row_x_uhf1 = pi1.uhf[0].Row(y); + float* BUTTERAUGLI_RESTRICT row0 = mask0.Row(y); + float* BUTTERAUGLI_RESTRICT row1 = mask1.Row(y); + for (size_t x = 0; x < xsize; ++x) { + float xdiff0 = (row_x_uhf0[x] + row_x_hf0[x]) * muls[0]; + float xdiff1 = (row_x_uhf1[x] + row_x_hf1[x]) * muls[0]; + float ydiff0 = row_y_uhf0[x] * muls[1] + row_y_hf0[x] * muls[2]; + float ydiff1 = row_y_uhf1[x] * muls[1] + row_y_hf1[x] * muls[2]; + row0[x] = xdiff0 * xdiff0 + ydiff0 * ydiff0; + row0[x] = sqrt(row0[x]); + row1[x] = xdiff1 * xdiff1 + ydiff1 * ydiff1; + row1[x] = sqrt(row1[x]); + } + } + Mask(mask0, mask1, params, blur_temp, mask, diff_ac); +} + +double MaskY(double delta) { + static const double offset = 0.829591754942; + static const double scaler = 0.451936922203; + static const double mul = 2.5485944793; + const double c = mul / ((scaler * delta) + offset); + const double retval = kGlobalScale * (1.0 + c); + return retval * retval; +} + +double MaskDcY(double delta) { + static const double offset = 0.20025578522; + static const double scaler = 3.87449418804; + static const double mul = 0.505054525019; + const double c = mul / ((scaler * delta) + offset); + const double retval = kGlobalScale * (1.0 + c); + return retval * retval; +} + +inline float MaskColor(const float color[3], const float mask) { + return color[0] * mask + color[1] * mask + color[2] * mask; +} + +// Diffmap := sqrt of sum{diff images by multiplied by X and Y/B masks} +void CombineChannelsToDiffmap(const ImageF& mask, const Image3F& block_diff_dc, + const Image3F& block_diff_ac, float xmul, + ImageF* result) { + PROFILER_FUNC; + JXL_CHECK(SameSize(mask, *result)); + size_t xsize = mask.xsize(); + size_t ysize = mask.ysize(); + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_out = result->Row(y); + for (size_t x = 0; x < xsize; ++x) { + float val = mask.Row(y)[x]; + float maskval = MaskY(val); + float dc_maskval = MaskDcY(val); + float diff_dc[3]; + float diff_ac[3]; + for (int i = 0; i < 3; ++i) { + diff_dc[i] = block_diff_dc.PlaneRow(i, y)[x]; + diff_ac[i] = block_diff_ac.PlaneRow(i, y)[x]; + } + diff_ac[0] *= xmul; + diff_dc[0] *= xmul; + row_out[x] = + sqrt(MaskColor(diff_dc, dc_maskval) + MaskColor(diff_ac, maskval)); + } + } +} + +// Adds weighted L2 difference between i0 and i1 to diffmap. +static void L2Diff(const ImageF& i0, const ImageF& i1, const float w, + Image3F* BUTTERAUGLI_RESTRICT diffmap, size_t c) { + if (w == 0) return; + + const HWY_FULL(float) d; + const auto weight = Set(d, w); + + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = i0.ConstRow(y); + const float* BUTTERAUGLI_RESTRICT row1 = i1.ConstRow(y); + float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); + + for (size_t x = 0; x < i0.xsize(); x += Lanes(d)) { + const auto diff = Load(d, row0 + x) - Load(d, row1 + x); + const auto diff2 = diff * diff; + const auto prev = Load(d, row_diff + x); + Store(MulAdd(diff2, weight, prev), d, row_diff + x); + } + } +} + +// Initializes diffmap to the weighted L2 difference between i0 and i1. +static void SetL2Diff(const ImageF& i0, const ImageF& i1, const float w, + Image3F* BUTTERAUGLI_RESTRICT diffmap, size_t c) { + if (w == 0) return; + + const HWY_FULL(float) d; + const auto weight = Set(d, w); + + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = i0.ConstRow(y); + const float* BUTTERAUGLI_RESTRICT row1 = i1.ConstRow(y); + float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); + + for (size_t x = 0; x < i0.xsize(); x += Lanes(d)) { + const auto diff = Load(d, row0 + x) - Load(d, row1 + x); + const auto diff2 = diff * diff; + Store(diff2 * weight, d, row_diff + x); + } + } +} + +// i0 is the original image. +// i1 is the deformed copy. +static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, float w_0gt1, + float w_0lt1, + Image3F* BUTTERAUGLI_RESTRICT diffmap, size_t c) { + if (w_0gt1 == 0 && w_0lt1 == 0) { + return; + } + + const HWY_FULL(float) d; + const auto vw_0gt1 = Set(d, w_0gt1 * 0.8); + const auto vw_0lt1 = Set(d, w_0lt1 * 0.8); + + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = i0.Row(y); + const float* BUTTERAUGLI_RESTRICT row1 = i1.Row(y); + float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); + + for (size_t x = 0; x < i0.xsize(); x += Lanes(d)) { + const auto val0 = Load(d, row0 + x); + const auto val1 = Load(d, row1 + x); + + // Primary symmetric quadratic objective. + const auto diff = val0 - val1; + auto total = MulAdd(diff * diff, vw_0gt1, Load(d, row_diff + x)); + + // Secondary half-open quadratic objectives. + const auto fabs0 = Abs(val0); + const auto too_small = Set(d, 0.4) * fabs0; + const auto too_big = fabs0; + + const auto if_neg = + IfThenElse(val1 > Neg(too_small), val1 + too_small, + IfThenElseZero(val1 < Neg(too_big), Neg(val1) - too_big)); + const auto if_pos = + IfThenElse(val1 < too_small, too_small - val1, + IfThenElseZero(val1 > too_big, val1 - too_big)); + const auto v = IfThenElse(val0 < Zero(d), if_neg, if_pos); + total += vw_0lt1 * v * v; + Store(total, d, row_diff + x); + } + } +} + +// A simple HDR compatible gamma function. +template +V Gamma(const DF df, V v) { + // ln(2) constant folded in because we want std::log but have FastLog2f. + const auto kRetMul = Set(df, 19.245013259874995f * 0.693147180559945f); + const auto kRetAdd = Set(df, -23.16046239805755); + // This should happen rarely, but may lead to a NaN in log, which is + // undesirable. Since negative photons don't exist we solve the NaNs by + // clamping here. + v = ZeroIfNegative(v); + + const auto biased = v + Set(df, 9.9710635769299145); + const auto log = FastLog2f(df, biased); + // We could fold this into a custom Log2 polynomial, but there would be + // relatively little gain. + return MulAdd(kRetMul, log, kRetAdd); +} + +template +BUTTERAUGLI_INLINE void OpsinAbsorbance(const DF df, const V& in0, const V& in1, + const V& in2, V* JXL_RESTRICT out0, + V* JXL_RESTRICT out1, + V* JXL_RESTRICT out2) { + // https://en.wikipedia.org/wiki/Photopsin absorbance modeling. + static const double mixi0 = 0.29956550340058319; + static const double mixi1 = 0.63373087833825936; + static const double mixi2 = 0.077705617820981968; + static const double mixi3 = 1.7557483643287353; + static const double mixi4 = 0.22158691104574774; + static const double mixi5 = 0.69391388044116142; + static const double mixi6 = 0.0987313588422; + static const double mixi7 = 1.7557483643287353; + static const double mixi8 = 0.02; + static const double mixi9 = 0.02; + static const double mixi10 = 0.20480129041026129; + static const double mixi11 = 12.226454707163354; + + const V mix0 = Set(df, mixi0); + const V mix1 = Set(df, mixi1); + const V mix2 = Set(df, mixi2); + const V mix3 = Set(df, mixi3); + const V mix4 = Set(df, mixi4); + const V mix5 = Set(df, mixi5); + const V mix6 = Set(df, mixi6); + const V mix7 = Set(df, mixi7); + const V mix8 = Set(df, mixi8); + const V mix9 = Set(df, mixi9); + const V mix10 = Set(df, mixi10); + const V mix11 = Set(df, mixi11); + + *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; + *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; + *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; + + if (Clamp) { + *out0 = Max(*out0, mix3); + *out1 = Max(*out1, mix7); + *out2 = Max(*out2, mix11); + } +} + +// `blurred` is a temporary image used inside this function and not returned. +Image3F OpsinDynamicsImage(const Image3F& rgb, const ButteraugliParams& params, + Image3F* blurred, BlurTemp* blur_temp) { + PROFILER_FUNC; + Image3F xyb(rgb.xsize(), rgb.ysize()); + const double kSigma = 1.2; + Blur(rgb.Plane(0), kSigma, params, blur_temp, &blurred->Plane(0)); + Blur(rgb.Plane(1), kSigma, params, blur_temp, &blurred->Plane(1)); + Blur(rgb.Plane(2), kSigma, params, blur_temp, &blurred->Plane(2)); + const HWY_FULL(float) df; + const auto intensity_target_multiplier = Set(df, params.intensity_target); + for (size_t y = 0; y < rgb.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_r = rgb.ConstPlaneRow(0, y); + const float* BUTTERAUGLI_RESTRICT row_g = rgb.ConstPlaneRow(1, y); + const float* BUTTERAUGLI_RESTRICT row_b = rgb.ConstPlaneRow(2, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_r = + blurred->ConstPlaneRow(0, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_g = + blurred->ConstPlaneRow(1, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_b = + blurred->ConstPlaneRow(2, y); + float* BUTTERAUGLI_RESTRICT row_out_x = xyb.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_out_y = xyb.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_out_b = xyb.PlaneRow(2, y); + const auto min = Set(df, 1e-4f); + for (size_t x = 0; x < rgb.xsize(); x += Lanes(df)) { + auto sensitivity0 = Undefined(df); + auto sensitivity1 = Undefined(df); + auto sensitivity2 = Undefined(df); + { + // Calculate sensitivity based on the smoothed image gamma derivative. + auto pre_mixed0 = Undefined(df); + auto pre_mixed1 = Undefined(df); + auto pre_mixed2 = Undefined(df); + OpsinAbsorbance( + df, Load(df, row_blurred_r + x) * intensity_target_multiplier, + Load(df, row_blurred_g + x) * intensity_target_multiplier, + Load(df, row_blurred_b + x) * intensity_target_multiplier, + &pre_mixed0, &pre_mixed1, &pre_mixed2); + pre_mixed0 = Max(pre_mixed0, min); + pre_mixed1 = Max(pre_mixed1, min); + pre_mixed2 = Max(pre_mixed2, min); + sensitivity0 = Gamma(df, pre_mixed0) / pre_mixed0; + sensitivity1 = Gamma(df, pre_mixed1) / pre_mixed1; + sensitivity2 = Gamma(df, pre_mixed2) / pre_mixed2; + sensitivity0 = Max(sensitivity0, min); + sensitivity1 = Max(sensitivity1, min); + sensitivity2 = Max(sensitivity2, min); + } + auto cur_mixed0 = Undefined(df); + auto cur_mixed1 = Undefined(df); + auto cur_mixed2 = Undefined(df); + OpsinAbsorbance(df, + Load(df, row_r + x) * intensity_target_multiplier, + Load(df, row_g + x) * intensity_target_multiplier, + Load(df, row_b + x) * intensity_target_multiplier, + &cur_mixed0, &cur_mixed1, &cur_mixed2); + cur_mixed0 *= sensitivity0; + cur_mixed1 *= sensitivity1; + cur_mixed2 *= sensitivity2; + // This is a kludge. The negative values should be zeroed away before + // blurring. Ideally there would be no negative values in the first place. + const auto min01 = Set(df, 1.7557483643287353f); + const auto min2 = Set(df, 12.226454707163354f); + cur_mixed0 = Max(cur_mixed0, min01); + cur_mixed1 = Max(cur_mixed1, min01); + cur_mixed2 = Max(cur_mixed2, min2); + + Store(cur_mixed0 - cur_mixed1, df, row_out_x + x); + Store(cur_mixed0 + cur_mixed1, df, row_out_y + x); + Store(cur_mixed2, df, row_out_b + x); + } + } + return xyb; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(SeparateFrequencies); // Local function. +HWY_EXPORT(MaskPsychoImage); // Local function. +HWY_EXPORT(L2DiffAsymmetric); // Local function. +HWY_EXPORT(L2Diff); // Local function. +HWY_EXPORT(SetL2Diff); // Local function. +HWY_EXPORT(CombineChannelsToDiffmap); // Local function. +HWY_EXPORT(MaltaDiffMap); // Local function. +HWY_EXPORT(MaltaDiffMapLF); // Local function. +HWY_EXPORT(OpsinDynamicsImage); // Local function. + +#if BUTTERAUGLI_ENABLE_CHECKS + +static inline bool IsNan(const float x) { + uint32_t bits; + memcpy(&bits, &x, sizeof(bits)); + const uint32_t bitmask_exp = 0x7F800000; + return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); +} + +static inline bool IsNan(const double x) { + uint64_t bits; + memcpy(&bits, &x, sizeof(bits)); + return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || + (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); +} + +static inline void CheckImage(const ImageF& image, const char* name) { + PROFILER_FUNC; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row = image.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + if (IsNan(row[x])) { + printf("NAN: Image %s @ %zu,%zu (of %zu,%zu)\n", name, x, y, + image.xsize(), image.ysize()); + exit(1); + } + } + } +} + +#define CHECK_NAN(x, str) \ + do { \ + if (IsNan(x)) { \ + printf("%d: %s\n", __LINE__, str); \ + abort(); \ + } \ + } while (0) + +#define CHECK_IMAGE(image, name) CheckImage(image, name) + +#else // BUTTERAUGLI_ENABLE_CHECKS + +#define CHECK_NAN(x, str) +#define CHECK_IMAGE(image, name) + +#endif // BUTTERAUGLI_ENABLE_CHECKS + +// Calculate a 2x2 subsampled image for purposes of recursive butteraugli at +// multiresolution. +static Image3F SubSample2x(const Image3F& in) { + size_t xs = (in.xsize() + 1) / 2; + size_t ys = (in.ysize() + 1) / 2; + Image3F retval(xs, ys); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ys; ++y) { + for (size_t x = 0; x < xs; ++x) { + retval.PlaneRow(c, y)[x] = 0; + } + } + } + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < in.ysize(); ++y) { + for (size_t x = 0; x < in.xsize(); ++x) { + retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * in.PlaneRow(c, y)[x]; + } + } + if ((in.xsize() & 1) != 0) { + for (size_t y = 0; y < retval.ysize(); ++y) { + size_t last_column = retval.xsize() - 1; + retval.PlaneRow(c, y)[last_column] *= 2.0f; + } + } + if ((in.ysize() & 1) != 0) { + for (size_t x = 0; x < retval.xsize(); ++x) { + size_t last_row = retval.ysize() - 1; + retval.PlaneRow(c, last_row)[x] *= 2.0f; + } + } + } + return retval; +} + +// Supersample src by 2x and add it to dest. +static void AddSupersampled2x(const ImageF& src, float w, ImageF& dest) { + for (size_t y = 0; y < dest.ysize(); ++y) { + for (size_t x = 0; x < dest.xsize(); ++x) { + // There will be less errors from the more averaged images. + // We take it into account to some extent using a scaler. + static const double kHeuristicMixingValue = 0.3; + dest.Row(y)[x] *= 1.0 - kHeuristicMixingValue * w; + dest.Row(y)[x] += w * src.Row(y / 2)[x / 2]; + } + } +} + +Image3F* ButteraugliComparator::Temp() const { + bool was_in_use = temp_in_use_.test_and_set(std::memory_order_acq_rel); + JXL_ASSERT(!was_in_use); + (void)was_in_use; + return &temp_; +} + +void ButteraugliComparator::ReleaseTemp() const { temp_in_use_.clear(); } + +ButteraugliComparator::ButteraugliComparator(const Image3F& rgb0, + const ButteraugliParams& params) + : xsize_(rgb0.xsize()), + ysize_(rgb0.ysize()), + params_(params), + temp_(xsize_, ysize_) { + if (xsize_ < 8 || ysize_ < 8) { + return; + } + + Image3F xyb0 = HWY_DYNAMIC_DISPATCH(OpsinDynamicsImage)(rgb0, params, Temp(), + &blur_temp_); + ReleaseTemp(); + HWY_DYNAMIC_DISPATCH(SeparateFrequencies) + (xsize_, ysize_, params_, &blur_temp_, xyb0, pi0_); + + // Awful recursive construction of samples of different resolution. + // This is an after-thought and possibly somewhat parallel in + // functionality with the PsychoImage multi-resolution approach. + sub_.reset(new ButteraugliComparator(SubSample2x(rgb0), params)); +} + +void ButteraugliComparator::Mask(ImageF* BUTTERAUGLI_RESTRICT mask) const { + HWY_DYNAMIC_DISPATCH(MaskPsychoImage) + (pi0_, pi0_, xsize_, ysize_, params_, Temp(), &blur_temp_, mask, nullptr); + ReleaseTemp(); +} + +void ButteraugliComparator::Diffmap(const Image3F& rgb1, ImageF& result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&result); + return; + } + const Image3F xyb1 = HWY_DYNAMIC_DISPATCH(OpsinDynamicsImage)( + rgb1, params_, Temp(), &blur_temp_); + ReleaseTemp(); + DiffmapOpsinDynamicsImage(xyb1, result); + if (sub_) { + if (sub_->xsize_ < 8 || sub_->ysize_ < 8) { + return; + } + const Image3F sub_xyb = HWY_DYNAMIC_DISPATCH(OpsinDynamicsImage)( + SubSample2x(rgb1), params_, sub_->Temp(), &sub_->blur_temp_); + sub_->ReleaseTemp(); + ImageF subresult; + sub_->DiffmapOpsinDynamicsImage(sub_xyb, subresult); + AddSupersampled2x(subresult, 0.5, result); + } +} + +void ButteraugliComparator::DiffmapOpsinDynamicsImage(const Image3F& xyb1, + ImageF& result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&result); + return; + } + PsychoImage pi1; + HWY_DYNAMIC_DISPATCH(SeparateFrequencies) + (xsize_, ysize_, params_, &blur_temp_, xyb1, pi1); + result = ImageF(xsize_, ysize_); + DiffmapPsychoImage(pi1, result); +} + +namespace { + +void MaltaDiffMap(const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, + ImageF* HWY_RESTRICT diffs, + Image3F* HWY_RESTRICT block_diff_ac, size_t c) { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.39905817637; + HWY_DYNAMIC_DISPATCH(MaltaDiffMap) + (lum0, lum1, w_0gt1, w_0lt1, norm1, len, mulli, diffs, block_diff_ac, c); +} + +void MaltaDiffMapLF(const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, + ImageF* HWY_RESTRICT diffs, + Image3F* HWY_RESTRICT block_diff_ac, size_t c) { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.611612573796; + HWY_DYNAMIC_DISPATCH(MaltaDiffMapLF) + (lum0, lum1, w_0gt1, w_0lt1, norm1, len, mulli, diffs, block_diff_ac, c); +} + +} // namespace + +void ButteraugliComparator::DiffmapPsychoImage(const PsychoImage& pi1, + ImageF& diffmap) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&diffmap); + return; + } + + const float hf_asymmetry_ = params_.hf_asymmetry; + const float xmul_ = params_.xmul; + + ImageF diffs(xsize_, ysize_); + Image3F block_diff_ac(xsize_, ysize_); + ZeroFillImage(&block_diff_ac); + static const double wUhfMalta = 1.10039032555; + static const double norm1Uhf = 71.7800275169; + MaltaDiffMap(pi0_.uhf[1], pi1.uhf[1], wUhfMalta * hf_asymmetry_, + wUhfMalta / hf_asymmetry_, norm1Uhf, &diffs, &block_diff_ac, 1); + + static const double wUhfMaltaX = 173.5; + static const double norm1UhfX = 5.0; + MaltaDiffMap(pi0_.uhf[0], pi1.uhf[0], wUhfMaltaX * hf_asymmetry_, + wUhfMaltaX / hf_asymmetry_, norm1UhfX, &diffs, &block_diff_ac, + 0); + + static const double wHfMalta = 18.7237414387; + static const double norm1Hf = 4498534.45232; + MaltaDiffMapLF(pi0_.hf[1], pi1.hf[1], wHfMalta * std::sqrt(hf_asymmetry_), + wHfMalta / std::sqrt(hf_asymmetry_), norm1Hf, &diffs, + &block_diff_ac, 1); + + static const double wHfMaltaX = 6923.99476109; + static const double norm1HfX = 8051.15833247; + MaltaDiffMapLF(pi0_.hf[0], pi1.hf[0], wHfMaltaX * std::sqrt(hf_asymmetry_), + wHfMaltaX / std::sqrt(hf_asymmetry_), norm1HfX, &diffs, + &block_diff_ac, 0); + + static const double wMfMalta = 37.0819870399; + static const double norm1Mf = 130262059.556; + MaltaDiffMapLF(pi0_.mf.Plane(1), pi1.mf.Plane(1), wMfMalta, wMfMalta, norm1Mf, + &diffs, &block_diff_ac, 1); + + static const double wMfMaltaX = 8246.75321353; + static const double norm1MfX = 1009002.70582; + MaltaDiffMapLF(pi0_.mf.Plane(0), pi1.mf.Plane(0), wMfMaltaX, wMfMaltaX, + norm1MfX, &diffs, &block_diff_ac, 0); + + static const double wmul[9] = { + 400.0, 1.50815703118, 0, + 2150.0, 10.6195433239, 16.2176043152, + 29.2353797994, 0.844626970982, 0.703646627719, + }; + Image3F block_diff_dc(xsize_, ysize_); + for (size_t c = 0; c < 3; ++c) { + if (c < 2) { // No blue channel error accumulated at HF. + HWY_DYNAMIC_DISPATCH(L2DiffAsymmetric) + (pi0_.hf[c], pi1.hf[c], wmul[c] * hf_asymmetry_, wmul[c] / hf_asymmetry_, + &block_diff_ac, c); + } + HWY_DYNAMIC_DISPATCH(L2Diff) + (pi0_.mf.Plane(c), pi1.mf.Plane(c), wmul[3 + c], &block_diff_ac, c); + HWY_DYNAMIC_DISPATCH(SetL2Diff) + (pi0_.lf.Plane(c), pi1.lf.Plane(c), wmul[6 + c], &block_diff_dc, c); + } + + ImageF mask; + HWY_DYNAMIC_DISPATCH(MaskPsychoImage) + (pi0_, pi1, xsize_, ysize_, params_, Temp(), &blur_temp_, &mask, + &block_diff_ac.Plane(1)); + ReleaseTemp(); + + HWY_DYNAMIC_DISPATCH(CombineChannelsToDiffmap) + (mask, block_diff_dc, block_diff_ac, xmul_, &diffmap); +} + +double ButteraugliScoreFromDiffmap(const ImageF& diffmap, + const ButteraugliParams* params) { + PROFILER_FUNC; + // In approximate-border mode, skip pixels on the border likely to be affected + // by FastGauss' zero-valued-boundary behavior. The border is about half of + // the largest-diameter kernel (37x37 pixels), but only if the image is big. + size_t border = (params != nullptr && params->approximate_border) ? 8 : 0; + if (diffmap.xsize() <= 2 * border || diffmap.ysize() <= 2 * border) { + border = 0; + } + float retval = 0.0f; + for (size_t y = border; y < diffmap.ysize() - border; ++y) { + const float* BUTTERAUGLI_RESTRICT row = diffmap.ConstRow(y); + for (size_t x = border; x < diffmap.xsize() - border; ++x) { + retval = std::max(retval, row[x]); + } + } + return retval; +} + +bool ButteraugliDiffmap(const Image3F& rgb0, const Image3F& rgb1, + double hf_asymmetry, double xmul, ImageF& diffmap) { + ButteraugliParams params; + params.hf_asymmetry = hf_asymmetry; + params.xmul = xmul; + return ButteraugliDiffmap(rgb0, rgb1, params, diffmap); +} + +bool ButteraugliDiffmap(const Image3F& rgb0, const Image3F& rgb1, + const ButteraugliParams& params, ImageF& diffmap) { + PROFILER_FUNC; + const size_t xsize = rgb0.xsize(); + const size_t ysize = rgb0.ysize(); + if (xsize < 1 || ysize < 1) { + return JXL_FAILURE("Zero-sized image"); + } + if (!SameSize(rgb0, rgb1)) { + return JXL_FAILURE("Size mismatch"); + } + static const int kMax = 8; + if (xsize < kMax || ysize < kMax) { + // Butteraugli values for small (where xsize or ysize is smaller + // than 8 pixels) images are non-sensical, but most likely it is + // less disruptive to try to compute something than just give up. + // Temporarily extend the borders of the image to fit 8 x 8 size. + size_t xborder = xsize < kMax ? (kMax - xsize) / 2 : 0; + size_t yborder = ysize < kMax ? (kMax - ysize) / 2 : 0; + size_t xscaled = std::max(kMax, xsize); + size_t yscaled = std::max(kMax, ysize); + Image3F scaled0(xscaled, yscaled); + Image3F scaled1(xscaled, yscaled); + for (int i = 0; i < 3; ++i) { + for (size_t y = 0; y < yscaled; ++y) { + for (size_t x = 0; x < xscaled; ++x) { + size_t x2 = + std::min(xsize - 1, std::max(0, x - xborder)); + size_t y2 = + std::min(ysize - 1, std::max(0, y - yborder)); + scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; + scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + } + } + } + ImageF diffmap_scaled; + const bool ok = + ButteraugliDiffmap(scaled0, scaled1, params, diffmap_scaled); + diffmap = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + diffmap.Row(y)[x] = diffmap_scaled.Row(y + yborder)[x + xborder]; + } + } + return ok; + } + ButteraugliComparator butteraugli(rgb0, params); + butteraugli.Diffmap(rgb1, diffmap); + return true; +} + +bool ButteraugliInterface(const Image3F& rgb0, const Image3F& rgb1, + float hf_asymmetry, float xmul, ImageF& diffmap, + double& diffvalue) { + ButteraugliParams params; + params.hf_asymmetry = hf_asymmetry; + params.xmul = xmul; + return ButteraugliInterface(rgb0, rgb1, params, diffmap, diffvalue); +} + +bool ButteraugliInterface(const Image3F& rgb0, const Image3F& rgb1, + const ButteraugliParams& params, ImageF& diffmap, + double& diffvalue) { +#if PROFILER_ENABLED + auto trace_start = std::chrono::steady_clock::now(); +#endif + if (!ButteraugliDiffmap(rgb0, rgb1, params, diffmap)) { + return false; + } +#if PROFILER_ENABLED + auto trace_end = std::chrono::steady_clock::now(); + std::chrono::duration elapsed = trace_end - trace_start; + const size_t mp = rgb0.xsize() * rgb0.ysize(); + printf("diff MP/s %f\n", mp / elapsed.count() * 1E-6); +#endif + diffvalue = ButteraugliScoreFromDiffmap(diffmap, ¶ms); + return true; +} + +double ButteraugliFuzzyClass(double score) { + static const double fuzzy_width_up = 4.8; + static const double fuzzy_width_down = 4.8; + static const double m0 = 2.0; + static const double scaler = 0.7777; + double val; + if (score < 1.0) { + // val in [scaler .. 2.0] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_down)); + val -= 1.0; // from [1 .. 2] to [0 .. 1] + val *= 2.0 - scaler; // from [0 .. 1] to [0 .. 2.0 - scaler] + val += scaler; // from [0 .. 2.0 - scaler] to [scaler .. 2.0] + } else { + // val in [0 .. scaler] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_up)); + val *= scaler; + } + return val; +} + +// #define PRINT_OUT_NORMALIZATION + +double ButteraugliFuzzyInverse(double seek) { + double pos = 0; + // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) + for (double range = 1.0; range >= 1e-10; range *= 0.5) { + double cur = ButteraugliFuzzyClass(pos); + if (cur < seek) { + pos -= range; + } else { + pos += range; + } + } +#ifdef PRINT_OUT_NORMALIZATION + if (seek == 1.0) { + fprintf(stderr, "Fuzzy inverse %g\n", pos); + } +#endif + return pos; +} + +#ifdef PRINT_OUT_NORMALIZATION +static double print_out_normalization = ButteraugliFuzzyInverse(1.0); +#endif + +namespace { + +void ScoreToRgb(double score, double good_threshold, double bad_threshold, + float rgb[3]) { + double heatmap[12][3] = { + {0, 0, 0}, {0, 0, 1}, + {0, 1, 1}, {0, 1, 0}, // Good level + {1, 1, 0}, {1, 0, 0}, // Bad level + {1, 0, 1}, {0.5, 0.5, 1.0}, + {1.0, 0.5, 0.5}, // Pastel colors for the very bad quality range. + {1.0, 1.0, 0.5}, {1, 1, 1}, + {1, 1, 1}, // Last color repeated to have a solid range of white. + }; + if (score < good_threshold) { + score = (score / good_threshold) * 0.3; + } else if (score < bad_threshold) { + score = 0.3 + + (score - good_threshold) / (bad_threshold - good_threshold) * 0.15; + } else { + score = 0.45 + (score - bad_threshold) / (bad_threshold * 12) * 0.5; + } + static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); + score = std::min(std::max(score * (kTableSize - 1), 0.0), + kTableSize - 2); + int ix = static_cast(score); + ix = std::min(std::max(0, ix), kTableSize - 2); // Handle NaN + double mix = score - ix; + for (int i = 0; i < 3; ++i) { + double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; + rgb[i] = pow(v, 0.5); + } +} + +} // namespace + +Image3F CreateHeatMapImage(const ImageF& distmap, double good_threshold, + double bad_threshold) { + Image3F heatmap(distmap.xsize(), distmap.ysize()); + for (size_t y = 0; y < distmap.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_distmap = distmap.ConstRow(y); + float* BUTTERAUGLI_RESTRICT row_h0 = heatmap.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_h1 = heatmap.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_h2 = heatmap.PlaneRow(2, y); + for (size_t x = 0; x < distmap.xsize(); ++x) { + const float d = row_distmap[x]; + float rgb[3]; + ScoreToRgb(d, good_threshold, bad_threshold, rgb); + row_h0[x] = rgb[0]; + row_h1[x] = rgb[1]; + row_h2[x] = rgb[2]; + } + } + return heatmap; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/butteraugli/butteraugli.h b/lib/jxl/butteraugli/butteraugli.h new file mode 100644 index 0000000..d029722 --- /dev/null +++ b/lib/jxl/butteraugli/butteraugli.h @@ -0,0 +1,220 @@ +// 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. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) + +#ifndef LIB_JXL_BUTTERAUGLI_BUTTERAUGLI_H_ +#define LIB_JXL_BUTTERAUGLI_BUTTERAUGLI_H_ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +#define BUTTERAUGLI_ENABLE_CHECKS 0 +#define BUTTERAUGLI_RESTRICT JXL_RESTRICT + +// This is the main interface to butteraugli image similarity +// analysis function. + +namespace jxl { + +struct ButteraugliParams { + // Multiplier for penalizing new HF artifacts more than blurring away + // features. 1.0=neutral. + float hf_asymmetry = 1.0f; + + // Multiplier for the psychovisual difference in the X channel. + float xmul = 1.0f; + + // Number of nits that correspond to 1.0f input values. + float intensity_target = 80.0f; + + bool approximate_border = false; +}; + +// ButteraugliInterface defines the public interface for butteraugli. +// +// It calculates the difference between rgb0 and rgb1. +// +// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains +// the red image for c == 0, green for c == 1, blue for c == 2. Location index +// px is calculated as y * xsize + x. +// +// Value of pixels of images rgb0 and rgb1 need to be represented as raw +// intensity. Most image formats store gamma corrected intensity in pixel +// values. This gamma correction has to be removed, by applying the following +// function to values in the 0-1 range: +// butteraugli_val = pow(input_val, gamma); +// A typical value of gamma is 2.2. It is usually stored in the image header. +// Take care not to confuse that value with its inverse. The gamma value should +// be always greater than one. +// Butteraugli does not work as intended if the caller does not perform +// gamma correction. +// +// hf_asymmetry is a multiplier for penalizing new HF artifacts more than +// blurring away features (1.0 -> neutral). +// +// diffmap will contain an image of the size xsize * ysize, containing +// localized differences for values px (indexed with the px the same as rgb0 +// and rgb1). diffvalue will give a global score of similarity. +// +// A diffvalue smaller than kButteraugliGood indicates that images can be +// observed as the same image. +// diffvalue larger than kButteraugliBad indicates that a difference between +// the images can be observed. +// A diffvalue between kButteraugliGood and kButteraugliBad indicates that +// a subtle difference can be observed between the images. +// +// Returns true on success. +bool ButteraugliInterface(const Image3F &rgb0, const Image3F &rgb1, + const ButteraugliParams ¶ms, ImageF &diffmap, + double &diffvalue); + +// Deprecated (calls the previous function) +bool ButteraugliInterface(const Image3F &rgb0, const Image3F &rgb1, + float hf_asymmetry, float xmul, ImageF &diffmap, + double &diffvalue); + +// Converts the butteraugli score into fuzzy class values that are continuous +// at the class boundary. The class boundary location is based on human +// raters, but the slope is arbitrary. Particularly, it does not reflect +// the expectation value of probabilities of the human raters. It is just +// expected that a smoother class boundary will allow for higher-level +// optimization algorithms to work faster. +// +// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the +// scoring is fuzzy, a butteraugli score of 0.96 would return a class of +// around 1.9. +double ButteraugliFuzzyClass(double score); + +// Input values should be in range 0 (bad) to 2 (good). Use +// kButteraugliNormalization as normalization. +double ButteraugliFuzzyInverse(double seek); + +// Implementation details, don't use anything below or your code will +// break in the future. + +#ifdef _MSC_VER +#define BUTTERAUGLI_INLINE __forceinline +#else +#define BUTTERAUGLI_INLINE inline +#endif + +#ifdef __clang__ +// Early versions of Clang did not support __builtin_assume_aligned. +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) +#elif defined(__GNUC__) +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1 +#else +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0 +#endif + +// Returns a void* pointer which the compiler then assumes is N-byte aligned. +// Example: float* JXL_RESTRICT aligned = (float*)JXL_ASSUME_ALIGNED(in, 32); +// +// The assignment semantics are required by GCC/Clang. ICC provides an in-place +// __assume_aligned, whereas MSVC's __assume appears unsuitable. +#if BUTTERAUGLI_HAS_ASSUME_ALIGNED +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) \ + __builtin_assume_aligned((ptr), (align)) +#else +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr) +#endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED + +struct PsychoImage { + ImageF uhf[2]; // XY + ImageF hf[2]; // XY + Image3F mf; // XYB + Image3F lf; // XYB +}; + +// Depending on implementation, Blur either needs a normal or transposed image. +// Hold one or both of them here and only allocate on demand to reduce memory +// usage. +struct BlurTemp { + ImageF *Get(const ImageF &in) { + if (temp.xsize() == 0) { + temp = ImageF(in.xsize(), in.ysize()); + } + return &temp; + } + + ImageF *GetTransposed(const ImageF &in) { + if (transposed_temp.xsize() == 0) { + transposed_temp = ImageF(in.ysize(), in.xsize()); + } + return &transposed_temp; + } + + ImageF temp; + ImageF transposed_temp; +}; + +class ButteraugliComparator { + public: + // Butteraugli is calibrated at xmul = 1.0. We add a multiplier here so that + // we can test the hypothesis that a higher weighing of the X channel would + // improve results at higher Butteraugli values. + ButteraugliComparator(const Image3F &rgb0, const ButteraugliParams ¶ms); + virtual ~ButteraugliComparator() = default; + + // Computes the butteraugli map between the original image given in the + // constructor and the distorted image give here. + void Diffmap(const Image3F &rgb1, ImageF &result) const; + + // Same as above, but OpsinDynamicsImage() was already applied. + void DiffmapOpsinDynamicsImage(const Image3F &xyb1, ImageF &result) const; + + // Same as above, but the frequency decomposition was already applied. + void DiffmapPsychoImage(const PsychoImage &pi1, ImageF &diffmap) const; + + void Mask(ImageF *BUTTERAUGLI_RESTRICT mask) const; + + private: + Image3F *Temp() const; + void ReleaseTemp() const; + + const size_t xsize_; + const size_t ysize_; + ButteraugliParams params_; + PsychoImage pi0_; + + // Shared temporary image storage to reduce the number of allocations; + // obtained via Temp(), must call ReleaseTemp when no longer needed. + mutable Image3F temp_; + mutable std::atomic_flag temp_in_use_ = ATOMIC_FLAG_INIT; + + mutable BlurTemp blur_temp_; + std::unique_ptr sub_; +}; + +// Deprecated. +bool ButteraugliDiffmap(const Image3F &rgb0, const Image3F &rgb1, + double hf_asymmetry, double xmul, ImageF &diffmap); + +bool ButteraugliDiffmap(const Image3F &rgb0, const Image3F &rgb1, + const ButteraugliParams ¶ms, ImageF &diffmap); + +double ButteraugliScoreFromDiffmap(const ImageF &diffmap, + const ButteraugliParams *params = nullptr); + +// Generate rgb-representation of the distance between two images. +Image3F CreateHeatMapImage(const ImageF &distmap, double good_threshold, + double bad_threshold); + +} // namespace jxl + +#endif // LIB_JXL_BUTTERAUGLI_BUTTERAUGLI_H_ diff --git a/lib/jxl/butteraugli_test.cc b/lib/jxl/butteraugli_test.cc new file mode 100644 index 0000000..98ec788 --- /dev/null +++ b/lib/jxl/butteraugli_test.cc @@ -0,0 +1,102 @@ +// 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 "jxl/butteraugli.h" + +#include "gtest/gtest.h" +#include "jxl/butteraugli_cxx.h" +#include "lib/jxl/test_utils.h" + +TEST(ButteraugliTest, Lossless) { + uint32_t xsize = 171; + uint32_t ysize = 219; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + JxlButteraugliApiPtr api(JxlButteraugliApiCreate(nullptr)); + JxlButteraugliResultPtr result(JxlButteraugliCompute( + api.get(), xsize, ysize, &pixel_format, pixels.data(), pixels.size(), + &pixel_format, pixels.data(), pixels.size())); + EXPECT_EQ(0.0, JxlButteraugliResultGetDistance(result.get(), 8.0)); +} + +TEST(ButteraugliTest, Distmap) { + uint32_t xsize = 171; + uint32_t ysize = 219; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + JxlButteraugliApiPtr api(JxlButteraugliApiCreate(nullptr)); + JxlButteraugliResultPtr result(JxlButteraugliCompute( + api.get(), xsize, ysize, &pixel_format, pixels.data(), pixels.size(), + &pixel_format, pixels.data(), pixels.size())); + EXPECT_EQ(0.0, JxlButteraugliResultGetDistance(result.get(), 8.0)); + const float* distmap; + uint32_t row_stride; + JxlButteraugliResultGetDistmap(result.get(), &distmap, &row_stride); + for (uint32_t y = 0; y < ysize; y++) { + for (uint32_t x = 0; x < xsize; x++) { + EXPECT_EQ(0.0, distmap[y * row_stride + x]); + } + } +} + +TEST(ButteraugliTest, Distorted) { + uint32_t xsize = 171; + uint32_t ysize = 219; + std::vector orig_pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + std::vector dist_pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + dist_pixels[0] += 128; + + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + JxlButteraugliApiPtr api(JxlButteraugliApiCreate(nullptr)); + JxlButteraugliResultPtr result(JxlButteraugliCompute( + api.get(), xsize, ysize, &pixel_format, orig_pixels.data(), + orig_pixels.size(), &pixel_format, dist_pixels.data(), + dist_pixels.size())); + EXPECT_NE(0.0, JxlButteraugliResultGetDistance(result.get(), 8.0)); +} + +TEST(ButteraugliTest, Api) { + uint32_t xsize = 171; + uint32_t ysize = 219; + std::vector orig_pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + std::vector dist_pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + dist_pixels[0] += 128; + + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + JxlButteraugliApiPtr api(JxlButteraugliApiCreate(nullptr)); + JxlButteraugliApiSetHFAsymmetry(api.get(), 1.0f); + JxlButteraugliApiSetIntensityTarget(api.get(), 250.0f); + JxlButteraugliResultPtr result(JxlButteraugliCompute( + api.get(), xsize, ysize, &pixel_format, orig_pixels.data(), + orig_pixels.size(), &pixel_format, dist_pixels.data(), + dist_pixels.size())); + double distance0 = JxlButteraugliResultGetDistance(result.get(), 8.0); + + JxlButteraugliApiSetHFAsymmetry(api.get(), 2.0f); + result.reset(JxlButteraugliCompute(api.get(), xsize, ysize, &pixel_format, + orig_pixels.data(), orig_pixels.size(), + &pixel_format, dist_pixels.data(), + dist_pixels.size())); + double distance1 = JxlButteraugliResultGetDistance(result.get(), 8.0); + + EXPECT_NE(distance0, distance1); + + JxlButteraugliApiSetIntensityTarget(api.get(), 80.0f); + result.reset(JxlButteraugliCompute(api.get(), xsize, ysize, &pixel_format, + orig_pixels.data(), orig_pixels.size(), + &pixel_format, dist_pixels.data(), + dist_pixels.size())); + double distance2 = JxlButteraugliResultGetDistance(result.get(), 8.0); + + EXPECT_NE(distance1, distance2); +} diff --git a/lib/jxl/butteraugli_wrapper.cc b/lib/jxl/butteraugli_wrapper.cc new file mode 100644 index 0000000..a2d2bc3 --- /dev/null +++ b/lib/jxl/butteraugli_wrapper.cc @@ -0,0 +1,207 @@ +// 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 +#include +#include + +#include + +#include "jxl/butteraugli.h" +#include "jxl/parallel_runner.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_butteraugli_pnorm.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/memory_manager_internal.h" + +namespace { + +void SetMetadataFromPixelFormat(const JxlPixelFormat* pixel_format, + jxl::ImageMetadata* metadata) { + uint32_t potential_alpha_bits = 0; + switch (pixel_format->data_type) { + case JXL_TYPE_FLOAT: + metadata->SetFloat32Samples(); + potential_alpha_bits = 16; + break; + case JXL_TYPE_FLOAT16: + 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; + break; + case JXL_TYPE_UINT8: + metadata->SetUintSamples(8); + potential_alpha_bits = 8; + break; + case JXL_TYPE_BOOLEAN: + metadata->SetUintSamples(2); + potential_alpha_bits = 2; + break; + } + if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) { + metadata->SetAlphaBits(potential_alpha_bits); + } +} + +} // namespace + +struct JxlButteraugliResultStruct { + JxlMemoryManager memory_manager; + + jxl::ImageF distmap; + jxl::ButteraugliParams params; +}; + +struct JxlButteraugliApiStruct { + // Multiplier for penalizing new HF artifacts more than blurring away + // features. 1.0=neutral. + float hf_asymmetry = 1.0f; + + // Multiplier for the psychovisual difference in the X channel. + float xmul = 1.0f; + + // Number of nits that correspond to 1.0f input values. + float intensity_target = jxl::kDefaultIntensityTarget; + + bool approximate_border = false; + + JxlMemoryManager memory_manager; + std::unique_ptr thread_pool{nullptr}; +}; + +JxlButteraugliApi* JxlButteraugliApiCreate( + const JxlMemoryManager* memory_manager) { + JxlMemoryManager local_memory_manager; + if (!jxl::MemoryManagerInit(&local_memory_manager, memory_manager)) + return nullptr; + + void* alloc = + jxl::MemoryManagerAlloc(&local_memory_manager, sizeof(JxlButteraugliApi)); + if (!alloc) return nullptr; + // Placement new constructor on allocated memory + JxlButteraugliApi* ret = new (alloc) JxlButteraugliApi(); + ret->memory_manager = local_memory_manager; + return ret; +} + +void JxlButteraugliApiSetParallelRunner(JxlButteraugliApi* api, + JxlParallelRunner parallel_runner, + void* parallel_runner_opaque) { + api->thread_pool = jxl::make_unique(parallel_runner, + parallel_runner_opaque); +} + +void JxlButteraugliApiSetHFAsymmetry(JxlButteraugliApi* api, float v) { + api->hf_asymmetry = v; +} + +void JxlButteraugliApiSetIntensityTarget(JxlButteraugliApi* api, float v) { + api->intensity_target = v; +} + +void JxlButteraugliApiDestroy(JxlButteraugliApi* api) { + if (api) { + // Call destructor directly since custom free function is used. + api->~JxlButteraugliApi(); + jxl::MemoryManagerFree(&api->memory_manager, api); + } +} + +JxlButteraugliResult* JxlButteraugliCompute( + const JxlButteraugliApi* api, uint32_t xsize, uint32_t ysize, + const JxlPixelFormat* pixel_format_orig, const void* buffer_orig, + size_t size_orig, const JxlPixelFormat* pixel_format_dist, + const void* buffer_dist, size_t size_dist) { + jxl::ImageMetadata orig_metadata; + SetMetadataFromPixelFormat(pixel_format_orig, &orig_metadata); + jxl::ImageBundle orig_ib(&orig_metadata); + jxl::ColorEncoding c_current; + if (pixel_format_orig->data_type == JXL_TYPE_FLOAT) { + c_current = + jxl::ColorEncoding::LinearSRGB(pixel_format_orig->num_channels < 3); + } else { + c_current = jxl::ColorEncoding::SRGB(pixel_format_orig->num_channels < 3); + } + if (!jxl::BufferToImageBundle(*pixel_format_orig, xsize, ysize, buffer_orig, + size_orig, api->thread_pool.get(), c_current, + &orig_ib)) { + return nullptr; + } + + jxl::ImageMetadata dist_metadata; + SetMetadataFromPixelFormat(pixel_format_dist, &dist_metadata); + jxl::ImageBundle dist_ib(&dist_metadata); + if (pixel_format_dist->data_type == JXL_TYPE_FLOAT) { + c_current = + jxl::ColorEncoding::LinearSRGB(pixel_format_dist->num_channels < 3); + } else { + c_current = jxl::ColorEncoding::SRGB(pixel_format_dist->num_channels < 3); + } + if (!jxl::BufferToImageBundle(*pixel_format_dist, xsize, ysize, buffer_dist, + size_dist, api->thread_pool.get(), c_current, + &dist_ib)) { + return nullptr; + } + + void* alloc = jxl::MemoryManagerAlloc(&api->memory_manager, + sizeof(JxlButteraugliResult)); + if (!alloc) return nullptr; + // Placement new constructor on allocated memory + JxlButteraugliResult* result = new (alloc) JxlButteraugliResult(); + result->memory_manager = api->memory_manager; + result->params.hf_asymmetry = api->hf_asymmetry; + result->params.xmul = api->xmul; + result->params.intensity_target = api->intensity_target; + result->params.approximate_border = api->approximate_border; + jxl::ButteraugliDistance(orig_ib, dist_ib, result->params, &result->distmap, + api->thread_pool.get()); + + return result; +} + +float JxlButteraugliResultGetDistance(const JxlButteraugliResult* result, + float pnorm) { + return static_cast( + jxl::ComputeDistanceP(result->distmap, result->params, pnorm)); +} + +void JxlButteraugliResultGetDistmap(const JxlButteraugliResult* result, + const float** buffer, + uint32_t* row_stride) { + *buffer = result->distmap.Row(0); + *row_stride = result->distmap.PixelsPerRow(); +} + +float JxlButteraugliResultGetMaxDistance(const JxlButteraugliResult* result) { + float max_distance = 0.0; + for (uint32_t y = 0; y < result->distmap.ysize(); y++) { + for (uint32_t x = 0; x < result->distmap.xsize(); x++) { + if (result->distmap.ConstRow(y)[x] > max_distance) { + max_distance = result->distmap.ConstRow(y)[x]; + } + } + } + return max_distance; +} + +void JxlButteraugliResultDestroy(JxlButteraugliResult* result) { + if (result) { + // Call destructor directly since custom free function is used. + result->~JxlButteraugliResult(); + jxl::MemoryManagerFree(&result->memory_manager, result); + } +} diff --git a/lib/jxl/byte_order_test.cc b/lib/jxl/byte_order_test.cc new file mode 100644 index 0000000..c1ea19f --- /dev/null +++ b/lib/jxl/byte_order_test.cc @@ -0,0 +1,53 @@ +// 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/byte_order.h" + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(ByteOrderTest, TestRoundTripBE16) { + const uint32_t in = 0x1234; + uint8_t buf[2]; + StoreBE16(in, buf); + EXPECT_EQ(in, LoadBE16(buf)); + EXPECT_NE(in, LoadLE16(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE16) { + const uint32_t in = 0x1234; + uint8_t buf[2]; + StoreLE16(in, buf); + EXPECT_EQ(in, LoadLE16(buf)); + EXPECT_NE(in, LoadBE16(buf)); +} + +TEST(ByteOrderTest, TestRoundTripBE32) { + const uint32_t in = 0xFEDCBA98u; + uint8_t buf[4]; + StoreBE32(in, buf); + EXPECT_EQ(in, LoadBE32(buf)); + EXPECT_NE(in, LoadLE32(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE32) { + const uint32_t in = 0xFEDCBA98u; + uint8_t buf[4]; + StoreLE32(in, buf); + EXPECT_EQ(in, LoadLE32(buf)); + EXPECT_NE(in, LoadBE32(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE64) { + const uint64_t in = 0xFEDCBA9876543210ull; + uint8_t buf[8]; + StoreLE64(in, buf); + EXPECT_EQ(in, LoadLE64(buf)); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/chroma_from_luma.cc b/lib/jxl/chroma_from_luma.cc new file mode 100644 index 0000000..63d21cb --- /dev/null +++ b/lib/jxl/chroma_from_luma.cc @@ -0,0 +1,21 @@ +// 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/chroma_from_luma.h" + +namespace jxl { + +ColorCorrelationMap::ColorCorrelationMap(size_t xsize, size_t ysize, bool XYB) + : ytox_map(DivCeil(xsize, kColorTileDim), DivCeil(ysize, kColorTileDim)), + ytob_map(DivCeil(xsize, kColorTileDim), DivCeil(ysize, kColorTileDim)) { + ZeroFillImage(&ytox_map); + ZeroFillImage(&ytob_map); + if (!XYB) { + base_correlation_b_ = 0; + } + RecomputeDCFactors(); +} + +} // namespace jxl diff --git a/lib/jxl/chroma_from_luma.h b/lib/jxl/chroma_from_luma.h new file mode 100644 index 0000000..cf2f90e --- /dev/null +++ b/lib/jxl/chroma_from_luma.h @@ -0,0 +1,151 @@ +// 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_CHROMA_FROM_LUMA_H_ +#define LIB_JXL_CHROMA_FROM_LUMA_H_ + +// Chroma-from-luma, computed using heuristics to determine the best linear +// model for the X and B channels from the Y channel. + +#include +#include + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quant_weights.h" + +namespace jxl { + +// Tile is the rectangular grid of blocks that share color correlation +// parameters ("factor_x/b" such that residual_b = blue - Y * factor_b). +static constexpr size_t kColorTileDim = 64; + +static_assert(kColorTileDim % kBlockDim == 0, + "Color tile dim should be divisible by block dim"); +static constexpr size_t kColorTileDimInBlocks = kColorTileDim / kBlockDim; + +static_assert(kGroupDimInBlocks % kColorTileDimInBlocks == 0, + "Group dim should be divisible by color tile dim"); + +static constexpr uint8_t kDefaultColorFactor = 84; + +// JPEG DCT coefficients are at most 1024. CfL constants are at most 127, and +// the ratio of two entries in a JPEG quantization table is at most 255. Thus, +// since the CfL denominator is 84, this leaves 12 bits of mantissa to be used. +// For extra caution, we use 11. +static constexpr uint8_t kCFLFixedPointPrecision = 11; + +static constexpr U32Enc kColorFactorDist(Val(kDefaultColorFactor), Val(256), + BitsOffset(8, 2), BitsOffset(16, 258)); + +struct ColorCorrelationMap { + ColorCorrelationMap() = default; + // xsize/ysize are in pixels + // set XYB=false to do something close to no-op cmap (needed for now since + // cmap is mandatory) + ColorCorrelationMap(size_t xsize, size_t ysize, bool XYB = true); + + float YtoXRatio(int32_t x_factor) const { + return base_correlation_x_ + x_factor * color_scale_; + } + + float YtoBRatio(int32_t b_factor) const { + return base_correlation_b_ + b_factor * color_scale_; + } + + Status DecodeDC(BitReader* br) { + if (br->ReadFixedBits<1>() == 1) { + // All default. + return true; + } + SetColorFactor(U32Coder::Read(kColorFactorDist, br)); + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &base_correlation_x_)); + if (std::abs(base_correlation_x_) > 4.0f) { + return JXL_FAILURE("Base X correlation is out of range"); + } + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &base_correlation_b_)); + if (std::abs(base_correlation_b_) > 4.0f) { + return JXL_FAILURE("Base B correlation is out of range"); + } + ytox_dc_ = static_cast(br->ReadFixedBits()) + + std::numeric_limits::min(); + ytob_dc_ = static_cast(br->ReadFixedBits()) + + std::numeric_limits::min(); + RecomputeDCFactors(); + return true; + } + + // We consider a CfL map to be JPEG-reconstruction-compatible if base + // correlation is 0, no DC correlation is used, and we use the default color + // factor. + bool IsJPEGCompatible() const { + return base_correlation_x_ == 0 && base_correlation_b_ == 0 && + ytob_dc_ == 0 && ytox_dc_ == 0 && + color_factor_ == kDefaultColorFactor; + } + + int32_t RatioJPEG(int32_t factor) const { + return factor * (1 << kCFLFixedPointPrecision) / kDefaultColorFactor; + } + + void SetColorFactor(uint32_t factor) { + color_factor_ = factor; + color_scale_ = 1.0f / color_factor_; + RecomputeDCFactors(); + } + + void SetYToBDC(int32_t ytob_dc) { + ytob_dc_ = ytob_dc; + RecomputeDCFactors(); + } + void SetYToXDC(int32_t ytox_dc) { + ytox_dc_ = ytox_dc; + RecomputeDCFactors(); + } + + int32_t GetYToXDC() const { return ytox_dc_; } + int32_t GetYToBDC() const { return ytob_dc_; } + float GetColorFactor() const { return color_factor_; } + float GetBaseCorrelationX() const { return base_correlation_x_; } + float GetBaseCorrelationB() const { return base_correlation_b_; } + + const float* DCFactors() const { return dc_factors_; } + + void RecomputeDCFactors() { + dc_factors_[0] = YtoXRatio(ytox_dc_); + dc_factors_[2] = YtoBRatio(ytob_dc_); + } + + ImageSB ytox_map; + ImageSB ytob_map; + + private: + float dc_factors_[4] = {}; + // range of factor: -1.51 to +1.52 + uint32_t color_factor_ = kDefaultColorFactor; + float color_scale_ = 1.0f / color_factor_; + float base_correlation_x_ = 0.0f; + float base_correlation_b_ = kYToBRatio; + int32_t ytox_dc_ = 0; + int32_t ytob_dc_ = 0; +}; + +} // namespace jxl + +#endif // LIB_JXL_CHROMA_FROM_LUMA_H_ diff --git a/lib/jxl/codec_in_out.h b/lib/jxl/codec_in_out.h new file mode 100644 index 0000000..d7ea67b --- /dev/null +++ b/lib/jxl/codec_in_out.h @@ -0,0 +1,213 @@ +// 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_CODEC_IN_OUT_H_ +#define LIB_JXL_CODEC_IN_OUT_H_ + +// Holds inputs/outputs for decoding/encoding images. + +#include + +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/common.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/luminance.h" + +namespace jxl { + +// Per-channel interval, used to convert between (full-range) external and +// (bounded or unbounded) temp values. See external_image.cc for the definitions +// of temp/external. +struct CodecInterval { + CodecInterval() = default; + constexpr CodecInterval(float min, float max) : min(min), width(max - min) {} + // Defaults for temp. + float min = 0.0f; + float width = 1.0f; +}; + +struct SizeConstraints { + // Upper limit on pixel dimensions/area, enforced by VerifyDimensions + // (called from decoders). Fuzzers set smaller values to limit memory use. + uint32_t dec_max_xsize = 0xFFFFFFFFu; + uint32_t dec_max_ysize = 0xFFFFFFFFu; + uint64_t dec_max_pixels = 0xFFFFFFFFu; // Might be up to ~0ull +}; + +template ::value>::type> +Status VerifyDimensions(const SizeConstraints* constraints, T xs, T ys) { + if (!constraints) return true; + + if (xs == 0 || ys == 0) return JXL_FAILURE("Empty image."); + if (xs > constraints->dec_max_xsize) return JXL_FAILURE("Image too wide."); + if (ys > constraints->dec_max_ysize) return JXL_FAILURE("Image too tall."); + + const uint64_t num_pixels = static_cast(xs) * ys; + if (num_pixels > constraints->dec_max_pixels) { + return JXL_FAILURE("Image too big."); + } + + return true; +} + +using CodecIntervals = std::array; // RGB[A] or Y[A] + +// Optional text/EXIF metadata. +struct Blobs { + PaddedBytes exif; + PaddedBytes iptc; + PaddedBytes jumbf; + PaddedBytes xmp; +}; + +// For Codec::kJPG, convert between JPEG and pixels or between JPEG and +// quantized DCT coefficients +// For pixel data, the nominal range is 0..1. +enum class DecodeTarget { kPixels, kQuantizedCoeffs }; + +// Holds a preview, a main image or one or more frames, plus the inputs/outputs +// to/from decoding/encoding. +class CodecInOut { + public: + CodecInOut() : preview_frame(&metadata.m) { + frames.reserve(1); + frames.emplace_back(&metadata.m); + } + + // Move-only. + CodecInOut(CodecInOut&&) = default; + CodecInOut& operator=(CodecInOut&&) = default; + + size_t LastStillFrame() const { + JXL_DASSERT(frames.size() > 0); + size_t last = 0; + for (size_t i = 0; i < frames.size(); i++) { + last = i; + if (frames[i].duration > 0) break; + } + return last; + } + + ImageBundle& Main() { return frames[LastStillFrame()]; } + const ImageBundle& Main() const { return frames[LastStillFrame()]; } + + // If c_current.IsGray(), all planes must be identical. + void SetFromImage(Image3F&& color, const ColorEncoding& c_current) { + Main().SetFromImage(std::move(color), c_current); + SetIntensityTarget(this); + SetSize(Main().xsize(), Main().ysize()); + } + + void SetSize(size_t xsize, size_t ysize) { + JXL_CHECK(metadata.size.Set(xsize, ysize)); + } + + void CheckMetadata() const { + JXL_CHECK(metadata.m.bit_depth.bits_per_sample != 0); + JXL_CHECK(!metadata.m.color_encoding.ICC().empty()); + + if (preview_frame.xsize() != 0) preview_frame.VerifyMetadata(); + JXL_CHECK(preview_frame.metadata() == &metadata.m); + + for (const ImageBundle& ib : frames) { + ib.VerifyMetadata(); + JXL_CHECK(ib.metadata() == &metadata.m); + } + } + + size_t xsize() const { return metadata.size.xsize(); } + size_t ysize() const { return metadata.size.ysize(); } + void ShrinkTo(size_t xsize, size_t ysize) { + // preview is unaffected. + for (ImageBundle& ib : frames) { + ib.ShrinkTo(xsize, ysize); + } + SetSize(xsize, ysize); + } + + // Calls TransformTo for each ImageBundle (preview/frames). + Status TransformTo(const ColorEncoding& c_desired, + ThreadPool* pool = nullptr) { + if (metadata.m.have_preview) { + JXL_RETURN_IF_ERROR(preview_frame.TransformTo(c_desired, pool)); + } + for (ImageBundle& ib : frames) { + JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, pool)); + } + return true; + } + // Calls PremultiplyAlpha for each ImageBundle (preview/frames). + void PremultiplyAlpha() { + ExtraChannelInfo* eci = metadata.m.Find(ExtraChannel::kAlpha); + if (eci == nullptr || eci->alpha_associated) return; // nothing to do + if (metadata.m.have_preview) { + preview_frame.PremultiplyAlpha(); + } + for (ImageBundle& ib : frames) { + ib.PremultiplyAlpha(); + } + eci->alpha_associated = true; + return; + } + + // -- DECODER INPUT: + + SizeConstraints constraints; + // Decode to pixels or keep JPEG as quantized DCT coefficients + DecodeTarget dec_target = DecodeTarget::kPixels; + + // Intended white luminance, in nits (cd/m^2). + // It is used by codecs that do not know the absolute luminance of their + // images. For those codecs, decoders map from white to this luminance. There + // is no other way of knowing the target brightness for those codecs - depends + // on source material. 709 typically targets 100 nits, BT.2100 PQ up to 10K, + // but HDR content is more typically mastered to 4K nits. Codecs that do know + // the absolute luminance of their images will typically ignore it as a + // decoder input. The corresponding decoder output and encoder input is the + // intensity target in the metadata. ALL decoders MUST set that metadata + // appropriately, but it does not have to be identical to this hint. Encoders + // for codecs that do not encode absolute luminance levels should use that + // metadata to decide on what to map to white. Encoders for codecs that *do* + // encode absolute luminance levels may use it to decide on encoding values, + // but not in a way that would affect the range of interpreted luminance. + // + // 0 means that it is up to the codec to decide on a reasonable value to use. + + float target_nits = 0; + + // -- DECODER OUTPUT: + + // Total number of pixels decoded (may differ from #frames * xsize * ysize + // if frames are cropped) + uint64_t dec_pixels = 0; + + // -- DECODER OUTPUT, ENCODER INPUT: + + // Metadata stored into / retrieved from bitstreams. + + Blobs blobs; + + CodecMetadata metadata; // applies to preview and all frames + + // If metadata.have_preview: + ImageBundle preview_frame; + + std::vector frames; // size=1 if !metadata.have_animation + + bool use_sjpeg = false; + // If the image should be written to a JPEG, use this quality for encoding. + size_t jpeg_quality; +}; + +} // namespace jxl + +#endif // LIB_JXL_CODEC_IN_OUT_H_ diff --git a/lib/jxl/coeff_order.cc b/lib/jxl/coeff_order.cc new file mode 100644 index 0000000..e877283 --- /dev/null +++ b/lib/jxl/coeff_order.cc @@ -0,0 +1,154 @@ +// 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/coeff_order.h" + +#include + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/lehmer_code.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +void SetDefaultOrder(AcStrategy acs, coeff_order_t* JXL_RESTRICT order) { + PROFILER_FUNC; + const size_t size = + kDCTBlockSize * acs.covered_blocks_x() * acs.covered_blocks_y(); + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + for (size_t k = 0; k < size; ++k) { + order[k] = natural_coeff_order[k]; + } +} + +uint32_t CoeffOrderContext(uint32_t val) { + uint32_t token, nbits, bits; + HybridUintConfig(0, 0, 0).Encode(val, &token, &nbits, &bits); + return std::min(token, kPermutationContexts - 1); +} + +namespace { +Status ReadPermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br, ANSSymbolReader* reader, + const std::vector& context_map) { + std::vector lehmer(size); + // temp space needs to be as large as the next power of 2, so doubling the + // allocated size is enough. + std::vector temp(size * 2); + uint32_t end = + reader->ReadHybridUint(CoeffOrderContext(size), br, context_map) + skip; + if (end > size) { + return JXL_FAILURE("Invalid permutation size"); + } + uint32_t last = 0; + for (size_t i = skip; i < end; ++i) { + lehmer[i] = + reader->ReadHybridUint(CoeffOrderContext(last), br, context_map); + last = lehmer[i]; + if (lehmer[i] + i >= size) { + return JXL_FAILURE("Invalid lehmer code"); + } + } + if (order == nullptr) return true; + DecodeLehmerCode(lehmer.data(), temp.data(), size, order); + return true; +} + +} // namespace + +Status DecodePermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br) { + std::vector context_map; + ANSCode code; + JXL_RETURN_IF_ERROR( + DecodeHistograms(br, kPermutationContexts, &code, &context_map)); + ANSSymbolReader reader(&code, br); + JXL_RETURN_IF_ERROR( + ReadPermutation(skip, size, order, br, &reader, context_map)); + if (!reader.CheckANSFinalState()) { + return JXL_FAILURE("Invalid ANS stream"); + } + return true; +} + +namespace { + +Status DecodeCoeffOrder(AcStrategy acs, coeff_order_t* order, BitReader* br, + ANSSymbolReader* reader, + const std::vector& context_map) { + PROFILER_FUNC; + const size_t llf = acs.covered_blocks_x() * acs.covered_blocks_y(); + const size_t size = kDCTBlockSize * llf; + + JXL_RETURN_IF_ERROR( + ReadPermutation(llf, size, order, br, reader, context_map)); + if (order == nullptr) return true; + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + for (size_t k = 0; k < size; ++k) { + order[k] = natural_coeff_order[order[k]]; + } + return true; +} + +} // namespace + +Status DecodeCoeffOrders(uint16_t used_orders, uint32_t used_acs, + coeff_order_t* order, BitReader* br) { + uint16_t computed = 0; + std::vector context_map; + ANSCode code; + std::unique_ptr reader; + // Bitstream does not have histograms if no coefficient order is used. + if (used_orders != 0) { + JXL_RETURN_IF_ERROR( + DecodeHistograms(br, kPermutationContexts, &code, &context_map)); + reader = make_unique(&code, br); + } + uint32_t acs_mask = 0; + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + if ((used_acs & (1 << o)) == 0) continue; + acs_mask |= 1 << kStrategyOrder[o]; + } + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + AcStrategy acs = AcStrategy::FromRawStrategy(o); + bool used = (acs_mask & (1 << ord)) != 0; + if ((used_orders & (1 << ord)) == 0) { + // No need to set the default order if no ACS uses this order. + if (used) { + for (size_t c = 0; c < 3; c++) { + SetDefaultOrder(acs, &order[CoeffOrderOffset(ord, c)]); + } + } + } else { + for (size_t c = 0; c < 3; c++) { + coeff_order_t* dest = used ? &order[CoeffOrderOffset(ord, c)] : nullptr; + JXL_RETURN_IF_ERROR( + DecodeCoeffOrder(acs, dest, br, reader.get(), context_map)); + } + } + } + if (used_orders && !reader->CheckANSFinalState()) { + return JXL_FAILURE("Invalid ANS stream"); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/coeff_order.h b/lib/jxl/coeff_order.h new file mode 100644 index 0000000..c600b7b --- /dev/null +++ b/lib/jxl/coeff_order.h @@ -0,0 +1,66 @@ +// 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_COEFF_ORDER_H_ +#define LIB_JXL_COEFF_ORDER_H_ + +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_bit_reader.h" + +namespace jxl { + +// Those offsets get multiplied by kDCTBlockSize. +static constexpr size_t kCoeffOrderOffset[] = { + 0, 1, 2, 3, 4, 5, 6, 10, 14, 18, + 34, 50, 66, 68, 70, 72, 76, 80, 84, 92, + 100, 108, 172, 236, 300, 332, 364, 396, 652, 908, + 1164, 1292, 1420, 1548, 2572, 3596, 4620, 5132, 5644, 6156, +}; +static_assert(3 * kNumOrders + 1 == + sizeof(kCoeffOrderOffset) / sizeof(*kCoeffOrderOffset), + "Update this array when adding or removing order types."); + +static constexpr size_t CoeffOrderOffset(size_t order, size_t c) { + return kCoeffOrderOffset[3 * order + c] * kDCTBlockSize; +} + +static constexpr size_t kCoeffOrderMaxSize = + kCoeffOrderOffset[3 * kNumOrders] * kDCTBlockSize; + +// Mapping from AC strategy to order bucket. Strategies with different natural +// orders must have different buckets. +constexpr uint8_t kStrategyOrder[] = { + 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, + 1, 1, 1, 1, 7, 8, 8, 9, 10, 10, 11, 12, 12, +}; + +static_assert(AcStrategy::kNumValidStrategies == + sizeof(kStrategyOrder) / sizeof(*kStrategyOrder), + "Update this array when adding or removing AC strategies."); + +constexpr uint32_t kPermutationContexts = 8; + +uint32_t CoeffOrderContext(uint32_t val); + +void SetDefaultOrder(AcStrategy acs, coeff_order_t* JXL_RESTRICT order); + +Status DecodeCoeffOrders(uint16_t used_orders, uint32_t used_acs, + coeff_order_t* order, BitReader* br); + +Status DecodePermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br); + +} // namespace jxl + +#endif // LIB_JXL_COEFF_ORDER_H_ diff --git a/lib/jxl/coeff_order_fwd.h b/lib/jxl/coeff_order_fwd.h new file mode 100644 index 0000000..700e9a8 --- /dev/null +++ b/lib/jxl/coeff_order_fwd.h @@ -0,0 +1,47 @@ +// 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_COEFF_ORDER_FWD_H_ +#define LIB_JXL_COEFF_ORDER_FWD_H_ + +// Breaks circular dependency between ac_strategy and coeff_order. + +#include +#include + +#include "base/compiler_specific.h" + +namespace jxl { + +// Needs at least 16 bits. A 32-bit type speeds up DecodeAC by 2% at the cost of +// more memory. +using coeff_order_t = uint32_t; + +// Maximum number of orders to be used. Note that this needs to be multiplied by +// the number of channels. One per "size class" (plus one extra for DCT8), +// shared between transforms of size XxY and of size YxX. +constexpr uint8_t kNumOrders = 13; + +// DCT coefficients are laid out in such a way that the number of rows of +// coefficients is always the smaller coordinate. +JXL_INLINE constexpr size_t CoefficientRows(size_t rows, size_t columns) { + return rows < columns ? rows : columns; +} + +JXL_INLINE constexpr size_t CoefficientColumns(size_t rows, size_t columns) { + return rows < columns ? columns : rows; +} + +JXL_INLINE void CoefficientLayout(size_t* JXL_RESTRICT rows, + size_t* JXL_RESTRICT columns) { + size_t r = *rows; + size_t c = *columns; + *rows = CoefficientRows(r, c); + *columns = CoefficientColumns(r, c); +} + +} // namespace jxl + +#endif // LIB_JXL_COEFF_ORDER_FWD_H_ diff --git a/lib/jxl/coeff_order_test.cc b/lib/jxl/coeff_order_test.cc new file mode 100644 index 0000000..2408905 --- /dev/null +++ b/lib/jxl/coeff_order_test.cc @@ -0,0 +1,101 @@ +// 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/coeff_order.h" + +#include + +#include +#include // iota +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_coeff_order.h" + +namespace jxl { +namespace { + +void RoundtripPermutation(coeff_order_t* perm, coeff_order_t* out, size_t len, + size_t* size) { + BitWriter writer; + EncodePermutation(perm, 0, len, &writer, 0, nullptr); + writer.ZeroPadToByte(); + Status status = true; + { + BitReader reader(writer.GetSpan()); + BitReaderScopedCloser closer(&reader, &status); + ASSERT_TRUE(DecodePermutation(0, len, out, &reader)); + } + ASSERT_TRUE(status); + *size = writer.GetSpan().size(); +} + +enum Permutation { kIdentity, kFewSwaps, kFewSlides, kRandom }; + +constexpr size_t kNumReps = 128; +constexpr size_t kSwaps = 32; + +void TestPermutation(Permutation kind, size_t len) { + std::vector perm(len); + std::iota(perm.begin(), perm.end(), 0); + std::mt19937 rng; + if (kind == kFewSwaps) { + std::uniform_int_distribution dist(0, len - 1); + for (size_t i = 0; i < kSwaps; i++) { + size_t a = dist(rng); + size_t b = dist(rng); + std::swap(perm[a], perm[b]); + } + } + if (kind == kFewSlides) { + std::uniform_int_distribution dist(0, len - 1); + for (size_t i = 0; i < kSwaps; i++) { + size_t a = dist(rng); + size_t b = dist(rng); + size_t from = std::min(a, b); + size_t to = std::max(a, b); + size_t start = perm[from]; + for (size_t j = from; j < to; j++) { + perm[j] = perm[j + 1]; + } + perm[to] = start; + } + } + if (kind == kRandom) { + std::shuffle(perm.begin(), perm.end(), rng); + } + std::vector out(len); + size_t size = 0; + for (size_t i = 0; i < kNumReps; i++) { + RoundtripPermutation(perm.data(), out.data(), len, &size); + for (size_t idx = 0; idx < len; idx++) { + EXPECT_EQ(perm[idx], out[idx]); + } + } + printf("Encoded size: %zu\n", size); +} + +TEST(CoeffOrderTest, IdentitySmall) { TestPermutation(kIdentity, 256); } +TEST(CoeffOrderTest, FewSlidesSmall) { TestPermutation(kFewSlides, 256); } +TEST(CoeffOrderTest, FewSwapsSmall) { TestPermutation(kFewSwaps, 256); } +TEST(CoeffOrderTest, RandomSmall) { TestPermutation(kRandom, 256); } + +TEST(CoeffOrderTest, IdentityMedium) { TestPermutation(kIdentity, 1 << 12); } +TEST(CoeffOrderTest, FewSlidesMedium) { TestPermutation(kFewSlides, 1 << 12); } +TEST(CoeffOrderTest, FewSwapsMedium) { TestPermutation(kFewSwaps, 1 << 12); } +TEST(CoeffOrderTest, RandomMedium) { TestPermutation(kRandom, 1 << 12); } + +TEST(CoeffOrderTest, IdentityBig) { TestPermutation(kIdentity, 1 << 16); } +TEST(CoeffOrderTest, FewSlidesBig) { TestPermutation(kFewSlides, 1 << 16); } +TEST(CoeffOrderTest, FewSwapsBig) { TestPermutation(kFewSwaps, 1 << 16); } +TEST(CoeffOrderTest, RandomBig) { TestPermutation(kRandom, 1 << 16); } + +} // namespace +} // namespace jxl diff --git a/lib/jxl/color_encoding_internal.cc b/lib/jxl/color_encoding_internal.cc new file mode 100644 index 0000000..af3d333 --- /dev/null +++ b/lib/jxl/color_encoding_internal.cc @@ -0,0 +1,731 @@ +// 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/color_encoding_internal.h" + +#include + +#include +#include + +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/linalg.h" + +namespace jxl { +namespace { + +// Highest reasonable value for the gamma of a transfer curve. +constexpr uint32_t kMaxGamma = 8192; + +// These strings are baked into Description - do not change. + +std::string ToString(ColorSpace color_space) { + switch (color_space) { + case ColorSpace::kRGB: + return "RGB"; + case ColorSpace::kGray: + return "Gra"; + case ColorSpace::kXYB: + return "XYB"; + case ColorSpace::kUnknown: + return "CS?"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid ColorSpace %u", static_cast(color_space)); +} + +std::string ToString(WhitePoint white_point) { + switch (white_point) { + case WhitePoint::kD65: + return "D65"; + case WhitePoint::kCustom: + return "Cst"; + case WhitePoint::kE: + return "EER"; + case WhitePoint::kDCI: + return "DCI"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid WhitePoint %u", static_cast(white_point)); +} + +std::string ToString(Primaries primaries) { + switch (primaries) { + case Primaries::kSRGB: + return "SRG"; + case Primaries::k2100: + return "202"; + case Primaries::kP3: + return "DCI"; + case Primaries::kCustom: + return "Cst"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid Primaries %u", static_cast(primaries)); +} + +std::string ToString(TransferFunction transfer_function) { + switch (transfer_function) { + case TransferFunction::kSRGB: + return "SRG"; + case TransferFunction::kLinear: + return "Lin"; + case TransferFunction::k709: + return "709"; + case TransferFunction::kPQ: + return "PeQ"; + case TransferFunction::kHLG: + return "HLG"; + case TransferFunction::kDCI: + return "DCI"; + case TransferFunction::kUnknown: + return "TF?"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid TransferFunction %u", + static_cast(transfer_function)); +} + +std::string ToString(RenderingIntent rendering_intent) { + switch (rendering_intent) { + case RenderingIntent::kPerceptual: + return "Per"; + case RenderingIntent::kRelative: + return "Rel"; + case RenderingIntent::kSaturation: + return "Sat"; + case RenderingIntent::kAbsolute: + return "Abs"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid RenderingIntent %u", + static_cast(rendering_intent)); +} + +static double F64FromCustomxyI32(const int32_t i) { return i * 1E-6; } +static Status F64ToCustomxyI32(const double f, int32_t* JXL_RESTRICT i) { + if (!(-4 <= f && f <= 4)) { + return JXL_FAILURE("F64 out of bounds for CustomxyI32"); + } + *i = static_cast(roundf(f * 1E6)); + return true; +} + +Status ConvertExternalToInternalWhitePoint(const JxlWhitePoint external, + WhitePoint* internal) { + switch (external) { + case JXL_WHITE_POINT_D65: + *internal = WhitePoint::kD65; + return true; + case JXL_WHITE_POINT_CUSTOM: + *internal = WhitePoint::kCustom; + return true; + case JXL_WHITE_POINT_E: + *internal = WhitePoint::kE; + return true; + case JXL_WHITE_POINT_DCI: + *internal = WhitePoint::kDCI; + return true; + } + return JXL_FAILURE("Invalid WhitePoint enum value"); +} + +Status ConvertExternalToInternalPrimaries(const JxlPrimaries external, + Primaries* internal) { + switch (external) { + case JXL_PRIMARIES_SRGB: + *internal = Primaries::kSRGB; + return true; + case JXL_PRIMARIES_CUSTOM: + *internal = Primaries::kCustom; + return true; + case JXL_PRIMARIES_2100: + *internal = Primaries::k2100; + return true; + case JXL_PRIMARIES_P3: + *internal = Primaries::kP3; + return true; + } + return JXL_FAILURE("Invalid Primaries enum value"); +} + +Status ConvertExternalToInternalTransferFunction( + const JxlTransferFunction external, TransferFunction* internal) { + switch (external) { + case JXL_TRANSFER_FUNCTION_709: + *internal = TransferFunction::k709; + return true; + case JXL_TRANSFER_FUNCTION_UNKNOWN: + *internal = TransferFunction::kUnknown; + return true; + case JXL_TRANSFER_FUNCTION_LINEAR: + *internal = TransferFunction::kLinear; + return true; + case JXL_TRANSFER_FUNCTION_SRGB: + *internal = TransferFunction::kSRGB; + return true; + case JXL_TRANSFER_FUNCTION_PQ: + *internal = TransferFunction::kPQ; + return true; + case JXL_TRANSFER_FUNCTION_DCI: + *internal = TransferFunction::kDCI; + return true; + case JXL_TRANSFER_FUNCTION_HLG: + *internal = TransferFunction::kHLG; + return true; + case JXL_TRANSFER_FUNCTION_GAMMA: + return JXL_FAILURE("Gamma should be handled separately"); + } + return JXL_FAILURE("Invalid TransferFunction enum value"); +} + +Status ConvertExternalToInternalRenderingIntent( + const JxlRenderingIntent external, RenderingIntent* internal) { + switch (external) { + case JXL_RENDERING_INTENT_PERCEPTUAL: + *internal = RenderingIntent::kPerceptual; + return true; + case JXL_RENDERING_INTENT_RELATIVE: + *internal = RenderingIntent::kRelative; + return true; + case JXL_RENDERING_INTENT_SATURATION: + *internal = RenderingIntent::kSaturation; + return true; + case JXL_RENDERING_INTENT_ABSOLUTE: + *internal = RenderingIntent::kAbsolute; + return true; + } + return JXL_FAILURE("Invalid RenderingIntent enum value"); +} + +} // namespace + +CIExy Customxy::Get() const { + CIExy xy; + xy.x = F64FromCustomxyI32(x); + xy.y = F64FromCustomxyI32(y); + return xy; +} + +Status Customxy::Set(const CIExy& xy) { + JXL_RETURN_IF_ERROR(F64ToCustomxyI32(xy.x, &x)); + JXL_RETURN_IF_ERROR(F64ToCustomxyI32(xy.y, &y)); + size_t extension_bits, total_bits; + if (!Bundle::CanEncode(*this, &extension_bits, &total_bits)) { + return JXL_FAILURE("Unable to encode XY %f %f", xy.x, xy.y); + } + return true; +} + +bool CustomTransferFunction::SetImplicit() { + if (nonserialized_color_space == ColorSpace::kXYB) { + if (!SetGamma(1.0 / 3)) JXL_ASSERT(false); + return true; + } + return false; +} + +Status CustomTransferFunction::SetGamma(double gamma) { + if (gamma < (1.0f / kMaxGamma) || gamma > 1.0) { + return JXL_FAILURE("Invalid gamma %f", gamma); + } + + have_gamma_ = false; + if (ApproxEq(gamma, 1.0)) { + transfer_function_ = TransferFunction::kLinear; + return true; + } + if (ApproxEq(gamma, 1.0 / 2.6)) { + transfer_function_ = TransferFunction::kDCI; + return true; + } + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + + have_gamma_ = true; + gamma_ = roundf(gamma * kGammaMul); + transfer_function_ = TransferFunction::kUnknown; + return true; +} + +namespace { + +std::array CreateC2(const Primaries pr, + const TransferFunction tf) { + std::array c2; + + { + ColorEncoding* c_rgb = c2.data() + 0; + c_rgb->SetColorSpace(ColorSpace::kRGB); + c_rgb->white_point = WhitePoint::kD65; + c_rgb->primaries = pr; + c_rgb->tf.SetTransferFunction(tf); + JXL_CHECK(c_rgb->CreateICC()); + } + + { + ColorEncoding* c_gray = c2.data() + 1; + c_gray->SetColorSpace(ColorSpace::kGray); + c_gray->white_point = WhitePoint::kD65; + c_gray->primaries = pr; + c_gray->tf.SetTransferFunction(tf); + JXL_CHECK(c_gray->CreateICC()); + } + + return c2; +} + +} // namespace + +const ColorEncoding& ColorEncoding::SRGB(bool is_gray) { + static std::array c2 = + CreateC2(Primaries::kSRGB, TransferFunction::kSRGB); + return c2[is_gray]; +} +const ColorEncoding& ColorEncoding::LinearSRGB(bool is_gray) { + static std::array c2 = + CreateC2(Primaries::kSRGB, TransferFunction::kLinear); + return c2[is_gray]; +} + +CIExy ColorEncoding::GetWhitePoint() const { + JXL_DASSERT(have_fields_); + CIExy xy; + switch (white_point) { + case WhitePoint::kCustom: + return white_.Get(); + + case WhitePoint::kD65: + xy.x = 0.3127; + xy.y = 0.3290; + return xy; + + case WhitePoint::kDCI: + // From https://ieeexplore.ieee.org/document/7290729 C.2 page 11 + xy.x = 0.314; + xy.y = 0.351; + return xy; + + case WhitePoint::kE: + xy.x = xy.y = 1.0 / 3; + return xy; + } + JXL_ABORT("Invalid WhitePoint %u", static_cast(white_point)); +} + +Status ColorEncoding::SetWhitePoint(const CIExy& xy) { + JXL_DASSERT(have_fields_); + if (xy.x == 0.0 || xy.y == 0.0) { + return JXL_FAILURE("Invalid white point %f %f", xy.x, xy.y); + } + if (ApproxEq(xy.x, 0.3127) && ApproxEq(xy.y, 0.3290)) { + white_point = WhitePoint::kD65; + return true; + } + if (ApproxEq(xy.x, 1.0 / 3) && ApproxEq(xy.y, 1.0 / 3)) { + white_point = WhitePoint::kE; + return true; + } + if (ApproxEq(xy.x, 0.314) && ApproxEq(xy.y, 0.351)) { + white_point = WhitePoint::kDCI; + return true; + } + white_point = WhitePoint::kCustom; + return white_.Set(xy); +} + +PrimariesCIExy ColorEncoding::GetPrimaries() const { + JXL_DASSERT(have_fields_); + JXL_ASSERT(HasPrimaries()); + PrimariesCIExy xy; + switch (primaries) { + case Primaries::kCustom: + xy.r = red_.Get(); + xy.g = green_.Get(); + xy.b = blue_.Get(); + return xy; + + case Primaries::kSRGB: + xy.r.x = 0.639998686; + xy.r.y = 0.330010138; + xy.g.x = 0.300003784; + xy.g.y = 0.600003357; + xy.b.x = 0.150002046; + xy.b.y = 0.059997204; + return xy; + + case Primaries::k2100: + xy.r.x = 0.708; + xy.r.y = 0.292; + xy.g.x = 0.170; + xy.g.y = 0.797; + xy.b.x = 0.131; + xy.b.y = 0.046; + return xy; + + case Primaries::kP3: + xy.r.x = 0.680; + xy.r.y = 0.320; + xy.g.x = 0.265; + xy.g.y = 0.690; + xy.b.x = 0.150; + xy.b.y = 0.060; + return xy; + } + JXL_ABORT("Invalid Primaries %u", static_cast(primaries)); +} + +Status ColorEncoding::SetPrimaries(const PrimariesCIExy& xy) { + JXL_DASSERT(have_fields_); + JXL_ASSERT(HasPrimaries()); + if (xy.r.x == 0.0 || xy.r.y == 0.0 || xy.g.x == 0.0 || xy.g.y == 0.0 || + xy.b.x == 0.0 || xy.b.y == 0.0) { + return JXL_FAILURE("Invalid primaries %f %f %f %f %f %f", xy.r.x, xy.r.y, + xy.g.x, xy.g.y, xy.b.x, xy.b.y); + } + + if (ApproxEq(xy.r.x, 0.64) && ApproxEq(xy.r.y, 0.33) && + ApproxEq(xy.g.x, 0.30) && ApproxEq(xy.g.y, 0.60) && + ApproxEq(xy.b.x, 0.15) && ApproxEq(xy.b.y, 0.06)) { + primaries = Primaries::kSRGB; + return true; + } + + if (ApproxEq(xy.r.x, 0.708) && ApproxEq(xy.r.y, 0.292) && + ApproxEq(xy.g.x, 0.170) && ApproxEq(xy.g.y, 0.797) && + ApproxEq(xy.b.x, 0.131) && ApproxEq(xy.b.y, 0.046)) { + primaries = Primaries::k2100; + return true; + } + if (ApproxEq(xy.r.x, 0.680) && ApproxEq(xy.r.y, 0.320) && + ApproxEq(xy.g.x, 0.265) && ApproxEq(xy.g.y, 0.690) && + ApproxEq(xy.b.x, 0.150) && ApproxEq(xy.b.y, 0.060)) { + primaries = Primaries::kP3; + return true; + } + + primaries = Primaries::kCustom; + JXL_RETURN_IF_ERROR(red_.Set(xy.r)); + JXL_RETURN_IF_ERROR(green_.Set(xy.g)); + JXL_RETURN_IF_ERROR(blue_.Set(xy.b)); + return true; +} + +Status ColorEncoding::CreateICC() { + InternalRemoveICC(); + if (!MaybeCreateProfile(*this, &icc_)) { + return JXL_FAILURE("Failed to create profile from fields"); + } + return true; +} + +std::string Description(const ColorEncoding& c_in) { + // Copy required for Implicit* + ColorEncoding c = c_in; + + std::string d = ToString(c.GetColorSpace()); + + if (!c.ImplicitWhitePoint()) { + d += '_'; + if (c.white_point == WhitePoint::kCustom) { + const CIExy wp = c.GetWhitePoint(); + d += ToString(wp.x) + ';'; + d += ToString(wp.y); + } else { + d += ToString(c.white_point); + } + } + + if (c.HasPrimaries()) { + d += '_'; + if (c.primaries == Primaries::kCustom) { + const PrimariesCIExy pr = c.GetPrimaries(); + d += ToString(pr.r.x) + ';'; + d += ToString(pr.r.y) + ';'; + d += ToString(pr.g.x) + ';'; + d += ToString(pr.g.y) + ';'; + d += ToString(pr.b.x) + ';'; + d += ToString(pr.b.y); + } else { + d += ToString(c.primaries); + } + } + + d += '_'; + d += ToString(c.rendering_intent); + + if (!c.tf.SetImplicit()) { + d += '_'; + if (c.tf.IsGamma()) { + d += 'g'; + d += ToString(c.tf.GetGamma()); + } else { + d += ToString(c.tf.GetTransferFunction()); + } + } + + return d; +} + +Customxy::Customxy() { Bundle::Init(this); } +Status Customxy::VisitFields(Visitor* JXL_RESTRICT visitor) { + uint32_t ux = PackSigned(x); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Bits(19), BitsOffset(19, 524288), + BitsOffset(20, 1048576), + BitsOffset(21, 2097152), 0, &ux)); + x = UnpackSigned(ux); + uint32_t uy = PackSigned(y); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Bits(19), BitsOffset(19, 524288), + BitsOffset(20, 1048576), + BitsOffset(21, 2097152), 0, &uy)); + y = UnpackSigned(uy); + return true; +} + +CustomTransferFunction::CustomTransferFunction() { Bundle::Init(this); } +Status CustomTransferFunction::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->Conditional(!SetImplicit())) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &have_gamma_)); + + if (visitor->Conditional(have_gamma_)) { + // Gamma is represented as a 24-bit int, the exponent used is + // gamma_ / 1e7. Valid values are (0, 1]. On the low end side, we also + // limit it to kMaxGamma/1e7. + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(24, kGammaMul, &gamma_)); + if (gamma_ > kGammaMul || + static_cast(gamma_) * kMaxGamma < kGammaMul) { + return JXL_FAILURE("Invalid gamma %u", gamma_); + } + } + + if (visitor->Conditional(!have_gamma_)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->Enum(TransferFunction::kSRGB, &transfer_function_)); + } + } + + return true; +} + +ColorEncoding::ColorEncoding() { Bundle::Init(this); } +Status ColorEncoding::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &want_icc_)); + + // Always send even if want_icc_ because this affects decoding. + // We can skip the white point/primaries because they do not. + JXL_QUIET_RETURN_IF_ERROR(visitor->Enum(ColorSpace::kRGB, &color_space_)); + + if (visitor->Conditional(!WantICC())) { + // Serialize enums. NOTE: we set the defaults to the most common values so + // ImageMetadata.all_default is true in the common case. + + if (visitor->Conditional(!ImplicitWhitePoint())) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Enum(WhitePoint::kD65, &white_point)); + if (visitor->Conditional(white_point == WhitePoint::kCustom)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&white_)); + } + } + + if (visitor->Conditional(HasPrimaries())) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Enum(Primaries::kSRGB, &primaries)); + if (visitor->Conditional(primaries == Primaries::kCustom)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&red_)); + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&green_)); + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&blue_)); + } + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&tf)); + + JXL_QUIET_RETURN_IF_ERROR( + visitor->Enum(RenderingIntent::kRelative, &rendering_intent)); + + // We didn't have ICC, so all fields should be known. + if (color_space_ == ColorSpace::kUnknown || tf.IsUnknown()) { + return JXL_FAILURE( + "No ICC but cs %u and tf %u%s", + static_cast(color_space_), + tf.IsGamma() ? 0 + : static_cast(tf.GetTransferFunction()), + tf.IsGamma() ? "(gamma)" : ""); + } + + JXL_RETURN_IF_ERROR(CreateICC()); + } + + if (WantICC() && visitor->IsReading()) { + // Haven't called SetICC() yet, do nothing. + } else { + if (ICC().empty()) return JXL_FAILURE("Empty ICC"); + } + + return true; +} + +void ConvertInternalToExternalColorEncoding(const ColorEncoding& internal, + JxlColorEncoding* external) { + external->color_space = static_cast(internal.GetColorSpace()); + + external->white_point = static_cast(internal.white_point); + + jxl::CIExy whitepoint = internal.GetWhitePoint(); + external->white_point_xy[0] = whitepoint.x; + external->white_point_xy[1] = whitepoint.y; + + if (external->color_space == JXL_COLOR_SPACE_RGB || + external->color_space == JXL_COLOR_SPACE_UNKNOWN) { + external->primaries = static_cast(internal.primaries); + jxl::PrimariesCIExy primaries = internal.GetPrimaries(); + external->primaries_red_xy[0] = primaries.r.x; + external->primaries_red_xy[1] = primaries.r.y; + external->primaries_green_xy[0] = primaries.g.x; + external->primaries_green_xy[1] = primaries.g.y; + external->primaries_blue_xy[0] = primaries.b.x; + external->primaries_blue_xy[1] = primaries.b.y; + } + + if (internal.tf.IsGamma()) { + external->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + external->gamma = internal.tf.GetGamma(); + } else { + external->transfer_function = + static_cast(internal.tf.GetTransferFunction()); + external->gamma = 0; + } + + external->rendering_intent = + static_cast(internal.rendering_intent); +} + +Status ConvertExternalToInternalColorEncoding(const JxlColorEncoding& external, + ColorEncoding* internal) { + internal->SetColorSpace(static_cast(external.color_space)); + + JXL_RETURN_IF_ERROR(ConvertExternalToInternalWhitePoint( + external.white_point, &internal->white_point)); + if (external.white_point == JXL_WHITE_POINT_CUSTOM) { + CIExy wp; + wp.x = external.white_point_xy[0]; + wp.y = external.white_point_xy[1]; + JXL_RETURN_IF_ERROR(internal->SetWhitePoint(wp)); + } + + if (external.color_space == JXL_COLOR_SPACE_RGB || + external.color_space == JXL_COLOR_SPACE_UNKNOWN) { + JXL_RETURN_IF_ERROR(ConvertExternalToInternalPrimaries( + external.primaries, &internal->primaries)); + if (external.primaries == JXL_PRIMARIES_CUSTOM) { + PrimariesCIExy primaries; + primaries.r.x = external.primaries_red_xy[0]; + primaries.r.y = external.primaries_red_xy[1]; + primaries.g.x = external.primaries_green_xy[0]; + primaries.g.y = external.primaries_green_xy[1]; + primaries.b.x = external.primaries_blue_xy[0]; + primaries.b.y = external.primaries_blue_xy[1]; + JXL_RETURN_IF_ERROR(internal->SetPrimaries(primaries)); + } + } + CustomTransferFunction tf; + if (external.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + JXL_RETURN_IF_ERROR(tf.SetGamma(external.gamma)); + } else { + TransferFunction tf_enum; + // JXL_TRANSFER_FUNCTION_GAMMA is not handled by this function since there's + // no internal enum value for it. + JXL_RETURN_IF_ERROR(ConvertExternalToInternalTransferFunction( + external.transfer_function, &tf_enum)); + tf.SetTransferFunction(tf_enum); + } + internal->tf = tf; + + JXL_RETURN_IF_ERROR(ConvertExternalToInternalRenderingIntent( + external.rendering_intent, &internal->rendering_intent)); + + return true; +} + +/* Chromatic adaptation matrices*/ +static const float kBradford[9] = { + 0.8951f, 0.2664f, -0.1614f, -0.7502f, 1.7135f, + 0.0367f, 0.0389f, -0.0685f, 1.0296f, +}; + +static const float kBradfordInv[9] = { + 0.9869929f, -0.1470543f, 0.1599627f, 0.4323053f, 0.5183603f, + 0.0492912f, -0.0085287f, 0.0400428f, 0.9684867f, +}; + +// Adapts whitepoint x, y to D50 +Status AdaptToXYZD50(float wx, float wy, float matrix[9]) { + if (wx < 0 || wx > 1 || wy <= 0 || wy > 1) { + // Out of range values can cause division through zero + // further down with the bradford adaptation too. + return JXL_FAILURE("Invalid white point"); + } + float w[3] = {wx / wy, 1.0f, (1.0f - wx - wy) / wy}; + // 1 / tiny float can still overflow + JXL_RETURN_IF_ERROR(std::isfinite(w[0]) && std::isfinite(w[2])); + float w50[3] = {0.96422f, 1.0f, 0.82521f}; + + float lms[3]; + float lms50[3]; + + MatMul(kBradford, w, 3, 3, 1, lms); + MatMul(kBradford, w50, 3, 3, 1, lms50); + + float a[9] = { + lms50[0] / lms[0], 0, 0, 0, lms50[1] / lms[1], 0, 0, 0, lms50[2] / lms[2], + }; + + float b[9]; + MatMul(a, kBradford, 3, 3, 3, b); + MatMul(kBradfordInv, b, 3, 3, 3, matrix); + + return true; +} + +Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, float bx, + float by, float wx, float wy, float matrix[9]) { + if (wx < 0 || wx > 1 || wy <= 0 || wy > 1) { + return JXL_FAILURE("Invalid white point"); + } + // TODO(lode): also require rx, ry, gx, gy, bx, to be in range 0-1? ICC + // profiles in theory forbid negative XYZ values, but in practice the ACES P0 + // color space uses a negative y for the blue primary. + float primaries[9] = { + rx, gx, bx, ry, gy, by, 1.0f - rx - ry, 1.0f - gx - gy, 1.0f - bx - by}; + float primaries_inv[9]; + memcpy(primaries_inv, primaries, sizeof(float) * 9); + JXL_RETURN_IF_ERROR(Inv3x3Matrix(primaries_inv)); + + float w[3] = {wx / wy, 1.0f, (1.0f - wx - wy) / wy}; + // 1 / tiny float can still overflow + JXL_RETURN_IF_ERROR(std::isfinite(w[0]) && std::isfinite(w[2])); + float xyz[3]; + MatMul(primaries_inv, w, 3, 3, 1, xyz); + + float a[9] = { + xyz[0], 0, 0, 0, xyz[1], 0, 0, 0, xyz[2], + }; + + float toXYZ[9]; + MatMul(primaries, a, 3, 3, 3, toXYZ); + + float d50[9]; + JXL_RETURN_IF_ERROR(AdaptToXYZD50(wx, wy, d50)); + + MatMul(d50, toXYZ, 3, 3, 3, matrix); + return true; +} + +} // namespace jxl diff --git a/lib/jxl/color_encoding_internal.h b/lib/jxl/color_encoding_internal.h new file mode 100644 index 0000000..c9fda52 --- /dev/null +++ b/lib/jxl/color_encoding_internal.h @@ -0,0 +1,459 @@ +// 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_COLOR_ENCODING_INTERNAL_H_ +#define LIB_JXL_COLOR_ENCODING_INTERNAL_H_ + +// Metadata for color space conversions. + +#include +#include +#include + +#include // std::abs +#include +#include +#include + +#include "jxl/color_encoding.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +// (All CIE units are for the standard 1931 2 degree observer) + +// Color space the color pixel data is encoded in. The color pixel data is +// 3-channel in all cases except in case of kGray, where it uses only 1 channel. +// This also determines the amount of channels used in modular encoding. +enum class ColorSpace : uint32_t { + // Trichromatic color data. This also includes CMYK if a kBlack + // ExtraChannelInfo is present. This implies, if there is an ICC profile, that + // the ICC profile uses a 3-channel color space if no kBlack extra channel is + // present, or uses color space 'CMYK' if a kBlack extra channel is present. + kRGB, + // Single-channel data. This implies, if there is an ICC profile, that the ICC + // profile also represents single-channel data and has the appropriate color + // space ('GRAY'). + kGray, + // Like kRGB, but implies fixed values for primaries etc. + kXYB, + // For non-RGB/gray data, e.g. from non-electro-optical sensors. Otherwise + // the same conditions as kRGB apply. + kUnknown +}; + +static inline const char* EnumName(ColorSpace /*unused*/) { + return "ColorSpace"; +} +static inline constexpr uint64_t EnumBits(ColorSpace /*unused*/) { + using CS = ColorSpace; + return MakeBit(CS::kRGB) | MakeBit(CS::kGray) | MakeBit(CS::kXYB) | + MakeBit(CS::kUnknown); +} + +// Values from CICP ColourPrimaries. +enum class WhitePoint : uint32_t { + kD65 = 1, // sRGB/BT.709/Display P3/BT.2020 + kCustom = 2, // Actual values encoded in separate fields + kE = 10, // XYZ + kDCI = 11, // DCI-P3 +}; + +static inline const char* EnumName(WhitePoint /*unused*/) { + return "WhitePoint"; +} +static inline constexpr uint64_t EnumBits(WhitePoint /*unused*/) { + return MakeBit(WhitePoint::kD65) | MakeBit(WhitePoint::kCustom) | + MakeBit(WhitePoint::kE) | MakeBit(WhitePoint::kDCI); +} + +// Values from CICP ColourPrimaries +enum class Primaries : uint32_t { + kSRGB = 1, // Same as BT.709 + kCustom = 2, // Actual values encoded in separate fields + k2100 = 9, // Same as BT.2020 + kP3 = 11, +}; + +static inline const char* EnumName(Primaries /*unused*/) { return "Primaries"; } +static inline constexpr uint64_t EnumBits(Primaries /*unused*/) { + using Pr = Primaries; + return MakeBit(Pr::kSRGB) | MakeBit(Pr::kCustom) | MakeBit(Pr::k2100) | + MakeBit(Pr::kP3); +} + +// Values from CICP TransferCharacteristics +enum TransferFunction : uint32_t { + k709 = 1, + kUnknown = 2, + kLinear = 8, + kSRGB = 13, + kPQ = 16, // from BT.2100 + kDCI = 17, // from SMPTE RP 431-2 reference projector + kHLG = 18, // from BT.2100 +}; + +static inline const char* EnumName(TransferFunction /*unused*/) { + return "TransferFunction"; +} +static inline constexpr uint64_t EnumBits(TransferFunction /*unused*/) { + using TF = TransferFunction; + return MakeBit(TF::k709) | MakeBit(TF::kLinear) | MakeBit(TF::kSRGB) | + MakeBit(TF::kPQ) | MakeBit(TF::kDCI) | MakeBit(TF::kHLG) | + MakeBit(TF::kUnknown); +} + +enum class RenderingIntent : uint32_t { + // Values match ICC sRGB encodings. + kPerceptual = 0, // good for photos, requires a profile with LUT. + kRelative, // good for logos. + kSaturation, // perhaps useful for CG with fully saturated colors. + kAbsolute, // leaves white point unchanged; good for proofing. +}; + +static inline const char* EnumName(RenderingIntent /*unused*/) { + return "RenderingIntent"; +} +static inline constexpr uint64_t EnumBits(RenderingIntent /*unused*/) { + using RI = RenderingIntent; + return MakeBit(RI::kPerceptual) | MakeBit(RI::kRelative) | + MakeBit(RI::kSaturation) | MakeBit(RI::kAbsolute); +} + +// Chromaticity (Y is omitted because it is 1 for primaries/white points) +struct CIExy { + double x = 0.0; + double y = 0.0; +}; + +struct PrimariesCIExy { + CIExy r; + CIExy g; + CIExy b; +}; + +// Serializable form of CIExy. +struct Customxy : public Fields { + Customxy(); + const char* Name() const override { return "Customxy"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + CIExy Get() const; + // Returns false if x or y do not fit in the encoding. + Status Set(const CIExy& xy); + + int32_t x; + int32_t y; +}; + +struct CustomTransferFunction : public Fields { + CustomTransferFunction(); + const char* Name() const override { return "CustomTransferFunction"; } + + // Sets fields and returns true if nonserialized_color_space has an implicit + // transfer function, otherwise leaves fields unchanged and returns false. + bool SetImplicit(); + + // Gamma: only used for PNG inputs + bool IsGamma() const { return have_gamma_; } + double GetGamma() const { + JXL_ASSERT(IsGamma()); + return gamma_ * 1E-7; // (0, 1) + } + Status SetGamma(double gamma); + + TransferFunction GetTransferFunction() const { + JXL_ASSERT(!IsGamma()); + return transfer_function_; + } + void SetTransferFunction(const TransferFunction tf) { + have_gamma_ = false; + transfer_function_ = tf; + } + + bool IsUnknown() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kUnknown); + } + bool IsSRGB() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kSRGB); + } + bool IsLinear() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kLinear); + } + bool IsPQ() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kPQ); + } + bool IsHLG() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kHLG); + } + bool Is709() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::k709); + } + bool IsDCI() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kDCI); + } + bool IsSame(const CustomTransferFunction& other) const { + if (have_gamma_ != other.have_gamma_) return false; + if (have_gamma_) { + if (gamma_ != other.gamma_) return false; + } else { + if (transfer_function_ != other.transfer_function_) return false; + } + return true; + } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Must be set before calling VisitFields! + ColorSpace nonserialized_color_space = ColorSpace::kRGB; + + private: + static constexpr uint32_t kGammaMul = 10000000; + + bool have_gamma_; + + // OETF exponent to go from linear to gamma-compressed. + uint32_t gamma_; // Only used if have_gamma_. + + // Can be kUnknown. + TransferFunction transfer_function_; // Only used if !have_gamma_. +}; + +// Compact encoding of data required to interpret and translate pixels to a +// known color space. Stored in Metadata. Thread-compatible. +struct ColorEncoding : public Fields { + ColorEncoding(); + const char* Name() const override { return "ColorEncoding"; } + + // Returns ready-to-use color encodings (initialized on-demand). + static const ColorEncoding& SRGB(bool is_gray = false); + static const ColorEncoding& LinearSRGB(bool is_gray = false); + + // Returns true if an ICC profile was successfully created from fields. + // Must be called after modifying fields. Defined in color_management.cc. + Status CreateICC(); + + // Returns non-empty and valid ICC profile, unless: + // - between calling InternalRemoveICC() and CreateICC() in tests; + // - WantICC() == true and SetICC() was not yet called; + // - after a failed call to SetSRGB(), SetICC(), or CreateICC(). + const PaddedBytes& ICC() const { return icc_; } + + // Internal only, do not call except from tests. + void InternalRemoveICC() { icc_.clear(); } + + // Returns true if `icc` is assigned and decoded successfully. If so, + // subsequent WantICC() will return true until DecideIfWantICC() changes it. + // Returning false indicates data has been lost. + Status SetICC(PaddedBytes&& icc) { + if (icc.empty()) return false; + icc_ = std::move(icc); + + if (!SetFieldsFromICC()) { + InternalRemoveICC(); + return false; + } + + want_icc_ = true; + return true; + } + + // Sets the raw ICC profile bytes, without parsing the ICC, and without + // updating the direct fields such as whitepoint, primaries and color + // space. Functions to get and set fields, such as SetWhitePoint, cannot be + // used anymore after this and functions such as IsSRGB return false no matter + // what the contents of the icc profile. + Status SetICCRaw(PaddedBytes&& icc) { + if (icc.empty()) return false; + icc_ = std::move(icc); + + want_icc_ = true; + have_fields_ = false; + return true; + } + + // Returns whether to send the ICC profile in the codestream. + bool WantICC() const { return want_icc_; } + + // Return whether the direct fields are set, if false but ICC is set, only + // raw ICC bytes are known. + bool HaveFields() const { return have_fields_; } + + // Causes WantICC() to return false if ICC() can be reconstructed from fields. + // Defined in color_management.cc. + void DecideIfWantICC(); + + bool IsGray() const { return color_space_ == ColorSpace::kGray; } + size_t Channels() const { return IsGray() ? 1 : 3; } + + // Returns false if the field is invalid and unusable. + bool HasPrimaries() const { + return !IsGray() && color_space_ != ColorSpace::kXYB; + } + + // Returns true after setting the field to a value defined by color_space, + // otherwise false and leaves the field unchanged. + bool ImplicitWhitePoint() { + if (color_space_ == ColorSpace::kXYB) { + white_point = WhitePoint::kD65; + return true; + } + return false; + } + + // Returns whether the color space is known to be sRGB. If a raw unparsed ICC + // profile is set without the fields being set, this returns false, even if + // the content of the ICC profile would match sRGB. + bool IsSRGB() const { + if (!have_fields_) return false; + if (!IsGray() && color_space_ != ColorSpace::kRGB) return false; + if (white_point != WhitePoint::kD65) return false; + if (primaries != Primaries::kSRGB) return false; + if (!tf.IsSRGB()) return false; + return true; + } + + // Returns whether the color space is known to be linear sRGB. If a raw + // unparsed ICC profile is set without the fields being set, this returns + // false, even if the content of the ICC profile would match linear sRGB. + bool IsLinearSRGB() const { + if (!have_fields_) return false; + if (!IsGray() && color_space_ != ColorSpace::kRGB) return false; + if (white_point != WhitePoint::kD65) return false; + if (primaries != Primaries::kSRGB) return false; + if (!tf.IsLinear()) return false; + return true; + } + + Status SetSRGB(const ColorSpace cs, + const RenderingIntent ri = RenderingIntent::kRelative) { + InternalRemoveICC(); + JXL_ASSERT(cs == ColorSpace::kGray || cs == ColorSpace::kRGB); + color_space_ = cs; + white_point = WhitePoint::kD65; + primaries = Primaries::kSRGB; + tf.SetTransferFunction(TransferFunction::kSRGB); + rendering_intent = ri; + return CreateICC(); + } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Accessors ensure tf.nonserialized_color_space is updated at the same time. + ColorSpace GetColorSpace() const { return color_space_; } + void SetColorSpace(const ColorSpace cs) { + color_space_ = cs; + tf.nonserialized_color_space = cs; + } + + CIExy GetWhitePoint() const; + Status SetWhitePoint(const CIExy& xy); + + PrimariesCIExy GetPrimaries() const; + Status SetPrimaries(const PrimariesCIExy& xy); + + // Checks if the color spaces (including white point / primaries) are the + // same, but ignores the transfer function, rendering intent and ICC bytes. + bool SameColorSpace(const ColorEncoding& other) const { + if (color_space_ != other.color_space_) return false; + + if (white_point != other.white_point) return false; + if (white_point == WhitePoint::kCustom) { + if (white_.x != other.white_.x || white_.y != other.white_.y) + return false; + } + + if (HasPrimaries() != other.HasPrimaries()) return false; + if (HasPrimaries()) { + if (primaries != other.primaries) return false; + if (primaries == Primaries::kCustom) { + if (red_.x != other.red_.x || red_.y != other.red_.y) return false; + if (green_.x != other.green_.x || green_.y != other.green_.y) + return false; + if (blue_.x != other.blue_.x || blue_.y != other.blue_.y) return false; + } + } + return true; + } + + // Checks if the color space and transfer function are the same, ignoring + // rendering intent and ICC bytes + bool SameColorEncoding(const ColorEncoding& other) const { + return SameColorSpace(other) && tf.IsSame(other.tf); + } + + mutable bool all_default; + + // Only valid if HaveFields() + WhitePoint white_point; + Primaries primaries; // Only valid if HasPrimaries() + CustomTransferFunction tf; + RenderingIntent rendering_intent; + + private: + // Returns true if all fields have been initialized (possibly to kUnknown). + // Returns false if the ICC profile is invalid or decoding it fails. + // Defined in color_management.cc. + Status SetFieldsFromICC(); + + // If true, the codestream contains an ICC profile and we do not serialize + // fields. Otherwise, fields are serialized and we create an ICC profile. + bool want_icc_; + + // When false, fields such as white_point and tf are invalid and must not be + // used. This occurs after setting a raw bytes-only ICC profile, only the + // ICC bytes may be used. The color_space_ field is still valid. + bool have_fields_ = true; + + PaddedBytes icc_; // Valid ICC profile + + ColorSpace color_space_; // Can be kUnknown + + // Only used if white_point == kCustom. + Customxy white_; + + // Only used if primaries == kCustom. + Customxy red_; + Customxy green_; + Customxy blue_; +}; + +// Returns whether the two inputs are approximately equal. +static inline bool ApproxEq(const double a, const double b, +#if JPEGXL_ENABLE_SKCMS + double max_l1 = 1E-3) { +#else + double max_l1 = 8E-5) { +#endif + // Threshold should be sufficient for ICC's 15-bit fixed-point numbers. + // We have seen differences of 7.1E-5 with lcms2 and 1E-3 with skcms. + return std::abs(a - b) <= max_l1; +} + +// Returns a representation of the ColorEncoding fields (not icc). +// Example description: "RGB_D65_SRG_Rel_Lin" +std::string Description(const ColorEncoding& c); +static inline std::ostream& operator<<(std::ostream& os, + const ColorEncoding& c) { + return os << Description(c); +} + +void ConvertInternalToExternalColorEncoding(const jxl::ColorEncoding& internal, + JxlColorEncoding* external); + +Status ConvertExternalToInternalColorEncoding(const JxlColorEncoding& external, + jxl::ColorEncoding* internal); + +Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, float bx, + float by, float wx, float wy, float matrix[9]); +Status AdaptToXYZD50(float wx, float wy, float matrix[9]); + +} // namespace jxl + +#endif // LIB_JXL_COLOR_ENCODING_INTERNAL_H_ diff --git a/lib/jxl/color_encoding_internal_test.cc b/lib/jxl/color_encoding_internal_test.cc new file mode 100644 index 0000000..32bd0cc --- /dev/null +++ b/lib/jxl/color_encoding_internal_test.cc @@ -0,0 +1,157 @@ +// 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/color_encoding_internal.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/test_utils.h" + +namespace jxl { +namespace { + +TEST(ColorEncodingTest, RoundTripAll) { + for (const test::ColorEncodingDescriptor& cdesc : test::AllEncodings()) { + const ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc); + // Verify Set(Get) yields the same white point/primaries/gamma. + { + ColorEncoding c; + EXPECT_TRUE(c.SetWhitePoint(c_original.GetWhitePoint())); + EXPECT_EQ(c_original.white_point, c.white_point); + } + { + ColorEncoding c; + EXPECT_TRUE(c.SetPrimaries(c_original.GetPrimaries())); + EXPECT_EQ(c_original.primaries, c.primaries); + } + if (c_original.tf.IsGamma()) { + ColorEncoding c; + EXPECT_TRUE(c.tf.SetGamma(c_original.tf.GetGamma())); + EXPECT_TRUE(c_original.tf.IsSame(c.tf)); + } + } +} + +TEST(ColorEncodingTest, CustomWhitePoint) { + ColorEncoding c; + // Nonsensical values + CIExy xy_in; + xy_in.x = 0.8; + xy_in.y = 0.01; + EXPECT_TRUE(c.SetWhitePoint(xy_in)); + const CIExy xy = c.GetWhitePoint(); + + ColorEncoding c2; + EXPECT_TRUE(c2.SetWhitePoint(xy)); + EXPECT_TRUE(c.SameColorSpace(c2)); +} + +TEST(ColorEncodingTest, CustomPrimaries) { + ColorEncoding c; + PrimariesCIExy xy_in; + // Nonsensical values + xy_in.r.x = -0.01; + xy_in.r.y = 0.2; + xy_in.g.x = 0.4; + xy_in.g.y = 0.401; + xy_in.b.x = 1.1; + xy_in.b.y = -1.2; + EXPECT_TRUE(c.SetPrimaries(xy_in)); + const PrimariesCIExy xy = c.GetPrimaries(); + + ColorEncoding c2; + EXPECT_TRUE(c2.SetPrimaries(xy)); + EXPECT_TRUE(c.SameColorSpace(c2)); +} + +TEST(ColorEncodingTest, CustomGamma) { + ColorEncoding c; +#ifndef JXL_CRASH_ON_ERROR + EXPECT_FALSE(c.tf.SetGamma(0.0)); + EXPECT_FALSE(c.tf.SetGamma(-1E-6)); + EXPECT_FALSE(c.tf.SetGamma(1.001)); +#endif + EXPECT_TRUE(c.tf.SetGamma(1.0)); + EXPECT_FALSE(c.tf.IsGamma()); + EXPECT_TRUE(c.tf.IsLinear()); + + EXPECT_TRUE(c.tf.SetGamma(0.123)); + EXPECT_TRUE(c.tf.IsGamma()); + const double gamma = c.tf.GetGamma(); + + ColorEncoding c2; + EXPECT_TRUE(c2.tf.SetGamma(gamma)); + EXPECT_TRUE(c.SameColorEncoding(c2)); + EXPECT_TRUE(c2.tf.IsGamma()); +} + +TEST(ColorEncodingTest, InternalExternalConversion) { + ColorEncoding source_internal; + JxlColorEncoding external; + ColorEncoding destination_internal; + + for (int i = 0; i < 100; i++) { + source_internal.SetColorSpace(static_cast(rand() % 4)); + CIExy wp; + wp.x = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + wp.y = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + EXPECT_TRUE(source_internal.SetWhitePoint(wp)); + if (source_internal.HasPrimaries()) { + PrimariesCIExy primaries; + primaries.r.x = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + primaries.r.y = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + primaries.g.x = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + primaries.g.y = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + primaries.b.x = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + primaries.b.y = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25; + EXPECT_TRUE(source_internal.SetPrimaries(primaries)); + } + CustomTransferFunction tf; + EXPECT_TRUE(tf.SetGamma((float(rand()) / float((RAND_MAX)) * 0.5) + 0.25)); + source_internal.tf = tf; + source_internal.rendering_intent = static_cast(rand() % 4); + + ConvertInternalToExternalColorEncoding(source_internal, &external); + EXPECT_TRUE(ConvertExternalToInternalColorEncoding(external, + &destination_internal)); + + EXPECT_EQ(source_internal.GetColorSpace(), + destination_internal.GetColorSpace()); + EXPECT_EQ(source_internal.white_point, destination_internal.white_point); + EXPECT_EQ(source_internal.GetWhitePoint().x, + destination_internal.GetWhitePoint().x); + EXPECT_EQ(source_internal.GetWhitePoint().y, + destination_internal.GetWhitePoint().y); + if (source_internal.HasPrimaries()) { + EXPECT_EQ(source_internal.GetPrimaries().r.x, + destination_internal.GetPrimaries().r.x); + EXPECT_EQ(source_internal.GetPrimaries().r.y, + destination_internal.GetPrimaries().r.y); + EXPECT_EQ(source_internal.GetPrimaries().g.x, + destination_internal.GetPrimaries().g.x); + EXPECT_EQ(source_internal.GetPrimaries().g.y, + destination_internal.GetPrimaries().g.y); + EXPECT_EQ(source_internal.GetPrimaries().b.x, + destination_internal.GetPrimaries().b.x); + EXPECT_EQ(source_internal.GetPrimaries().b.y, + destination_internal.GetPrimaries().b.y); + } + EXPECT_EQ(source_internal.tf.IsGamma(), destination_internal.tf.IsGamma()); + if (source_internal.tf.IsGamma()) { + EXPECT_EQ(source_internal.tf.GetGamma(), + destination_internal.tf.GetGamma()); + } else { + EXPECT_EQ(source_internal.tf.GetTransferFunction(), + destination_internal.tf.GetTransferFunction()); + } + EXPECT_EQ(source_internal.rendering_intent, + destination_internal.rendering_intent); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/color_management.cc b/lib/jxl/color_management.cc new file mode 100644 index 0000000..7929d1f --- /dev/null +++ b/lib/jxl/color_management.cc @@ -0,0 +1,519 @@ +// 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. + +// Defined by build system; this avoids IDE warnings. Must come before +// color_management.h (affects header definitions). +#ifndef JPEGXL_ENABLE_SKCMS +#define JPEGXL_ENABLE_SKCMS 0 +#endif + +#include "lib/jxl/color_management.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/color_management.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/linalg.h" // MatMul, Inv3x3Matrix +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// NOTE: this is only used to provide a reasonable ICC profile that other +// software can read. Our own transforms use ExtraTF instead because that is +// more precise and supports unbounded mode. +std::vector CreateTableCurve(uint32_t N, const ExtraTF tf) { + JXL_ASSERT(N <= 4096); // ICC MFT2 only allows 4K entries + JXL_ASSERT(tf == ExtraTF::kPQ || tf == ExtraTF::kHLG); + // No point using float - LCMS converts to 16-bit for A2B/MFT. + std::vector table(N); + for (uint32_t i = 0; i < N; ++i) { + const float x = static_cast(i) / (N - 1); // 1.0 at index N - 1. + const double dx = static_cast(x); + // LCMS requires EOTF (e.g. 2.4 exponent). + double y = (tf == ExtraTF::kHLG) ? TF_HLG().DisplayFromEncoded(dx) + : TF_PQ().DisplayFromEncoded(dx); + JXL_ASSERT(y >= 0.0); + // Clamp to table range - necessary for HLG. + if (y > 1.0) y = 1.0; + // 1.0 corresponds to table value 0xFFFF. + table[i] = static_cast(roundf(y * 65535.0)); + } + return table; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(CreateTableCurve); // Local function. + +Status CIEXYZFromWhiteCIExy(const CIExy& xy, float XYZ[3]) { + // Target Y = 1. + if (std::abs(xy.y) < 1e-12) return JXL_FAILURE("Y value is too small"); + const float factor = 1 / xy.y; + XYZ[0] = xy.x * factor; + XYZ[1] = 1; + XYZ[2] = (1 - xy.x - xy.y) * factor; + return true; +} + +namespace { + +// NOTE: this is only used to provide a reasonable ICC profile that other +// software can read. Our own transforms use ExtraTF instead because that is +// more precise and supports unbounded mode. +template +std::vector CreateTableCurve(uint32_t N, const Func& func) { + JXL_ASSERT(N <= 4096); // ICC MFT2 only allows 4K entries + // No point using float - LCMS converts to 16-bit for A2B/MFT. + std::vector table(N); + for (uint32_t i = 0; i < N; ++i) { + const float x = static_cast(i) / (N - 1); // 1.0 at index N - 1. + // LCMS requires EOTF (e.g. 2.4 exponent). + double y = func.DisplayFromEncoded(static_cast(x)); + JXL_ASSERT(y >= 0.0); + // Clamp to table range - necessary for HLG. + if (y > 1.0) y = 1.0; + // 1.0 corresponds to table value 0xFFFF. + table[i] = static_cast(roundf(y * 65535.0)); + } + return table; +} + +void ICCComputeMD5(const PaddedBytes& data, uint8_t sum[16]) + JXL_NO_SANITIZE("unsigned-integer-overflow") { + PaddedBytes data64 = data; + data64.push_back(128); + // Add bytes such that ((size + 8) & 63) == 0. + size_t extra = ((64 - ((data64.size() + 8) & 63)) & 63); + data64.resize(data64.size() + extra, 0); + for (uint64_t i = 0; i < 64; i += 8) { + data64.push_back(static_cast(data.size() << 3u) >> i); + } + + static const uint32_t sineparts[64] = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, + 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, + 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, + 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, + 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, + 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static const uint32_t shift[64] = { + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + }; + + uint32_t a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476; + + for (size_t i = 0; i < data64.size(); i += 64) { + uint32_t a = a0, b = b0, c = c0, d = d0, f, g; + for (size_t j = 0; j < 64; j++) { + if (j < 16) { + f = (b & c) | ((~b) & d); + g = j; + } else if (j < 32) { + f = (d & b) | ((~d) & c); + g = (5 * j + 1) & 0xf; + } else if (j < 48) { + f = b ^ c ^ d; + g = (3 * j + 5) & 0xf; + } else { + f = c ^ (b | (~d)); + g = (7 * j) & 0xf; + } + uint32_t dg0 = data64[i + g * 4 + 0], dg1 = data64[i + g * 4 + 1], + dg2 = data64[i + g * 4 + 2], dg3 = data64[i + g * 4 + 3]; + uint32_t u = dg0 | (dg1 << 8u) | (dg2 << 16u) | (dg3 << 24u); + f += a + sineparts[j] + u; + a = d; + d = c; + c = b; + b += (f << shift[j]) | (f >> (32u - shift[j])); + } + a0 += a; + b0 += b; + c0 += c; + d0 += d; + } + sum[0] = a0; + sum[1] = a0 >> 8u; + sum[2] = a0 >> 16u; + sum[3] = a0 >> 24u; + sum[4] = b0; + sum[5] = b0 >> 8u; + sum[6] = b0 >> 16u; + sum[7] = b0 >> 24u; + sum[8] = c0; + sum[9] = c0 >> 8u; + sum[10] = c0 >> 16u; + sum[11] = c0 >> 24u; + sum[12] = d0; + sum[13] = d0 >> 8u; + sum[14] = d0 >> 16u; + sum[15] = d0 >> 24u; +} + +Status CreateICCChadMatrix(CIExy w, float result[9]) { + float m[9]; + if (w.y == 0) { // WhitePoint can not be pitch-black. + return JXL_FAILURE("Invalid WhitePoint"); + } + JXL_RETURN_IF_ERROR(AdaptToXYZD50(w.x, w.y, m)); + memcpy(result, m, sizeof(float) * 9); + return true; +} + +// Creates RGB to XYZ matrix given RGB primaries and whitepoint in xy. +Status CreateICCRGBMatrix(CIExy r, CIExy g, CIExy b, CIExy w, float result[9]) { + float m[9]; + JXL_RETURN_IF_ERROR( + PrimariesToXYZD50(r.x, r.y, g.x, g.y, b.x, b.y, w.x, w.y, m)); + memcpy(result, m, sizeof(float) * 9); + return true; +} + +void WriteICCUint32(uint32_t value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 4) icc->resize(pos + 4); + (*icc)[pos + 0] = (value >> 24u) & 255; + (*icc)[pos + 1] = (value >> 16u) & 255; + (*icc)[pos + 2] = (value >> 8u) & 255; + (*icc)[pos + 3] = value & 255; +} + +void WriteICCUint16(uint16_t value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 2) icc->resize(pos + 2); + (*icc)[pos + 0] = (value >> 8u) & 255; + (*icc)[pos + 1] = value & 255; +} + +// Writes a 4-character tag +void WriteICCTag(const char* value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 4) icc->resize(pos + 4); + memcpy(icc->data() + pos, value, 4); +} + +Status WriteICCS15Fixed16(float value, size_t pos, + PaddedBytes* JXL_RESTRICT icc) { + // "nextafterf" for 32768.0f towards zero are: + // 32767.998046875, 32767.99609375, 32767.994140625 + // Even the first value works well,... + bool ok = (-32767.995f <= value) && (value <= 32767.995f); + if (!ok) return JXL_FAILURE("ICC value is out of range / NaN"); + int32_t i = value * 65536.0f + 0.5f; + // Use two's complement + uint32_t u = static_cast(i); + WriteICCUint32(u, pos, icc); + return true; +} + +Status CreateICCHeader(const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT header) { + // TODO(lode): choose color management engine name, e.g. "skia" if + // integrated in skia. + static const char* kCmm = "jxl "; + + header->resize(128, 0); + + WriteICCUint32(0, 0, header); // size, correct value filled in at end + WriteICCTag(kCmm, 4, header); + WriteICCUint32(0x04300000u, 8, header); + WriteICCTag("mntr", 12, header); + WriteICCTag(c.IsGray() ? "GRAY" : "RGB ", 16, header); + WriteICCTag("XYZ ", 20, header); + + // Three uint32_t's date/time encoding. + // TODO(lode): encode actual date and time, this is a placeholder + uint32_t year = 2019, month = 12, day = 1; + uint32_t hour = 0, minute = 0, second = 0; + WriteICCUint16(year, 24, header); + WriteICCUint16(month, 26, header); + WriteICCUint16(day, 28, header); + WriteICCUint16(hour, 30, header); + WriteICCUint16(minute, 32, header); + WriteICCUint16(second, 34, header); + + WriteICCTag("acsp", 36, header); + WriteICCTag("APPL", 40, header); + WriteICCUint32(0, 44, header); // flags + WriteICCUint32(0, 48, header); // device manufacturer + WriteICCUint32(0, 52, header); // device model + WriteICCUint32(0, 56, header); // device attributes + WriteICCUint32(0, 60, header); // device attributes + WriteICCUint32(static_cast(c.rendering_intent), 64, header); + + // Mandatory D50 white point of profile connection space + WriteICCUint32(0x0000f6d6, 68, header); + WriteICCUint32(0x00010000, 72, header); + WriteICCUint32(0x0000d32d, 76, header); + + WriteICCTag(kCmm, 80, header); + + return true; +} + +void AddToICCTagTable(const char* tag, size_t offset, size_t size, + PaddedBytes* JXL_RESTRICT tagtable, + std::vector* offsets) { + WriteICCTag(tag, tagtable->size(), tagtable); + // writing true offset deferred to later + WriteICCUint32(0, tagtable->size(), tagtable); + offsets->push_back(offset); + WriteICCUint32(size, tagtable->size(), tagtable); +} + +void FinalizeICCTag(PaddedBytes* JXL_RESTRICT tags, size_t* offset, + size_t* size) { + while ((tags->size() & 3) != 0) { + tags->push_back(0); + } + *offset += *size; + *size = tags->size() - *offset; +} + +// The input text must be ASCII, writing other characters to UTF-16 is not +// implemented. +void CreateICCMlucTag(const std::string& text, PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("mluc", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + WriteICCUint32(1, tags->size(), tags); + WriteICCUint32(12, tags->size(), tags); + WriteICCTag("enUS", tags->size(), tags); + WriteICCUint32(text.size() * 2, tags->size(), tags); + WriteICCUint32(28, tags->size(), tags); + for (size_t i = 0; i < text.size(); i++) { + tags->push_back(0); // prepend 0 for UTF-16 + tags->push_back(text[i]); + } +} + +Status CreateICCXYZTag(float xyz[3], PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("XYZ ", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + for (size_t i = 0; i < 3; ++i) { + JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(xyz[i], tags->size(), tags)); + } + return true; +} + +Status CreateICCChadTag(float chad[9], PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("sf32", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + for (size_t i = 0; i < 9; i++) { + JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(chad[i], tags->size(), tags)); + } + return true; +} + +void CreateICCCurvCurvTag(const std::vector& curve, + PaddedBytes* JXL_RESTRICT tags) { + size_t pos = tags->size(); + tags->resize(tags->size() + 12 + curve.size() * 2, 0); + WriteICCTag("curv", pos, tags); + WriteICCUint32(0, pos + 4, tags); + WriteICCUint32(curve.size(), pos + 8, tags); + for (size_t i = 0; i < curve.size(); i++) { + WriteICCUint16(curve[i], pos + 12 + i * 2, tags); + } +} + +Status CreateICCCurvParaTag(std::vector params, size_t curve_type, + PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("para", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + WriteICCUint16(curve_type, tags->size(), tags); + WriteICCUint16(0, tags->size(), tags); + for (size_t i = 0; i < params.size(); i++) { + JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(params[i], tags->size(), tags)); + } + return true; +} +} // namespace + +Status MaybeCreateProfile(const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT icc) { + PaddedBytes header, tagtable, tags; + + if (c.GetColorSpace() == ColorSpace::kUnknown || c.tf.IsUnknown()) { + return false; // Not an error + } + + switch (c.GetColorSpace()) { + case ColorSpace::kRGB: + case ColorSpace::kGray: + break; // OK + case ColorSpace::kXYB: + return JXL_FAILURE("XYB ICC not yet implemented"); + default: + return JXL_FAILURE("Invalid CS %u", + static_cast(c.GetColorSpace())); + } + + JXL_RETURN_IF_ERROR(CreateICCHeader(c, &header)); + + std::vector offsets; + // tag count, deferred to later + WriteICCUint32(0, tagtable.size(), &tagtable); + + size_t tag_offset = 0, tag_size = 0; + + CreateICCMlucTag(Description(c), &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("desc", tag_offset, tag_size, &tagtable, &offsets); + + const std::string copyright = + "Copyright 2019 Google LLC, CC-BY-SA 3.0 Unported " + "license(https://creativecommons.org/licenses/by-sa/3.0/legalcode)"; + CreateICCMlucTag(copyright, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("cprt", tag_offset, tag_size, &tagtable, &offsets); + + // TODO(eustas): isn't it the other way round: gray image has d50 WhitePoint? + if (c.IsGray()) { + float wtpt[3]; + JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(c.GetWhitePoint(), wtpt)); + JXL_RETURN_IF_ERROR(CreateICCXYZTag(wtpt, &tags)); + } else { + float d50[3] = {0.964203, 1.0, 0.824905}; + JXL_RETURN_IF_ERROR(CreateICCXYZTag(d50, &tags)); + } + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("wtpt", tag_offset, tag_size, &tagtable, &offsets); + + if (!c.IsGray()) { + // Chromatic adaptation matrix + float chad[9]; + JXL_RETURN_IF_ERROR(CreateICCChadMatrix(c.GetWhitePoint(), chad)); + + const PrimariesCIExy primaries = c.GetPrimaries(); + float m[9]; + JXL_RETURN_IF_ERROR(CreateICCRGBMatrix(primaries.r, primaries.g, + primaries.b, c.GetWhitePoint(), m)); + float r[3] = {m[0], m[3], m[6]}; + float g[3] = {m[1], m[4], m[7]}; + float b[3] = {m[2], m[5], m[8]}; + + JXL_RETURN_IF_ERROR(CreateICCChadTag(chad, &tags)); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("chad", tag_offset, tag_size, &tagtable, &offsets); + + JXL_RETURN_IF_ERROR(CreateICCXYZTag(r, &tags)); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("rXYZ", tag_offset, tag_size, &tagtable, &offsets); + + JXL_RETURN_IF_ERROR(CreateICCXYZTag(g, &tags)); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("gXYZ", tag_offset, tag_size, &tagtable, &offsets); + + JXL_RETURN_IF_ERROR(CreateICCXYZTag(b, &tags)); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("bXYZ", tag_offset, tag_size, &tagtable, &offsets); + } + + if (c.tf.IsGamma()) { + float gamma = 1.0 / c.tf.GetGamma(); + JXL_RETURN_IF_ERROR( + CreateICCCurvParaTag({gamma, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); + } else { + switch (c.tf.GetTransferFunction()) { + case TransferFunction::kHLG: + CreateICCCurvCurvTag( + HWY_DYNAMIC_DISPATCH(CreateTableCurve)(4096, ExtraTF::kHLG), &tags); + break; + case TransferFunction::kPQ: + CreateICCCurvCurvTag( + HWY_DYNAMIC_DISPATCH(CreateTableCurve)(4096, ExtraTF::kPQ), &tags); + break; + case TransferFunction::kSRGB: + JXL_RETURN_IF_ERROR(CreateICCCurvParaTag( + {2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045}, 3, &tags)); + break; + case TransferFunction::k709: + JXL_RETURN_IF_ERROR(CreateICCCurvParaTag( + {1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081}, 3, + &tags)); + break; + case TransferFunction::kLinear: + JXL_RETURN_IF_ERROR( + CreateICCCurvParaTag({1.0, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); + break; + case TransferFunction::kDCI: + JXL_RETURN_IF_ERROR( + CreateICCCurvParaTag({2.6, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); + break; + default: + JXL_ABORT("Unknown TF %d", c.tf.GetTransferFunction()); + } + } + FinalizeICCTag(&tags, &tag_offset, &tag_size); + if (c.IsGray()) { + AddToICCTagTable("kTRC", tag_offset, tag_size, &tagtable, &offsets); + } else { + AddToICCTagTable("rTRC", tag_offset, tag_size, &tagtable, &offsets); + AddToICCTagTable("gTRC", tag_offset, tag_size, &tagtable, &offsets); + AddToICCTagTable("bTRC", tag_offset, tag_size, &tagtable, &offsets); + } + + // Tag count + WriteICCUint32(offsets.size(), 0, &tagtable); + for (size_t i = 0; i < offsets.size(); i++) { + WriteICCUint32(offsets[i] + header.size() + tagtable.size(), 4 + 12 * i + 4, + &tagtable); + } + + // ICC profile size + WriteICCUint32(header.size() + tagtable.size() + tags.size(), 0, &header); + + *icc = header; + icc->append(tagtable); + icc->append(tags); + + // The MD5 checksum must be computed on the profile with profile flags, + // rendering intent, and region of the checksum itself, set to 0. + // TODO(lode): manually verify with a reliable tool that this creates correct + // signature (profile id) for ICC profiles. + PaddedBytes icc_sum = *icc; + memset(icc_sum.data() + 44, 0, 4); + memset(icc_sum.data() + 64, 0, 4); + uint8_t checksum[16]; + ICCComputeMD5(icc_sum, checksum); + + memcpy(icc->data() + 84, checksum, sizeof(checksum)); + + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/color_management.h b/lib/jxl/color_management.h new file mode 100644 index 0000000..f728fe5 --- /dev/null +++ b/lib/jxl/color_management.h @@ -0,0 +1,38 @@ +// 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_COLOR_MANAGEMENT_H_ +#define LIB_JXL_COLOR_MANAGEMENT_H_ + +// ICC profiles and color space conversions. + +#include +#include + +#include + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" + +namespace jxl { + +enum class ExtraTF { + kNone, + kPQ, + kHLG, + kSRGB, +}; + +Status MaybeCreateProfile(const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT icc); + +Status CIEXYZFromWhiteCIExy(const CIExy& xy, float XYZ[3]); + +} // namespace jxl + +#endif // LIB_JXL_COLOR_MANAGEMENT_H_ diff --git a/lib/jxl/color_management_test.cc b/lib/jxl/color_management_test.cc new file mode 100644 index 0000000..0747e5c --- /dev/null +++ b/lib/jxl/color_management_test.cc @@ -0,0 +1,237 @@ +// 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/color_management.h" + +#include +#include + +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { + +std::ostream& operator<<(std::ostream& os, const CIExy& xy) { + return os << "{x=" << xy.x << ", y=" << xy.y << "}"; +} + +std::ostream& operator<<(std::ostream& os, const PrimariesCIExy& primaries) { + return os << "{r=" << primaries.r << ", g=" << primaries.g + << ", b=" << primaries.b << "}"; +} + +namespace { + +using ::testing::ElementsAre; +using ::testing::FloatNear; + +// Small enough to be fast. If changed, must update Generate*. +static constexpr size_t kWidth = 16; + +struct Globals { + // TODO(deymo): Make this a const. + static Globals* GetInstance() { + static Globals ret; + return &ret; + } + + private: + static constexpr size_t kNumThreads = 0; // only have a single row. + + Globals() : pool(kNumThreads) { + in_gray = GenerateGray(); + in_color = GenerateColor(); + out_gray = ImageF(kWidth, 1); + out_color = ImageF(kWidth * 3, 1); + + c_native = ColorEncoding::LinearSRGB(/*is_gray=*/false); + c_gray = ColorEncoding::LinearSRGB(/*is_gray=*/true); + } + + static ImageF GenerateGray() { + ImageF gray(kWidth, 1); + float* JXL_RESTRICT row = gray.Row(0); + // Increasing left to right + for (uint32_t x = 0; x < kWidth; ++x) { + row[x] = x * 1.0f / (kWidth - 1); // [0, 1] + } + return gray; + } + + static ImageF GenerateColor() { + ImageF image(kWidth * 3, 1); + float* JXL_RESTRICT interleaved = image.Row(0); + std::fill(interleaved, interleaved + kWidth * 3, 0.0f); + + // [0, 4): neutral + for (int32_t x = 0; x < 4; ++x) { + interleaved[3 * x + 0] = x * 1.0f / 3; // [0, 1] + interleaved[3 * x + 2] = interleaved[3 * x + 1] = interleaved[3 * x + 0]; + } + + // [4, 13): pure RGB with low/medium/high saturation + for (int32_t c = 0; c < 3; ++c) { + interleaved[3 * (4 + c) + c] = 0.08f + c * 0.01f; + interleaved[3 * (7 + c) + c] = 0.75f + c * 0.01f; + interleaved[3 * (10 + c) + c] = 1.0f; + } + + // [13, 16): impure, not quite saturated RGB + interleaved[3 * 13 + 0] = 0.86f; + interleaved[3 * 13 + 2] = interleaved[3 * 13 + 1] = 0.16f; + interleaved[3 * 14 + 1] = 0.87f; + interleaved[3 * 14 + 2] = interleaved[3 * 14 + 0] = 0.16f; + interleaved[3 * 15 + 2] = 0.88f; + interleaved[3 * 15 + 1] = interleaved[3 * 15 + 0] = 0.16f; + + return image; + } + + public: + ThreadPoolInternal pool; + + // ImageF so we can use VerifyRelativeError; all are interleaved RGB. + ImageF in_gray; + ImageF in_color; + ImageF out_gray; + ImageF out_color; + ColorEncoding c_native; + ColorEncoding c_gray; +}; + +class ColorManagementTest + : public ::testing::TestWithParam { + public: + static void VerifySameFields(const ColorEncoding& c, + const ColorEncoding& c2) { + ASSERT_EQ(c.rendering_intent, c2.rendering_intent); + ASSERT_EQ(c.GetColorSpace(), c2.GetColorSpace()); + ASSERT_EQ(c.white_point, c2.white_point); + if (c.HasPrimaries()) { + ASSERT_EQ(c.primaries, c2.primaries); + } + ASSERT_TRUE(c.tf.IsSame(c2.tf)); + } + + // "Same" pixels after converting g->c_native -> c -> g->c_native. + static void VerifyPixelRoundTrip(const ColorEncoding& c) { + Globals* g = Globals::GetInstance(); + const ColorEncoding& c_native = c.IsGray() ? g->c_gray : g->c_native; + ColorSpaceTransform xform_fwd; + ColorSpaceTransform xform_rev; + ASSERT_TRUE(xform_fwd.Init(c_native, c, kDefaultIntensityTarget, kWidth, + g->pool.NumThreads())); + ASSERT_TRUE(xform_rev.Init(c, c_native, kDefaultIntensityTarget, kWidth, + g->pool.NumThreads())); + + const size_t thread = 0; + const ImageF& in = c.IsGray() ? g->in_gray : g->in_color; + ImageF* JXL_RESTRICT out = c.IsGray() ? &g->out_gray : &g->out_color; + DoColorSpaceTransform(&xform_fwd, thread, in.Row(0), + xform_fwd.BufDst(thread)); + DoColorSpaceTransform(&xform_rev, thread, xform_fwd.BufDst(thread), + out->Row(0)); + +#if JPEGXL_ENABLE_SKCMS + double max_l1 = 7E-4; + double max_rel = 4E-7; +#else + double max_l1 = 5E-5; + // Most are lower; reached 3E-7 with D60 AP0. + double max_rel = 4E-7; +#endif + if (c.IsGray()) max_rel = 2E-5; + VerifyRelativeError(in, *out, max_l1, max_rel); + } +}; +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(ColorManagementTestInstantiation, + ColorManagementTest, + ::testing::ValuesIn(test::AllEncodings())); + +// Exercises the ColorManagement interface for ALL ColorEncoding synthesizable +// via enums. +TEST_P(ColorManagementTest, VerifyAllProfiles) { + ColorEncoding c = ColorEncodingFromDescriptor(GetParam()); + printf("%s\n", Description(c).c_str()); + + // Can create profile. + ASSERT_TRUE(c.CreateICC()); + + // Can set an equivalent ColorEncoding from the generated ICC profile. + ColorEncoding c3; + ASSERT_TRUE(c3.SetICC(PaddedBytes(c.ICC()))); + VerifySameFields(c, c3); + + VerifyPixelRoundTrip(c); +} + +testing::Matcher CIExyIs(const double x, const double y) { + static constexpr double kMaxError = 1e-4; + return testing::AllOf( + testing::Field(&CIExy::x, testing::DoubleNear(x, kMaxError)), + testing::Field(&CIExy::y, testing::DoubleNear(y, kMaxError))); +} + +testing::Matcher PrimariesAre( + const testing::Matcher& r, const testing::Matcher& g, + const testing::Matcher& b) { + return testing::AllOf(testing::Field(&PrimariesCIExy::r, r), + testing::Field(&PrimariesCIExy::g, g), + testing::Field(&PrimariesCIExy::b, b)); +} + +TEST_F(ColorManagementTest, sRGBChromaticity) { + const ColorEncoding sRGB = ColorEncoding::SRGB(); + EXPECT_THAT(sRGB.GetWhitePoint(), CIExyIs(0.3127, 0.3290)); + EXPECT_THAT(sRGB.GetPrimaries(), + PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), + CIExyIs(0.15, 0.06))); +} + +TEST_F(ColorManagementTest, D2700Chromaticity) { + PaddedBytes icc = ReadTestData("jxl/color_management/sRGB-D2700.icc"); + ColorEncoding sRGB_D2700; + ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc))); + + EXPECT_THAT(sRGB_D2700.GetWhitePoint(), CIExyIs(0.45986, 0.41060)); + // The illuminant-relative chromaticities of this profile's primaries are the + // same as for sRGB. It is the PCS-relative chromaticities that would be + // different. + EXPECT_THAT(sRGB_D2700.GetPrimaries(), + PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), + CIExyIs(0.15, 0.06))); +} + +TEST_F(ColorManagementTest, D2700ToSRGB) { + PaddedBytes icc = ReadTestData("jxl/color_management/sRGB-D2700.icc"); + ColorEncoding sRGB_D2700; + ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc))); + + ColorSpaceTransform transform; + ASSERT_TRUE(transform.Init(sRGB_D2700, ColorEncoding::SRGB(), + kDefaultIntensityTarget, 1, 1)); + const float sRGB_D2700_values[3] = {0.863, 0.737, 0.490}; + float sRGB_values[3]; + DoColorSpaceTransform(&transform, 0, sRGB_D2700_values, sRGB_values); + EXPECT_THAT(sRGB_values, + ElementsAre(FloatNear(0.914, 1e-3), FloatNear(0.745, 1e-3), + FloatNear(0.601, 1e-3))); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/common.h b/lib/jxl/common.h new file mode 100644 index 0000000..98c98d3 --- /dev/null +++ b/lib/jxl/common.h @@ -0,0 +1,197 @@ +// 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_COMMON_H_ +#define LIB_JXL_COMMON_H_ + +// Shared constants and helper functions. + +#include +#include +#include + +#include // numeric_limits +#include // unique_ptr +#include + +#include "lib/jxl/base/compiler_specific.h" + +#ifndef JXL_HIGH_PRECISION +#define JXL_HIGH_PRECISION 1 +#endif + +// Macro that defines whether support for decoding JXL files to JPEG is enabled. +#ifndef JPEGXL_ENABLE_TRANSCODE_JPEG +#define JPEGXL_ENABLE_TRANSCODE_JPEG 1 +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +namespace jxl { +// Some enums and typedefs used by more than one header file. + +constexpr size_t kBitsPerByte = 8; // more clear than CHAR_BIT + +constexpr inline size_t RoundUpBitsToByteMultiple(size_t bits) { + return (bits + 7) & ~size_t(7); +} + +constexpr inline size_t RoundUpToBlockDim(size_t dim) { + return (dim + 7) & ~size_t(7); +} + +static inline bool JXL_MAYBE_UNUSED SafeAdd(const uint64_t a, const uint64_t b, + uint64_t& sum) { + sum = a + b; + return sum >= a; // no need to check b - either sum >= both or < both. +} + +template +constexpr inline T1 DivCeil(T1 a, T2 b) { + return (a + b - 1) / b; +} + +// Works for any `align`; if a power of two, compiler emits ADD+AND. +constexpr inline size_t RoundUpTo(size_t what, size_t align) { + return DivCeil(what, align) * align; +} + +constexpr double kPi = 3.14159265358979323846264338327950288; + +// Reasonable default for sRGB, matches common monitors. We map white to this +// many nits (cd/m^2) by default. Butteraugli was tuned for 250 nits, which is +// very close. +static constexpr float kDefaultIntensityTarget = 255; + +template +constexpr T Pi(T multiplier) { + return static_cast(multiplier * kPi); +} + +// Block is the square grid of pixels to which an "energy compaction" +// transformation (e.g. DCT) is applied. Each block has its own AC quantizer. +constexpr size_t kBlockDim = 8; + +constexpr size_t kDCTBlockSize = kBlockDim * kBlockDim; + +constexpr size_t kGroupDim = 256; +static_assert(kGroupDim % kBlockDim == 0, + "Group dim should be divisible by block dim"); +constexpr size_t kGroupDimInBlocks = kGroupDim / kBlockDim; + +// Maximum number of passes in an image. +constexpr size_t kMaxNumPasses = 11; + +// Maximum number of reference frames. +constexpr size_t kMaxNumReferenceFrames = 4; + +// Dimensions of a frame, in pixels, and other derived dimensions. +// Computed from FrameHeader. +// TODO(veluca): add extra channels. +struct FrameDimensions { + void Set(size_t xsize, size_t ysize, size_t group_size_shift, + size_t max_hshift, size_t max_vshift, bool modular_mode, + size_t upsampling) { + group_dim = (kGroupDim >> 1) << group_size_shift; + dc_group_dim = group_dim * kBlockDim; + xsize_upsampled = xsize; + ysize_upsampled = ysize; + this->xsize = DivCeil(xsize, upsampling); + this->ysize = DivCeil(ysize, upsampling); + xsize_blocks = DivCeil(this->xsize, kBlockDim << max_hshift) << max_hshift; + ysize_blocks = DivCeil(this->ysize, kBlockDim << max_vshift) << max_vshift; + xsize_padded = xsize_blocks * kBlockDim; + ysize_padded = ysize_blocks * kBlockDim; + if (modular_mode) { + // Modular mode doesn't have any padding. + xsize_padded = this->xsize; + ysize_padded = this->ysize; + } + xsize_upsampled_padded = xsize_padded * upsampling; + ysize_upsampled_padded = ysize_padded * upsampling; + xsize_groups = DivCeil(this->xsize, group_dim); + ysize_groups = DivCeil(this->ysize, group_dim); + xsize_dc_groups = DivCeil(xsize_blocks, group_dim); + ysize_dc_groups = DivCeil(ysize_blocks, group_dim); + num_groups = xsize_groups * ysize_groups; + num_dc_groups = xsize_dc_groups * ysize_dc_groups; + } + + // Image size without any upsampling, i.e. original_size / upsampling. + size_t xsize; + size_t ysize; + // Original image size. + size_t xsize_upsampled; + size_t ysize_upsampled; + // Image size after upsampling the padded image. + size_t xsize_upsampled_padded; + size_t ysize_upsampled_padded; + // Image size after padding to a multiple of kBlockDim (if VarDCT mode). + size_t xsize_padded; + size_t ysize_padded; + // Image size in kBlockDim blocks. + size_t xsize_blocks; + size_t ysize_blocks; + // Image size in number of groups. + size_t xsize_groups; + size_t ysize_groups; + // Image size in number of DC groups. + size_t xsize_dc_groups; + size_t ysize_dc_groups; + // Number of AC or DC groups. + size_t num_groups; + size_t num_dc_groups; + // Size of a group. + size_t group_dim; + size_t dc_group_dim; +}; + +// Prior to C++14 (i.e. C++11): provide our own make_unique +#if __cplusplus < 201402L +template +std::unique_ptr make_unique(Args&&... args) { + return std::unique_ptr(new T(std::forward(args)...)); +} +#else +using std::make_unique; +#endif + +template +JXL_INLINE T Clamp1(T val, T low, T hi) { + return val < low ? low : val > hi ? hi : val; +} + +// Encodes non-negative (X) into (2 * X), negative (-X) into (2 * X - 1) +constexpr uint32_t PackSigned(int32_t value) + JXL_NO_SANITIZE("unsigned-integer-overflow") { + return (static_cast(value) << 1) ^ + ((static_cast(~value) >> 31) - 1); +} + +// Reverse to PackSigned, i.e. UnpackSigned(PackSigned(X)) == X. +// (((~value) & 1) - 1) is either 0 or 0xFF...FF and it will have an expected +// unsigned-integer-overflow. +constexpr intptr_t UnpackSigned(size_t value) + JXL_NO_SANITIZE("unsigned-integer-overflow") { + return static_cast((value >> 1) ^ (((~value) & 1) - 1)); +} + +// conversion from integer to string. +template +std::string ToString(T n) { + char data[32] = {}; + if (T(0.1) != T(0)) { + // float + snprintf(data, sizeof(data), "%g", static_cast(n)); + } else if (T(-1) > T(0)) { + // unsigned + snprintf(data, sizeof(data), "%llu", static_cast(n)); + } else { + // signed + snprintf(data, sizeof(data), "%lld", static_cast(n)); + } + return data; +} +} // namespace jxl + +#endif // LIB_JXL_COMMON_H_ diff --git a/lib/jxl/compressed_dc.cc b/lib/jxl/compressed_dc.cc new file mode 100644 index 0000000..bac580a --- /dev/null +++ b/lib/jxl/compressed_dc.cc @@ -0,0 +1,312 @@ +// 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/compressed_dc.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/compressed_dc.cc" +#include +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/image.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +using D = HWY_FULL(float); +using DScalar = HWY_CAPPED(float, 1); + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::Vec; + +// TODO(veluca): optimize constants. +const float w1 = 0.20345139757231578f; +const float w2 = 0.0334829185968739f; +const float w0 = 1.0f - 4.0f * (w1 + w2); + +template +V MaxWorkaround(V a, V b) { +#if (HWY_TARGET == HWY_AVX3) && HWY_COMPILER_CLANG <= 800 + // Prevents "Do not know how to split the result of this operator" error + return IfThenElse(a > b, a, b); +#else + return Max(a, b); +#endif +} + +template +JXL_INLINE void ComputePixelChannel(const D d, const float dc_factor, + const float* JXL_RESTRICT row_top, + const float* JXL_RESTRICT row, + const float* JXL_RESTRICT row_bottom, + Vec* JXL_RESTRICT mc, + Vec* JXL_RESTRICT sm, + Vec* JXL_RESTRICT gap, size_t x) { + const auto tl = LoadU(d, row_top + x - 1); + const auto tc = Load(d, row_top + x); + const auto tr = LoadU(d, row_top + x + 1); + + const auto ml = LoadU(d, row + x - 1); + *mc = Load(d, row + x); + const auto mr = LoadU(d, row + x + 1); + + const auto bl = LoadU(d, row_bottom + x - 1); + const auto bc = Load(d, row_bottom + x); + const auto br = LoadU(d, row_bottom + x + 1); + + const auto w_center = Set(d, w0); + const auto w_side = Set(d, w1); + const auto w_corner = Set(d, w2); + + const auto corner = tl + tr + bl + br; + const auto side = ml + mr + tc + bc; + *sm = corner * w_corner + side * w_side + *mc * w_center; + + const auto dc_quant = Set(d, dc_factor); + *gap = MaxWorkaround(*gap, Abs((*mc - *sm) / dc_quant)); +} + +template +JXL_INLINE void ComputePixel( + const float* JXL_RESTRICT dc_factors, + const float* JXL_RESTRICT* JXL_RESTRICT rows_top, + const float* JXL_RESTRICT* JXL_RESTRICT rows, + const float* JXL_RESTRICT* JXL_RESTRICT rows_bottom, + float* JXL_RESTRICT* JXL_RESTRICT out_rows, size_t x) { + const D d; + auto mc_x = Undefined(d); + auto mc_y = Undefined(d); + auto mc_b = Undefined(d); + auto sm_x = Undefined(d); + auto sm_y = Undefined(d); + auto sm_b = Undefined(d); + auto gap = Set(d, 0.5f); + ComputePixelChannel(d, dc_factors[0], rows_top[0], rows[0], rows_bottom[0], + &mc_x, &sm_x, &gap, x); + ComputePixelChannel(d, dc_factors[1], rows_top[1], rows[1], rows_bottom[1], + &mc_y, &sm_y, &gap, x); + ComputePixelChannel(d, dc_factors[2], rows_top[2], rows[2], rows_bottom[2], + &mc_b, &sm_b, &gap, x); + auto factor = MulAdd(Set(d, -4.0f), gap, Set(d, 3.0f)); + factor = ZeroIfNegative(factor); + + auto out = MulAdd(sm_x - mc_x, factor, mc_x); + Store(out, d, out_rows[0] + x); + out = MulAdd(sm_y - mc_y, factor, mc_y); + Store(out, d, out_rows[1] + x); + out = MulAdd(sm_b - mc_b, factor, mc_b); + Store(out, d, out_rows[2] + x); +} + +void AdaptiveDCSmoothing(const float* dc_factors, Image3F* dc, + ThreadPool* pool) { + const size_t xsize = dc->xsize(); + const size_t ysize = dc->ysize(); + if (ysize <= 2 || xsize <= 2) return; + + // TODO(veluca): use tile-based processing? + // TODO(veluca): decide if changes to the y channel should be propagated to + // the x and b channels through color correlation. + JXL_ASSERT(w1 + w2 < 0.25f); + + PROFILER_FUNC; + + Image3F smoothed(xsize, ysize); + // Fill in borders that the loop below will not. First and last are unused. + for (size_t c = 0; c < 3; c++) { + for (size_t y : {size_t(0), ysize - 1}) { + memcpy(smoothed.PlaneRow(c, y), dc->PlaneRow(c, y), + xsize * sizeof(float)); + } + } + auto process_row = [&](int y, int /*thread*/) { + const float* JXL_RESTRICT rows_top[3]{ + dc->ConstPlaneRow(0, y - 1), + dc->ConstPlaneRow(1, y - 1), + dc->ConstPlaneRow(2, y - 1), + }; + const float* JXL_RESTRICT rows[3] = { + dc->ConstPlaneRow(0, y), + dc->ConstPlaneRow(1, y), + dc->ConstPlaneRow(2, y), + }; + const float* JXL_RESTRICT rows_bottom[3] = { + dc->ConstPlaneRow(0, y + 1), + dc->ConstPlaneRow(1, y + 1), + dc->ConstPlaneRow(2, y + 1), + }; + float* JXL_RESTRICT rows_out[3] = { + smoothed.PlaneRow(0, y), + smoothed.PlaneRow(1, y), + smoothed.PlaneRow(2, y), + }; + for (size_t x : {size_t(0), xsize - 1}) { + for (size_t c = 0; c < 3; c++) { + rows_out[c][x] = rows[c][x]; + } + } + + size_t x = 1; + // First pixels + const size_t N = Lanes(D()); + for (; x < std::min(N, xsize - 1); x++) { + ComputePixel(dc_factors, rows_top, rows, rows_bottom, rows_out, + x); + } + // Full vectors. + for (; x + N <= xsize - 1; x += N) { + ComputePixel(dc_factors, rows_top, rows, rows_bottom, rows_out, x); + } + // Last pixels. + for (; x < xsize - 1; x++) { + ComputePixel(dc_factors, rows_top, rows, rows_bottom, rows_out, + x); + } + }; + RunOnPool(pool, 1, ysize - 1, ThreadPool::SkipInit(), process_row, + "DCSmoothingRow"); + dc->Swap(smoothed); +} + +// DC dequantization. +void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in, + const float* dc_factors, float mul, const float* cfl_factors, + YCbCrChromaSubsampling chroma_subsampling, + const BlockCtxMap& bctx) { + const HWY_FULL(float) df; + const Rebind di; // assumes pixel_type <= float + if (chroma_subsampling.Is444()) { + const auto fac_x = Set(df, dc_factors[0] * mul); + const auto fac_y = Set(df, dc_factors[1] * mul); + const auto fac_b = Set(df, dc_factors[2] * mul); + const auto cfl_fac_x = Set(df, cfl_factors[0]); + const auto cfl_fac_b = Set(df, cfl_factors[2]); + for (size_t y = 0; y < r.ysize(); y++) { + float* dec_row_x = r.PlaneRow(dc, 0, y); + float* dec_row_y = r.PlaneRow(dc, 1, y); + float* dec_row_b = r.PlaneRow(dc, 2, y); + const int32_t* quant_row_x = in.channel[1].plane.Row(y); + const int32_t* quant_row_y = in.channel[0].plane.Row(y); + const int32_t* quant_row_b = in.channel[2].plane.Row(y); + for (size_t x = 0; x < r.xsize(); x += Lanes(di)) { + const auto in_q_x = Load(di, quant_row_x + x); + const auto in_q_y = Load(di, quant_row_y + x); + const auto in_q_b = Load(di, quant_row_b + x); + const auto in_x = ConvertTo(df, in_q_x) * fac_x; + const auto in_y = ConvertTo(df, in_q_y) * fac_y; + const auto in_b = ConvertTo(df, in_q_b) * fac_b; + Store(in_y, df, dec_row_y + x); + Store(MulAdd(in_y, cfl_fac_x, in_x), df, dec_row_x + x); + Store(MulAdd(in_y, cfl_fac_b, in_b), df, dec_row_b + x); + } + } + } else { + for (size_t c : {1, 0, 2}) { + Rect rect(r.x0() >> chroma_subsampling.HShift(c), + r.y0() >> chroma_subsampling.VShift(c), + r.xsize() >> chroma_subsampling.HShift(c), + r.ysize() >> chroma_subsampling.VShift(c)); + const auto fac = Set(df, dc_factors[c] * mul); + const Channel& ch = in.channel[c < 2 ? c ^ 1 : c]; + for (size_t y = 0; y < rect.ysize(); y++) { + const int32_t* quant_row = ch.plane.Row(y); + float* row = rect.PlaneRow(dc, c, y); + for (size_t x = 0; x < rect.xsize(); x += Lanes(di)) { + const auto in_q = Load(di, quant_row + x); + const auto in = ConvertTo(df, in_q) * fac; + Store(in, df, row + x); + } + } + } + } + if (bctx.num_dc_ctxs <= 1) { + for (size_t y = 0; y < r.ysize(); y++) { + uint8_t* qdc_row = r.Row(quant_dc, y); + memset(qdc_row, 0, sizeof(*qdc_row) * r.xsize()); + } + } else { + for (size_t y = 0; y < r.ysize(); y++) { + uint8_t* qdc_row_val = r.Row(quant_dc, y); + const int32_t* quant_row_x = + in.channel[1].plane.Row(y >> chroma_subsampling.VShift(0)); + const int32_t* quant_row_y = + in.channel[0].plane.Row(y >> chroma_subsampling.VShift(1)); + const int32_t* quant_row_b = + in.channel[2].plane.Row(y >> chroma_subsampling.VShift(2)); + for (size_t x = 0; x < r.xsize(); x++) { + int bucket_x = 0, bucket_y = 0, bucket_b = 0; + for (int t : bctx.dc_thresholds[0]) { + if (quant_row_x[x >> chroma_subsampling.HShift(0)] > t) bucket_x++; + } + for (int t : bctx.dc_thresholds[1]) { + if (quant_row_y[x >> chroma_subsampling.HShift(1)] > t) bucket_y++; + } + for (int t : bctx.dc_thresholds[2]) { + if (quant_row_b[x >> chroma_subsampling.HShift(2)] > t) bucket_b++; + } + int bucket = bucket_x; + bucket *= bctx.dc_thresholds[2].size() + 1; + bucket += bucket_b; + bucket *= bctx.dc_thresholds[1].size() + 1; + bucket += bucket_y; + qdc_row_val[x] = bucket; + } + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(DequantDC); +HWY_EXPORT(AdaptiveDCSmoothing); +void AdaptiveDCSmoothing(const float* dc_factors, Image3F* dc, + ThreadPool* pool) { + return HWY_DYNAMIC_DISPATCH(AdaptiveDCSmoothing)(dc_factors, dc, pool); +} + +void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in, + const float* dc_factors, float mul, const float* cfl_factors, + YCbCrChromaSubsampling chroma_subsampling, + const BlockCtxMap& bctx) { + return HWY_DYNAMIC_DISPATCH(DequantDC)(r, dc, quant_dc, in, dc_factors, mul, + cfl_factors, chroma_subsampling, bctx); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/compressed_dc.h b/lib/jxl/compressed_dc.h new file mode 100644 index 0000000..b06e593 --- /dev/null +++ b/lib/jxl/compressed_dc.h @@ -0,0 +1,34 @@ +// 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_COMPRESSED_DC_H_ +#define LIB_JXL_COMPRESSED_DC_H_ + +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/modular/modular_image.h" + +// DC handling functions: encoding and decoding of DC to and from bitstream, and +// related function to initialize the per-group decoder cache. + +namespace jxl { + +// Smooth DC in already-smooth areas, to counteract banding. +void AdaptiveDCSmoothing(const float* dc_factors, Image3F* dc, + ThreadPool* pool); + +void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in, + const float* dc_factors, float mul, const float* cfl_factors, + YCbCrChromaSubsampling chroma_subsampling, + const BlockCtxMap& bctx); + +} // namespace jxl + +#endif // LIB_JXL_COMPRESSED_DC_H_ diff --git a/lib/jxl/compressed_image_test.cc b/lib/jxl/compressed_image_test.cc new file mode 100644 index 0000000..7546127 --- /dev/null +++ b/lib/jxl/compressed_image_test.cc @@ -0,0 +1,102 @@ +// 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 +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/gaborish.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { + +// Verifies ReconOpsinImage reconstructs with low butteraugli distance. +void RunRGBRoundTrip(float distance, bool fast) { + ThreadPoolInternal pool(4); + + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + JXL_CHECK(SetFromBytes(Span(orig), &io, &pool)); + // This test can only handle a single group. + io.ShrinkTo(std::min(io.xsize(), kGroupDim), std::min(io.ysize(), kGroupDim)); + + Image3F opsin(io.xsize(), io.ysize()); + (void)ToXYB(io.Main(), &pool, &opsin); + opsin = PadImageToMultiple(opsin, kBlockDim); + GaborishInverse(&opsin, 1.0f, &pool); + + CompressParams cparams; + cparams.butteraugli_distance = distance; + if (fast) { + cparams.speed_tier = SpeedTier::kWombat; + } + + JXL_CHECK(io.metadata.size.Set(opsin.xsize(), opsin.ysize())); + FrameHeader frame_header(&io.metadata); + frame_header.color_transform = ColorTransform::kXYB; + frame_header.loop_filter.epf_iters = 0; + + // Use custom weights for Gaborish. + frame_header.loop_filter.gab_custom = true; + frame_header.loop_filter.gab_x_weight1 = 0.11501538179658321f; + frame_header.loop_filter.gab_x_weight2 = 0.089979079587015454f; + frame_header.loop_filter.gab_y_weight1 = 0.11501538179658321f; + frame_header.loop_filter.gab_y_weight2 = 0.089979079587015454f; + frame_header.loop_filter.gab_b_weight1 = 0.11501538179658321f; + frame_header.loop_filter.gab_b_weight2 = 0.089979079587015454f; + + PassesEncoderState enc_state; + JXL_CHECK(InitializePassesSharedState(frame_header, &enc_state.shared)); + + enc_state.shared.quantizer.SetQuant(4.0f, 4.0f, + &enc_state.shared.raw_quant_field); + enc_state.shared.ac_strategy.FillDCT8(); + enc_state.cparams = cparams; + ZeroFillImage(&enc_state.shared.epf_sharpness); + CodecInOut io1; + io1.Main() = RoundtripImage(opsin, &enc_state, &pool); + io1.metadata.m.color_encoding = io1.Main().c_current(); + + EXPECT_LE(ButteraugliDistance(io, io1, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 1.2); +} + +TEST(CompressedImageTest, RGBRoundTrip_1) { RunRGBRoundTrip(1.0, false); } + +TEST(CompressedImageTest, RGBRoundTrip_1_fast) { RunRGBRoundTrip(1.0, true); } + +TEST(CompressedImageTest, RGBRoundTrip_2) { RunRGBRoundTrip(2.0, false); } + +TEST(CompressedImageTest, RGBRoundTrip_2_fast) { RunRGBRoundTrip(2.0, true); } + +} // namespace +} // namespace jxl diff --git a/lib/jxl/convolve-inl.h b/lib/jxl/convolve-inl.h new file mode 100644 index 0000000..255bb9d --- /dev/null +++ b/lib/jxl/convolve-inl.h @@ -0,0 +1,119 @@ +// 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. + +#if defined(LIB_JXL_CONVOLVE_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_CONVOLVE_INL_H_ +#undef LIB_JXL_CONVOLVE_INL_H_ +#else +#define LIB_JXL_CONVOLVE_INL_H_ +#endif + +#include + +#include "lib/jxl/base/status.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Broadcast; +#if HWY_TARGET != HWY_SCALAR +using hwy::HWY_NAMESPACE::CombineShiftRightBytes; +#endif +using hwy::HWY_NAMESPACE::Vec; + +// Synthesizes left/right neighbors from a vector of center pixels. +class Neighbors { + public: + using D = HWY_CAPPED(float, 16); + using V = Vec; + + // Returns l[i] == c[Mirror(i - 1)]. + HWY_INLINE HWY_MAYBE_UNUSED static V FirstL1(const V c) { +#if HWY_CAP_GE256 + const D d; + HWY_ALIGN constexpr int32_t lanes[16] = {0, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // ONML'KJII +#elif HWY_TARGET == HWY_SCALAR + return c; // Same (the first mirrored value is the last valid one) +#else // 128 bit + // c = LKJI +#if HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(2, 1, 0, 0))}; // KJII +#else + const D d; + // TODO(deymo): Figure out if this can be optimized using a single vsri + // instruction to convert LKJI to KJII. + HWY_ALIGN constexpr int lanes[4] = {0, 0, 1, 2}; // KJII + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#endif + } + + // Returns l[i] == c[Mirror(i - 2)]. + HWY_INLINE HWY_MAYBE_UNUSED static V FirstL2(const V c) { +#if HWY_CAP_GE256 + const D d; + HWY_ALIGN constexpr int32_t lanes[16] = {1, 0, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // NMLK'JIIJ +#elif HWY_TARGET == HWY_SCALAR + const D d; + JXL_ASSERT(false); // unsupported, avoid calling this. + return Zero(d); +#else // 128 bit + // c = LKJI +#if HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(1, 0, 0, 1))}; // JIIJ +#else + const D d; + HWY_ALIGN constexpr int lanes[4] = {1, 0, 0, 1}; // JIIJ + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#endif + } + + // Returns l[i] == c[Mirror(i - 3)]. + HWY_INLINE HWY_MAYBE_UNUSED static V FirstL3(const V c) { +#if HWY_CAP_GE256 + const D d; + HWY_ALIGN constexpr int32_t lanes[16] = {2, 1, 0, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // MLKJ'IIJK +#elif HWY_TARGET == HWY_SCALAR + const D d; + JXL_ASSERT(false); // unsupported, avoid calling this. + return Zero(d); +#else // 128 bit + // c = LKJI +#if HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(0, 0, 1, 2))}; // IIJK +#else + const D d; + HWY_ALIGN constexpr int lanes[4] = {2, 1, 0, 0}; // IIJK + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#endif + } +}; + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_CONVOLVE_INL_H_ diff --git a/lib/jxl/convolve.cc b/lib/jxl/convolve.cc new file mode 100644 index 0000000..cc7fc3f --- /dev/null +++ b/lib/jxl/convolve.cc @@ -0,0 +1,1332 @@ +// 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/convolve.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve.cc" +#include +#include + +#include "lib/jxl/common.h" // RoundUpTo +#include "lib/jxl/convolve-inl.h" +#include "lib/jxl/image_ops.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Vec; + +// Weighted sum of 1x5 pixels around ix, iy with [wx2 wx1 wx0 wx1 wx2]. +template +static float WeightedSumBorder(const ImageF& in, const WrapY wrap_y, + const int64_t ix, const int64_t iy, + const size_t xsize, const size_t ysize, + const float wx0, const float wx1, + const float wx2) { + const WrapMirror wrap_x; + const float* JXL_RESTRICT row = in.ConstRow(wrap_y(iy, ysize)); + const float in_m2 = row[wrap_x(ix - 2, xsize)]; + const float in_p2 = row[wrap_x(ix + 2, xsize)]; + const float in_m1 = row[wrap_x(ix - 1, xsize)]; + const float in_p1 = row[wrap_x(ix + 1, xsize)]; + const float in_00 = row[ix]; + const float sum_2 = wx2 * (in_m2 + in_p2); + const float sum_1 = wx1 * (in_m1 + in_p1); + const float sum_0 = wx0 * in_00; + return sum_2 + sum_1 + sum_0; +} + +template +static V WeightedSum(const ImageF& in, const WrapY wrap_y, const size_t ix, + const int64_t iy, const size_t ysize, const V wx0, + const V wx1, const V wx2) { + const HWY_FULL(float) d; + const float* JXL_RESTRICT center = in.ConstRow(wrap_y(iy, ysize)) + ix; + const auto in_m2 = LoadU(d, center - 2); + const auto in_p2 = LoadU(d, center + 2); + const auto in_m1 = LoadU(d, center - 1); + const auto in_p1 = LoadU(d, center + 1); + const auto in_00 = Load(d, center); + const auto sum_2 = wx2 * (in_m2 + in_p2); + const auto sum_1 = wx1 * (in_m1 + in_p1); + const auto sum_0 = wx0 * in_00; + return sum_2 + sum_1 + sum_0; +} + +// Produces result for one pixel +template +float Symmetric5Border(const ImageF& in, const Rect& rect, const int64_t ix, + const int64_t iy, const WeightsSymmetric5& weights) { + const float w0 = weights.c[0]; + const float w1 = weights.r[0]; + const float w2 = weights.R[0]; + const float w4 = weights.d[0]; + const float w5 = weights.L[0]; + const float w8 = weights.D[0]; + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + const WrapY wrap_y; + // Unrolled loop over all 5 rows of the kernel. + float sum0 = WeightedSumBorder(in, wrap_y, ix, iy, xsize, ysize, w0, w1, w2); + + sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 2, xsize, ysize, w2, w5, w8); + float sum1 = + WeightedSumBorder(in, wrap_y, ix, iy + 2, xsize, ysize, w2, w5, w8); + + sum0 += WeightedSumBorder(in, wrap_y, ix, iy - 1, xsize, ysize, w1, w4, w5); + sum1 += WeightedSumBorder(in, wrap_y, ix, iy + 1, xsize, ysize, w1, w4, w5); + + return sum0 + sum1; +} + +// Produces result for one vector's worth of pixels +template +static void Symmetric5Interior(const ImageF& in, const Rect& rect, + const int64_t ix, const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + const HWY_FULL(float) d; + + const auto w0 = LoadDup128(d, weights.c); + const auto w1 = LoadDup128(d, weights.r); + const auto w2 = LoadDup128(d, weights.R); + const auto w4 = LoadDup128(d, weights.d); + const auto w5 = LoadDup128(d, weights.L); + const auto w8 = LoadDup128(d, weights.D); + + const size_t ysize = rect.ysize(); + const WrapY wrap_y; + // Unrolled loop over all 5 rows of the kernel. + auto sum0 = WeightedSum(in, wrap_y, ix, iy, ysize, w0, w1, w2); + + sum0 += WeightedSum(in, wrap_y, ix, iy - 2, ysize, w2, w5, w8); + auto sum1 = WeightedSum(in, wrap_y, ix, iy + 2, ysize, w2, w5, w8); + + sum0 += WeightedSum(in, wrap_y, ix, iy - 1, ysize, w1, w4, w5); + sum1 += WeightedSum(in, wrap_y, ix, iy + 1, ysize, w1, w4, w5); + + Store(sum0 + sum1, d, row_out + ix); +} + +template +static void Symmetric5Row(const ImageF& in, const Rect& rect, const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + const int64_t kRadius = 2; + const size_t xsize = rect.xsize(); + + size_t ix = 0; + const HWY_FULL(float) d; + const size_t N = Lanes(d); + const size_t aligned_x = RoundUpTo(kRadius, N); + for (; ix < std::min(aligned_x, xsize); ++ix) { + row_out[ix] = Symmetric5Border(in, rect, ix, iy, weights); + } + for (; ix + N + kRadius <= xsize; ix += N) { + Symmetric5Interior(in, rect, ix, iy, weights, row_out); + } + for (; ix < xsize; ++ix) { + row_out[ix] = Symmetric5Border(in, rect, ix, iy, weights); + } +} + +static JXL_NOINLINE void Symmetric5BorderRow(const ImageF& in, const Rect& rect, + const int64_t iy, + const WeightsSymmetric5& weights, + float* JXL_RESTRICT row_out) { + return Symmetric5Row(in, rect, iy, weights, row_out); +} + +#if HWY_TARGET != HWY_SCALAR + +// Returns indices for SetTableIndices such that TableLookupLanes on the +// rightmost unaligned vector (rightmost sample in its most-significant lane) +// returns the mirrored values, with the mirror outside the last valid sample. +static inline const int32_t* MirrorLanes(const size_t mod) { + const HWY_CAPPED(float, 16) d; + constexpr size_t kN = MaxLanes(d); + + // For mod = `image width mod 16` 0..15: + // last full vec mirrored (mem order) loadedVec mirrorVec idxVec + // 0123456789abcdef| fedcba9876543210 fed..210 012..def 012..def + // 0123456789abcdef|0 0fedcba98765432 0fe..321 234..f00 123..eff + // 0123456789abcdef|01 10fedcba987654 10f..432 456..110 234..ffe + // 0123456789abcdef|012 210fedcba9876 210..543 67..2210 34..ffed + // 0123456789abcdef|0123 3210fedcba98 321..654 8..33210 4..ffedc + // 0123456789abcdef|01234 43210fedcba + // 0123456789abcdef|012345 543210fedc + // 0123456789abcdef|0123456 6543210fe + // 0123456789abcdef|01234567 76543210 + // 0123456789abcdef|012345678 8765432 + // 0123456789abcdef|0123456789 987654 + // 0123456789abcdef|0123456789A A9876 + // 0123456789abcdef|0123456789AB BA98 + // 0123456789abcdef|0123456789ABC CBA + // 0123456789abcdef|0123456789ABCD DC + // 0123456789abcdef|0123456789ABCDE E EDC..10f EED..210 ffe..321 +#if HWY_CAP_GE512 + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, // + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; +#elif HWY_CAP_GE256 + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = { + 1, 2, 3, 4, 5, 6, 7, 7, // + 6, 5, 4, 3, 2, 1, 0}; +#else // 128-bit + HWY_ALIGN static constexpr int32_t idx_lanes[2 * kN - 1] = {1, 2, 3, 3, // + 2, 1, 0}; +#endif + return idx_lanes + kN - 1 - mod; +} + +#endif // HWY_TARGET != HWY_SCALAR + +namespace strategy { + +struct StrategyBase { + using D = HWY_CAPPED(float, 16); + using V = Vec; +}; + +// 3x3 convolution by symmetric kernel with a single scan through the input. +class Symmetric3 : public StrategyBase { + public: + static constexpr int64_t kRadius = 1; + + // Only accesses pixels in [0, xsize). + template + static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, + const size_t xsize, const int64_t stride, + const WrapRow& wrap_row, + const WeightsSymmetric3& weights, + float* const JXL_RESTRICT row_out) { + const D d; + // t, m, b = top, middle, bottom row; + const float* const JXL_RESTRICT row_t = wrap_row(row_m - stride, stride); + const float* const JXL_RESTRICT row_b = wrap_row(row_m + stride, stride); + + // Must load in advance - compiler doesn't understand LoadDup128 and + // schedules them too late. + const V w0 = LoadDup128(d, weights.c); + const V w1 = LoadDup128(d, weights.r); + const V w2 = LoadDup128(d, weights.d); + + // l, c, r = left, center, right. Leftmost vector: need FirstL1. + { + const V tc = LoadU(d, row_t + 0); + const V mc = LoadU(d, row_m + 0); + const V bc = LoadU(d, row_b + 0); + const V tl = Neighbors::FirstL1(tc); + const V tr = LoadU(d, row_t + 0 + 1); + const V ml = Neighbors::FirstL1(mc); + const V mr = LoadU(d, row_m + 0 + 1); + const V bl = Neighbors::FirstL1(bc); + const V br = LoadU(d, row_b + 0 + 1); + const V conv = + WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + 0); + } + + // Loop as long as we can load enough new values: + const size_t N = Lanes(d); + size_t x = N; + for (; x + N + kRadius <= xsize; x += N) { + const auto conv = ConvolveValid(row_t, row_m, row_b, x, w0, w1, w2); + Store(conv, d, row_out + x); + } + + // For final (partial) vector: + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + + V tr, mr, br; +#if HWY_TARGET == HWY_SCALAR + tr = tc; // Single-lane => mirrored right neighbor = center value. + mr = mc; + br = bc; +#else + if (kSizeModN == 0) { + // The above loop didn't handle the last vector because it needs an + // additional right neighbor (generated via mirroring). + auto mirror = SetTableIndices(d, MirrorLanes(N - 1)); + tr = TableLookupLanes(tc, mirror); + mr = TableLookupLanes(mc, mirror); + br = TableLookupLanes(bc, mirror); + } else { + auto mirror = SetTableIndices(d, MirrorLanes((xsize % N) - 1)); + // Loads last valid value into uppermost lane and mirrors. + tr = TableLookupLanes(LoadU(d, row_t + xsize - N), mirror); + mr = TableLookupLanes(LoadU(d, row_m + xsize - N), mirror); + br = TableLookupLanes(LoadU(d, row_b + xsize - N), mirror); + } +#endif + + const V tl = LoadU(d, row_t + x - 1); + const V ml = LoadU(d, row_m + x - 1); + const V bl = LoadU(d, row_b + x - 1); + const V conv = WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + x); + } + + private: + // Returns sum{x_i * w_i}. + template + static JXL_INLINE V WeightedSum(const V tl, const V tc, const V tr, + const V ml, const V mc, const V mr, + const V bl, const V bc, const V br, + const V w0, const V w1, const V w2) { + const V sum_tb = tc + bc; + + // Faster than 5 mul + 4 FMA. + const V mul0 = mc * w0; + const V sum_lr = ml + mr; + + const V x1 = sum_tb + sum_lr; + const V mul1 = MulAdd(x1, w1, mul0); + + const V sum_t2 = tl + tr; + const V sum_b2 = bl + br; + const V x2 = sum_t2 + sum_b2; + const V mul2 = MulAdd(x2, w2, mul1); + return mul2; + } + + static JXL_INLINE V ConvolveValid(const float* JXL_RESTRICT row_t, + const float* JXL_RESTRICT row_m, + const float* JXL_RESTRICT row_b, + const int64_t x, const V w0, const V w1, + const V w2) { + const D d; + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + const V tl = LoadU(d, row_t + x - 1); + const V tr = LoadU(d, row_t + x + 1); + const V ml = LoadU(d, row_m + x - 1); + const V mr = LoadU(d, row_m + x + 1); + const V bl = LoadU(d, row_b + x - 1); + const V br = LoadU(d, row_b + x + 1); + return WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + } +}; + +// 5x5 convolution by separable kernel with a single scan through the input. +// This is more cache-efficient than separate horizontal/vertical passes, and +// possibly faster (given enough registers) than tiling and/or transposing. +// +// Overview: imagine a 5x5 window around a central pixel. First convolve the +// rows by multiplying the pixels with the corresponding weights from +// WeightsSeparable5.horz[abs(x_offset) * 4]. Then multiply each of these +// intermediate results by the corresponding vertical weight, i.e. +// vert[abs(y_offset) * 4]. Finally, store the sum of these values as the +// convolution result at the position of the central pixel in the output. +// +// Each of these operations uses SIMD vectors. The central pixel and most +// importantly the output are aligned, so neighnoring pixels (e.g. x_offset=1) +// require unaligned loads. Because weights are supplied in identical groups of +// 4, we can use LoadDup128 to load them (slightly faster). +// +// Uses mirrored boundary handling. Until x >= kRadius, the horizontal +// convolution uses Neighbors class to shuffle vectors as if each of its lanes +// had been loaded from the mirrored offset. Similarly, the last full vector to +// write uses mirroring. In the case of scalar vectors, Neighbors is not usable +// and the value is loaded directly. Otherwise, the number of valid pixels +// modulo the vector size enables a small optimization: for smaller offsets, +// a non-mirrored load is sufficient. +class Separable5 : public StrategyBase { + public: + static constexpr int64_t kRadius = 2; + + template + static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, + const size_t xsize, const int64_t stride, + const WrapRow& wrap_row, + const WeightsSeparable5& weights, + float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + + size_t x = 0; + + // More than one iteration for scalars. + for (; x < kRadius; x += Lanes(d)) { + const V conv0 = HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2) * wv0; + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { + const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2) * wv0; + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_TARGET == HWY_SCALAR + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + HorzConvolveLast(row_m, x, xsize, wh0, wh1, wh2) * wv0; + + const V conv1t = + HorzConvolveLast(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = + HorzConvolveLast(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = + HorzConvolveLast(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = + HorzConvolveLast(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + x += Lanes(d); + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static JXL_INLINE V HorzConvolveFirst(const float* const JXL_RESTRICT row, + const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + +#if HWY_TARGET == HWY_SCALAR + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); +#else + (void)xsize; + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); +#endif + + const V r1 = LoadU(d, row + x + 1); + const V r2 = LoadU(d, row + x + 2); + + const V mul1 = MulAdd(l1 + r1, wh1, mul0); + const V mul2 = MulAdd(l2 + r2, wh2, mul1); + return mul2; + } + + template + static JXL_INLINE V HorzConvolveLast(const float* const JXL_RESTRICT row, + const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + + V r1, r2; +#if HWY_TARGET == HWY_SCALAR + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); +#else + const size_t N = Lanes(d); + if (kSizeModN == 0) { + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); + } else { // == 1 + const auto last = LoadU(d, row + xsize - N); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r1 = last; + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } + + // Requires kRadius valid pixels before/after pos. + static JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = c * wh0; + + // Loading anew is faster than combining vectors. + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } +}; // namespace strategy + +// 7x7 convolution by separable kernel with a single scan through the input. +// Extended version of Separable5, see documentation there. +class Separable7 : public StrategyBase { + public: + static constexpr int64_t kRadius = 3; + + template + static JXL_INLINE void ConvolveRow(const float* const JXL_RESTRICT row_m, + const size_t xsize, const int64_t stride, + const WrapRow& wrap_row, + const WeightsSeparable7& weights, + float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t3 = + wrap_row(row_m + 3 * neg_stride, stride); + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + const float* const JXL_RESTRICT row_b3 = + wrap_row(row_m + 3 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wh3 = LoadDup128(d, weights.horz + 3 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + const V wv3 = LoadDup128(d, weights.vert + 3 * 4); + + size_t x = 0; + + // More than one iteration for scalars. + for (; x < kRadius; x += Lanes(d)) { + const V conv0 = + HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2, wh3) * wv0; + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = HorzConvolveFirst(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolveFirst(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + Lanes(d) + kRadius <= xsize; x += Lanes(d)) { + const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2, wh3) * wv0; + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = HorzConvolve(row_t3 + x, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolve(row_b3 + x, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_TARGET == HWY_SCALAR + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + HorzConvolveLast(row_m, x, xsize, wh0, wh1, wh2, wh3) * + wv0; + + const V conv1t = + HorzConvolveLast(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = + HorzConvolveLast(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = + HorzConvolveLast(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = + HorzConvolveLast(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = + HorzConvolveLast(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = + HorzConvolveLast(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + x += Lanes(d); + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static JXL_INLINE V HorzConvolveFirst(const float* const JXL_RESTRICT row, + const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, + const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + +#if HWY_TARGET == HWY_SCALAR + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); + const V l3 = LoadU(d, row + Mirror(x - 3, xsize)); +#else + (void)xsize; + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); + const V l3 = Neighbors::FirstL3(c); +#endif + + const V r1 = LoadU(d, row + x + 1); + const V r2 = LoadU(d, row + x + 2); + const V r3 = LoadU(d, row + x + 3); + + const V mul1 = MulAdd(l1 + r1, wh1, mul0); + const V mul2 = MulAdd(l2 + r2, wh2, mul1); + const V mul3 = MulAdd(l3 + r3, wh3, mul2); + return mul3; + } + + template + static JXL_INLINE V HorzConvolveLast(const float* const JXL_RESTRICT row, + const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, + const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + const V l3 = LoadU(d, row + x - 3); + + V r1, r2, r3; +#if HWY_TARGET == HWY_SCALAR + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); + r3 = LoadU(d, row + Mirror(x + 3, xsize)); +#else + const size_t N = Lanes(d); + if (kSizeModN == 0) { + r3 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 3))); + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(N - 1))); + } else if (kSizeModN == 1) { + const auto last = LoadU(d, row + xsize - N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 2))); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r1 = last; + } else /* kSizeModN >= 2 */ { + const auto last = LoadU(d, row + xsize - N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(N - 1))); + r2 = last; + r1 = LoadU(d, row + x + 1); + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = l3 + r3; + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } + + // Returns one vector of horizontal convolution results; lane i is the result + // for pixel pos + i. This is the fast path for interior pixels, i.e. kRadius + // valid pixels before/after pos. + static JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, const V wh2, + const V wh3) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = c * wh0; + + // TODO(janwas): better to Combine + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + const V l3 = LoadU(d, pos - 3); + const V r3 = LoadU(d, pos + 3); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = l3 + r3; + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } +}; // namespace HWY_NAMESPACE + +} // namespace strategy + +// Single entry point for convolution. +// "Strategy" (Direct*/Separable*) decides kernel size and how to evaluate it. +template +class ConvolveT { + static constexpr int64_t kRadius = Strategy::kRadius; + using Simd = HWY_CAPPED(float, 16); + + public: + static size_t MinWidth() { +#if HWY_TARGET == HWY_SCALAR + // First/Last use mirrored loads of up to +/- kRadius. + return 2 * kRadius; +#else + return Lanes(Simd()) + kRadius; +#endif + } + + // "Image" is ImageF or Image3F. + template + static void Run(const Image& in, const Rect& rect, const Weights& weights, + ThreadPool* pool, Image* out) { + PROFILER_ZONE("ConvolveT::Run"); + JXL_CHECK(SameSize(rect, *out)); + JXL_CHECK(rect.xsize() >= MinWidth()); + + static_assert(int64_t(kRadius) <= 3, + "Must handle [0, kRadius) and >= kRadius"); + switch (rect.xsize() % Lanes(Simd())) { + case 0: + return RunRows<0>(in, rect, weights, pool, out); + case 1: + return RunRows<1>(in, rect, weights, pool, out); + case 2: + return RunRows<2>(in, rect, weights, pool, out); + default: + return RunRows<3>(in, rect, weights, pool, out); + } + } + + private: + template + static JXL_INLINE void RunRow(const float* JXL_RESTRICT in, + const size_t xsize, const int64_t stride, + const WrapRow& wrap_row, const Weights& weights, + float* JXL_RESTRICT out) { + Strategy::template ConvolveRow(in, xsize, stride, wrap_row, + weights, out); + } + + template + static JXL_INLINE void RunBorderRows(const ImageF& in, const Rect& rect, + const int64_t ybegin, const int64_t yend, + const Weights& weights, ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + const WrapRowMirror wrap_row(in, rect.ysize()); + for (int64_t y = ybegin; y < yend; ++y) { + RunRow(rect.ConstRow(in, y), rect.xsize(), stride, wrap_row, + weights, out->Row(y)); + } + } + + // Image3F. + template + static JXL_INLINE void RunBorderRows(const Image3F& in, const Rect& rect, + const int64_t ybegin, const int64_t yend, + const Weights& weights, Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + for (int64_t y = ybegin; y < yend; ++y) { + for (size_t c = 0; c < 3; ++c) { + const WrapRowMirror wrap_row(in.Plane(c), rect.ysize()); + RunRow(rect.ConstPlaneRow(in, c, y), rect.xsize(), stride, + wrap_row, weights, out->PlaneRow(c, y)); + } + } + } + + template + static JXL_INLINE void RunInteriorRows(const ImageF& in, const Rect& rect, + const int64_t ybegin, + const int64_t yend, + const Weights& weights, + ThreadPool* pool, ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + RunOnPool( + pool, ybegin, yend, ThreadPool::SkipInit(), + [&](const int y, int /*thread*/) HWY_ATTR { + RunRow(rect.ConstRow(in, y), rect.xsize(), stride, + WrapRowUnchanged(), weights, out->Row(y)); + }, + "Convolve"); + } + + // Image3F. + template + static JXL_INLINE void RunInteriorRows(const Image3F& in, const Rect& rect, + const int64_t ybegin, + const int64_t yend, + const Weights& weights, + ThreadPool* pool, Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + RunOnPool( + pool, ybegin, yend, ThreadPool::SkipInit(), + [&](const int y, int /*thread*/) HWY_ATTR { + for (size_t c = 0; c < 3; ++c) { + RunRow(rect.ConstPlaneRow(in, c, y), rect.xsize(), + stride, WrapRowUnchanged(), weights, + out->PlaneRow(c, y)); + } + }, + "Convolve3"); + } + + template + static JXL_INLINE void RunRows(const Image& in, const Rect& rect, + const Weights& weights, ThreadPool* pool, + Image* out) { + const int64_t ysize = rect.ysize(); + RunBorderRows(in, rect, 0, std::min(int64_t(kRadius), ysize), + weights, out); + if (ysize > 2 * int64_t(kRadius)) { + RunInteriorRows(in, rect, int64_t(kRadius), + ysize - int64_t(kRadius), weights, pool, out); + } + if (ysize > int64_t(kRadius)) { + RunBorderRows(in, rect, ysize - int64_t(kRadius), ysize, + weights, out); + } + } +}; + +void Symmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSymmetric3(in, rect, weights, pool, out); +} + +// Symmetric5 is implemented above without ConvolveT. + +void Separable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable5(in, rect, weights, pool, out); +} +void Separable5_3(const Image3F& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + Image3F* out) { + using Conv = ConvolveT; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable5(in, rect, weights, pool, out); +} + +void Separable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + using Conv = ConvolveT; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable7(in, rect, weights, pool, out); +} +void Separable7_3(const Image3F& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + Image3F* out) { + using Conv = ConvolveT; + if (rect.xsize() >= Conv::MinWidth()) { + return Conv::Run(in, rect, weights, pool, out); + } + + return SlowSeparable7(in, rect, weights, pool, out); +} + +// Semi-vectorized (interior pixels Fonly); called directly like slow::, unlike +// the fully vectorized strategies below. +void Symmetric5(const ImageF& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t iy = task; + + if (iy < 2 || iy >= static_cast(ysize) - 2) { + Symmetric5BorderRow(in, rect, iy, weights, out->Row(iy)); + } else { + Symmetric5Row(in, rect, iy, weights, out->Row(iy)); + } + }, + "Symmetric5x5Convolution"); +} + +void Symmetric5_3(const Image3F& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + Image3F* JXL_RESTRICT out) { + PROFILER_FUNC; + + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t iy = task; + + if (iy < 2 || iy >= ysize - 2) { + for (size_t c = 0; c < 3; ++c) { + Symmetric5BorderRow(in.Plane(c), rect, iy, weights, + out->PlaneRow(c, iy)); + } + } else { + for (size_t c = 0; c < 3; ++c) { + Symmetric5Row(in.Plane(c), rect, iy, weights, + out->PlaneRow(c, iy)); + } + } + }, + "Symmetric5x5Convolution3"); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(Symmetric3); +void Symmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Symmetric3)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Symmetric5); +void Symmetric5(const ImageF& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + return HWY_DYNAMIC_DISPATCH(Symmetric5)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Symmetric5_3); +void Symmetric5_3(const Image3F& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + Image3F* JXL_RESTRICT out) { + return HWY_DYNAMIC_DISPATCH(Symmetric5_3)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Separable5); +void Separable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Separable5)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Separable5_3); +void Separable5_3(const Image3F& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + Image3F* out) { + return HWY_DYNAMIC_DISPATCH(Separable5_3)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Separable7); +void Separable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + return HWY_DYNAMIC_DISPATCH(Separable7)(in, rect, weights, pool, out); +} + +HWY_EXPORT(Separable7_3); +void Separable7_3(const Image3F& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + Image3F* out) { + return HWY_DYNAMIC_DISPATCH(Separable7_3)(in, rect, weights, pool, out); +} + +//------------------------------------------------------------------------------ +// Kernels + +// Concentrates energy in low-frequency components (e.g. for antialiasing). +const WeightsSymmetric3& WeightsSymmetric3Lowpass() { + // Computed by research/convolve_weights.py's cubic spline approximations of + // prolate spheroidal wave functions. + constexpr float w0 = 0.36208932f; + constexpr float w1 = 0.12820096f; + constexpr float w2 = 0.03127668f; + static constexpr WeightsSymmetric3 weights = { + {HWY_REP4(w0)}, {HWY_REP4(w1)}, {HWY_REP4(w2)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Lowpass() { + constexpr float w0 = 0.41714928f; + constexpr float w1 = 0.25539268f; + constexpr float w2 = 0.03603267f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; +} + +const WeightsSymmetric5& WeightsSymmetric5Lowpass() { + static constexpr WeightsSymmetric5 weights = { + {HWY_REP4(0.1740135f)}, {HWY_REP4(0.1065369f)}, {HWY_REP4(0.0150310f)}, + {HWY_REP4(0.0652254f)}, {HWY_REP4(0.0012984f)}, {HWY_REP4(0.0092025f)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Gaussian1() { + constexpr float w0 = 0.38774f; + constexpr float w1 = 0.24477f; + constexpr float w2 = 0.06136f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; +} + +const WeightsSeparable5& WeightsSeparable5Gaussian2() { + constexpr float w0 = 0.250301f; + constexpr float w1 = 0.221461f; + constexpr float w2 = 0.153388f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; +} + +//------------------------------------------------------------------------------ +// Slow + +namespace { + +template +float SlowSymmetric3Pixel(const ImageF& in, const int64_t ix, const int64_t iy, + const int64_t xsize, const int64_t ysize, + const WeightsSymmetric3& weights) { + float sum = 0.0f; + + // ix: image; kx: kernel + for (int64_t ky = -1; ky <= 1; ky++) { + const int64_t y = WrapY()(iy + ky, ysize); + const float* JXL_RESTRICT row_in = in.ConstRow(static_cast(y)); + + const float wc = ky == 0 ? weights.c[0] : weights.r[0]; + const float wlr = ky == 0 ? weights.r[0] : weights.d[0]; + + const int64_t xm1 = WrapX()(ix - 1, xsize); + const int64_t xp1 = WrapX()(ix + 1, xsize); + sum += row_in[ix] * wc + (row_in[xm1] + row_in[xp1]) * wlr; + } + return sum; +} + +template +void SlowSymmetric3Row(const ImageF& in, const int64_t iy, const int64_t xsize, + const int64_t ysize, const WeightsSymmetric3& weights, + float* JXL_RESTRICT row_out) { + row_out[0] = + SlowSymmetric3Pixel(in, 0, iy, xsize, ysize, weights); + for (int64_t ix = 1; ix < xsize - 1; ix++) { + row_out[ix] = SlowSymmetric3Pixel(in, ix, iy, xsize, + ysize, weights); + } + { + const int64_t ix = xsize - 1; + row_out[ix] = SlowSymmetric3Pixel(in, ix, iy, xsize, + ysize, weights); + } +} + +} // namespace + +void SlowSymmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + + const int64_t xsize = static_cast(rect.xsize()); + const int64_t ysize = static_cast(rect.ysize()); + const int64_t kRadius = 1; + + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t iy = task; + float* JXL_RESTRICT out_row = out->Row(static_cast(iy)); + + if (iy < kRadius || iy >= ysize - kRadius) { + SlowSymmetric3Row(in, iy, xsize, ysize, weights, out_row); + } else { + SlowSymmetric3Row(in, iy, xsize, ysize, weights, + out_row); + } + }, + "SlowSymmetric3"); +} + +void SlowSymmetric3(const Image3F& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + Image3F* JXL_RESTRICT out) { + PROFILER_FUNC; + + const int64_t xsize = static_cast(rect.xsize()); + const int64_t ysize = static_cast(rect.ysize()); + const int64_t kRadius = 1; + + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t iy = task; + const size_t oy = static_cast(iy); + + if (iy < kRadius || iy >= ysize - kRadius) { + for (size_t c = 0; c < 3; ++c) { + SlowSymmetric3Row(in.Plane(c), iy, xsize, ysize, + weights, out->PlaneRow(c, oy)); + } + } else { + for (size_t c = 0; c < 3; ++c) { + SlowSymmetric3Row(in.Plane(c), iy, xsize, ysize, + weights, out->PlaneRow(c, oy)); + } + } + }, + "SlowSymmetric3"); +} + +namespace { + +// Separable kernels, any radius. +float SlowSeparablePixel(const ImageF& in, const Rect& rect, const int64_t x, + const int64_t y, const int64_t radius, + const float* JXL_RESTRICT horz_weights, + const float* JXL_RESTRICT vert_weights) { + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + const WrapMirror wrap; + + float mul = 0.0f; + for (int dy = -radius; dy <= radius; ++dy) { + const float wy = vert_weights[std::abs(dy) * 4]; + const size_t sy = wrap(y + dy, ysize); + JXL_CHECK(sy < ysize); + const float* const JXL_RESTRICT row = rect.ConstRow(in, sy); + for (int dx = -radius; dx <= radius; ++dx) { + const float wx = horz_weights[std::abs(dx) * 4]; + const size_t sx = wrap(x + dx, xsize); + JXL_CHECK(sx < xsize); + mul += row[sx] * wx * wy; + } + } + return mul; +} + +} // namespace + +void SlowSeparable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out) { + PROFILER_FUNC; + const float* horz_weights = &weights.horz[0]; + const float* vert_weights = &weights.vert[0]; + + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + + float* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/2, + horz_weights, vert_weights); + } + }, + "SlowSeparable5"); +} + +void SlowSeparable5(const Image3F& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + SlowSeparable5(in.Plane(c), rect, weights, pool, &out->Plane(c)); + } +} + +void SlowSeparable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out) { + PROFILER_FUNC; + const float* horz_weights = &weights.horz[0]; + const float* vert_weights = &weights.vert[0]; + + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + + float* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row_out[x] = SlowSeparablePixel(in, rect, x, y, /*radius=*/3, + horz_weights, vert_weights); + } + }, + "SlowSeparable7"); +} + +void SlowSeparable7(const Image3F& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + SlowSeparable7(in.Plane(c), rect, weights, pool, &out->Plane(c)); + } +} + +void SlowLaplacian5(const ImageF& in, const Rect& rect, ThreadPool* pool, + ImageF* out) { + PROFILER_FUNC; + JXL_CHECK(SameSize(rect, *out)); + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + const WrapMirror wrap; + + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + + const float* const JXL_RESTRICT row_t = + rect.ConstRow(in, wrap(y - 2, ysize)); + const float* const JXL_RESTRICT row_m = rect.ConstRow(in, y); + const float* const JXL_RESTRICT row_b = + rect.ConstRow(in, wrap(y + 2, ysize)); + float* const JXL_RESTRICT row_out = out->Row(y); + + for (int64_t x = 0; static_cast(x) < xsize; ++x) { + const int64_t xm2 = wrap(x - 2, xsize); + const int64_t xp2 = wrap(x + 2, xsize); + float r = 0.0f; + r += /* */ 1.0f * row_t[x]; + r += 1.0f * row_m[xm2] - 4.0f * row_m[x] + 1.0f * row_m[xp2]; + r += /* */ 1.0f * row_b[x]; + row_out[x] = r; + } + }, + "SlowLaplacian5"); +} + +void SlowLaplacian5(const Image3F& in, const Rect& rect, ThreadPool* pool, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + SlowLaplacian5(in.Plane(c), rect, pool, &out->Plane(c)); + } +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/convolve.h b/lib/jxl/convolve.h new file mode 100644 index 0000000..c2e2ae4 --- /dev/null +++ b/lib/jxl/convolve.h @@ -0,0 +1,131 @@ +// 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_CONVOLVE_H_ +#define LIB_JXL_CONVOLVE_H_ + +// 2D convolution. + +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// No valid values outside [0, xsize), but the strategy may still safely load +// the preceding vector, and/or round xsize up to the vector lane count. This +// avoids needing PadImage. +// Requires xsize >= kConvolveLanes + kConvolveMaxRadius. +static constexpr size_t kConvolveMaxRadius = 3; + +// Weights must already be normalized. + +struct WeightsSymmetric3 { + // d r d (each replicated 4x) + // r c r + // d r d + float c[4]; + float r[4]; + float d[4]; +}; + +struct WeightsSymmetric5 { + // The lower-right quadrant is: c r R (each replicated 4x) + // r d L + // R L D + float c[4]; + float r[4]; + float R[4]; + float d[4]; + float D[4]; + float L[4]; +}; + +// Weights for separable 5x5 filters (typically but not necessarily the same +// values for horizontal and vertical directions). The kernel must already be +// normalized, but note that values for negative offsets are omitted, so the +// given values do not sum to 1. +struct WeightsSeparable5 { + // Horizontal 1D, distances 0..2 (each replicated 4x) + float horz[3 * 4]; + float vert[3 * 4]; +}; + +// Weights for separable 7x7 filters (typically but not necessarily the same +// values for horizontal and vertical directions). The kernel must already be +// normalized, but note that values for negative offsets are omitted, so the +// given values do not sum to 1. +// +// NOTE: for >= 7x7 Gaussian kernels, it is faster to use FastGaussian instead, +// at least when images exceed the L1 cache size. +struct WeightsSeparable7 { + // Horizontal 1D, distances 0..3 (each replicated 4x) + float horz[4 * 4]; + float vert[4 * 4]; +}; + +const WeightsSymmetric3& WeightsSymmetric3Lowpass(); +const WeightsSeparable5& WeightsSeparable5Lowpass(); +const WeightsSymmetric5& WeightsSymmetric5Lowpass(); + +void SlowSymmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out); +void SlowSymmetric3(const Image3F& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + Image3F* JXL_RESTRICT out); + +void SlowSeparable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out); +void SlowSeparable5(const Image3F& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + Image3F* out); + +void SlowSeparable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out); +void SlowSeparable7(const Image3F& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + Image3F* out); + +void SlowLaplacian5(const ImageF& in, const Rect& rect, ThreadPool* pool, + ImageF* out); +void SlowLaplacian5(const Image3F& in, const Rect& rect, ThreadPool* pool, + Image3F* out); + +void Symmetric3(const ImageF& in, const Rect& rect, + const WeightsSymmetric3& weights, ThreadPool* pool, + ImageF* out); + +void Symmetric5(const ImageF& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + ImageF* JXL_RESTRICT out); + +void Symmetric5_3(const Image3F& in, const Rect& rect, + const WeightsSymmetric5& weights, ThreadPool* pool, + Image3F* JXL_RESTRICT out); + +void Separable5(const ImageF& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + ImageF* out); + +void Separable5_3(const Image3F& in, const Rect& rect, + const WeightsSeparable5& weights, ThreadPool* pool, + Image3F* out); + +void Separable7(const ImageF& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + ImageF* out); + +void Separable7_3(const Image3F& in, const Rect& rect, + const WeightsSeparable7& weights, ThreadPool* pool, + Image3F* out); + +} // namespace jxl + +#endif // LIB_JXL_CONVOLVE_H_ diff --git a/lib/jxl/convolve_test.cc b/lib/jxl/convolve_test.cc new file mode 100644 index 0000000..45e7e45 --- /dev/null +++ b/lib/jxl/convolve_test.cc @@ -0,0 +1,250 @@ +// 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/convolve.h" + +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/convolve_test.cc" +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" + +#ifndef JXL_DEBUG_CONVOLVE +#define JXL_DEBUG_CONVOLVE 0 +#endif + +#include "lib/jxl/convolve-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +void TestNeighbors() { + const Neighbors::D d; + const Neighbors::V v = Iota(d, 0); + HWY_ALIGN float actual[hwy::kTestMaxVectorSize / sizeof(float)] = {0}; + + HWY_ALIGN float first_l1[hwy::kTestMaxVectorSize / sizeof(float)] = { + 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + Store(Neighbors::FirstL1(v), d, actual); + const size_t N = Lanes(d); + EXPECT_EQ(std::vector(first_l1, first_l1 + N), + std::vector(actual, actual + N)); + +#if HWY_TARGET != HWY_SCALAR + HWY_ALIGN float first_l2[hwy::kTestMaxVectorSize / sizeof(float)] = { + 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + Store(Neighbors::FirstL2(v), d, actual); + EXPECT_EQ(std::vector(first_l2, first_l2 + N), + std::vector(actual, actual + N)); + + HWY_ALIGN float first_l3[hwy::kTestMaxVectorSize / sizeof(float)] = { + 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + Store(Neighbors::FirstL3(v), d, actual); + EXPECT_EQ(std::vector(first_l3, first_l3 + N), + std::vector(actual, actual + N)); +#endif // HWY_TARGET != HWY_SCALAR +} + +template +void VerifySymmetric3(const size_t xsize, const size_t ysize, ThreadPool* pool, + Random* rng) { + const Rect rect(0, 0, xsize, ysize); + + ImageF in(xsize, ysize); + GenerateImage(GeneratorRandom(rng, 1.0f), &in); + + ImageF out_expected(xsize, ysize); + ImageF out_actual(xsize, ysize); + + const WeightsSymmetric3& weights = WeightsSymmetric3Lowpass(); + Symmetric3(in, rect, weights, pool, &out_expected); + SlowSymmetric3(in, rect, weights, pool, &out_actual); + + VerifyRelativeError(out_expected, out_actual, 1E-5f, 1E-5f); +} + +// Ensures Symmetric and Separable give the same result. +template +void VerifySymmetric5(const size_t xsize, const size_t ysize, ThreadPool* pool, + Random* rng) { + const Rect rect(0, 0, xsize, ysize); + + ImageF in(xsize, ysize); + GenerateImage(GeneratorRandom(rng, 1.0f), &in); + + ImageF out_expected(xsize, ysize); + ImageF out_actual(xsize, ysize); + + Separable5(in, Rect(in), WeightsSeparable5Lowpass(), pool, &out_expected); + Symmetric5(in, rect, WeightsSymmetric5Lowpass(), pool, &out_actual); + + VerifyRelativeError(out_expected, out_actual, 1E-5f, 1E-5f); +} + +template +void VerifySeparable5(const size_t xsize, const size_t ysize, ThreadPool* pool, + Random* rng) { + const Rect rect(0, 0, xsize, ysize); + + ImageF in(xsize, ysize); + GenerateImage(GeneratorRandom(rng, 1.0f), &in); + + ImageF out_expected(xsize, ysize); + ImageF out_actual(xsize, ysize); + + const WeightsSeparable5& weights = WeightsSeparable5Lowpass(); + Separable5(in, Rect(in), weights, pool, &out_expected); + SlowSeparable5(in, rect, weights, pool, &out_actual); + + VerifyRelativeError(out_expected, out_actual, 1E-5f, 1E-5f); +} + +template +void VerifySeparable7(const size_t xsize, const size_t ysize, ThreadPool* pool, + Random* rng) { + const Rect rect(0, 0, xsize, ysize); + + ImageF in(xsize, ysize); + GenerateImage(GeneratorRandom(rng, 1.0f), &in); + + ImageF out_expected(xsize, ysize); + ImageF out_actual(xsize, ysize); + + // Gaussian sigma 1.0 + const WeightsSeparable7 weights = {{HWY_REP4(0.383103f), HWY_REP4(0.241843f), + HWY_REP4(0.060626f), HWY_REP4(0.00598f)}, + {HWY_REP4(0.383103f), HWY_REP4(0.241843f), + HWY_REP4(0.060626f), HWY_REP4(0.00598f)}}; + + SlowSeparable7(in, rect, weights, pool, &out_expected); + Separable7(in, Rect(in), weights, pool, &out_actual); + + VerifyRelativeError(out_expected, out_actual, 1E-5f, 1E-5f); +} + +// For all xsize/ysize and kernels: +void TestConvolve() { + TestNeighbors(); + + ThreadPoolInternal pool(4); + pool.Run(kConvolveMaxRadius, 40, ThreadPool::SkipInit(), + [](const int task, int /*thread*/) { + const size_t xsize = task; + std::mt19937_64 rng(129 + 13 * xsize); + + ThreadPool* null_pool = nullptr; + ThreadPoolInternal pool3(3); + for (size_t ysize = kConvolveMaxRadius; ysize < 16; ++ysize) { + JXL_DEBUG(JXL_DEBUG_CONVOLVE, + "%zu x %zu (target %d)===============================", + xsize, ysize, HWY_TARGET); + + JXL_DEBUG(JXL_DEBUG_CONVOLVE, "Sym3------------------"); + VerifySymmetric3(xsize, ysize, null_pool, &rng); + VerifySymmetric3(xsize, ysize, &pool3, &rng); + + JXL_DEBUG(JXL_DEBUG_CONVOLVE, "Sym5------------------"); + VerifySymmetric5(xsize, ysize, null_pool, &rng); + VerifySymmetric5(xsize, ysize, &pool3, &rng); + + JXL_DEBUG(JXL_DEBUG_CONVOLVE, "Sep5------------------"); + VerifySeparable5(xsize, ysize, null_pool, &rng); + VerifySeparable5(xsize, ysize, &pool3, &rng); + + JXL_DEBUG(JXL_DEBUG_CONVOLVE, "Sep7------------------"); + VerifySeparable7(xsize, ysize, null_pool, &rng); + VerifySeparable7(xsize, ysize, &pool3, &rng); + } + }); +} + +// Measures durations, verifies results, prints timings. `unpredictable1` +// must have value 1 (unknown to the compiler to prevent elision). +template +void BenchmarkConv(const char* caption, const Conv& conv, + const hwy::FuncInput unpredictable1) { + const size_t kNumInputs = 1; + const hwy::FuncInput inputs[kNumInputs] = {unpredictable1}; + hwy::Result results[kNumInputs]; + + const size_t kDim = 160; // in+out fit in L2 + ImageF in(kDim, kDim); + ZeroFillImage(&in); + in.Row(kDim / 2)[kDim / 2] = unpredictable1; + ImageF out(kDim, kDim); + + hwy::Params p; + p.verbose = false; + p.max_evals = 7; + p.target_rel_mad = 0.002; + const size_t num_results = MeasureClosure( + [&in, &conv, &out](const hwy::FuncInput input) { + conv(in, &out); + return out.Row(input)[0]; + }, + inputs, kNumInputs, results, p); + if (num_results != kNumInputs) { + fprintf(stderr, "MeasureClosure failed.\n"); + } + for (size_t i = 0; i < num_results; ++i) { + const double seconds = static_cast(results[i].ticks) / + hwy::platform::InvariantTicksPerSecond(); + printf("%12s: %7.2f MP/s (MAD=%4.2f%%)\n", caption, + kDim * kDim * 1E-6 / seconds, + static_cast(results[i].variability) * 100.0); + } +} + +struct ConvSymmetric3 { + void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) const { + ThreadPool* null_pool = nullptr; + Symmetric3(in, Rect(in), WeightsSymmetric3Lowpass(), null_pool, out); + } +}; + +struct ConvSeparable5 { + void operator()(const ImageF& in, ImageF* JXL_RESTRICT out) const { + ThreadPool* null_pool = nullptr; + Separable5(in, Rect(in), WeightsSeparable5Lowpass(), null_pool, out); + } +}; + +void BenchmarkAll() { +#if 0 // disabled to avoid test timeouts, run manually on demand + const hwy::FuncInput unpredictable1 = time(nullptr) != 1234; + BenchmarkConv("Symmetric3", ConvSymmetric3(), unpredictable1); + BenchmarkConv("Separable5", ConvSeparable5(), unpredictable1); +#endif +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +class ConvolveTest : public hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(ConvolveTest); + +HWY_EXPORT_AND_TEST_P(ConvolveTest, TestConvolve); + +HWY_EXPORT_AND_TEST_P(ConvolveTest, BenchmarkAll); + +} // namespace jxl +#endif diff --git a/lib/jxl/data_parallel_test.cc b/lib/jxl/data_parallel_test.cc new file mode 100644 index 0000000..63db1f8 --- /dev/null +++ b/lib/jxl/data_parallel_test.cc @@ -0,0 +1,111 @@ +// 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/data_parallel.h" + +#include "gtest/gtest.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/test_utils.h" + +namespace jxl { +namespace { + +class DataParallelTest : public ::testing::Test { + protected: + // A fake class to verify that DataParallel is properly calling the + // client-provided runner functions. + static int FakeRunner(void* runner_opaque, void* jpegxl_opaque, + JxlParallelRunInit init, JxlParallelRunFunction func, + uint32_t start_range, uint32_t end_range) { + DataParallelTest* self = static_cast(runner_opaque); + self->runner_called_++; + self->jpegxl_opaque_ = jpegxl_opaque; + self->init_ = init; + self->func_ = func; + self->start_range_ = start_range; + self->end_range_ = end_range; + return self->runner_return_; + } + + ThreadPool pool_{&DataParallelTest::FakeRunner, this}; + + // Number of times FakeRunner() was called. + int runner_called_ = 0; + + // Parameters passed to FakeRunner. + void* jpegxl_opaque_ = nullptr; + JxlParallelRunInit init_ = nullptr; + JxlParallelRunFunction func_ = nullptr; + uint32_t start_range_ = -1; + uint32_t end_range_ = -1; + + // Return value that FakeRunner will return. + int runner_return_ = 0; +}; + +// JxlParallelRunInit interface. +typedef int (*JxlParallelRunInit)(); +int TestInit(void* jpegxl_opaque, size_t num_threads) { return 0; } + +} // namespace + +TEST_F(DataParallelTest, RunnerCalledParamenters) { + EXPECT_TRUE(pool_.Run( + 1234, 5678, [](const size_t num_threads) { return true; }, + [](const int task, const int thread) { return; })); + EXPECT_EQ(1, runner_called_); + EXPECT_NE(nullptr, init_); + EXPECT_NE(nullptr, func_); + EXPECT_NE(nullptr, jpegxl_opaque_); + EXPECT_EQ(1234u, start_range_); + EXPECT_EQ(5678u, end_range_); +} + +TEST_F(DataParallelTest, RunnerFailurePropagates) { + runner_return_ = -1; // FakeRunner return value. + EXPECT_FALSE(pool_.Run( + 1234, 5678, [](const size_t num_threads) { return false; }, + [](const int task, const int thread) { return; })); + EXPECT_FALSE(RunOnPool( + nullptr, 1234, 5678, [](const size_t num_threads) { return false; }, + [](const int task, const int thread) { return; }, "Test")); +} + +TEST_F(DataParallelTest, RunnerNotCalledOnEmptyRange) { + runner_return_ = -1; // FakeRunner return value. + EXPECT_TRUE(pool_.Run( + 123, 123, [](const size_t num_threads) { return false; }, + [](const int task, const int thread) { return; })); + EXPECT_TRUE(RunOnPool( + nullptr, 123, 123, [](const size_t num_threads) { return false; }, + [](const int task, const int thread) { return; }, "Test")); + // We don't call the external runner when the range is empty. We don't even + // need to call the init function. + EXPECT_EQ(0, runner_called_); +} + +// The TestDivider is slow when compiled in debug mode. +TEST_F(DataParallelTest, JXL_SLOW_TEST(TestDivider)) { + jxl::ThreadPoolInternal pool(8); + // 1, 2 are powers of two. + pool.Run(3, 2 * 1024, ThreadPool::SkipInit(), + [](const int d, const int thread) { + // powers of two are not supported. + if ((d & (d - 1)) == 0) return; + + const Divider div(d); +#ifdef NDEBUG + const int max_dividend = 4 * 1024 * 1024; +#else + const int max_dividend = 2 * 1024 + 1; +#endif + for (int x = 0; x < max_dividend; ++x) { + const int q = div(x); + ASSERT_EQ(x / d, q) << x << "/" << d; + } + }); +} + +} // namespace jxl diff --git a/lib/jxl/dct-inl.h b/lib/jxl/dct-inl.h new file mode 100644 index 0000000..ecc3935 --- /dev/null +++ b/lib/jxl/dct-inl.h @@ -0,0 +1,361 @@ +// 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. + +// Fast SIMD floating-point (I)DCT, any power of two. + +#if defined(LIB_JXL_DCT_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_DCT_INL_H_ +#undef LIB_JXL_DCT_INL_H_ +#else +#define LIB_JXL_DCT_INL_H_ +#endif + +#include + +#include + +#include "lib/jxl/dct_block-inl.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/transpose-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +template +struct FVImpl { + using type = HWY_CAPPED(float, SZ); +}; + +template <> +struct FVImpl<0> { + using type = HWY_FULL(float); +}; + +template +using FV = typename FVImpl::type; + +// Implementation of Lowest Complexity Self Recursive Radix-2 DCT II/III +// Algorithms, by Siriani M. Perera and Jianhua Liu. + +template +struct CoeffBundle { + static void AddReverse(const float* JXL_RESTRICT ain1, + const float* JXL_RESTRICT ain2, + float* JXL_RESTRICT aout) { + for (size_t i = 0; i < N; i++) { + auto in1 = Load(FV(), ain1 + i * SZ); + auto in2 = Load(FV(), ain2 + (N - i - 1) * SZ); + Store(in1 + in2, FV(), aout + i * SZ); + } + } + static void SubReverse(const float* JXL_RESTRICT ain1, + const float* JXL_RESTRICT ain2, + float* JXL_RESTRICT aout) { + for (size_t i = 0; i < N; i++) { + auto in1 = Load(FV(), ain1 + i * SZ); + auto in2 = Load(FV(), ain2 + (N - i - 1) * SZ); + Store(in1 - in2, FV(), aout + i * SZ); + } + } + static void B(float* JXL_RESTRICT coeff) { + auto sqrt2 = Set(FV(), square_root<2>::value); + auto in1 = Load(FV(), coeff); + auto in2 = Load(FV(), coeff + SZ); + Store(MulAdd(in1, sqrt2, in2), FV(), coeff); + for (size_t i = 1; i + 1 < N; i++) { + auto in1 = Load(FV(), coeff + i * SZ); + auto in2 = Load(FV(), coeff + (i + 1) * SZ); + Store(in1 + in2, FV(), coeff + i * SZ); + } + } + static void BTranspose(float* JXL_RESTRICT coeff) { + for (size_t i = N - 1; i > 0; i--) { + auto in1 = Load(FV(), coeff + i * SZ); + auto in2 = Load(FV(), coeff + (i - 1) * SZ); + Store(in1 + in2, FV(), coeff + i * SZ); + } + auto sqrt2 = Set(FV(), square_root<2>::value); + auto in1 = Load(FV(), coeff); + Store(in1 * sqrt2, FV(), coeff); + } + // Ideally optimized away by compiler (except the multiply). + static void InverseEvenOdd(const float* JXL_RESTRICT ain, + float* JXL_RESTRICT aout) { + for (size_t i = 0; i < N / 2; i++) { + auto in1 = Load(FV(), ain + i * SZ); + Store(in1, FV(), aout + 2 * i * SZ); + } + for (size_t i = N / 2; i < N; i++) { + auto in1 = Load(FV(), ain + i * SZ); + Store(in1, FV(), aout + (2 * (i - N / 2) + 1) * SZ); + } + } + // Ideally optimized away by compiler. + static void ForwardEvenOdd(const float* JXL_RESTRICT ain, size_t ain_stride, + float* JXL_RESTRICT aout) { + for (size_t i = 0; i < N / 2; i++) { + auto in1 = LoadU(FV(), ain + 2 * i * ain_stride); + Store(in1, FV(), aout + i * SZ); + } + for (size_t i = N / 2; i < N; i++) { + auto in1 = LoadU(FV(), ain + (2 * (i - N / 2) + 1) * ain_stride); + Store(in1, FV(), aout + i * SZ); + } + } + // Invoked on full vector. + static void Multiply(float* JXL_RESTRICT coeff) { + for (size_t i = 0; i < N / 2; i++) { + auto in1 = Load(FV(), coeff + (N / 2 + i) * SZ); + auto mul = Set(FV(), WcMultipliers::kMultipliers[i]); + Store(in1 * mul, FV(), coeff + (N / 2 + i) * SZ); + } + } + static void MultiplyAndAdd(const float* JXL_RESTRICT coeff, + float* JXL_RESTRICT out, size_t out_stride) { + for (size_t i = 0; i < N / 2; i++) { + auto mul = Set(FV(), WcMultipliers::kMultipliers[i]); + auto in1 = Load(FV(), coeff + i * SZ); + auto in2 = Load(FV(), coeff + (N / 2 + i) * SZ); + auto out1 = MulAdd(mul, in2, in1); + auto out2 = NegMulAdd(mul, in2, in1); + StoreU(out1, FV(), out + i * out_stride); + StoreU(out2, FV(), out + (N - i - 1) * out_stride); + } + } + template + static void LoadFromBlock(const Block& in, size_t off, + float* JXL_RESTRICT coeff) { + for (size_t i = 0; i < N; i++) { + Store(in.LoadPart(FV(), i, off), FV(), coeff + i * SZ); + } + } + template + static void StoreToBlockAndScale(const float* JXL_RESTRICT coeff, + const Block& out, size_t off) { + auto mul = Set(FV(), 1.0f / N); + for (size_t i = 0; i < N; i++) { + out.StorePart(FV(), mul * Load(FV(), coeff + i * SZ), i, off); + } + } +}; + +template +struct DCT1DImpl; + +template +struct DCT1DImpl<1, SZ> { + JXL_INLINE void operator()(float* JXL_RESTRICT mem) {} +}; + +template +struct DCT1DImpl<2, SZ> { + JXL_INLINE void operator()(float* JXL_RESTRICT mem) { + auto in1 = Load(FV(), mem); + auto in2 = Load(FV(), mem + SZ); + Store(in1 + in2, FV(), mem); + Store(in1 - in2, FV(), mem + SZ); + } +}; + +template +struct DCT1DImpl { + void operator()(float* JXL_RESTRICT mem) { + // This is relatively small (4kB with 64-DCT and AVX-512) + HWY_ALIGN float tmp[N * SZ]; + CoeffBundle::AddReverse(mem, mem + N / 2 * SZ, tmp); + DCT1DImpl()(tmp); + CoeffBundle::SubReverse(mem, mem + N / 2 * SZ, tmp + N / 2 * SZ); + CoeffBundle::Multiply(tmp); + DCT1DImpl()(tmp + N / 2 * SZ); + CoeffBundle::B(tmp + N / 2 * SZ); + CoeffBundle::InverseEvenOdd(tmp, mem); + } +}; + +template +struct IDCT1DImpl; + +template +struct IDCT1DImpl<1, SZ> { + JXL_INLINE void operator()(const float* from, size_t from_stride, float* to, + size_t to_stride) { + StoreU(LoadU(FV(), from), FV(), to); + } +}; + +template +struct IDCT1DImpl<2, SZ> { + JXL_INLINE void operator()(const float* from, size_t from_stride, float* to, + size_t to_stride) { + JXL_DASSERT(from_stride >= SZ); + JXL_DASSERT(to_stride >= SZ); + auto in1 = LoadU(FV(), from); + auto in2 = LoadU(FV(), from + from_stride); + StoreU(in1 + in2, FV(), to); + StoreU(in1 - in2, FV(), to + to_stride); + } +}; + +template +struct IDCT1DImpl { + void operator()(const float* from, size_t from_stride, float* to, + size_t to_stride) { + JXL_DASSERT(from_stride >= SZ); + JXL_DASSERT(to_stride >= SZ); + // This is relatively small (4kB with 64-DCT and AVX-512) + HWY_ALIGN float tmp[N * SZ]; + CoeffBundle::ForwardEvenOdd(from, from_stride, tmp); + IDCT1DImpl()(tmp, SZ, tmp, SZ); + CoeffBundle::BTranspose(tmp + N / 2 * SZ); + IDCT1DImpl()(tmp + N / 2 * SZ, SZ, tmp + N / 2 * SZ, SZ); + CoeffBundle::MultiplyAndAdd(tmp, to, to_stride); + } +}; + +template +void DCT1DWrapper(const FromBlock& from, const ToBlock& to, size_t Mp) { + size_t M = M_or_0 != 0 ? M_or_0 : Mp; + constexpr size_t SZ = MaxLanes(FV()); + HWY_ALIGN float tmp[N * SZ]; + for (size_t i = 0; i < M; i += Lanes(FV())) { + // TODO(veluca): consider removing the temporary memory here (as is done in + // IDCT), if it turns out that some compilers don't optimize away the loads + // and this is performance-critical. + CoeffBundle::LoadFromBlock(from, i, tmp); + DCT1DImpl()(tmp); + CoeffBundle::StoreToBlockAndScale(tmp, to, i); + } +} + +template +void IDCT1DWrapper(const FromBlock& from, const ToBlock& to, size_t Mp) { + size_t M = M_or_0 != 0 ? M_or_0 : Mp; + constexpr size_t SZ = MaxLanes(FV()); + for (size_t i = 0; i < M; i += Lanes(FV())) { + IDCT1DImpl()(from.Address(0, i), from.Stride(), to.Address(0, i), + to.Stride()); + } +} + +template +struct DCT1D { + template + void operator()(const FromBlock& from, const ToBlock& to) { + return DCT1DWrapper(from, to, M); + } +}; + +template +struct DCT1D MaxLanes(FV<0>()))>::type> { + template + void operator()(const FromBlock& from, const ToBlock& to) { + return NoInlineWrapper(DCT1DWrapper, from, to, M); + } +}; + +template +struct IDCT1D { + template + void operator()(const FromBlock& from, const ToBlock& to) { + return IDCT1DWrapper(from, to, M); + } +}; + +template +struct IDCT1D MaxLanes(FV<0>()))>::type> { + template + void operator()(const FromBlock& from, const ToBlock& to) { + return NoInlineWrapper(IDCT1DWrapper, from, to, + M); + } +}; + +// Computes the in-place NxN transposed-scaled-DCT (tsDCT) of block. +// Requires that block is HWY_ALIGN'ed. +// +// See also DCTSlow, ComputeDCT +template +struct ComputeTransposedScaledDCT { + // scratch_space must be aligned, and should have space for N*N floats. + template + HWY_MAYBE_UNUSED void operator()(const From& from, float* JXL_RESTRICT to, + float* JXL_RESTRICT scratch_space) { + float* JXL_RESTRICT block = scratch_space; + DCT1D()(from, DCTTo(to, N)); + Transpose::Run(DCTFrom(to, N), DCTTo(block, N)); + DCT1D()(DCTFrom(block, N), DCTTo(to, N)); + } +}; + +// Computes the in-place NxN transposed-scaled-iDCT (tsIDCT)of block. +// Requires that block is HWY_ALIGN'ed. +// +// See also IDCTSlow, ComputeIDCT. + +template +struct ComputeTransposedScaledIDCT { + // scratch_space must be aligned, and should have space for N*N floats. + template + HWY_MAYBE_UNUSED void operator()(float* JXL_RESTRICT from, const To& to, + float* JXL_RESTRICT scratch_space) { + float* JXL_RESTRICT block = scratch_space; + IDCT1D()(DCTFrom(from, N), DCTTo(block, N)); + Transpose::Run(DCTFrom(block, N), DCTTo(from, N)); + IDCT1D()(DCTFrom(from, N), to); + } +}; +// Computes the non-transposed, scaled DCT of a block, that needs to be +// HWY_ALIGN'ed. Used for rectangular blocks. +template +struct ComputeScaledDCT { + // scratch_space must be aligned, and should have space for ROWS*COLS + // floats. + template + HWY_MAYBE_UNUSED void operator()(const From& from, float* to, + float* JXL_RESTRICT scratch_space) { + float* JXL_RESTRICT block = scratch_space; + if (ROWS < COLS) { + DCT1D()(from, DCTTo(block, COLS)); + Transpose::Run(DCTFrom(block, COLS), DCTTo(to, ROWS)); + DCT1D()(DCTFrom(to, ROWS), DCTTo(block, ROWS)); + Transpose::Run(DCTFrom(block, ROWS), DCTTo(to, COLS)); + } else { + DCT1D()(from, DCTTo(to, COLS)); + Transpose::Run(DCTFrom(to, COLS), DCTTo(block, ROWS)); + DCT1D()(DCTFrom(block, ROWS), DCTTo(to, ROWS)); + } + } +}; +// Computes the non-transposed, scaled DCT of a block, that needs to be +// HWY_ALIGN'ed. Used for rectangular blocks. +template +struct ComputeScaledIDCT { + // scratch_space must be aligned, and should have space for ROWS*COLS + // floats. + template + HWY_MAYBE_UNUSED void operator()(float* JXL_RESTRICT from, const To& to, + float* JXL_RESTRICT scratch_space) { + float* JXL_RESTRICT block = scratch_space; + // Reverse the steps done in ComputeScaledDCT. + if (ROWS < COLS) { + Transpose::Run(DCTFrom(from, COLS), DCTTo(block, ROWS)); + IDCT1D()(DCTFrom(block, ROWS), DCTTo(from, ROWS)); + Transpose::Run(DCTFrom(from, ROWS), DCTTo(block, COLS)); + IDCT1D()(DCTFrom(block, COLS), to); + } else { + IDCT1D()(DCTFrom(from, ROWS), DCTTo(block, ROWS)); + Transpose::Run(DCTFrom(block, ROWS), DCTTo(from, COLS)); + IDCT1D()(DCTFrom(from, COLS), to); + } + } +}; + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); +#endif // LIB_JXL_DCT_INL_H_ diff --git a/lib/jxl/dct_block-inl.h b/lib/jxl/dct_block-inl.h new file mode 100644 index 0000000..50646a7 --- /dev/null +++ b/lib/jxl/dct_block-inl.h @@ -0,0 +1,108 @@ +// 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. + +// Adapters for DCT input/output: from/to contiguous blocks or image rows. + +#if defined(LIB_JXL_DCT_BLOCK_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_DCT_BLOCK_INL_H_ +#undef LIB_JXL_DCT_BLOCK_INL_H_ +#else +#define LIB_JXL_DCT_BLOCK_INL_H_ +#endif + +#include + +#include + +#include "lib/jxl/base/status.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Vec; + +// Block: (x, y) <-> (N * y + x) +// Lines: (x, y) <-> (stride * y + x) +// +// I.e. Block is a specialization of Lines with fixed stride. +// +// FromXXX should implement Read and Load (Read vector). +// ToXXX should implement Write and Store (Write vector). + +template +using BlockDesc = HWY_CAPPED(float, N); + +// Here and in the following, the SZ template parameter specifies the number of +// values to load/store. Needed because we want to handle 4x4 sub-blocks of +// 16x16 blocks. +class DCTFrom { + public: + DCTFrom(const float* data, size_t stride) : stride_(stride), data_(data) {} + + template + HWY_INLINE Vec LoadPart(D, const size_t row, size_t i) const { + JXL_DASSERT(Lanes(D()) <= stride_); + // Since these functions are used also for DC, no alignment at all is + // guaranteed in the case of floating blocks. + // TODO(veluca): consider using a different class for DC-to-LF and + // DC-from-LF, or copying DC values to/from a temporary aligned location. + return LoadU(D(), Address(row, i)); + } + + HWY_INLINE float Read(const size_t row, const size_t i) const { + return *Address(row, i); + } + + constexpr HWY_INLINE const float* Address(const size_t row, + const size_t i) const { + return data_ + row * stride_ + i; + } + + size_t Stride() const { return stride_; } + + private: + size_t stride_; + const float* JXL_RESTRICT data_; +}; + +class DCTTo { + public: + DCTTo(float* data, size_t stride) : stride_(stride), data_(data) {} + + template + HWY_INLINE void StorePart(D, const Vec& v, const size_t row, + size_t i) const { + JXL_DASSERT(Lanes(D()) <= stride_); + // Since these functions are used also for DC, no alignment at all is + // guaranteed in the case of floating blocks. + // TODO(veluca): consider using a different class for DC-to-LF and + // DC-from-LF, or copying DC values to/from a temporary aligned location. + StoreU(v, D(), Address(row, i)); + } + + HWY_INLINE void Write(float v, const size_t row, const size_t i) const { + *Address(row, i) = v; + } + + constexpr HWY_INLINE float* Address(const size_t row, const size_t i) const { + return data_ + row * stride_ + i; + } + + size_t Stride() const { return stride_; } + + private: + size_t stride_; + float* JXL_RESTRICT data_; +}; + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_DCT_BLOCK_INL_H_ diff --git a/lib/jxl/dct_for_test.h b/lib/jxl/dct_for_test.h new file mode 100644 index 0000000..8e32aa7 --- /dev/null +++ b/lib/jxl/dct_for_test.h @@ -0,0 +1,99 @@ +// 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_DCT_FOR_TEST_H_ +#define LIB_JXL_DCT_FOR_TEST_H_ + +// Unoptimized DCT only for use in tests. + +#include // memcpy + +#include +#include + +#include "lib/jxl/common.h" // Pi + +namespace jxl { + +namespace test { +static inline double alpha(int u) { return u == 0 ? 0.7071067811865475 : 1.0; } + +// N-DCT on M columns, divided by sqrt(N). Matches the definition in the spec. +template +void DCT1D(double block[N * M], double out[N * M]) { + std::vector matrix(N * N); + const double scale = std::sqrt(2.0) / N; + for (size_t y = 0; y < N; y++) { + for (size_t u = 0; u < N; u++) { + matrix[N * u + y] = alpha(u) * cos((y + 0.5) * u * Pi(1.0 / N)) * scale; + } + } + for (size_t x = 0; x < M; x++) { + for (size_t u = 0; u < N; u++) { + out[M * u + x] = 0; + for (size_t y = 0; y < N; y++) { + out[M * u + x] += matrix[N * u + y] * block[M * y + x]; + } + } + } +} + +// N-IDCT on M columns, multiplied by sqrt(N). Matches the definition in the +// spec. +template +void IDCT1D(double block[N * M], double out[N * M]) { + std::vector matrix(N * N); + const double scale = std::sqrt(2.0); + for (size_t y = 0; y < N; y++) { + for (size_t u = 0; u < N; u++) { + // Transpose of DCT matrix. + matrix[N * y + u] = alpha(u) * cos((y + 0.5) * u * Pi(1.0 / N)) * scale; + } + } + for (size_t x = 0; x < M; x++) { + for (size_t u = 0; u < N; u++) { + out[M * u + x] = 0; + for (size_t y = 0; y < N; y++) { + out[M * u + x] += matrix[N * u + y] * block[M * y + x]; + } + } + } +} + +template +void TransposeBlock(double in[N * M], double out[M * N]) { + for (size_t x = 0; x < N; x++) { + for (size_t y = 0; y < M; y++) { + out[y * N + x] = in[x * M + y]; + } + } +} +} // namespace test + +// Untransposed DCT. +template +void DCTSlow(double block[N * N]) { + constexpr size_t kBlockSize = N * N; + std::vector g(kBlockSize); + test::DCT1D(block, g.data()); + test::TransposeBlock(g.data(), block); + test::DCT1D(block, g.data()); + test::TransposeBlock(g.data(), block); +} + +// Untransposed IDCT. +template +void IDCTSlow(double block[N * N]) { + constexpr size_t kBlockSize = N * N; + std::vector g(kBlockSize); + test::IDCT1D(block, g.data()); + test::TransposeBlock(g.data(), block); + test::IDCT1D(block, g.data()); + test::TransposeBlock(g.data(), block); +} + +} // namespace jxl + +#endif // LIB_JXL_DCT_FOR_TEST_H_ diff --git a/lib/jxl/dct_scales.cc b/lib/jxl/dct_scales.cc new file mode 100644 index 0000000..f9e89a6 --- /dev/null +++ b/lib/jxl/dct_scales.cc @@ -0,0 +1,31 @@ +// 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/dct_scales.h" + +namespace jxl { + +// Definition of constexpr arrays. +constexpr float DCTResampleScales<1, 8>::kScales[]; +constexpr float DCTResampleScales<2, 16>::kScales[]; +constexpr float DCTResampleScales<4, 32>::kScales[]; +constexpr float DCTResampleScales<8, 64>::kScales[]; +constexpr float DCTResampleScales<16, 128>::kScales[]; +constexpr float DCTResampleScales<32, 256>::kScales[]; +constexpr float DCTResampleScales<8, 1>::kScales[]; +constexpr float DCTResampleScales<16, 2>::kScales[]; +constexpr float DCTResampleScales<32, 4>::kScales[]; +constexpr float DCTResampleScales<64, 8>::kScales[]; +constexpr float DCTResampleScales<128, 16>::kScales[]; +constexpr float DCTResampleScales<256, 32>::kScales[]; +constexpr float WcMultipliers<4>::kMultipliers[]; +constexpr float WcMultipliers<8>::kMultipliers[]; +constexpr float WcMultipliers<16>::kMultipliers[]; +constexpr float WcMultipliers<32>::kMultipliers[]; +constexpr float WcMultipliers<64>::kMultipliers[]; +constexpr float WcMultipliers<128>::kMultipliers[]; +constexpr float WcMultipliers<256>::kMultipliers[]; + +} // namespace jxl diff --git a/lib/jxl/dct_scales.h b/lib/jxl/dct_scales.h new file mode 100644 index 0000000..9ec670a --- /dev/null +++ b/lib/jxl/dct_scales.h @@ -0,0 +1,390 @@ +// 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_DCT_SCALES_H_ +#define LIB_JXL_DCT_SCALES_H_ + +// Scaling factors. + +#include + +namespace jxl { +template +struct square_root { + static constexpr float value = square_root::value * 2; +}; + +template <> +struct square_root<1> { + static constexpr float value = 1.0f; +}; + +template <> +struct square_root<2> { + static constexpr float value = 1.4142135623730951f; +}; + +// For n != 0, the n-th basis function of a N-DCT, evaluated in pixel k, has a +// value of cos((k+1/2) n/(2N) pi). When downsampling by 2x, we average +// the values for pixel k and k+1 to get the value for pixel (k/2), thus we get +// +// [cos((k+1/2) n/N pi) + cos((k+3/2) n/N pi)]/2 = +// cos(n/(2N) pi) cos((k+1) n/N pi) = +// cos(n/(2N) pi) cos(((k/2)+1/2) n/(N/2) pi) +// +// which is exactly the same as the value of pixel k/2 of a N/2-sized DCT, +// except for the cos(n/(2N) pi) scaling factor (which does *not* +// depend on the pixel). Thus, when using the lower-frequency coefficients of a +// DCT-N to compute a DCT-(N/2), they should be scaled by this constant. Scaling +// factors for a DCT-(N/4) etc can then be obtained by successive +// multiplications. The structs below contain the above-mentioned scaling +// factors. +// +// Python code for the tables below: +// +// for i in range(N // 8): +// v = math.cos(i / (2 * N) * math.pi) +// v *= math.cos(i / (N) * math.pi) +// v *= math.cos(i / (N / 2) * math.pi) +// print(v, end=", ") + +template +struct DCTResampleScales; + +template <> +struct DCTResampleScales<8, 1> { + static constexpr float kScales[] = { + 1.000000000000000000, + }; +}; + +template <> +struct DCTResampleScales<16, 2> { + static constexpr float kScales[] = { + 1.000000000000000000, + 0.901764195028874394, + }; +}; + +template <> +struct DCTResampleScales<32, 4> { + static constexpr float kScales[] = { + 1.000000000000000000, + 0.974886821136879522, + 0.901764195028874394, + 0.787054918159101335, + }; +}; + +template <> +struct DCTResampleScales<64, 8> { + static constexpr float kScales[] = { + 1.0000000000000000, 0.9936866130906366, 0.9748868211368796, + 0.9440180941651672, 0.9017641950288744, 0.8490574973847023, + 0.7870549181591013, 0.7171081282466044, + }; +}; + +template <> +struct DCTResampleScales<128, 16> { + static constexpr float kScales[] = { + 1.0, + 0.9984194528776054, + 0.9936866130906366, + 0.9858278282666936, + 0.9748868211368796, + 0.9609244059440204, + 0.9440180941651672, + 0.9242615922757944, + 0.9017641950288744, + 0.8766500784429904, + 0.8490574973847023, + 0.8191378932865928, + 0.7870549181591013, + 0.7529833816270532, + 0.7171081282466044, + 0.6796228528314651, + }; +}; + +template <> +struct DCTResampleScales<256, 32> { + static constexpr float kScales[] = { + 1.0, + 0.9996047255830407, + 0.9984194528776054, + 0.9964458326264695, + 0.9936866130906366, + 0.9901456355893141, + 0.9858278282666936, + 0.9807391980963174, + 0.9748868211368796, + 0.9682788310563117, + 0.9609244059440204, + 0.9528337534340876, + 0.9440180941651672, + 0.9344896436056892, + 0.9242615922757944, + 0.913348084400198, + 0.9017641950288744, + 0.8895259056651056, + 0.8766500784429904, + 0.8631544288990163, + 0.8490574973847023, + 0.8343786191696513, + 0.8191378932865928, + 0.8033561501721485, + 0.7870549181591013, + 0.7702563888779096, + 0.7529833816270532, + 0.7352593067735488, + 0.7171081282466044, + 0.6985543251889097, + 0.6796228528314651, + 0.6603391026591464, + }; +}; + +// Inverses of the above. +template <> +struct DCTResampleScales<1, 8> { + static constexpr float kScales[] = { + 1.000000000000000000, + }; +}; + +template <> +struct DCTResampleScales<2, 16> { + static constexpr float kScales[] = { + 1.000000000000000000, + 1.108937353592731823, + }; +}; + +template <> +struct DCTResampleScales<4, 32> { + static constexpr float kScales[] = { + 1.000000000000000000, + 1.025760096781116015, + 1.108937353592731823, + 1.270559368765487251, + }; +}; + +template <> +struct DCTResampleScales<8, 64> { + static constexpr float kScales[] = { + 1.0000000000000000, 1.0063534990068217, 1.0257600967811158, + 1.0593017296817173, 1.1089373535927318, 1.1777765381970435, + 1.2705593687654873, 1.3944898413647777, + }; +}; + +template <> +struct DCTResampleScales<16, 128> { + static constexpr float kScales[] = { + 1.0, + 1.0015830492062623, + 1.0063534990068217, + 1.0143759095928793, + 1.0257600967811158, + 1.0406645869480142, + 1.0593017296817173, + 1.0819447744633812, + 1.1089373535927318, + 1.1407059950032632, + 1.1777765381970435, + 1.2207956782315876, + 1.2705593687654873, + 1.3280505578213306, + 1.3944898413647777, + 1.4714043176061107, + }; +}; + +template <> +struct DCTResampleScales<32, 256> { + static constexpr float kScales[] = { + 1.0, + 1.0003954307206069, + 1.0015830492062623, + 1.0035668445360069, + 1.0063534990068217, + 1.009952439375063, + 1.0143759095928793, + 1.0196390660647288, + 1.0257600967811158, + 1.0327603660498115, + 1.0406645869480142, + 1.049501024072585, + 1.0593017296817173, + 1.0701028169146336, + 1.0819447744633812, + 1.0948728278734026, + 1.1089373535927318, + 1.124194353004584, + 1.1407059950032632, + 1.158541237256391, + 1.1777765381970435, + 1.1984966740820495, + 1.2207956782315876, + 1.244777922949508, + 1.2705593687654873, + 1.2982690107339132, + 1.3280505578213306, + 1.3600643892400104, + 1.3944898413647777, + 1.4315278911623237, + 1.4714043176061107, + 1.5143734423314616, + }; +}; + +// Constants for DCT implementation. Generated by the following snippet: +// for i in range(N // 2): +// print(1.0 / (2 * math.cos((i + 0.5) * math.pi / N)), end=", ") +template +struct WcMultipliers; + +template <> +struct WcMultipliers<4> { + static constexpr float kMultipliers[] = { + 0.541196100146197, + 1.3065629648763764, + }; +}; + +template <> +struct WcMultipliers<8> { + static constexpr float kMultipliers[] = { + 0.5097955791041592, + 0.6013448869350453, + 0.8999762231364156, + 2.5629154477415055, + }; +}; + +template <> +struct WcMultipliers<16> { + static constexpr float kMultipliers[] = { + 0.5024192861881557, 0.5224986149396889, 0.5669440348163577, + 0.6468217833599901, 0.7881546234512502, 1.060677685990347, + 1.7224470982383342, 5.101148618689155, + }; +}; + +template <> +struct WcMultipliers<32> { + static constexpr float kMultipliers[] = { + 0.5006029982351963, 0.5054709598975436, 0.5154473099226246, + 0.5310425910897841, 0.5531038960344445, 0.5829349682061339, + 0.6225041230356648, 0.6748083414550057, 0.7445362710022986, + 0.8393496454155268, 0.9725682378619608, 1.1694399334328847, + 1.4841646163141662, 2.057781009953411, 3.407608418468719, + 10.190008123548033, + }; +}; +template <> +struct WcMultipliers<64> { + static constexpr float kMultipliers[] = { + 0.500150636020651, 0.5013584524464084, 0.5037887256810443, + 0.5074711720725553, 0.5124514794082247, 0.5187927131053328, + 0.52657731515427, 0.535909816907992, 0.5469204379855088, + 0.5597698129470802, 0.57465518403266, 0.5918185358574165, + 0.6115573478825099, 0.6342389366884031, 0.6603198078137061, + 0.6903721282002123, 0.7251205223771985, 0.7654941649730891, + 0.8127020908144905, 0.8683447152233481, 0.9345835970364075, + 1.0144082649970547, 1.1120716205797176, 1.233832737976571, + 1.3892939586328277, 1.5939722833856311, 1.8746759800084078, + 2.282050068005162, 2.924628428158216, 4.084611078129248, + 6.796750711673633, 20.373878167231453, + }; +}; +template <> +struct WcMultipliers<128> { + static constexpr float kMultipliers[] = { + 0.5000376519155477, 0.5003390374428216, 0.5009427176380873, + 0.5018505174842379, 0.5030651913013697, 0.5045904432216454, + 0.5064309549285542, 0.5085924210498143, 0.5110815927066812, + 0.5139063298475396, 0.5170756631334912, 0.5205998663018917, + 0.524490540114724, 0.5287607092074876, 0.5334249333971333, + 0.538499435291984, 0.5440022463817783, 0.549953374183236, + 0.5563749934898856, 0.5632916653417023, 0.5707305880121454, + 0.5787218851348208, 0.5872989370937893, 0.5964987630244563, + 0.606362462272146, 0.6169357260050706, 0.6282694319707711, + 0.6404203382416639, 0.6534518953751283, 0.6674352009263413, + 0.6824501259764195, 0.6985866506472291, 0.7159464549705746, + 0.7346448236478627, 0.7548129391165311, 0.776600658233963, + 0.8001798956216941, 0.8257487738627852, 0.8535367510066064, + 0.8838110045596234, 0.9168844461846523, 0.9531258743921193, + 0.9929729612675466, 1.036949040910389, 1.0856850642580145, + 1.1399486751015042, 1.2006832557294167, 1.2690611716991191, + 1.346557628206286, 1.4350550884414341, 1.5369941008524954, + 1.6555965242641195, 1.7952052190778898, 1.961817848571166, + 2.163957818751979, 2.4141600002500763, 2.7316450287739396, + 3.147462191781909, 3.7152427383269746, 4.5362909369693565, + 5.827688377844654, 8.153848602466814, 13.58429025728446, + 40.744688103351834, + }; +}; + +template <> +struct WcMultipliers<256> { + static constexpr float kMultipliers[128] = { + 0.5000094125358878, 0.500084723455784, 0.5002354020255269, + 0.5004615618093246, 0.5007633734146156, 0.5011410648064231, + 0.5015949217281668, 0.502125288230386, 0.5027325673091954, + 0.5034172216566842, 0.5041797745258774, 0.5050208107132756, + 0.5059409776624396, 0.5069409866925212, 0.5080216143561264, + 0.509183703931388, 0.5104281670536573, 0.5117559854927805, + 0.5131682130825206, 0.5146659778093218, 0.516250484068288, + 0.5179230150949777, 0.5196849355823947, 0.5215376944933958, + 0.5234828280796439, 0.52552196311921, 0.5276568203859896, + 0.5298892183652453, 0.5322210772308335, 0.5346544231010253, + 0.537191392591309, 0.5398342376841637, 0.5425853309375497, + 0.545447171055775, 0.5484223888484947, 0.551513753605893, + 0.554724179920619, 0.5580567349898085, 0.5615146464335654, + 0.5651013106696203, 0.5688203018875696, 0.5726753816701664, + 0.5766705093136241, 0.5808098529038624, 0.5850978012111273, + 0.58953897647151, 0.5941382481306648, 0.5989007476325463, + 0.6038318843443582, 0.6089373627182432, 0.614223200800649, + 0.6196957502119484, 0.6253617177319102, 0.6312281886412079, + 0.6373026519855411, 0.6435930279473415, 0.6501076975307724, + 0.6568555347890955, 0.6638459418498757, 0.6710888870233562, + 0.6785949463131795, 0.6863753486870501, 0.6944420255086364, + 0.7028076645818034, 0.7114857693151208, 0.7204907235796304, + 0.7298378629074134, 0.7395435527641373, 0.749625274727372, + 0.7601017215162176, 0.7709929019493761, 0.7823202570613161, + 0.7941067887834509, 0.8063772028037925, 0.8191580674598145, + 0.83247799080191, 0.8463678182968619, 0.860860854031955, + 0.8759931087426972, 0.8918035785352535, 0.9083345588266809, + 0.9256319988042384, 0.9437459026371479, 0.962730784794803, + 0.9826461881778968, 1.0035572754078206, 1.0255355056139732, + 1.048659411496106, 1.0730154944316674, 1.0986992590905857, + 1.1258164135986009, 1.1544842669978943, 1.184833362908442, + 1.217009397314603, 1.2511754798461228, 1.287514812536712, + 1.326233878832723, 1.3675662599582539, 1.411777227500661, + 1.459169302866857, 1.5100890297227016, 1.5649352798258847, + 1.6241695131835794, 1.6883285509131505, 1.7580406092704062, + 1.8340456094306077, 1.9172211551275689, 2.0086161135167564, + 2.1094945286246385, 2.22139377701127, 2.346202662531156, + 2.486267909203593, 2.644541877144861, 2.824791402350551, + 3.0318994541759925, 3.2723115884254845, 3.5547153325075804, + 3.891107790700307, 4.298537526449054, 4.802076008665048, + 5.440166215091329, 6.274908408039339, 7.413566756422303, + 9.058751453879703, 11.644627325175037, 16.300023088031555, + 27.163977662448232, 81.48784219222516, + }; +}; + +// Apply the DCT algorithm-intrinsic constants to DCTResampleScale. +template +constexpr float DCTTotalResampleScale(size_t x) { + return DCTResampleScales::kScales[x]; +} + +} // namespace jxl + +#endif // LIB_JXL_DCT_SCALES_H_ diff --git a/lib/jxl/dct_test.cc b/lib/jxl/dct_test.cc new file mode 100644 index 0000000..a51a317 --- /dev/null +++ b/lib/jxl/dct_test.cc @@ -0,0 +1,390 @@ +// 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 + +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dct_test.cc" +#include +#include +#include + +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct-inl.h" +#include "lib/jxl/dct_for_test.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/image.h" +#include "lib/jxl/test_utils.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// Computes the in-place NxN DCT of block. +// Requires that block is HWY_ALIGN'ed. +// +// Performs ComputeTransposedScaledDCT and then transposes and scales it to +// obtain "vanilla" DCT. +template +void ComputeDCT(float block[N * N]) { + HWY_ALIGN float tmp_block[N * N]; + HWY_ALIGN float scratch_space[N * N]; + ComputeTransposedScaledDCT()(DCTFrom(block, N), tmp_block, scratch_space); + + // Untranspose. + Transpose::Run(DCTFrom(tmp_block, N), DCTTo(block, N)); +} + +// Computes the in-place 8x8 iDCT of block. +// Requires that block is HWY_ALIGN'ed. +template +void ComputeIDCT(float block[N * N]) { + HWY_ALIGN float tmp_block[N * N]; + HWY_ALIGN float scratch_space[N * N]; + // Untranspose. + Transpose::Run(DCTFrom(block, N), DCTTo(tmp_block, N)); + + ComputeTransposedScaledIDCT()(tmp_block, DCTTo(block, N), scratch_space); +} + +template +void TransposeTestT(float accuracy) { + constexpr size_t kBlockSize = N * N; + HWY_ALIGN float src[kBlockSize]; + DCTTo to_src(src, N); + for (size_t y = 0; y < N; ++y) { + for (size_t x = 0; x < N; ++x) { + to_src.Write(y * N + x, y, x); + } + } + HWY_ALIGN float dst[kBlockSize]; + Transpose::Run(DCTFrom(src, N), DCTTo(dst, N)); + DCTFrom from_dst(dst, N); + for (size_t y = 0; y < N; ++y) { + for (size_t x = 0; x < N; ++x) { + float expected = x * N + y; + float actual = from_dst.Read(y, x); + EXPECT_NEAR(expected, actual, accuracy) << "x = " << x << ", y = " << y; + } + } +} + +void TransposeTest() { + TransposeTestT<8>(1e-7f); + TransposeTestT<16>(1e-7f); + TransposeTestT<32>(1e-7f); +} + +template +void ColumnDctRoundtripT(float accuracy) { + constexpr size_t kBlockSize = N * N; + // Though we are only interested in single column result, dct.h has built-in + // limit on minimal number of columns processed. So, to be safe, we do + // regular 8x8 block transformation. On the bright side - we could check all + // 8 basis vectors at once. + HWY_ALIGN float block[kBlockSize]; + DCTTo to(block, N); + DCTFrom from(block, N); + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + to.Write((i == j) ? 1.0f : 0.0f, i, j); + } + } + + // Running (I)DCT on the same memory block seems to trigger a compiler bug on + // ARMv7 with clang6. + HWY_ALIGN float tmp[kBlockSize]; + DCTTo to_tmp(tmp, N); + DCTFrom from_tmp(tmp, N); + + DCT1D()(from, to_tmp); + IDCT1D()(from_tmp, to); + + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + float expected = (i == j) ? 1.0f : 0.0f; + float actual = from.Read(i, j); + EXPECT_NEAR(expected, actual, accuracy) << " i=" << i << ", j=" << j; + } + } +} + +void ColumnDctRoundtrip() { + ColumnDctRoundtripT<8>(1e-6f); + ColumnDctRoundtripT<16>(1e-6f); + ColumnDctRoundtripT<32>(1e-6f); +} + +template +void TestDctAccuracy(float accuracy, size_t start = 0, size_t end = N * N) { + constexpr size_t kBlockSize = N * N; + for (size_t i = start; i < end; i++) { + HWY_ALIGN float fast[kBlockSize] = {0.0f}; + double slow[kBlockSize] = {0.0}; + fast[i] = 1.0; + slow[i] = 1.0; + DCTSlow(slow); + ComputeDCT(fast); + for (size_t k = 0; k < kBlockSize; ++k) { + EXPECT_NEAR(fast[k], slow[k], accuracy / N) + << "i = " << i << ", k = " << k << ", N = " << N; + } + } +} + +template +void TestIdctAccuracy(float accuracy, size_t start = 0, size_t end = N * N) { + constexpr size_t kBlockSize = N * N; + for (size_t i = start; i < end; i++) { + HWY_ALIGN float fast[kBlockSize] = {0.0f}; + double slow[kBlockSize] = {0.0}; + fast[i] = 1.0; + slow[i] = 1.0; + IDCTSlow(slow); + ComputeIDCT(fast); + for (size_t k = 0; k < kBlockSize; ++k) { + EXPECT_NEAR(fast[k], slow[k], accuracy * N) + << "i = " << i << ", k = " << k << ", N = " << N; + } + } +} + +template +void TestInverseT(float accuracy) { + ThreadPoolInternal pool(N < 32 ? 0 : 8); + enum { kBlockSize = N * N }; + RunOnPool( + &pool, 0, kBlockSize, ThreadPool::SkipInit(), + [accuracy](const int task, int /*thread*/) { + const size_t i = static_cast(task); + HWY_ALIGN float x[kBlockSize] = {0.0f}; + x[i] = 1.0; + + ComputeIDCT(x); + ComputeDCT(x); + + for (size_t k = 0; k < kBlockSize; ++k) { + EXPECT_NEAR(x[k], (k == i) ? 1.0f : 0.0f, accuracy) + << "i = " << i << ", k = " << k; + } + }, + "TestInverse"); +} + +void InverseTest() { + TestInverseT<8>(1e-6f); + TestInverseT<16>(1e-6f); + TestInverseT<32>(3e-6f); +} + +template +void TestDctTranspose(float accuracy, size_t start = 0, size_t end = N * N) { + constexpr size_t kBlockSize = N * N; + for (size_t i = start; i < end; i++) { + for (size_t j = 0; j < kBlockSize; ++j) { + // We check that = . + // That means (Me_j)_i = (M^\dagger{}e_i)_j + + // x := Me_j + HWY_ALIGN float x[kBlockSize] = {0.0f}; + x[j] = 1.0; + ComputeIDCT(x); + // y := M^\dagger{}e_i + HWY_ALIGN float y[kBlockSize] = {0.0f}; + y[i] = 1.0; + ComputeDCT(y); + + EXPECT_NEAR(x[i] / N, y[j] * N, accuracy) << "i = " << i << ", j = " << j; + } + } +} + +template +void TestSlowInverse(float accuracy, size_t start = 0, size_t end = N * N) { + constexpr size_t kBlockSize = N * N; + for (size_t i = start; i < end; i++) { + double x[kBlockSize] = {0.0f}; + x[i] = 1.0; + + DCTSlow(x); + IDCTSlow(x); + + for (size_t k = 0; k < kBlockSize; ++k) { + EXPECT_NEAR(x[k], (k == i) ? 1.0f : 0.0f, accuracy) + << "i = " << i << ", k = " << k; + } + } +} + +template +void TestRectInverseT(float accuracy) { + constexpr size_t kBlockSize = ROWS * COLS; + for (size_t i = 0; i < kBlockSize; ++i) { + HWY_ALIGN float x[kBlockSize] = {0.0f}; + HWY_ALIGN float out[kBlockSize] = {0.0f}; + x[i] = 1.0; + HWY_ALIGN float coeffs[kBlockSize] = {0.0f}; + HWY_ALIGN float scratch_space[kBlockSize * 2]; + + ComputeScaledDCT()(DCTFrom(x, COLS), coeffs, scratch_space); + ComputeScaledIDCT()(coeffs, DCTTo(out, COLS), scratch_space); + + for (size_t k = 0; k < kBlockSize; ++k) { + EXPECT_NEAR(out[k], (k == i) ? 1.0f : 0.0f, accuracy) + << "i = " << i << ", k = " << k << " ROWS = " << ROWS + << " COLS = " << COLS; + } + } +} + +void TestRectInverse() { + TestRectInverseT<16, 32>(1e-6f); + TestRectInverseT<8, 32>(1e-6f); + TestRectInverseT<8, 16>(1e-6f); + TestRectInverseT<4, 8>(1e-6f); + TestRectInverseT<2, 4>(1e-6f); + TestRectInverseT<1, 4>(1e-6f); + TestRectInverseT<1, 2>(1e-6f); + + TestRectInverseT<32, 16>(1e-6f); + TestRectInverseT<32, 8>(1e-6f); + TestRectInverseT<16, 8>(1e-6f); + TestRectInverseT<8, 4>(1e-6f); + TestRectInverseT<4, 2>(1e-6f); + TestRectInverseT<4, 1>(1e-6f); + TestRectInverseT<2, 1>(1e-6f); +} + +template +void TestRectTransposeT(float accuracy) { + constexpr size_t kBlockSize = ROWS * COLS; + HWY_ALIGN float scratch_space[kBlockSize * 2]; + for (size_t px = 0; px < COLS; ++px) { + for (size_t py = 0; py < ROWS; ++py) { + HWY_ALIGN float x1[kBlockSize] = {0.0f}; + HWY_ALIGN float x2[kBlockSize] = {0.0f}; + HWY_ALIGN float coeffs1[kBlockSize] = {0.0f}; + HWY_ALIGN float coeffs2[kBlockSize] = {0.0f}; + x1[py * COLS + px] = 1; + x2[px * ROWS + py] = 1; + + constexpr size_t OUT_ROWS = ROWS < COLS ? ROWS : COLS; + constexpr size_t OUT_COLS = ROWS < COLS ? COLS : ROWS; + + ComputeScaledDCT()(DCTFrom(x1, COLS), coeffs1, scratch_space); + ComputeScaledDCT()(DCTFrom(x2, ROWS), coeffs2, scratch_space); + + for (size_t x = 0; x < OUT_COLS; ++x) { + for (size_t y = 0; y < OUT_ROWS; ++y) { + EXPECT_NEAR(coeffs1[y * OUT_COLS + x], coeffs2[y * OUT_COLS + x], + accuracy) + << " px = " << px << ", py = " << py << ", x = " << x + << ", y = " << y; + } + } + } + } +} + +void TestRectTranspose() { + TestRectTransposeT<16, 32>(1e-6f); + TestRectTransposeT<8, 32>(1e-6f); + TestRectTransposeT<8, 16>(1e-6f); + TestRectTransposeT<4, 8>(1e-6f); + TestRectTransposeT<2, 4>(1e-6f); + TestRectTransposeT<1, 4>(1e-6f); + TestRectTransposeT<1, 2>(1e-6f); + + // Identical to 8, 16 + // TestRectTranspose<16, 8>(1e-6f); +} + +void TestDctAccuracyShard(size_t shard) { + if (shard == 0) { + TestDctAccuracy<1>(1.1E-7f); + TestDctAccuracy<2>(1.1E-7f); + TestDctAccuracy<4>(1.1E-7f); + TestDctAccuracy<8>(1.1E-7f); + TestDctAccuracy<16>(1.3E-7f); + } + TestDctAccuracy<32>(1.1E-7f, 32 * shard, 32 * (shard + 1)); +} + +void TestIdctAccuracyShard(size_t shard) { + if (shard == 0) { + TestIdctAccuracy<1>(1E-7f); + TestIdctAccuracy<2>(1E-7f); + TestIdctAccuracy<4>(1E-7f); + TestIdctAccuracy<8>(1E-7f); + TestIdctAccuracy<16>(1E-7f); + } + TestIdctAccuracy<32>(1E-7f, 32 * shard, 32 * (shard + 1)); +} + +void TestDctTransposeShard(size_t shard) { + if (shard == 0) { + TestDctTranspose<8>(1E-6f); + TestDctTranspose<16>(1E-6f); + } + TestDctTranspose<32>(3E-6f, 32 * shard, 32 * (shard + 1)); +} + +void TestSlowInverseShard(size_t shard) { + if (shard == 0) { + TestSlowInverse<1>(1E-5f); + TestSlowInverse<2>(1E-5f); + TestSlowInverse<4>(1E-5f); + TestSlowInverse<8>(1E-5f); + TestSlowInverse<16>(1E-5f); + } + TestSlowInverse<32>(1E-5f, 32 * shard, 32 * (shard + 1)); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +class TransposeTest : public hwy::TestWithParamTarget {}; + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(TransposeTest); + +HWY_EXPORT_AND_TEST_P(TransposeTest, TransposeTest); +HWY_EXPORT_AND_TEST_P(TransposeTest, InverseTest); +HWY_EXPORT_AND_TEST_P(TransposeTest, ColumnDctRoundtrip); +HWY_EXPORT_AND_TEST_P(TransposeTest, TestRectInverse); +HWY_EXPORT_AND_TEST_P(TransposeTest, TestRectTranspose); + +// Tests in the DctShardedTest class are sharded for N=32. +class DctShardedTest : public ::hwy::TestWithParamTargetAndT {}; + +std::vector ShardRange(uint32_t n) { +#ifdef JXL_DISABLE_SLOW_TESTS + JXL_ASSERT(n > 6); + std::vector ret = {0, 1, 3, 5, n - 1}; +#else + std::vector ret(n); + std::iota(ret.begin(), ret.end(), 0); +#endif // JXL_DISABLE_SLOW_TESTS + return ret; +} + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P_T(DctShardedTest, + ::testing::ValuesIn(ShardRange(32))); + +HWY_EXPORT_AND_TEST_P_T(DctShardedTest, TestDctAccuracyShard); +HWY_EXPORT_AND_TEST_P_T(DctShardedTest, TestIdctAccuracyShard); +HWY_EXPORT_AND_TEST_P_T(DctShardedTest, TestDctTransposeShard); +HWY_EXPORT_AND_TEST_P_T(DctShardedTest, TestSlowInverseShard); + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dct_util.h b/lib/jxl/dct_util.h new file mode 100644 index 0000000..fb6ce3b --- /dev/null +++ b/lib/jxl/dct_util.h @@ -0,0 +1,86 @@ +// 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_DCT_UTIL_H_ +#define LIB_JXL_DCT_UTIL_H_ + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +union ACPtr { + int32_t* ptr32; + int16_t* ptr16; + ACPtr() = default; + explicit ACPtr(int16_t* p) : ptr16(p) {} + explicit ACPtr(int32_t* p) : ptr32(p) {} +}; + +union ConstACPtr { + const int32_t* ptr32; + const int16_t* ptr16; + ConstACPtr() = default; + explicit ConstACPtr(const int16_t* p) : ptr16(p) {} + explicit ConstACPtr(const int32_t* p) : ptr32(p) {} +}; + +enum class ACType { k16 = 0, k32 = 1 }; + +class ACImage { + public: + virtual ~ACImage() = default; + virtual ACType Type() const = 0; + virtual ACPtr PlaneRow(size_t c, size_t y, size_t xbase) = 0; + virtual ConstACPtr PlaneRow(size_t c, size_t y, size_t xbase) const = 0; + virtual size_t PixelsPerRow() const = 0; + virtual void ZeroFill() = 0; + virtual void ZeroFillPlane(size_t c) = 0; + virtual bool IsEmpty() const = 0; +}; + +template +class ACImageT final : public ACImage { + public: + ACImageT() = default; + ACImageT(size_t xsize, size_t ysize) { + static_assert( + std::is_same::value || std::is_same::value, + "ACImage must be either 32- or 16- bit"); + img_ = Image3(xsize, ysize); + } + ACType Type() const override { + return sizeof(T) == 2 ? ACType::k16 : ACType::k32; + } + ACPtr PlaneRow(size_t c, size_t y, size_t xbase) override { + return ACPtr(img_.PlaneRow(c, y) + xbase); + } + ConstACPtr PlaneRow(size_t c, size_t y, size_t xbase) const override { + return ConstACPtr(img_.PlaneRow(c, y) + xbase); + } + + size_t PixelsPerRow() const override { return img_.PixelsPerRow(); } + + void ZeroFill() override { ZeroFillImage(&img_); } + + void ZeroFillPlane(size_t c) override { ZeroFillImage(&img_.Plane(c)); } + + bool IsEmpty() const override { + return img_.xsize() == 0 || img_.ysize() == 0; + } + + private: + Image3 img_; +}; + +} // namespace jxl + +#endif // LIB_JXL_DCT_UTIL_H_ diff --git a/lib/jxl/dec_ans.cc b/lib/jxl/dec_ans.cc new file mode 100644 index 0000000..06709d7 --- /dev/null +++ b/lib/jxl/dec_ans.cc @@ -0,0 +1,375 @@ +// 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/dec_ans.h" + +#include + +#include + +#include "lib/jxl/ans_common.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_context_map.h" +#include "lib/jxl/fields.h" + +namespace jxl { +namespace { + +// Decodes a number in the range [0..255], by reading 1 - 11 bits. +inline int DecodeVarLenUint8(BitReader* input) { + if (input->ReadFixedBits<1>()) { + int nbits = static_cast(input->ReadFixedBits<3>()); + if (nbits == 0) { + return 1; + } else { + return static_cast(input->ReadBits(nbits)) + (1 << nbits); + } + } + return 0; +} + +// Decodes a number in the range [0..65535], by reading 1 - 21 bits. +inline int DecodeVarLenUint16(BitReader* input) { + if (input->ReadFixedBits<1>()) { + int nbits = static_cast(input->ReadFixedBits<4>()); + if (nbits == 0) { + return 1; + } else { + return static_cast(input->ReadBits(nbits)) + (1 << nbits); + } + } + return 0; +} + +Status ReadHistogram(int precision_bits, std::vector* counts, + BitReader* input) { + int simple_code = input->ReadBits(1); + if (simple_code == 1) { + int i; + int symbols[2] = {0}; + int max_symbol = 0; + const int num_symbols = input->ReadBits(1) + 1; + for (i = 0; i < num_symbols; ++i) { + symbols[i] = DecodeVarLenUint8(input); + if (symbols[i] > max_symbol) max_symbol = symbols[i]; + } + counts->resize(max_symbol + 1); + if (num_symbols == 1) { + (*counts)[symbols[0]] = 1 << precision_bits; + } else { + if (symbols[0] == symbols[1]) { // corrupt data + return false; + } + (*counts)[symbols[0]] = input->ReadBits(precision_bits); + (*counts)[symbols[1]] = (1 << precision_bits) - (*counts)[symbols[0]]; + } + } else { + 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; + } + + uint32_t shift; + { + // TODO(veluca): speed up reading with table lookups. + int upper_bound_log = FloorLog2Nonzero(ANS_LOG_TAB_SIZE + 1); + int log = 0; + for (; log < upper_bound_log; log++) { + if (input->ReadFixedBits<1>() == 0) break; + } + shift = (input->ReadBits(log) | (1 << log)) - 1; + if (shift > ANS_LOG_TAB_SIZE + 1) { + return JXL_FAILURE("Invalid shift value"); + } + } + + int length = DecodeVarLenUint8(input) + 3; + counts->resize(length); + int total_count = 0; + + static const uint8_t huff[128][2] = { + {3, 10}, {7, 12}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {5, 0}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {6, 11}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {5, 0}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {7, 13}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {5, 0}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {6, 11}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + {3, 10}, {5, 0}, {3, 7}, {4, 3}, {3, 6}, {3, 8}, {3, 9}, {4, 5}, + {3, 10}, {4, 4}, {3, 7}, {4, 1}, {3, 6}, {3, 8}, {3, 9}, {4, 2}, + }; + + std::vector logcounts(counts->size()); + int omit_log = -1; + int omit_pos = -1; + // This array remembers which symbols have an RLE length. + std::vector same(counts->size(), 0); + for (size_t i = 0; i < logcounts.size(); ++i) { + input->Refill(); // for PeekFixedBits + Advance + int idx = input->PeekFixedBits<7>(); + input->Consume(huff[idx][0]); + logcounts[i] = huff[idx][1]; + // The RLE symbol. + if (logcounts[i] == ANS_LOG_TAB_SIZE + 1) { + int rle_length = DecodeVarLenUint8(input); + same[i] = rle_length + 5; + i += rle_length + 3; + continue; + } + if (logcounts[i] > omit_log) { + omit_log = logcounts[i]; + omit_pos = i; + } + } + // Invalid input, e.g. due to invalid usage of RLE. + if (omit_pos < 0) return JXL_FAILURE("Invalid histogram."); + if (static_cast(omit_pos) + 1 < logcounts.size() && + logcounts[omit_pos + 1] == ANS_TAB_SIZE + 1) { + return JXL_FAILURE("Invalid histogram."); + } + int prev = 0; + int numsame = 0; + for (size_t i = 0; i < logcounts.size(); ++i) { + if (same[i]) { + // RLE sequence, let this loop output the same count for the next + // iterations. + numsame = same[i] - 1; + prev = i > 0 ? (*counts)[i - 1] : 0; + } + if (numsame > 0) { + (*counts)[i] = prev; + numsame--; + } else { + int code = logcounts[i]; + // omit_pos may not be negative at this point (checked before). + if (i == static_cast(omit_pos)) { + continue; + } else if (code == 0) { + continue; + } else if (code == 1) { + (*counts)[i] = 1; + } else { + int bitcount = GetPopulationCountPrecision(code - 1, shift); + (*counts)[i] = (1 << (code - 1)) + + (input->ReadBits(bitcount) << (code - 1 - bitcount)); + } + } + total_count += (*counts)[i]; + } + (*counts)[omit_pos] = (1 << precision_bits) - total_count; + if ((*counts)[omit_pos] <= 0) { + // The histogram we've read sums to more than total_count (including at + // least 1 for the omitted value). + return JXL_FAILURE("Invalid histogram count."); + } + } + return true; +} + +} // namespace + +Status DecodeANSCodes(const size_t num_histograms, + const size_t max_alphabet_size, BitReader* in, + ANSCode* result) { + result->degenerate_symbols.resize(num_histograms, -1); + if (result->use_prefix_code) { + JXL_ASSERT(max_alphabet_size <= 1 << PREFIX_MAX_BITS); + result->huffman_data.resize(num_histograms); + std::vector alphabet_sizes(num_histograms); + for (size_t c = 0; c < num_histograms; c++) { + alphabet_sizes[c] = DecodeVarLenUint16(in) + 1; + if (alphabet_sizes[c] > max_alphabet_size) { + return JXL_FAILURE("Alphabet size is too long: %u", alphabet_sizes[c]); + } + } + for (size_t c = 0; c < num_histograms; c++) { + if (alphabet_sizes[c] > 1) { + if (!result->huffman_data[c].ReadFromBitStream(alphabet_sizes[c], in)) { + if (!in->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for huffman code"); + } + return JXL_FAILURE( + "Invalid huffman tree number %zu, alphabet size %u", c, + alphabet_sizes[c]); + } + } else { + // 0-bit codes does not require extension tables. + result->huffman_data[c].table_.clear(); + result->huffman_data[c].table_.resize(1u << kHuffmanTableBits); + } + for (const auto& h : result->huffman_data[c].table_) { + if (h.bits <= kHuffmanTableBits) { + result->UpdateMaxNumBits(c, h.value); + } + } + } + } else { + JXL_ASSERT(max_alphabet_size <= ANS_MAX_ALPHABET_SIZE); + result->alias_tables = + AllocateArray(num_histograms * (1 << result->log_alpha_size) * + sizeof(AliasTable::Entry)); + AliasTable::Entry* alias_tables = + reinterpret_cast(result->alias_tables.get()); + for (size_t c = 0; c < num_histograms; ++c) { + std::vector counts; + if (!ReadHistogram(ANS_LOG_TAB_SIZE, &counts, in)) { + return JXL_FAILURE("Invalid histogram bitstream."); + } + if (counts.size() > max_alphabet_size) { + return JXL_FAILURE("Alphabet size is too long: %zu", counts.size()); + } + while (!counts.empty() && counts.back() == 0) { + counts.pop_back(); + } + for (size_t s = 0; s < counts.size(); s++) { + if (counts[s] != 0) { + result->UpdateMaxNumBits(c, s); + } + } + // InitAliasTable "fixes" empty counts to contain degenerate "0" symbol. + int degenerate_symbol = counts.empty() ? 0 : (counts.size() - 1); + for (int s = 0; s < degenerate_symbol; ++s) { + if (counts[s] != 0) { + degenerate_symbol = -1; + break; + } + } + result->degenerate_symbols[c] = degenerate_symbol; + InitAliasTable(counts, ANS_TAB_SIZE, result->log_alpha_size, + alias_tables + c * (1 << result->log_alpha_size)); + } + } + return true; +} +Status DecodeUintConfig(size_t log_alpha_size, HybridUintConfig* uint_config, + BitReader* br) { + br->Refill(); + size_t split_exponent = br->ReadBits(CeilLog2Nonzero(log_alpha_size + 1)); + size_t msb_in_token = 0, lsb_in_token = 0; + if (split_exponent != log_alpha_size) { + // otherwise, msb/lsb don't matter. + size_t nbits = CeilLog2Nonzero(split_exponent + 1); + msb_in_token = br->ReadBits(nbits); + if (msb_in_token > split_exponent) { + // This could be invalid here already and we need to check this before + // we use its value to read more bits. + return JXL_FAILURE("Invalid HybridUintConfig"); + } + nbits = CeilLog2Nonzero(split_exponent - msb_in_token + 1); + lsb_in_token = br->ReadBits(nbits); + } + if (lsb_in_token + msb_in_token > split_exponent) { + return JXL_FAILURE("Invalid HybridUintConfig"); + } + *uint_config = HybridUintConfig(split_exponent, msb_in_token, lsb_in_token); + return true; +} + +Status DecodeUintConfigs(size_t log_alpha_size, + std::vector* uint_config, + BitReader* br) { + // TODO(veluca): RLE? + for (size_t i = 0; i < uint_config->size(); i++) { + JXL_RETURN_IF_ERROR( + DecodeUintConfig(log_alpha_size, &(*uint_config)[i], br)); + } + return true; +} + +LZ77Params::LZ77Params() { Bundle::Init(this); } +Status LZ77Params::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &enabled)); + if (!visitor->Conditional(enabled)) return true; + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(224), Val(512), Val(4096), + BitsOffset(15, 8), 224, &min_symbol)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(3), Val(4), BitsOffset(2, 5), + BitsOffset(8, 9), 3, &min_length)); + return true; +} + +void ANSCode::UpdateMaxNumBits(size_t ctx, size_t symbol) { + HybridUintConfig* cfg = &uint_config[ctx]; + // LZ77 symbols use a different uint config. + if (lz77.enabled && lz77.nonserialized_distance_context != ctx && + symbol >= lz77.min_symbol) { + symbol -= lz77.min_symbol; + cfg = &lz77.length_uint_config; + } + size_t split_token = cfg->split_token; + size_t msb_in_token = cfg->msb_in_token; + size_t lsb_in_token = cfg->lsb_in_token; + size_t split_exponent = cfg->split_exponent; + if (symbol < split_token) { + max_num_bits = std::max(max_num_bits, split_exponent); + return; + } + uint32_t n_extra_bits = + split_exponent - (msb_in_token + lsb_in_token) + + ((symbol - split_token) >> (msb_in_token + lsb_in_token)); + size_t total_bits = msb_in_token + lsb_in_token + n_extra_bits + 1; + max_num_bits = std::max(max_num_bits, total_bits); +} + +Status DecodeHistograms(BitReader* br, size_t num_contexts, ANSCode* code, + std::vector* context_map, bool disallow_lz77) { + PROFILER_FUNC; + JXL_RETURN_IF_ERROR(Bundle::Read(br, &code->lz77)); + if (code->lz77.enabled) { + num_contexts++; + JXL_RETURN_IF_ERROR(DecodeUintConfig(/*log_alpha_size=*/8, + &code->lz77.length_uint_config, br)); + } + if (code->lz77.enabled && disallow_lz77) { + return JXL_FAILURE("Using LZ77 when explicitly disallowed"); + } + size_t num_histograms = 1; + context_map->resize(num_contexts); + if (num_contexts > 1) { + JXL_RETURN_IF_ERROR(DecodeContextMap(context_map, &num_histograms, br)); + } + code->lz77.nonserialized_distance_context = context_map->back(); + code->use_prefix_code = br->ReadFixedBits<1>(); + if (code->use_prefix_code) { + code->log_alpha_size = PREFIX_MAX_BITS; + } else { + code->log_alpha_size = br->ReadFixedBits<2>() + 5; + } + code->uint_config.resize(num_histograms); + JXL_RETURN_IF_ERROR( + DecodeUintConfigs(code->log_alpha_size, &code->uint_config, br)); + const size_t max_alphabet_size = 1 << code->log_alpha_size; + JXL_RETURN_IF_ERROR( + DecodeANSCodes(num_histograms, max_alphabet_size, br, code)); + // When using LZ77, flat codes might result in valid codestreams with + // histograms that potentially allow very large bit counts. + // TODO(veluca): in principle, a valid codestream might contain a histogram + // that could allow very large numbers of bits that is never used during ANS + // decoding. There's no benefit to doing that, though. + if (!code->lz77.enabled && code->max_num_bits > 32) { + // Just emit a warning as there are many opportunities for false positives. + JXL_WARNING("Histogram can represent numbers that are too large: %zu\n", + code->max_num_bits); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/dec_ans.h b/lib/jxl/dec_ans.h new file mode 100644 index 0000000..15273a8 --- /dev/null +++ b/lib/jxl/dec_ans.h @@ -0,0 +1,432 @@ +// 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_DEC_ANS_H_ +#define LIB_JXL_DEC_ANS_H_ + +// Library to decode the ANS population counts from the bit-stream and build a +// decoding table from them. + +#include +#include + +#include +#include + +#include "lib/jxl/ans_common.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_huffman.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +class ANSSymbolReader; + +// Experiments show that best performance is typically achieved for a +// split-exponent of 3 or 4. Trend seems to be that '4' is better +// for large-ish pictures, and '3' better for rather small-ish pictures. +// This is plausible - the more special symbols we have, the better +// statistics we need to get a benefit out of them. + +// Our hybrid-encoding scheme has dedicated tokens for the smallest +// (1 << split_exponents) numbers, and for the rest +// encodes (number of bits) + (msb_in_token sub-leading binary digits) + +// (lsb_in_token lowest binary digits) in the token, with the remaining bits +// then being encoded as data. +// +// Example with split_exponent = 4, msb_in_token = 2, lsb_in_token = 0. +// +// Numbers N in [0 .. 15]: +// These get represented as (token=N, bits=''). +// Numbers N >= 16: +// If n is such that 2**n <= N < 2**(n+1), +// and m = N - 2**n is the 'mantissa', +// these get represented as: +// (token=split_token + +// ((n - split_exponent) * 4) + +// (m >> (n - msb_in_token)), +// bits=m & (1 << (n - msb_in_token)) - 1) +// Specifically, we would get: +// N = 0 - 15: (token=N, nbits=0, bits='') +// N = 16 (10000): (token=16, nbits=2, bits='00') +// N = 17 (10001): (token=16, nbits=2, bits='01') +// N = 20 (10100): (token=17, nbits=2, bits='00') +// N = 24 (11000): (token=18, nbits=2, bits='00') +// N = 28 (11100): (token=19, nbits=2, bits='00') +// N = 32 (100000): (token=20, nbits=3, bits='000') +// N = 65535: (token=63, nbits=13, bits='1111111111111') +struct HybridUintConfig { + uint32_t split_exponent; + uint32_t split_token; + uint32_t msb_in_token; + uint32_t lsb_in_token; + JXL_INLINE void Encode(uint32_t value, uint32_t* JXL_RESTRICT token, + uint32_t* JXL_RESTRICT nbits, + uint32_t* JXL_RESTRICT bits) const { + if (value < split_token) { + *token = value; + *nbits = 0; + *bits = 0; + } else { + uint32_t n = FloorLog2Nonzero(value); + uint32_t m = value - (1 << n); + *token = split_token + + ((n - split_exponent) << (msb_in_token + lsb_in_token)) + + ((m >> (n - msb_in_token)) << lsb_in_token) + + (m & ((1 << lsb_in_token) - 1)); + *nbits = n - msb_in_token - lsb_in_token; + *bits = (value >> lsb_in_token) & ((1UL << *nbits) - 1); + } + } + + explicit HybridUintConfig(uint32_t split_exponent = 4, + uint32_t msb_in_token = 2, + uint32_t lsb_in_token = 0) + : split_exponent(split_exponent), + split_token(1 << split_exponent), + msb_in_token(msb_in_token), + lsb_in_token(lsb_in_token) { + JXL_DASSERT(split_exponent >= msb_in_token + lsb_in_token); + } +}; + +struct LZ77Params : public Fields { + LZ77Params(); + const char* Name() const override { return "LZ77Params"; } + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + bool enabled; + + // Symbols above min_symbol use a special hybrid uint encoding and + // represent a length, to be added to min_length. + uint32_t min_symbol; + uint32_t min_length; + + // Not serialized by VisitFields. + HybridUintConfig length_uint_config{0, 0, 0}; + + size_t nonserialized_distance_context; +}; + +static constexpr size_t kWindowSize = 1 << 20; +static constexpr size_t kNumSpecialDistances = 120; +// Table of special distance codes from WebP lossless. +static constexpr int8_t kSpecialDistances[kNumSpecialDistances][2] = { + {0, 1}, {1, 0}, {1, 1}, {-1, 1}, {0, 2}, {2, 0}, {1, 2}, {-1, 2}, + {2, 1}, {-2, 1}, {2, 2}, {-2, 2}, {0, 3}, {3, 0}, {1, 3}, {-1, 3}, + {3, 1}, {-3, 1}, {2, 3}, {-2, 3}, {3, 2}, {-3, 2}, {0, 4}, {4, 0}, + {1, 4}, {-1, 4}, {4, 1}, {-4, 1}, {3, 3}, {-3, 3}, {2, 4}, {-2, 4}, + {4, 2}, {-4, 2}, {0, 5}, {3, 4}, {-3, 4}, {4, 3}, {-4, 3}, {5, 0}, + {1, 5}, {-1, 5}, {5, 1}, {-5, 1}, {2, 5}, {-2, 5}, {5, 2}, {-5, 2}, + {4, 4}, {-4, 4}, {3, 5}, {-3, 5}, {5, 3}, {-5, 3}, {0, 6}, {6, 0}, + {1, 6}, {-1, 6}, {6, 1}, {-6, 1}, {2, 6}, {-2, 6}, {6, 2}, {-6, 2}, + {4, 5}, {-4, 5}, {5, 4}, {-5, 4}, {3, 6}, {-3, 6}, {6, 3}, {-6, 3}, + {0, 7}, {7, 0}, {1, 7}, {-1, 7}, {5, 5}, {-5, 5}, {7, 1}, {-7, 1}, + {4, 6}, {-4, 6}, {6, 4}, {-6, 4}, {2, 7}, {-2, 7}, {7, 2}, {-7, 2}, + {3, 7}, {-3, 7}, {7, 3}, {-7, 3}, {5, 6}, {-5, 6}, {6, 5}, {-6, 5}, + {8, 0}, {4, 7}, {-4, 7}, {7, 4}, {-7, 4}, {8, 1}, {8, 2}, {6, 6}, + {-6, 6}, {8, 3}, {5, 7}, {-5, 7}, {7, 5}, {-7, 5}, {8, 4}, {6, 7}, + {-6, 7}, {7, 6}, {-7, 6}, {8, 5}, {7, 7}, {-7, 7}, {8, 6}, {8, 7}}; + +struct ANSCode { + CacheAlignedUniquePtr alias_tables; + std::vector huffman_data; + std::vector uint_config; + std::vector degenerate_symbols; + bool use_prefix_code; + uint8_t log_alpha_size; // for ANS. + LZ77Params lz77; + // Maximum number of bits necessary to represent the result of a + // ReadHybridUint call done with this ANSCode. + size_t max_num_bits = 0; + void UpdateMaxNumBits(size_t ctx, size_t symbol); +}; + +class ANSSymbolReader { + public: + // Invalid symbol reader, to be overwritten. + ANSSymbolReader() = default; + ANSSymbolReader(const ANSCode* code, BitReader* JXL_RESTRICT br, + size_t distance_multiplier = 0) + : alias_tables_( + reinterpret_cast(code->alias_tables.get())), + huffman_data_(code->huffman_data.data()), + use_prefix_code_(code->use_prefix_code), + configs(code->uint_config.data()) { + if (!use_prefix_code_) { + state_ = static_cast(br->ReadFixedBits<32>()); + log_alpha_size_ = code->log_alpha_size; + log_entry_size_ = ANS_LOG_TAB_SIZE - code->log_alpha_size; + entry_size_minus_1_ = (1 << log_entry_size_) - 1; + } else { + state_ = (ANS_SIGNATURE << 16u); + } + if (!code->lz77.enabled) return; + // a std::vector incurs unacceptable decoding speed loss because of + // initialization. + lz77_window_storage_ = AllocateArray(kWindowSize * sizeof(uint32_t)); + lz77_window_ = reinterpret_cast(lz77_window_storage_.get()); + lz77_ctx_ = code->lz77.nonserialized_distance_context; + lz77_length_uint_ = code->lz77.length_uint_config; + lz77_threshold_ = code->lz77.min_symbol; + lz77_min_length_ = code->lz77.min_length; + num_special_distances_ = + distance_multiplier == 0 ? 0 : kNumSpecialDistances; + for (size_t i = 0; i < num_special_distances_; i++) { + int dist = kSpecialDistances[i][0]; + dist += static_cast(distance_multiplier) * kSpecialDistances[i][1]; + if (dist < 1) dist = 1; + special_distances_[i] = dist; + } + } + + JXL_INLINE size_t ReadSymbolANSWithoutRefill(const size_t histo_idx, + BitReader* JXL_RESTRICT br) { + const uint32_t res = state_ & (ANS_TAB_SIZE - 1u); + + const AliasTable::Entry* table = + &alias_tables_[histo_idx << log_alpha_size_]; + const AliasTable::Symbol symbol = + AliasTable::Lookup(table, res, log_entry_size_, entry_size_minus_1_); + state_ = symbol.freq * (state_ >> ANS_LOG_TAB_SIZE) + symbol.offset; + +#if 1 + // Branchless version is about equally fast on SKX. + const uint32_t new_state = + (state_ << 16u) | static_cast(br->PeekFixedBits<16>()); + const bool normalize = state_ < (1u << 16u); + state_ = normalize ? new_state : state_; + br->Consume(normalize ? 16 : 0); +#else + if (JXL_UNLIKELY(state_ < (1u << 16u))) { + state_ = (state_ << 16u) | br->PeekFixedBits<16>(); + br->Consume(16); + } +#endif + const uint32_t next_res = state_ & (ANS_TAB_SIZE - 1u); + AliasTable::Prefetch(table, next_res, log_entry_size_); + + return symbol.value; + } + + JXL_INLINE size_t ReadSymbolHuffWithoutRefill(const size_t histo_idx, + BitReader* JXL_RESTRICT br) { + return huffman_data_[histo_idx].ReadSymbol(br); + } + + JXL_INLINE size_t ReadSymbolWithoutRefill(const size_t histo_idx, + BitReader* JXL_RESTRICT br) { + // TODO(veluca): hoist if in hotter loops. + if (JXL_UNLIKELY(use_prefix_code_)) { + return ReadSymbolHuffWithoutRefill(histo_idx, br); + } + return ReadSymbolANSWithoutRefill(histo_idx, br); + } + + JXL_INLINE size_t ReadSymbol(const size_t histo_idx, + BitReader* JXL_RESTRICT br) { + br->Refill(); + return ReadSymbolWithoutRefill(histo_idx, br); + } + + bool CheckANSFinalState() { return state_ == (ANS_SIGNATURE << 16u); } + + template + static JXL_INLINE uint32_t ReadHybridUintConfig( + const HybridUintConfig& config, size_t token, BitReader* br) { + size_t split_token = config.split_token; + size_t msb_in_token = config.msb_in_token; + size_t lsb_in_token = config.lsb_in_token; + size_t split_exponent = config.split_exponent; + // Fast-track version of hybrid integer decoding. + if (token < split_token) return token; + uint32_t nbits = split_exponent - (msb_in_token + lsb_in_token) + + ((token - split_token) >> (msb_in_token + lsb_in_token)); + // Max amount of bits for ReadBits is 32 and max valid left shift is 29 + // bits. However, for speed no error is propagated here, instead limit the + // nbits size. If nbits > 29, the code stream is invalid, but no error is + // returned. + // Note that in most cases we will emit an error if the histogram allows + // representing numbers that would cause invalid shifts, but we need to + // keep this check as when LZ77 is enabled it might make sense to have an + // histogram that could in principle cause invalid shifts. + nbits &= 31u; + uint32_t low = token & ((1 << lsb_in_token) - 1); + token >>= lsb_in_token; + const size_t bits = br->PeekBits(nbits); + br->Consume(nbits); + size_t ret = (((((1 << msb_in_token) | (token & ((1 << msb_in_token) - 1))) + << nbits) | + bits) + << lsb_in_token) | + low; + // TODO(eustas): mark BitReader as unhealthy if nbits > 29 or ret does not + // fit uint32_t + return static_cast(ret); + } + + // Takes a *clustered* idx. + size_t ReadHybridUintClustered(size_t ctx, BitReader* JXL_RESTRICT br) { + if (JXL_UNLIKELY(num_to_copy_ > 0)) { + size_t ret = lz77_window_[(copy_pos_++) & kWindowMask]; + num_to_copy_--; + lz77_window_[(num_decoded_++) & kWindowMask] = ret; + return ret; + } + br->Refill(); // covers ReadSymbolWithoutRefill + PeekBits + size_t token = ReadSymbolWithoutRefill(ctx, br); + if (JXL_UNLIKELY(token >= lz77_threshold_)) { + num_to_copy_ = + ReadHybridUintConfig(lz77_length_uint_, token - lz77_threshold_, br) + + lz77_min_length_; + br->Refill(); // covers ReadSymbolWithoutRefill + PeekBits + // Distance code. + size_t token = ReadSymbolWithoutRefill(lz77_ctx_, br); + size_t distance = ReadHybridUintConfig(configs[lz77_ctx_], token, br); + if (JXL_LIKELY(distance < num_special_distances_)) { + distance = special_distances_[distance]; + } else { + distance = distance + 1 - num_special_distances_; + } + if (JXL_UNLIKELY(distance > num_decoded_)) { + distance = num_decoded_; + } + if (JXL_UNLIKELY(distance > kWindowSize)) { + distance = kWindowSize; + } + copy_pos_ = num_decoded_ - distance; + if (JXL_UNLIKELY(distance == 0)) { + JXL_DASSERT(lz77_window_ != nullptr); + // distance 0 -> num_decoded_ == copy_pos_ == 0 + size_t to_fill = std::min(num_to_copy_, kWindowSize); + memset(lz77_window_, 0, to_fill * sizeof(lz77_window_[0])); + } + // TODO(eustas): overflow; mark BitReader as unhealthy + if (num_to_copy_ < lz77_min_length_) return 0; + return ReadHybridUintClustered(ctx, br); // will trigger a copy. + } + size_t ret = ReadHybridUintConfig(configs[ctx], token, br); + if (lz77_window_) lz77_window_[(num_decoded_++) & kWindowMask] = ret; + return ret; + } + + JXL_INLINE size_t ReadHybridUint(size_t ctx, BitReader* JXL_RESTRICT br, + const std::vector& context_map) { + return ReadHybridUintClustered(context_map[ctx], br); + } + + // ctx is a *clustered* context! + // This function will modify the ANS state as if `count` symbols have been + // decoded. + bool IsSingleValueAndAdvance(size_t ctx, uint32_t* value, size_t count) { + // TODO(veluca): No optimization for Huffman mode yet. + if (use_prefix_code_) return false; + // TODO(eustas): propagate "degenerate_symbol" to simplify this method. + const uint32_t res = state_ & (ANS_TAB_SIZE - 1u); + const AliasTable::Entry* table = &alias_tables_[ctx << log_alpha_size_]; + AliasTable::Symbol symbol = + AliasTable::Lookup(table, res, log_entry_size_, entry_size_minus_1_); + if (symbol.freq != ANS_TAB_SIZE) return false; + if (configs[ctx].split_token <= symbol.value) return false; + if (symbol.value >= lz77_threshold_) return false; + *value = symbol.value; + if (lz77_window_) { + for (size_t i = 0; i < count; i++) { + lz77_window_[(num_decoded_++) & kWindowMask] = symbol.value; + } + } + return true; + } + + static constexpr size_t kMaxCheckpointInterval = 512; + struct Checkpoint { + uint32_t state; + uint32_t num_to_copy; + uint32_t copy_pos; + uint32_t num_decoded; + uint32_t lz77_window[kMaxCheckpointInterval]; + }; + void Save(Checkpoint* checkpoint) { + checkpoint->state = state_; + checkpoint->num_decoded = num_decoded_; + checkpoint->num_to_copy = num_to_copy_; + checkpoint->copy_pos = copy_pos_; + if (lz77_window_) { + size_t win_start = num_decoded_ & kWindowMask; + size_t win_end = (num_decoded_ + kMaxCheckpointInterval) & kWindowMask; + if (win_end > win_start) { + memcpy(checkpoint->lz77_window, lz77_window_ + win_start, + (win_end - win_start) * sizeof(*lz77_window_)); + } else { + memcpy(checkpoint->lz77_window, lz77_window_ + win_start, + (kWindowSize - win_start) * sizeof(*lz77_window_)); + memcpy(checkpoint->lz77_window + (kWindowSize - win_start), + lz77_window_, win_end * sizeof(*lz77_window_)); + } + } + } + void Restore(const Checkpoint& checkpoint) { + state_ = checkpoint.state; + JXL_DASSERT(num_decoded_ <= + checkpoint.num_decoded + kMaxCheckpointInterval); + num_decoded_ = checkpoint.num_decoded; + num_to_copy_ = checkpoint.num_to_copy; + copy_pos_ = checkpoint.copy_pos; + if (lz77_window_) { + size_t win_start = num_decoded_ & kWindowMask; + size_t win_end = (num_decoded_ + kMaxCheckpointInterval) & kWindowMask; + if (win_end > win_start) { + memcpy(lz77_window_ + win_start, checkpoint.lz77_window, + (win_end - win_start) * sizeof(*lz77_window_)); + } else { + memcpy(lz77_window_ + win_start, checkpoint.lz77_window, + (kWindowSize - win_start) * sizeof(*lz77_window_)); + memcpy(lz77_window_, checkpoint.lz77_window + (kWindowSize - win_start), + win_end * sizeof(*lz77_window_)); + } + } + } + + private: + const AliasTable::Entry* JXL_RESTRICT alias_tables_; // not owned + const HuffmanDecodingData* huffman_data_; + bool use_prefix_code_; + uint32_t state_ = ANS_SIGNATURE << 16u; + const HybridUintConfig* JXL_RESTRICT configs; + uint32_t log_alpha_size_; + uint32_t log_entry_size_; + uint32_t entry_size_minus_1_; + + // LZ77 structures and constants. + static constexpr size_t kWindowMask = kWindowSize - 1; + CacheAlignedUniquePtr lz77_window_storage_; + uint32_t* lz77_window_ = nullptr; + uint32_t num_decoded_ = 0; + uint32_t num_to_copy_ = 0; + uint32_t copy_pos_ = 0; + uint32_t lz77_ctx_ = 0; + uint32_t lz77_min_length_ = 0; + uint32_t lz77_threshold_ = 1 << 20; // bigger than any symbol. + HybridUintConfig lz77_length_uint_; + uint32_t special_distances_[kNumSpecialDistances]; + uint32_t num_special_distances_; +}; + +Status DecodeHistograms(BitReader* br, size_t num_contexts, ANSCode* code, + std::vector* context_map, + bool disallow_lz77 = false); + +// Exposed for tests. +Status DecodeUintConfigs(size_t log_alpha_size, + std::vector* uint_config, + BitReader* br); + +} // namespace jxl + +#endif // LIB_JXL_DEC_ANS_H_ diff --git a/lib/jxl/dec_bit_reader.h b/lib/jxl/dec_bit_reader.h new file mode 100644 index 0000000..df70284 --- /dev/null +++ b/lib/jxl/dec_bit_reader.h @@ -0,0 +1,354 @@ +// 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_DEC_BIT_READER_H_ +#define LIB_JXL_DEC_BIT_READER_H_ + +// Bounds-checked bit reader; 64-bit buffer with support for deferred refills +// and switching to reading byte-aligned words. + +#include +#include +#include // memcpy + +#ifdef __BMI2__ +#include +#endif + +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" + +namespace jxl { + +// Reads bits previously written to memory by BitWriter. Uses unaligned 8-byte +// little-endian loads. +class BitReader { + public: + static constexpr size_t kMaxBitsPerCall = 56; + + // Constructs an invalid BitReader, to be overwritten before usage. + BitReader() + : buf_(0), + bits_in_buf_(0), + next_byte_{nullptr}, + end_minus_8_{nullptr}, + first_byte_(nullptr) {} + BitReader(const BitReader&) = delete; + + // bytes need not be aligned nor padded! + template + explicit BitReader(const ArrayLike& bytes) + : buf_(0), + bits_in_buf_(0), + next_byte_(bytes.data()), + // Assumes first_byte_ >= 8. + end_minus_8_(bytes.data() - 8 + bytes.size()), + first_byte_(bytes.data()) { + Refill(); + } + ~BitReader() { + // Close() must be called before destroying an initialized bit reader. + // Invalid bit readers will have a nullptr in first_byte_. + JXL_ASSERT(close_called_ || !first_byte_); + } + + // Move operator needs to invalidate the other BitReader such that it is + // irrelevant if we call Close() on it or not. + BitReader& operator=(BitReader&& other) noexcept { + // Ensure the current instance was already closed, before we overwrite it + // with other. + JXL_ASSERT(close_called_ || !first_byte_); + + JXL_DASSERT(!other.close_called_); + buf_ = other.buf_; + bits_in_buf_ = other.bits_in_buf_; + next_byte_ = other.next_byte_; + end_minus_8_ = other.end_minus_8_; + first_byte_ = other.first_byte_; + overread_bytes_ = other.overread_bytes_; + close_called_ = other.close_called_; + + other.first_byte_ = nullptr; + other.next_byte_ = nullptr; + return *this; + } + BitReader& operator=(const BitReader& other) = delete; + + // For time-critical reads, refills can be shared by multiple reads. + // Based on variant 4 (plus bounds-checking), see + // fgiesen.wordpress.com/2018/02/20/reading-bits-in-far-too-many-ways-part-2/ + JXL_INLINE void Refill() { + if (JXL_UNLIKELY(next_byte_ > end_minus_8_)) { + BoundsCheckedRefill(); + } else { + // It's safe to load 64 bits; insert valid (possibly nonzero) bits above + // bits_in_buf_. The shift requires bits_in_buf_ < 64. + buf_ |= LoadLE64(next_byte_) << bits_in_buf_; + + // Advance by bytes fully absorbed into the buffer. + next_byte_ += (63 - bits_in_buf_) >> 3; + + // We absorbed a multiple of 8 bits, so the lower 3 bits of bits_in_buf_ + // must remain unchanged, otherwise the next refill's shifted bits will + // not align with buf_. Set the three upper bits so the result >= 56. + bits_in_buf_ |= 56; + JXL_DASSERT(56 <= bits_in_buf_ && bits_in_buf_ < 64); + } + } + + // Returns the bits that would be returned by Read without calling Advance(). + // It is legal to PEEK at more bits than present in the bitstream (required + // by Huffman), and those bits will be zero. + template + JXL_INLINE uint64_t PeekFixedBits() const { + static_assert(N <= kMaxBitsPerCall, "Reading too many bits in one call."); + JXL_DASSERT(!close_called_); + return buf_ & ((1ULL << N) - 1); + } + + JXL_INLINE uint64_t PeekBits(size_t nbits) const { + JXL_DASSERT(nbits <= kMaxBitsPerCall); + JXL_DASSERT(!close_called_); + + // Slightly faster but requires BMI2. It is infeasible to make the many + // callers reside between begin/end_target, especially because only the + // callers in dec_ans are time-critical. Therefore only enabled if the + // entire binary is compiled for (and thus requires) BMI2. +#if defined(__BMI2__) && defined(__x86_64__) + return _bzhi_u64(buf_, nbits); +#else + const uint64_t mask = (1ULL << nbits) - 1; + return buf_ & mask; +#endif + } + + // Removes bits from the buffer. Need not match the previous Peek size, but + // the buffer must contain at least num_bits (this prevents consuming more + // than the total number of bits). + JXL_INLINE void Consume(size_t num_bits) { + JXL_DASSERT(!close_called_); + JXL_DASSERT(bits_in_buf_ >= num_bits); +#ifdef JXL_CRASH_ON_ERROR + // When JXL_CRASH_ON_ERROR is defined, it is a fatal error to read more bits + // than available in the stream. A non-zero overread_bytes_ implies that + // next_byte_ is already at the end of the stream, so we don't need to + // check that. + JXL_ASSERT(bits_in_buf_ >= num_bits + overread_bytes_ * kBitsPerByte); +#endif + bits_in_buf_ -= num_bits; + buf_ >>= num_bits; + } + + JXL_INLINE uint64_t ReadBits(size_t nbits) { + JXL_DASSERT(!close_called_); + Refill(); + const uint64_t bits = PeekBits(nbits); + Consume(nbits); + return bits; + } + + template + JXL_INLINE uint64_t ReadFixedBits() { + JXL_DASSERT(!close_called_); + Refill(); + const uint64_t bits = PeekFixedBits(); + Consume(N); + return bits; + } + + // Equivalent to calling ReadFixedBits(1) `skip` times, but much faster. + // `skip` is typically large. + void SkipBits(size_t skip) { + JXL_DASSERT(!close_called_); + // Buffer is large enough - don't zero buf_ below. + if (JXL_UNLIKELY(skip <= bits_in_buf_)) { + Consume(skip); + return; + } + + // First deduct what we can satisfy from the buffer + skip -= bits_in_buf_; + bits_in_buf_ = 0; + // Not enough to call Advance - that may leave some bits in the buffer + // which were previously ABOVE bits_in_buf. + buf_ = 0; + + // Skip whole bytes + const size_t whole_bytes = skip / kBitsPerByte; + skip %= kBitsPerByte; + if (JXL_UNLIKELY(whole_bytes > + static_cast(end_minus_8_ + 8 - next_byte_))) { + // This is already an overflow condition (skipping past the end of the bit + // stream). However if we increase next_byte_ too much we risk overflowing + // that value and potentially making it valid again (next_byte_ < end). + // This will set next_byte_ to the end of the stream and still consume + // some bits in overread_bytes_, however the TotalBitsConsumed() will be + // incorrect (still larger than the TotalBytes()). + next_byte_ = end_minus_8_ + 8; + skip += kBitsPerByte; + } else { + next_byte_ += whole_bytes; + } + + Refill(); + Consume(skip); + } + + size_t TotalBitsConsumed() const { + const size_t bytes_read = static_cast(next_byte_ - first_byte_); + return (bytes_read + overread_bytes_) * kBitsPerByte - bits_in_buf_; + } + + Status JumpToByteBoundary() { + const size_t remainder = TotalBitsConsumed() % kBitsPerByte; + if (remainder == 0) return true; + if (JXL_UNLIKELY(ReadBits(kBitsPerByte - remainder) != 0)) { + return JXL_FAILURE("Non-zero padding bits"); + } + return true; + } + + // For interoperability with other bitreaders (for resuming at + // non-byte-aligned positions). + const uint8_t* FirstByte() const { return first_byte_; } + size_t TotalBytes() const { + return static_cast(end_minus_8_ + 8 - first_byte_); + } + + // Returns span of the remaining (unconsumed) bytes, e.g. for passing to + // external decoders such as Brotli. + Span GetSpan() const { + JXL_DASSERT(first_byte_ != nullptr); + JXL_ASSERT(TotalBitsConsumed() % kBitsPerByte == 0); + const size_t offset = TotalBitsConsumed() / kBitsPerByte; // no remainder + JXL_ASSERT(offset <= TotalBytes()); + return Span(first_byte_ + offset, TotalBytes() - offset); + } + + // Returns whether all the bits read so far have been within the input bounds. + // When reading past the EOF, the Read*() and Consume() functions return zeros + // but flag a failure when calling Close() without checking this function. + Status AllReadsWithinBounds() { + // Mark up to which point the user checked the out of bounds condition. If + // the user handles the condition at higher level (e.g. fetch more bytes + // from network, return a custom JXL_FAILURE, ...), Close() should not + // output a debug error (which would break tests with JXL_CRASH_ON_ERROR + // even when legitimately handling the situation at higher level). This is + // used by Bundle::CanRead. + checked_out_of_bounds_bits_ = TotalBitsConsumed(); + if (TotalBitsConsumed() > TotalBytes() * kBitsPerByte) { + return false; + } + return true; + } + + // Close the bit reader and return whether all the previous reads were + // successful. Close must be called once. + Status Close() { + JXL_DASSERT(!close_called_); + close_called_ = true; + if (!first_byte_) return true; + if (TotalBitsConsumed() > checked_out_of_bounds_bits_ && + TotalBitsConsumed() > TotalBytes() * kBitsPerByte) { + return JXL_FAILURE("Read more bits than available in the bit_reader"); + } + return true; + } + + private: + // Separate function avoids inlining this relatively cold code into callers. + JXL_NOINLINE void BoundsCheckedRefill() { + PROFILER_FUNC; + const uint8_t* end = end_minus_8_ + 8; + + // Read whole bytes until we have [56, 64) bits (same as LoadLE64) + for (; bits_in_buf_ < 64 - kBitsPerByte; bits_in_buf_ += kBitsPerByte) { + if (next_byte_ >= end) break; + buf_ |= static_cast(*next_byte_++) << bits_in_buf_; + } + JXL_DASSERT(bits_in_buf_ < 64); + + // Add extra bytes as 0 at the end of the stream in the bit_buffer_. If + // these bits are read, Close() will return a failure. + size_t extra_bytes = (63 - bits_in_buf_) / kBitsPerByte; + overread_bytes_ += extra_bytes; + bits_in_buf_ += extra_bytes * kBitsPerByte; + + JXL_DASSERT(bits_in_buf_ < 64); + JXL_DASSERT(bits_in_buf_ >= 56); + } + + JXL_NOINLINE uint32_t BoundsCheckedReadByteAlignedWord() { + if (next_byte_ + 1 < end_minus_8_ + 8) { + uint32_t ret = LoadLE16(next_byte_); + next_byte_ += 2; + return ret; + } + overread_bytes_ += 2; + return 0; + } + + uint64_t buf_; + size_t bits_in_buf_; // [0, 64) + const uint8_t* JXL_RESTRICT next_byte_; + const uint8_t* end_minus_8_; // for refill bounds check + const uint8_t* first_byte_; // for GetSpan + + // Number of bytes past the end that were loaded into the buf_. These bytes + // are not read from memory, but instead assumed 0. It is an error (likely due + // to an invalid stream) to Consume() more bits than specified in the range + // passed to the constructor. + uint64_t overread_bytes_{0}; + bool close_called_{false}; + + uint64_t checked_out_of_bounds_bits_{0}; +}; + +// Closes a BitReader when the BitReaderScopedCloser goes out of scope. When +// closing the bit reader, if the status result was failure it sets this failure +// to the passed variable pointer. Typical usage. +// +// Status ret = true; +// { +// BitReader reader(...); +// BitReaderScopedCloser reader_closer(&reader, &ret); +// +// // ... code that can return errors here ... +// } +// // ... more code that doesn't use the BitReader. +// return ret; + +class BitReaderScopedCloser { + public: + BitReaderScopedCloser(BitReader* reader, Status* status) + : reader_(reader), status_(status) { + JXL_DASSERT(reader_ != nullptr); + JXL_DASSERT(status_ != nullptr); + } + ~BitReaderScopedCloser() { + if (reader_ != nullptr) { + Status close_ret = reader_->Close(); + if (!close_ret) *status_ = close_ret; + } + } + void CloseAndSuppressError() { + JXL_ASSERT(reader_ != nullptr); + (void)reader_->Close(); + reader_ = nullptr; + } + BitReaderScopedCloser(const BitReaderScopedCloser&) = delete; + + private: + BitReader* reader_; + Status* status_; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_BIT_READER_H_ diff --git a/lib/jxl/dec_cache.cc b/lib/jxl/dec_cache.cc new file mode 100644 index 0000000..0bd2a0a --- /dev/null +++ b/lib/jxl/dec_cache.cc @@ -0,0 +1,177 @@ +// 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/dec_cache.h" + +#include "lib/jxl/dec_reconstruct.h" + +namespace jxl { + +void PassesDecoderState::EnsureBordersStorage() { + if (!EagerFinalizeImageRect()) return; + size_t padding = FinalizeRectPadding(); + size_t bordery = 2 * padding; + size_t borderx = padding + group_border_assigner.PaddingX(padding); + Rect horizontal = Rect(0, 0, shared->frame_dim.xsize_padded, + bordery * shared->frame_dim.ysize_groups * 2); + if (!SameSize(horizontal, borders_horizontal)) { + borders_horizontal = Image3F(horizontal.xsize(), horizontal.ysize()); + } + Rect vertical = Rect(0, 0, borderx * shared->frame_dim.xsize_groups * 2, + shared->frame_dim.ysize_padded); + if (!SameSize(vertical, borders_vertical)) { + borders_vertical = Image3F(vertical.xsize(), vertical.ysize()); + } +} + +namespace { +void SaveBorders(const Rect& block_rect, size_t hshift, size_t vshift, + size_t padding, const ImageF& plane_in, + ImageF* border_storage_h, ImageF* border_storage_v) { + constexpr size_t kGroupDataXBorder = PassesDecoderState::kGroupDataXBorder; + constexpr size_t kGroupDataYBorder = PassesDecoderState::kGroupDataYBorder; + size_t x0 = DivCeil(block_rect.x0() * kBlockDim, 1 << hshift); + size_t x1 = + DivCeil((block_rect.x0() + block_rect.xsize()) * kBlockDim, 1 << hshift); + size_t y0 = DivCeil(block_rect.y0() * kBlockDim, 1 << vshift); + size_t y1 = + DivCeil((block_rect.y0() + block_rect.ysize()) * kBlockDim, 1 << vshift); + size_t gy = block_rect.y0() / kGroupDimInBlocks; + size_t gx = block_rect.x0() / kGroupDimInBlocks; + // TODO(veluca): this is too much with chroma upsampling. It's just + // inefficient though. + size_t borderx = GroupBorderAssigner::PaddingX(padding); + size_t bordery = padding; + size_t borderx_write = padding + borderx; + size_t bordery_write = padding + bordery; + CopyImageTo( + Rect(kGroupDataXBorder, kGroupDataYBorder, x1 - x0, bordery_write), + plane_in, Rect(x0, (gy * 2) * bordery_write, x1 - x0, bordery_write), + border_storage_h); + CopyImageTo( + Rect(kGroupDataXBorder, kGroupDataYBorder + y1 - y0 - bordery_write, + x1 - x0, bordery_write), + plane_in, Rect(x0, (gy * 2 + 1) * bordery_write, x1 - x0, bordery_write), + border_storage_h); + CopyImageTo( + Rect(kGroupDataXBorder, kGroupDataYBorder, borderx_write, y1 - y0), + plane_in, Rect((gx * 2) * borderx_write, y0, borderx_write, y1 - y0), + border_storage_v); + CopyImageTo(Rect(kGroupDataXBorder + x1 - x0 - borderx_write, + kGroupDataYBorder, borderx_write, y1 - y0), + plane_in, + Rect((gx * 2 + 1) * borderx_write, y0, borderx_write, y1 - y0), + border_storage_v); +} + +void LoadBorders(const Rect& block_rect, size_t hshift, size_t vshift, + const FrameDimensions& frame_dim, size_t padding, + const ImageF& border_storage_h, const ImageF& border_storage_v, + const Rect& r, ImageF* plane_out) { + constexpr size_t kGroupDataXBorder = PassesDecoderState::kGroupDataXBorder; + constexpr size_t kGroupDataYBorder = PassesDecoderState::kGroupDataYBorder; + size_t x0 = DivCeil(block_rect.x0() * kBlockDim, 1 << hshift); + size_t x1 = + DivCeil((block_rect.x0() + block_rect.xsize()) * kBlockDim, 1 << hshift); + size_t y0 = DivCeil(block_rect.y0() * kBlockDim, 1 << vshift); + size_t y1 = + DivCeil((block_rect.y0() + block_rect.ysize()) * kBlockDim, 1 << vshift); + size_t gy = block_rect.y0() / kGroupDimInBlocks; + size_t gx = block_rect.x0() / kGroupDimInBlocks; + size_t borderx = GroupBorderAssigner::PaddingX(padding); + size_t bordery = padding; + size_t borderx_write = padding + borderx; + size_t bordery_write = padding + bordery; + // Limits of the area to copy from, in image coordinates. + JXL_DASSERT(r.x0() == 0 || r.x0() >= borderx); + size_t x0src = DivCeil(r.x0() == 0 ? r.x0() : r.x0() - borderx, 1 << hshift); + // r may be such that r.x1 (namely x0() + xsize()) is within borderx of the + // right side of the image, so we use min() here. + size_t x1src = + DivCeil(std::min(r.x0() + r.xsize() + borderx, frame_dim.xsize_padded), + 1 << hshift); + JXL_DASSERT(r.y0() == 0 || r.y0() >= bordery); + size_t y0src = DivCeil(r.y0() == 0 ? r.y0() : r.y0() - bordery, 1 << vshift); + // Similar to x1, y1 might be closer than bordery from the bottom. + size_t y1src = + DivCeil(std::min(r.y0() + r.ysize() + bordery, frame_dim.ysize_padded), + 1 << vshift); + // Copy other groups' borders from the border storage. + if (y0src < y0) { + JXL_DASSERT(gy > 0); + CopyImageTo( + Rect(x0src, (gy * 2 - 1) * bordery_write, x1src - x0src, bordery_write), + border_storage_h, + Rect(kGroupDataXBorder + x0src - x0, kGroupDataYBorder - bordery_write, + x1src - x0src, bordery_write), + plane_out); + } + if (y1src > y1) { + // When copying the bottom border we must not be on the bottom groups. + JXL_DASSERT(gy + 1 < frame_dim.ysize_groups); + CopyImageTo( + Rect(x0src, (gy * 2 + 2) * bordery_write, x1src - x0src, bordery_write), + border_storage_h, + Rect(kGroupDataXBorder + x0src - x0, kGroupDataYBorder + y1 - y0, + x1src - x0src, bordery_write), + plane_out); + } + if (x0src < x0) { + JXL_DASSERT(gx > 0); + CopyImageTo( + Rect((gx * 2 - 1) * borderx_write, y0src, borderx_write, y1src - y0src), + border_storage_v, + Rect(kGroupDataXBorder - borderx_write, kGroupDataYBorder + y0src - y0, + borderx_write, y1src - y0src), + plane_out); + } + if (x1src > x1) { + // When copying the right border we must not be on the rightmost groups. + JXL_DASSERT(gx + 1 < frame_dim.xsize_groups); + CopyImageTo( + Rect((gx * 2 + 2) * borderx_write, y0src, borderx_write, y1src - y0src), + border_storage_v, + Rect(kGroupDataXBorder + x1 - x0, kGroupDataYBorder + y0src - y0, + borderx_write, y1src - y0src), + plane_out); + } +} + +} // namespace + +Status PassesDecoderState::FinalizeGroup(size_t group_idx, size_t thread, + Image3F* pixel_data, + ImageBundle* output) { + // Copy the group borders to the border storage. + const Rect block_rect = shared->BlockGroupRect(group_idx); + const YCbCrChromaSubsampling& cs = shared->frame_header.chroma_subsampling; + size_t padding = FinalizeRectPadding(); + for (size_t c = 0; c < 3; c++) { + SaveBorders(block_rect, cs.HShift(c), cs.VShift(c), padding, + pixel_data->Plane(c), &borders_horizontal.Plane(c), + &borders_vertical.Plane(c)); + } + Rect fir_rects[GroupBorderAssigner::kMaxToFinalize]; + size_t num_fir_rects = 0; + group_border_assigner.GroupDone(group_idx, FinalizeRectPadding(), fir_rects, + &num_fir_rects); + for (size_t i = 0; i < num_fir_rects; i++) { + const Rect& r = fir_rects[i]; + for (size_t c = 0; c < 3; c++) { + LoadBorders(block_rect, cs.HShift(c), cs.VShift(c), shared->frame_dim, + padding, borders_horizontal.Plane(c), + borders_vertical.Plane(c), r, &pixel_data->Plane(c)); + } + Rect pixel_data_rect( + kGroupDataXBorder + r.x0() - block_rect.x0() * kBlockDim, + kGroupDataYBorder + r.y0() - block_rect.y0() * kBlockDim, r.xsize(), + r.ysize()); + JXL_RETURN_IF_ERROR(FinalizeImageRect(pixel_data, pixel_data_rect, {}, this, + thread, output, r)); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/dec_cache.h b/lib/jxl/dec_cache.h new file mode 100644 index 0000000..85322aa --- /dev/null +++ b/lib/jxl/dec_cache.h @@ -0,0 +1,411 @@ +// 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_DEC_CACHE_H_ +#define LIB_JXL_DEC_CACHE_H_ + +#include + +#include // HWY_ALIGN_MAX + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/common.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/dec_group_border.h" +#include "lib/jxl/dec_noise.h" +#include "lib/jxl/dec_upsample.h" +#include "lib/jxl/filters.h" +#include "lib/jxl/image.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { + +// Per-frame decoder state. All the images here should be accessed through a +// group rect (either with block units or pixel units). +struct PassesDecoderState { + PassesSharedState shared_storage; + // Allows avoiding copies for encoder loop. + const PassesSharedState* JXL_RESTRICT shared = &shared_storage; + + // Upsamplers for all the possible upsampling factors (2 to 8). + Upsampler upsamplers[3]; + + // Storage for RNG output for noise synthesis. + Image3F noise; + + // Storage for pre-color-transform output for displayed + // save_before_color_transform frames. + Image3F pre_color_transform_frame; + // Non-empty (contains originals) if extra-channels were cropped. + std::vector pre_color_transform_ec; + + // For ANS decoding. + std::vector code; + std::vector> context_map; + + // Multiplier to be applied to the quant matrices of the x channel. + float x_dm_multiplier; + float b_dm_multiplier; + + // Decoded image. + Image3F decoded; + std::vector extra_channels; + + // Borders between groups. Only allocated if `decoded` is *not* allocated. + // We also store the extremal borders for simplicity. Horizontal borders are + // stored in an image as wide as the main frame, in top-to-bottom order (top + // border of a group first, followed by the bottom border, followed by top + // border of the next group). Vertical borders are similarly stored. + Image3F borders_horizontal; + Image3F borders_vertical; + + // RGB8 output buffer. If not nullptr, image data will be written to this + // buffer instead of being written to the output ImageBundle. The image data + // is assumed to have the stride given by `rgb_stride`, hence row `i` starts + // at position `i * rgb_stride`. + uint8_t* rgb_output; + size_t rgb_stride = 0; + + // Whether to use int16 float-XYB-to-uint8-srgb conversion. + bool fast_xyb_srgb8_conversion; + + // If true, rgb_output or callback output is RGBA using 4 instead of 3 bytes + // per pixel. + bool rgb_output_is_rgba; + + // Callback for line-by-line output. + std::function pixel_callback; + // Buffer of upsampling * kApplyImageFeaturesTileDim ones. + std::vector opaque_alpha; + // One row per thread + std::vector> pixel_callback_rows; + + // Seed for noise, to have different noise per-frame. + size_t noise_seed = 0; + + // Keep track of the transform types used. + std::atomic used_acs{0}; + + // Storage for coefficients if in "accumulate" mode. + std::unique_ptr coefficients = make_unique>(0, 0); + + // Filter application pipeline used by ApplyImageFeatures. One entry is needed + // per thread. + std::vector filter_pipelines; + + // Input weights used by the filters. These are shared from multiple threads + // but are read-only for the filter application. + FilterWeights filter_weights; + + // Manages the status of borders. + GroupBorderAssigner group_border_assigner; + + // TODO(veluca): this should eventually become "iff no global modular + // transform was applied". + bool EagerFinalizeImageRect() const { + return shared->frame_header.encoding == FrameEncoding::kVarDCT && + shared->frame_header.nonserialized_metadata->m.extra_channel_info + .empty(); + } + + // Amount of padding that will be accessed, in all directions, outside a rect + // during a call to FinalizeImageRect(). + size_t FinalizeRectPadding() const { + size_t padding = shared->frame_header.loop_filter.Padding(); + padding += shared->frame_header.upsampling == 1 ? 0 : 2; + JXL_DASSERT(padding <= kMaxFinalizeRectPadding); + for (auto ups : shared->frame_header.extra_channel_upsampling) { + if (ups > 1) { + padding = std::max(padding, size_t{2}); + } + } + // We could be making a distinction between h and w padding here, but it is + // likely not worth it. + if (!shared->frame_header.chroma_subsampling.Is444()) { + padding = std::max(padding / 2 + 1, padding); + } + return padding; + } + + // Storage for intermediate data during FinalizeRect steps. + // TODO(veluca): these buffers are larger than strictly necessary. + std::vector filter_input_storage; + std::vector padded_upsampling_input_storage; + std::vector upsampling_input_storage; + size_t upsampler_arena_size = 0; + std::vector> upsampler_storage; + // We keep four arrays, one per upsampling level, to reduce memory usage in + // the common case of no upsampling. + std::vector output_pixel_data_storage[4] = {}; + std::vector ec_temp_images; + std::vector ycbcr_temp_images; + std::vector ycbcr_out_images; + + // Buffer for decoded pixel data for a group. + std::vector group_data; + static constexpr size_t kGroupDataYBorder = kMaxFinalizeRectPadding * 2; + static constexpr size_t kGroupDataXBorder = + RoundUpToBlockDim(kMaxFinalizeRectPadding) * 2 + kBlockDim; + + void EnsureStorage(size_t num_threads) { + // We need one filter_storage per thread, ensure we have at least that many. + if (shared->frame_header.loop_filter.epf_iters != 0 || + shared->frame_header.loop_filter.gab) { + if (filter_pipelines.size() < num_threads) { + filter_pipelines.resize(num_threads); + } + } + // We allocate filter_input_storage unconditionally to ensure that the image + // is allocated if we need it for DC upsampling. + for (size_t _ = filter_input_storage.size(); _ < num_threads; _++) { + // Extra padding along the x dimension to ensure memory accesses don't + // load out-of-bounds pixels. + filter_input_storage.emplace_back( + kApplyImageFeaturesTileDim + 2 * kGroupDataXBorder, + kApplyImageFeaturesTileDim + 2 * kGroupDataYBorder); + } + if (shared->frame_header.upsampling != 1) { + for (size_t _ = upsampling_input_storage.size(); _ < num_threads; _++) { + // At this point, we only need up to 2 pixels of border per side for + // upsampling, but we add an extra border for aligned access. + upsampling_input_storage.emplace_back( + kApplyImageFeaturesTileDim + 2 * kBlockDim, + kApplyImageFeaturesTileDim + 4); + padded_upsampling_input_storage.emplace_back( + kApplyImageFeaturesTileDim + 2 * kBlockDim, + kApplyImageFeaturesTileDim + 4); + } + } + const size_t arena_size = Upsampler::GetArenaSize( + kApplyImageFeaturesTileDim * shared->frame_header.upsampling); + if (arena_size > upsampler_arena_size) upsampler_storage.clear(); + for (size_t _ = upsampler_storage.size(); _ < num_threads; _++) { + upsampler_storage.emplace_back(hwy::AllocateAligned(arena_size)); + } + upsampler_arena_size = arena_size; + for (size_t _ = group_data.size(); _ < num_threads; _++) { + group_data.emplace_back(kGroupDim + 2 * kGroupDataXBorder, + kGroupDim + 2 * kGroupDataYBorder); +#if MEMORY_SANITIZER + // Avoid errors due to loading vectors on the outermost padding. + FillImage(msan::kSanitizerSentinel, &group_data.back()); +#endif + } + if (!shared->frame_header.chroma_subsampling.Is444()) { + for (size_t _ = ycbcr_temp_images.size(); _ < num_threads; _++) { + ycbcr_temp_images.emplace_back(kGroupDim + 2 * kGroupDataXBorder, + kGroupDim + 2 * kGroupDataYBorder); + ycbcr_out_images.emplace_back(kGroupDim + 2 * kGroupDataXBorder, + kGroupDim + 2 * kGroupDataYBorder); + } + } + if (rgb_output || pixel_callback) { + size_t log2_upsampling = CeilLog2Nonzero(shared->frame_header.upsampling); + for (size_t _ = output_pixel_data_storage[log2_upsampling].size(); + _ < num_threads; _++) { + output_pixel_data_storage[log2_upsampling].emplace_back( + kApplyImageFeaturesTileDim << log2_upsampling, + kApplyImageFeaturesTileDim << log2_upsampling); + } + opaque_alpha.resize( + kApplyImageFeaturesTileDim * shared->frame_header.upsampling, 1.0f); + if (pixel_callback) { + pixel_callback_rows.resize(num_threads); + for (size_t i = 0; i < pixel_callback_rows.size(); ++i) { + pixel_callback_rows[i].resize(kApplyImageFeaturesTileDim * + shared->frame_header.upsampling * + (rgb_output_is_rgba ? 4 : 3)); + } + } + } + if (shared->metadata->m.num_extra_channels * num_threads > + ec_temp_images.size()) { + ec_temp_images.resize(shared->metadata->m.num_extra_channels * + num_threads); + } + for (size_t i = 0; i < shared->metadata->m.num_extra_channels; i++) { + if (shared->frame_header.extra_channel_upsampling[i] == 1) continue; + // We need up to 2 pixels of padding on each side. On the x axis, we round + // up padding so that 0 starts at a multiple of kBlockDim. + size_t xs = kApplyImageFeaturesTileDim * shared->frame_header.upsampling / + shared->frame_header.extra_channel_upsampling[i] + + 2 * kBlockDim; + size_t ys = kApplyImageFeaturesTileDim * shared->frame_header.upsampling / + shared->frame_header.extra_channel_upsampling[i] + + 4; + for (size_t t = 0; t < num_threads; t++) { + auto& eti = + ec_temp_images[t * shared->metadata->m.num_extra_channels + i]; + if (eti.xsize() < xs || eti.ysize() < ys) { + eti = ImageF(xs, ys); + } + } + } + } + + // Information for colour conversions. + OutputEncodingInfo output_encoding_info; + + // Initializes decoder-specific structures using information from *shared. + Status Init() { + x_dm_multiplier = + std::pow(1 / (1.25f), shared->frame_header.x_qm_scale - 2.0f); + b_dm_multiplier = + 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; + + group_border_assigner.Init(shared->frame_dim); + const LoopFilter& lf = shared->frame_header.loop_filter; + JXL_RETURN_IF_ERROR(filter_weights.Init(lf, shared->frame_dim)); + for (auto& fp : filter_pipelines) { + // De-initialize FilterPipelines. + fp.num_filters = 0; + } + for (size_t i = 0; i < 3; i++) { + upsamplers[i].Init(2 << i, shared->metadata->transform_data); + } + return true; + } + + // Initialize the decoder state after all of DC is decoded. + void InitForAC(ThreadPool* pool) { + shared_storage.coeff_order_size = 0; + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + if (((1 << o) & used_acs) == 0) continue; + uint8_t ord = kStrategyOrder[o]; + shared_storage.coeff_order_size = + std::max(kCoeffOrderOffset[3 * (ord + 1)] * kDCTBlockSize, + shared_storage.coeff_order_size); + } + size_t sz = shared_storage.frame_header.passes.num_passes * + shared_storage.coeff_order_size; + if (sz > shared_storage.coeff_orders.size()) { + shared_storage.coeff_orders.resize(sz); + } + if (shared->frame_header.flags & FrameHeader::kNoise) { + noise = Image3F(shared->frame_dim.xsize_upsampled_padded, + shared->frame_dim.ysize_upsampled_padded); + size_t num_x_groups = DivCeil(noise.xsize(), kGroupDim); + size_t num_y_groups = DivCeil(noise.ysize(), kGroupDim); + PROFILER_ZONE("GenerateNoise"); + auto generate_noise = [&](int group_index, int _) { + size_t gx = group_index % num_x_groups; + size_t gy = group_index / num_x_groups; + Rect rect(gx * kGroupDim, gy * kGroupDim, kGroupDim, kGroupDim, + noise.xsize(), noise.ysize()); + RandomImage3(noise_seed + group_index, rect, &noise); + }; + RunOnPool(pool, 0, num_x_groups * num_y_groups, ThreadPool::SkipInit(), + generate_noise, "Generate noise"); + { + PROFILER_ZONE("High pass noise"); + // 4 * (1 - box kernel) + WeightsSymmetric5 weights{{HWY_REP4(-3.84)}, {HWY_REP4(0.16)}, + {HWY_REP4(0.16)}, {HWY_REP4(0.16)}, + {HWY_REP4(0.16)}, {HWY_REP4(0.16)}}; + // TODO(veluca): avoid copy. + // TODO(veluca): avoid having a full copy of the image in main memory. + ImageF noise_tmp(noise.xsize(), noise.ysize()); + for (size_t c = 0; c < 3; c++) { + Symmetric5(noise.Plane(c), Rect(noise), weights, pool, &noise_tmp); + std::swap(noise.Plane(c), noise_tmp); + } + noise_seed += shared->frame_dim.num_groups; + } + } + EnsureBordersStorage(); + if (!EagerFinalizeImageRect()) { + // decoded must be padded to a multiple of kBlockDim rows since the last + // rows may be used by the filters even if they are outside the frame + // dimension. + decoded = Image3F(shared->frame_dim.xsize_padded, + shared->frame_dim.ysize_padded); + } +#if MEMORY_SANITIZER + // Avoid errors due to loading vectors on the outermost padding. + FillImage(msan::kSanitizerSentinel, &decoded); +#endif + } + + void EnsureBordersStorage(); + + Status FinalizeGroup(size_t group_idx, size_t thread, Image3F* pixel_data, + ImageBundle* output); +}; + +// Temp images required for decoding a single group. Reduces memory allocations +// for large images because we only initialize min(#threads, #groups) instances. +struct GroupDecCache { + void InitOnce(size_t num_passes, size_t used_acs) { + PROFILER_FUNC; + + for (size_t i = 0; i < num_passes; i++) { + if (num_nzeroes[i].xsize() == 0) { + // Allocate enough for a whole group - partial groups on the + // right/bottom border just use a subset. The valid size is passed via + // Rect. + + num_nzeroes[i] = Image3I(kGroupDimInBlocks, kGroupDimInBlocks); + } + } + size_t max_block_area = 0; + + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + AcStrategy acs = AcStrategy::FromRawStrategy(o); + if ((used_acs & (1 << o)) == 0) continue; + size_t area = + acs.covered_blocks_x() * acs.covered_blocks_y() * kDCTBlockSize; + max_block_area = std::max(area, max_block_area); + } + + if (max_block_area > max_block_area_) { + max_block_area_ = max_block_area; + // We need 3x float blocks for dequantized coefficients and 1x for scratch + // space for transforms. + float_memory_ = hwy::AllocateAligned(max_block_area_ * 4); + // We need 3x int32 or int16 blocks for quantized coefficients. + int32_memory_ = hwy::AllocateAligned(max_block_area_ * 3); + int16_memory_ = hwy::AllocateAligned(max_block_area_ * 3); + } + + dec_group_block = float_memory_.get(); + scratch_space = dec_group_block + max_block_area_ * 3; + dec_group_qblock = int32_memory_.get(); + dec_group_qblock16 = int16_memory_.get(); + } + + // Scratch space used by DecGroupImpl(). + float* dec_group_block; + int32_t* dec_group_qblock; + int16_t* dec_group_qblock16; + + // For TransformToPixels. + float* scratch_space; + // Note that scratch_space is never used at the same time as dec_group_qblock. + // Moreover, only one of dec_group_qblock16 is ever used. + // TODO(veluca): figure out if we can save allocations. + + // AC decoding + Image3I num_nzeroes[kMaxNumPasses]; + + private: + hwy::AlignedFreeUniquePtr float_memory_; + hwy::AlignedFreeUniquePtr int32_memory_; + hwy::AlignedFreeUniquePtr int16_memory_; + size_t max_block_area_ = 0; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_CACHE_H_ diff --git a/lib/jxl/dec_context_map.cc b/lib/jxl/dec_context_map.cc new file mode 100644 index 0000000..f7fc3d2 --- /dev/null +++ b/lib/jxl/dec_context_map.cc @@ -0,0 +1,105 @@ +// 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/dec_context_map.h" + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/entropy_coder.h" + +namespace jxl { + +namespace { + +void MoveToFront(uint8_t* v, uint8_t index) { + uint8_t value = v[index]; + uint8_t i = index; + for (; i; --i) v[i] = v[i - 1]; + v[0] = value; +} + +void InverseMoveToFrontTransform(uint8_t* v, int v_len) { + uint8_t mtf[256]; + int i; + for (i = 0; i < 256; ++i) { + mtf[i] = static_cast(i); + } + for (i = 0; i < v_len; ++i) { + uint8_t index = v[i]; + v[i] = mtf[index]; + if (index) MoveToFront(mtf, index); + } +} + +bool VerifyContextMap(const std::vector& context_map, + const size_t num_htrees) { + std::vector have_htree(num_htrees); + size_t num_found = 0; + for (const uint8_t htree : context_map) { + if (htree >= num_htrees) { + return JXL_FAILURE("Invalid histogram index in context map."); + } + if (!have_htree[htree]) { + have_htree[htree] = true; + ++num_found; + } + } + if (num_found != num_htrees) { + return JXL_FAILURE("Incomplete context map."); + } + return true; +} + +} // namespace + +bool DecodeContextMap(std::vector* context_map, size_t* num_htrees, + BitReader* input) { + bool is_simple = input->ReadFixedBits<1>(); + if (is_simple) { + int bits_per_entry = input->ReadFixedBits<2>(); + if (bits_per_entry != 0) { + for (size_t i = 0; i < context_map->size(); i++) { + (*context_map)[i] = input->ReadBits(bits_per_entry); + } + } else { + std::fill(context_map->begin(), context_map->end(), 0); + } + } else { + bool use_mtf = input->ReadFixedBits<1>(); + ANSCode code; + std::vector dummy_ctx_map; + // Usage of LZ77 is disallowed if decoding only two symbols. This doesn't + // make sense in non-malicious bitstreams, and could cause a stack overflow + // in malicious bitstreams by making every context map require its own + // context map. + JXL_RETURN_IF_ERROR( + DecodeHistograms(input, 1, &code, &dummy_ctx_map, + /*disallow_lz77=*/context_map->size() <= 2)); + ANSSymbolReader reader(&code, input); + size_t i = 0; + while (i < context_map->size()) { + uint32_t sym = reader.ReadHybridUint(0, input, dummy_ctx_map); + if (sym >= kMaxClusters) { + return JXL_FAILURE("Invalid cluster ID"); + } + (*context_map)[i] = sym; + i++; + } + if (!reader.CheckANSFinalState()) { + return JXL_FAILURE("Invalid context map"); + } + if (use_mtf) { + InverseMoveToFrontTransform(context_map->data(), context_map->size()); + } + } + *num_htrees = *std::max_element(context_map->begin(), context_map->end()) + 1; + return VerifyContextMap(*context_map, *num_htrees); +} + +} // namespace jxl diff --git a/lib/jxl/dec_context_map.h b/lib/jxl/dec_context_map.h new file mode 100644 index 0000000..1db2317 --- /dev/null +++ b/lib/jxl/dec_context_map.h @@ -0,0 +1,30 @@ +// 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_DEC_CONTEXT_MAP_H_ +#define LIB_JXL_DEC_CONTEXT_MAP_H_ + +#include +#include + +#include + +#include "lib/jxl/dec_bit_reader.h" + +namespace jxl { + +// Context map uses uint8_t. +constexpr size_t kMaxClusters = 256; + +// Reads the context map from the bit stream. On calling this function, +// context_map->size() must be the number of possible context ids. +// Sets *num_htrees to the number of different histogram ids in +// *context_map. +bool DecodeContextMap(std::vector* context_map, size_t* num_htrees, + BitReader* input); + +} // namespace jxl + +#endif // LIB_JXL_DEC_CONTEXT_MAP_H_ diff --git a/lib/jxl/dec_external_image.cc b/lib/jxl/dec_external_image.cc new file mode 100644 index 0000000..a36bf57 --- /dev/null +++ b/lib/jxl/dec_external_image.cc @@ -0,0 +1,528 @@ +// 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/dec_external_image.h" + +#include + +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_external_image.cc" +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +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(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 du; + + // Unpoison accessing partially-uninitialized vectors with memory sanitizer. + // This is because we run NearestInt() on the vector, which triggers msan even + // it it safe to do so since the values are not mixed between lanes. + const size_t num_round_up = RoundUpTo(num, Lanes(d)); + msan::UnpoisonMemory(in + num, sizeof(in[0]) * (num_round_up - num)); + + const auto one = Set(d, 1.0f); + const auto scale = Set(d, mul); + for (size_t x = 0; x < num; x += Lanes(d)) { + auto v = Load(d, in + x); + // Clamp turns NaN to 'min'. + v = Clamp(v, Zero(d), one); + auto i = NearestInt(v * scale); + Store(BitCast(du, i), du, out + x); + } + + // Poison back the output. + msan::PoisonMemory(out + num, sizeof(out[0]) * (num_round_up - num)); +} + +void FloatToF16(const float* in, hwy::float16_t* out, size_t num) { + const HWY_FULL(float) d; + const hwy::HWY_NAMESPACE::Rebind du; + + // Unpoison accessing partially-uninitialized vectors with memory sanitizer. + // This is because we run DemoteTo() on the vector which triggers msan. + const size_t num_round_up = RoundUpTo(num, Lanes(d)); + msan::UnpoisonMemory(in + num, sizeof(in[0]) * (num_round_up - num)); + + for (size_t x = 0; x < num; x += Lanes(d)) { + auto v = Load(d, in + x); + auto v16 = DemoteTo(du, v); + Store(v16, du, out + x); + } + + // Poison back the output. + msan::PoisonMemory(out + num, sizeof(out[0]) * (num_round_up - num)); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE + +namespace jxl { +namespace { + +// Stores a float in big endian +void StoreBEFloat(float value, uint8_t* p) { + uint32_t u; + memcpy(&u, &value, 4); + StoreBE32(u, p); +} + +// Stores a float in little endian +void StoreLEFloat(float value, uint8_t* p) { + uint32_t u; + memcpy(&u, &value, 4); + StoreLE32(u, p); +} + +// The orientation may not be identity. +// TODO(lode): SIMDify where possible +template +void UndoOrientation(jxl::Orientation undo_orientation, const Plane& image, + Plane& out, jxl::ThreadPool* pool) { + const size_t xsize = image.xsize(); + const size_t ysize = image.ysize(); + + if (undo_orientation == Orientation::kFlipHorizontal) { + out = Plane(xsize, ysize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + T* JXL_RESTRICT row_out = out.Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[xsize - x - 1] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kRotate180) { + out = Plane(xsize, ysize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + T* JXL_RESTRICT row_out = out.Row(ysize - y - 1); + for (size_t x = 0; x < xsize; ++x) { + row_out[xsize - x - 1] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kFlipVertical) { + out = Plane(xsize, ysize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + T* JXL_RESTRICT row_out = out.Row(ysize - y - 1); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kTranspose) { + out = Plane(ysize, xsize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + for (size_t x = 0; x < xsize; ++x) { + out.Row(x)[y] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kRotate90) { + out = Plane(ysize, xsize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + for (size_t x = 0; x < xsize; ++x) { + out.Row(x)[ysize - y - 1] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kAntiTranspose) { + out = Plane(ysize, xsize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + for (size_t x = 0; x < xsize; ++x) { + out.Row(xsize - x - 1)[ysize - y - 1] = row_in[x]; + } + }, + "UndoOrientation"); + } else if (undo_orientation == Orientation::kRotate270) { + out = Plane(ysize, xsize); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t y = task; + const T* JXL_RESTRICT row_in = image.Row(y); + for (size_t x = 0; x < xsize; ++x) { + out.Row(xsize - x - 1)[y] = row_in[x]; + } + }, + "UndoOrientation"); + } +} +} // namespace + +HWY_EXPORT(FloatToU32); +HWY_EXPORT(FloatToF16); + +namespace { + +using StoreFuncType = void(uint32_t value, uint8_t* dest); +template +void StoreUintRow(uint32_t* JXL_RESTRICT* rows_u32, size_t num_channels, + size_t xsize, size_t bytes_per_sample, + uint8_t* JXL_RESTRICT out) { + for (size_t x = 0; x < xsize; ++x) { + for (size_t c = 0; c < num_channels; c++) { + StoreFunc(rows_u32[c][x], + out + (num_channels * x + c) * bytes_per_sample); + } + } +} + +template +void StoreFloatRow(const float* JXL_RESTRICT* rows_in, size_t num_channels, + size_t xsize, uint8_t* JXL_RESTRICT out) { + for (size_t x = 0; x < xsize; ++x) { + for (size_t c = 0; c < num_channels; c++) { + StoreFunc(rows_in[c][x], out + (num_channels * x + c) * sizeof(float)); + } + } +} + +void JXL_INLINE Store8(uint32_t value, uint8_t* dest) { *dest = value & 0xff; } + +// Maximum number of channels for the ConvertChannelsToExternal function. +const size_t kConvertMaxChannels = 4; + +// Converts a list of channels to an interleaved image, applying transformations +// when needed. +// The input channels are given as a (non-const!) array of channel pointers and +// interleaved in that order. +// +// Note: if a pointer in channels[] is nullptr, a 1.0 value will be used +// instead. This is useful for handling when a user requests an alpha channel +// from an image that doesn't have one. The first channel in the list may not +// be nullptr, since it is used to determine the image size. +Status ConvertChannelsToExternal(const ImageF* channels[], size_t num_channels, + 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) { + 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) { + 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"); + } + + // 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> row_out_callback; + auto InitOutCallback = [&](size_t num_threads) { + if (out_callback) { + row_out_callback.resize(num_threads); + for (size_t i = 0; i < num_threads; ++i) { + row_out_callback[i].resize(stride); + } + } + }; + + // Channels used to store the transformed original channels if needed. + ImageF temp_channels[kConvertMaxChannels]; + if (undo_orientation != Orientation::kIdentity) { + for (size_t c = 0; c < num_channels; ++c) { + if (channels[c]) { + UndoOrientation(undo_orientation, *channels[c], temp_channels[c], pool); + channels[c] = &(temp_channels[c]); + } + } + } + + // First channel may not be nullptr. + size_t xsize = channels[0]->xsize(); + size_t ysize = channels[0]->ysize(); + + if (stride < bytes_per_pixel * xsize) { + return JXL_FAILURE( + "stride is smaller than scanline width in bytes: %zu vs %zu", stride, + bytes_per_pixel * xsize); + } + + const bool little_endian = + endianness == JXL_LITTLE_ENDIAN || + (endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()); + + // Handle the case where a channel is nullptr by creating a single row with + // ones to use instead. + ImageF ones; + for (size_t c = 0; c < num_channels; ++c) { + if (!channels[c]) { + ones = ImageF(xsize, 1); + FillImage(1.0f, &ones); + break; + } + } + + if (float_out) { + if (bits_per_sample == 16) { + bool swap_endianness = little_endian != IsLittleEndian(); + Plane f16_cache; + RunOnPool( + pool, 0, static_cast(ysize), + [&](size_t num_threads) { + f16_cache = + Plane(xsize, num_channels * num_threads); + InitOutCallback(num_threads); + return true; + }, + [&](const int task, int thread) { + const int64_t y = task; + const float* JXL_RESTRICT row_in[kConvertMaxChannels]; + for (size_t c = 0; c < num_channels; c++) { + row_in[c] = channels[c] ? channels[c]->Row(y) : ones.Row(0); + } + hwy::float16_t* JXL_RESTRICT row_f16[kConvertMaxChannels]; + for (size_t c = 0; c < num_channels; c++) { + row_f16[c] = f16_cache.Row(c + thread * num_channels); + HWY_DYNAMIC_DISPATCH(FloatToF16) + (row_in[c], row_f16[c], xsize); + } + uint8_t* row_out = + out_callback + ? row_out_callback[thread].data() + : &(reinterpret_cast(out_image))[stride * y]; + // interleave the one scanline + hwy::float16_t* row_f16_out = + reinterpret_cast(row_out); + for (size_t x = 0; x < xsize; x++) { + for (size_t c = 0; c < num_channels; c++) { + row_f16_out[x * num_channels + c] = row_f16[c][x]; + } + } + if (swap_endianness) { + size_t size = xsize * num_channels * 2; + for (size_t i = 0; i < size; i += 2) { + std::swap(row_out[i + 0], row_out[i + 1]); + } + } + if (out_callback) { + (*out_callback)(out_opaque, 0, y, xsize, row_out); + } + }, + "ConvertF16"); + } else if (bits_per_sample == 32) { + RunOnPool( + pool, 0, static_cast(ysize), + [&](size_t num_threads) { + InitOutCallback(num_threads); + return true; + }, + [&](const int task, int thread) { + const int64_t y = task; + uint8_t* row_out = + out_callback + ? row_out_callback[thread].data() + : &(reinterpret_cast(out_image))[stride * y]; + const float* JXL_RESTRICT row_in[kConvertMaxChannels]; + for (size_t c = 0; c < num_channels; c++) { + row_in[c] = channels[c] ? channels[c]->Row(y) : ones.Row(0); + } + if (little_endian) { + StoreFloatRow(row_in, num_channels, xsize, row_out); + } else { + StoreFloatRow(row_in, num_channels, xsize, row_out); + } + if (out_callback) { + (*out_callback)(out_opaque, 0, y, xsize, row_out); + } + }, + "ConvertFloat"); + } else { + return JXL_FAILURE("float other than 16-bit and 32-bit not supported"); + } + } else { + // Multiplier to convert from floating point 0-1 range to the integer + // range. + float mul = (1ull << bits_per_sample) - 1; + Plane u32_cache; + RunOnPool( + pool, 0, static_cast(ysize), + [&](size_t num_threads) { + u32_cache = Plane(xsize, num_channels * num_threads); + InitOutCallback(num_threads); + return true; + }, + [&](const int task, int thread) { + const int64_t y = task; + uint8_t* row_out = + out_callback + ? row_out_callback[thread].data() + : &(reinterpret_cast(out_image))[stride * y]; + const float* JXL_RESTRICT row_in[kConvertMaxChannels]; + for (size_t c = 0; c < num_channels; c++) { + row_in[c] = channels[c] ? channels[c]->Row(y) : ones.Row(0); + } + uint32_t* JXL_RESTRICT row_u32[kConvertMaxChannels]; + for (size_t c = 0; c < num_channels; c++) { + row_u32[c] = u32_cache.Row(c + thread * num_channels); + // row_u32[] is a per-thread temporary row storage, this isn't + // intended to be initialized on a previous run. + msan::PoisonMemory(row_u32[c], xsize * sizeof(row_u32[c][0])); + 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(row_u32, num_channels, xsize, 1, row_out); + } else if (bits_per_sample <= 16) { + if (little_endian) { + StoreUintRow(row_u32, num_channels, xsize, 2, row_out); + } else { + StoreUintRow(row_u32, num_channels, xsize, 2, row_out); + } + } else if (bits_per_sample <= 24) { + if (little_endian) { + StoreUintRow(row_u32, num_channels, xsize, 3, row_out); + } else { + StoreUintRow(row_u32, num_channels, xsize, 3, row_out); + } + } else { + if (little_endian) { + StoreUintRow(row_u32, num_channels, xsize, 4, row_out); + } else { + StoreUintRow(row_u32, num_channels, xsize, 4, row_out); + } + } + if (out_callback) { + (*out_callback)(out_opaque, 0, y, xsize, row_out); + } + }, + "ConvertUint"); + } + return true; +} + +} // namespace + +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) { + bool want_alpha = num_channels == 2 || num_channels == 4; + size_t color_channels = num_channels <= 2 ? 1 : 3; + + const Image3F* color = &ib.color(); + // Undo premultiplied alpha. + Image3F unpremul; + if (ib.AlphaIsPremultiplied() && ib.HasAlpha()) { + unpremul = Image3F(color->xsize(), color->ysize()); + CopyImageTo(*color, &unpremul); + for (size_t y = 0; y < unpremul.ysize(); y++) { + UnpremultiplyAlpha(unpremul.PlaneRow(0, y), unpremul.PlaneRow(1, y), + unpremul.PlaneRow(2, y), ib.alpha().Row(y), + unpremul.xsize()); + } + color = &unpremul; + } + + const ImageF* channels[kConvertMaxChannels]; + size_t c = 0; + for (; c < color_channels; c++) { + channels[c] = &color->Plane(c); + } + if (want_alpha) { + channels[c++] = ib.HasAlpha() ? &ib.alpha() : nullptr; + } + JXL_ASSERT(num_channels == c); + + return ConvertChannelsToExternal( + channels, num_channels, bits_per_sample, float_out, endianness, stride, + pool, out_image, out_size, out_callback, out_opaque, 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) { + 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); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_external_image.h b/lib/jxl/dec_external_image.h new file mode 100644 index 0000000..a8f6319 --- /dev/null +++ b/lib/jxl/dec_external_image.h @@ -0,0 +1,61 @@ +// 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_DEC_EXTERNAL_IMAGE_H_ +#define LIB_JXL_DEC_EXTERNAL_IMAGE_H_ + +// Interleaved image for color transforms and Codec. + +#include +#include + +#include "jxl/decode.h" +#include "jxl/types.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.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. +// 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. +// 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. +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); + +// 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. +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, + jxl::Orientation undo_orientation); +} // namespace jxl + +#endif // LIB_JXL_DEC_EXTERNAL_IMAGE_H_ diff --git a/lib/jxl/dec_external_image_gbench.cc b/lib/jxl/dec_external_image_gbench.cc new file mode 100644 index 0000000..283a975 --- /dev/null +++ b/lib/jxl/dec_external_image_gbench.cc @@ -0,0 +1,56 @@ +// 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 "benchmark/benchmark.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { +namespace { + +// Decoder case, interleaves an internal float image. +void BM_DecExternalImage_ConvertImageRGBA(benchmark::State& state) { + const size_t kNumIter = 5; + size_t xsize = state.range(); + size_t ysize = state.range(); + size_t num_channels = 4; + + ImageMetadata im; + im.SetAlphaBits(8); + ImageBundle ib(&im); + Image3F color(xsize, ysize); + ZeroFillImage(&color); + ib.SetFromImage(std::move(color), ColorEncoding::SRGB()); + ImageF alpha(xsize, ysize); + ZeroFillImage(&alpha); + ib.SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + + const size_t bytes_per_row = xsize * num_channels; + std::vector interleaved(bytes_per_row * ysize); + + for (auto _ : state) { + for (size_t i = 0; i < kNumIter; ++i) { + JXL_CHECK(ConvertToExternal( + ib, + /*bits_per_sample=*/8, + /*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, + /*undo_orientation=*/jxl::Orientation::kIdentity)); + } + } + + // Pixels per second. + state.SetItemsProcessed(kNumIter * state.iterations() * xsize * ysize); + state.SetBytesProcessed(kNumIter * state.iterations() * interleaved.size()); +} + +BENCHMARK(BM_DecExternalImage_ConvertImageRGBA) + ->RangeMultiplier(2) + ->Range(256, 2048); + +} // namespace +} // namespace jxl diff --git a/lib/jxl/dec_file.cc b/lib/jxl/dec_file.cc new file mode 100644 index 0000000..841e223 --- /dev/null +++ b/lib/jxl/dec_file.cc @@ -0,0 +1,193 @@ +// 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/dec_file.h" + +#include + +#include +#include + +#include "jxl/decode.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_frame.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/icc_codec.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" + +namespace jxl { +namespace { + +Status DecodeHeaders(BitReader* reader, CodecInOut* io) { + JXL_RETURN_IF_ERROR(ReadSizeHeader(reader, &io->metadata.size)); + + JXL_RETURN_IF_ERROR(ReadImageMetadata(reader, &io->metadata.m)); + + io->metadata.transform_data.nonserialized_xyb_encoded = + io->metadata.m.xyb_encoded; + JXL_RETURN_IF_ERROR(Bundle::Read(reader, &io->metadata.transform_data)); + + return true; +} + +} // namespace + +Status DecodePreview(const DecompressParams& dparams, + const CodecMetadata& metadata, + BitReader* JXL_RESTRICT reader, ThreadPool* pool, + ImageBundle* JXL_RESTRICT preview, uint64_t* dec_pixels, + const SizeConstraints* constraints) { + // No preview present in file. + if (!metadata.m.have_preview) { + if (dparams.preview == Override::kOn) { + return JXL_FAILURE("preview == kOn but no preview present"); + } + return true; + } + + // Have preview; prepare to skip or read it. + JXL_RETURN_IF_ERROR(reader->JumpToByteBoundary()); + + if (dparams.preview == Override::kOff) { + JXL_RETURN_IF_ERROR(SkipFrame(metadata, reader, /*is_preview=*/true)); + return true; + } + + // Else: default or kOn => decode preview. + PassesDecoderState dec_state; + JXL_RETURN_IF_ERROR(dec_state.output_encoding_info.Set( + metadata, ColorEncoding::LinearSRGB(metadata.m.color_encoding.IsGray()))); + JXL_RETURN_IF_ERROR(DecodeFrame(dparams, &dec_state, pool, reader, preview, + metadata, constraints, + /*is_preview=*/true)); + if (dec_pixels) { + *dec_pixels += dec_state.shared->frame_dim.xsize_upsampled * + dec_state.shared->frame_dim.ysize_upsampled; + } + return true; +} + +// To avoid the complexity of file I/O and buffering, we assume the bitstream +// is loaded (or for large images/sequences: mapped into) memory. +Status DecodeFile(const DecompressParams& dparams, + const Span file, CodecInOut* JXL_RESTRICT io, + ThreadPool* pool) { + PROFILER_ZONE("DecodeFile uninstrumented"); + + // Marker + JxlSignature signature = JxlSignatureCheck(file.data(), file.size()); + if (signature == JXL_SIG_NOT_ENOUGH_BYTES || signature == JXL_SIG_INVALID) { + return JXL_FAILURE("File does not start with known JPEG XL signature"); + } + + std::unique_ptr jpeg_data = nullptr; + if (dparams.keep_dct) { + if (io->Main().jpeg_data == nullptr) { + return JXL_FAILURE("Caller must set jpeg_data"); + } + jpeg_data = std::move(io->Main().jpeg_data); + } + + Status ret = true; + { + BitReader reader(file); + BitReaderScopedCloser reader_closer(&reader, &ret); + (void)reader.ReadFixedBits<16>(); // skip marker + + { + JXL_RETURN_IF_ERROR(DecodeHeaders(&reader, io)); + size_t xsize = io->metadata.xsize(); + size_t ysize = io->metadata.ysize(); + JXL_RETURN_IF_ERROR(VerifyDimensions(&io->constraints, xsize, ysize)); + } + + if (io->metadata.m.color_encoding.WantICC()) { + PaddedBytes icc; + JXL_RETURN_IF_ERROR(ReadICC(&reader, &icc)); + JXL_RETURN_IF_ERROR(io->metadata.m.color_encoding.SetICC(std::move(icc))); + } + // Set ICC profile in jpeg_data. + if (jpeg_data) { + Status res = jpeg::SetJPEGDataFromICC(io->metadata.m.color_encoding.ICC(), + jpeg_data.get()); + if (!res) { + return res; + } + } + + JXL_RETURN_IF_ERROR(DecodePreview(dparams, io->metadata, &reader, pool, + &io->preview_frame, &io->dec_pixels, + &io->constraints)); + + // Only necessary if no ICC and no preview. + JXL_RETURN_IF_ERROR(reader.JumpToByteBoundary()); + if (io->metadata.m.have_animation && dparams.keep_dct) { + return JXL_FAILURE("Cannot decode to JPEG an animation"); + } + + PassesDecoderState dec_state; + JXL_RETURN_IF_ERROR(dec_state.output_encoding_info.Set( + io->metadata, + ColorEncoding::LinearSRGB(io->metadata.m.color_encoding.IsGray()))); + + io->frames.clear(); + Status dec_ok(false); + do { + io->frames.emplace_back(&io->metadata.m); + if (jpeg_data) { + io->frames.back().jpeg_data = std::move(jpeg_data); + } + // Skip frames that are not displayed. + bool found_displayed_frame = true; + do { + dec_ok = + DecodeFrame(dparams, &dec_state, pool, &reader, &io->frames.back(), + io->metadata, &io->constraints); + if (!dparams.allow_partial_files) { + JXL_RETURN_IF_ERROR(dec_ok); + } else if (!dec_ok) { + io->frames.pop_back(); + found_displayed_frame = false; + break; + } + } while (dec_state.shared->frame_header.frame_type != + FrameType::kRegularFrame && + dec_state.shared->frame_header.frame_type != + FrameType::kSkipProgressive); + if (found_displayed_frame) { + // if found_displayed_frame is true io->frames shouldn't be empty + // because we added a frame before the loop. + JXL_ASSERT(!io->frames.empty()); + io->dec_pixels += io->frames.back().xsize() * io->frames.back().ysize(); + } + } while (!dec_state.shared->frame_header.is_last && dec_ok); + + if (io->frames.empty()) return JXL_FAILURE("Not enough data."); + + if (dparams.check_decompressed_size && !dparams.allow_partial_files && + dparams.max_downsampling == 1) { + if (reader.TotalBitsConsumed() != file.size() * kBitsPerByte) { + return JXL_FAILURE("DecodeFile reader position not at EOF."); + } + } + // Suppress errors when decoding partial files with DC frames. + if (!reader.AllReadsWithinBounds() && dparams.allow_partial_files) { + reader_closer.CloseAndSuppressError(); + } + + io->CheckMetadata(); + // reader is closed here. + } + return ret; +} + +} // namespace jxl diff --git a/lib/jxl/dec_file.h b/lib/jxl/dec_file.h new file mode 100644 index 0000000..cd04d5d --- /dev/null +++ b/lib/jxl/dec_file.h @@ -0,0 +1,48 @@ +// 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_DEC_FILE_H_ +#define LIB_JXL_DEC_FILE_H_ + +// Top-level interface for JXL decoding. + +#include + +#include "lib/jxl/base/data_parallel.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/dec_params.h" + +namespace jxl { + +// Decodes the preview image, if present, and stores it in `preview`. +// Must be the first frame in the file. Does nothing if there is no preview +// frame present according to the metadata. +Status DecodePreview(const DecompressParams& dparams, + const CodecMetadata& metadata, + BitReader* JXL_RESTRICT reader, ThreadPool* pool, + ImageBundle* JXL_RESTRICT preview, uint64_t* dec_pixels, + const SizeConstraints* constraints); + +// Implementation detail: currently decodes to linear sRGB. The contract is: +// `io` appears 'identical' (modulo compression artifacts) to the encoder input +// in a color-aware viewer. Note that `io->metadata.m.color_encoding` +// identifies the color space that was passed to the encoder; clients that want +// that same encoding must call `io->TransformTo` afterwards. +Status DecodeFile(const DecompressParams& params, + const Span file, CodecInOut* io, + ThreadPool* pool = nullptr); + +static inline Status DecodeFile(const DecompressParams& params, + const PaddedBytes& file, CodecInOut* io, + ThreadPool* pool = nullptr) { + return DecodeFile(params, Span(file), io, pool); +} + +} // namespace jxl + +#endif // LIB_JXL_DEC_FILE_H_ diff --git a/lib/jxl/dec_frame.cc b/lib/jxl/dec_frame.cc new file mode 100644 index 0000000..505b1b0 --- /dev/null +++ b/lib/jxl/dec_frame.cc @@ -0,0 +1,1027 @@ +// 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/dec_frame.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/compressed_dc.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_group.h" +#include "lib/jxl/dec_modular.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/dec_reconstruct.h" +#include "lib/jxl/dec_upsample.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/filters.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/jpeg/jpeg_data.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/luminance.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/splines.h" +#include "lib/jxl/toc.h" + +namespace jxl { + +namespace { +Status DecodeGlobalDCInfo(BitReader* reader, bool is_jpeg, + PassesDecoderState* state, ThreadPool* pool) { + PROFILER_FUNC; + JXL_RETURN_IF_ERROR(state->shared_storage.quantizer.Decode(reader)); + + JXL_RETURN_IF_ERROR( + DecodeBlockCtxMap(reader, &state->shared_storage.block_ctx_map)); + + JXL_RETURN_IF_ERROR(state->shared_storage.cmap.DecodeDC(reader)); + + // Pre-compute info for decoding a group. + if (is_jpeg) { + state->shared_storage.quantizer.ClearDCMul(); // Don't dequant DC + } + + state->shared_storage.ac_strategy.FillInvalid(); + return true; +} +} // namespace + +Status DecodeFrameHeader(BitReader* JXL_RESTRICT reader, + FrameHeader* JXL_RESTRICT frame_header) { + JXL_ASSERT(frame_header->nonserialized_metadata != nullptr); + JXL_RETURN_IF_ERROR(ReadFrameHeader(reader, frame_header)); + return true; +} + +Status SkipFrame(const CodecMetadata& metadata, BitReader* JXL_RESTRICT reader, + bool is_preview) { + FrameHeader header(&metadata); + header.nonserialized_is_preview = is_preview; + JXL_RETURN_IF_ERROR(DecodeFrameHeader(reader, &header)); + + // Read TOC. + std::vector group_offsets; + std::vector group_sizes; + uint64_t groups_total_size; + const bool has_ac_global = true; + const FrameDimensions frame_dim = header.ToFrameDimensions(); + const size_t toc_entries = + NumTocEntries(frame_dim.num_groups, frame_dim.num_dc_groups, + header.passes.num_passes, has_ac_global); + JXL_RETURN_IF_ERROR(ReadGroupOffsets(toc_entries, reader, &group_offsets, + &group_sizes, &groups_total_size)); + + // Pretend all groups are read. + reader->SkipBits(groups_total_size * kBitsPerByte); + if (reader->TotalBitsConsumed() > reader->TotalBytes() * kBitsPerByte) { + return JXL_FAILURE("Group code extends after stream end"); + } + + return true; +} + +static BitReader* GetReaderForSection( + size_t num_groups, size_t num_passes, size_t group_codes_begin, + const std::vector& group_offsets, + const std::vector& group_sizes, BitReader* JXL_RESTRICT reader, + BitReader* JXL_RESTRICT store, size_t index) { + if (num_groups == 1 && num_passes == 1) return reader; + const size_t group_offset = group_codes_begin + group_offsets[index]; + const size_t next_group_offset = + group_codes_begin + group_offsets[index] + group_sizes[index]; + // The order of these variables must be: + // group_codes_begin <= group_offset <= next_group_offset <= file.size() + JXL_DASSERT(group_codes_begin <= group_offset); + JXL_DASSERT(group_offset <= next_group_offset); + JXL_DASSERT(next_group_offset <= reader->TotalBytes()); + const size_t group_size = next_group_offset - group_offset; + const size_t remaining_size = reader->TotalBytes() - group_offset; + const size_t size = std::min(group_size + 8, remaining_size); + *store = + BitReader(Span(reader->FirstByte() + group_offset, size)); + return store; +} + +Status DecodeFrame(const DecompressParams& dparams, + PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, + BitReader* JXL_RESTRICT reader, ImageBundle* decoded, + const CodecMetadata& metadata, + const SizeConstraints* constraints, bool is_preview) { + PROFILER_ZONE("DecodeFrame uninstrumented"); + + FrameDecoder frame_decoder(dec_state, metadata, pool); + + frame_decoder.SetFrameSizeLimits(constraints); + + JXL_RETURN_IF_ERROR(frame_decoder.InitFrame( + reader, decoded, is_preview, dparams.allow_partial_files, + dparams.allow_partial_files && dparams.allow_more_progressive_steps)); + + // Handling of progressive decoding. + { + const FrameHeader& frame_header = frame_decoder.GetFrameHeader(); + size_t max_passes = dparams.max_passes; + size_t max_downsampling = std::max( + dparams.max_downsampling >> (frame_header.dc_level * 3), size_t(1)); + // TODO(veluca): deal with downsamplings >= 8. + if (max_downsampling >= 8) { + max_passes = 0; + } else { + for (uint32_t i = 0; i < frame_header.passes.num_downsample; ++i) { + if (max_downsampling >= frame_header.passes.downsample[i] && + max_passes > frame_header.passes.last_pass[i]) { + max_passes = frame_header.passes.last_pass[i] + 1; + } + } + } + // Do not use downsampling for kReferenceOnly frames. + if (frame_header.frame_type == FrameType::kReferenceOnly) { + max_passes = frame_header.passes.num_passes; + } + max_passes = std::min(max_passes, frame_header.passes.num_passes); + frame_decoder.SetMaxPasses(max_passes); + } + frame_decoder.SetRenderSpotcolors(dparams.render_spotcolors); + + size_t processed_bytes = reader->TotalBitsConsumed() / kBitsPerByte; + + Status close_ok = true; + std::vector> section_readers; + { + std::vector> section_closers; + std::vector section_info; + std::vector section_status; + size_t bytes_to_skip = 0; + for (size_t i = 0; i < frame_decoder.NumSections(); i++) { + size_t b = frame_decoder.SectionOffsets()[i]; + size_t e = b + frame_decoder.SectionSizes()[i]; + bytes_to_skip += e - b; + size_t pos = reader->TotalBitsConsumed() / kBitsPerByte; + if (pos + e <= reader->TotalBytes()) { + auto br = make_unique( + Span(reader->FirstByte() + b + pos, e - b)); + section_info.emplace_back(FrameDecoder::SectionInfo{br.get(), i}); + section_closers.emplace_back( + make_unique(br.get(), &close_ok)); + section_readers.emplace_back(std::move(br)); + } else if (!dparams.allow_partial_files) { + return JXL_FAILURE("Premature end of stream."); + } + } + // Skip over the to-be-decoded sections. + reader->SkipBits(kBitsPerByte * bytes_to_skip); + section_status.resize(section_info.size()); + + JXL_RETURN_IF_ERROR(frame_decoder.ProcessSections( + section_info.data(), section_info.size(), section_status.data())); + + for (size_t i = 0; i < section_status.size(); i++) { + auto s = section_status[i]; + if (s == FrameDecoder::kDone) { + processed_bytes += frame_decoder.SectionSizes()[i]; + continue; + } + if (dparams.allow_more_progressive_steps && s == FrameDecoder::kPartial) { + continue; + } + if (dparams.max_downsampling > 1 && s == FrameDecoder::kSkipped) { + continue; + } + return JXL_FAILURE("Invalid section %zu status: %d", section_info[i].id, + s); + } + } + + JXL_RETURN_IF_ERROR(close_ok); + + JXL_RETURN_IF_ERROR(frame_decoder.FinalizeFrame()); + decoded->SetDecodedBytes(processed_bytes); + return true; +} + +Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, + bool is_preview, bool allow_partial_frames, + bool allow_partial_dc_global) { + PROFILER_FUNC; + decoded_ = decoded; + JXL_ASSERT(is_finalized_); + + allow_partial_frames_ = allow_partial_frames; + allow_partial_dc_global_ = allow_partial_dc_global; + + // Reset the dequantization matrices to their default values. + dec_state_->shared_storage.matrices = DequantMatrices(); + + frame_header_.nonserialized_is_preview = is_preview; + JXL_RETURN_IF_ERROR(DecodeFrameHeader(br, &frame_header_)); + frame_dim_ = frame_header_.ToFrameDimensions(); + + const size_t num_passes = frame_header_.passes.num_passes; + const size_t xsize = frame_dim_.xsize; + const size_t ysize = frame_dim_.ysize; + const size_t num_groups = frame_dim_.num_groups; + + // Check validity of frame dimensions. + JXL_RETURN_IF_ERROR(VerifyDimensions(constraints_, xsize, ysize)); + + // If the previous frame was not a kRegularFrame, `decoded` may have different + // dimensions; must reset to avoid errors. + decoded->RemoveColor(); + decoded->ClearExtraChannels(); + + // Read TOC. + uint64_t groups_total_size; + const bool has_ac_global = true; + const size_t toc_entries = NumTocEntries(num_groups, frame_dim_.num_dc_groups, + num_passes, has_ac_global); + JXL_RETURN_IF_ERROR(ReadGroupOffsets(toc_entries, br, §ion_offsets_, + §ion_sizes_, &groups_total_size)); + + JXL_DASSERT((br->TotalBitsConsumed() % kBitsPerByte) == 0); + const size_t group_codes_begin = br->TotalBitsConsumed() / kBitsPerByte; + JXL_DASSERT(!section_offsets_.empty()); + + // Overflow check. + if (group_codes_begin + groups_total_size < group_codes_begin) { + return JXL_FAILURE("Invalid group codes"); + } + + if (!frame_header_.chroma_subsampling.Is444() && + !(frame_header_.flags & FrameHeader::kSkipAdaptiveDCSmoothing) && + frame_header_.encoding == FrameEncoding::kVarDCT) { + return JXL_FAILURE( + "Non-444 chroma subsampling is not allowed when adaptive DC " + "smoothing is enabled"); + } + JXL_RETURN_IF_ERROR( + InitializePassesSharedState(frame_header_, &dec_state_->shared_storage)); + JXL_RETURN_IF_ERROR(dec_state_->Init()); + modular_frame_decoder_.Init(frame_dim_); + + if (decoded->IsJPEG()) { + if (frame_header_.encoding == FrameEncoding::kModular) { + return JXL_FAILURE("Cannot output JPEG from Modular"); + } + jpeg::JPEGData* jpeg_data = decoded->jpeg_data.get(); + size_t num_components = jpeg_data->components.size(); + if (num_components != 1 && num_components != 3) { + return JXL_FAILURE("Invalid number of components"); + } + if (frame_header_.nonserialized_metadata->m.xyb_encoded) { + return JXL_FAILURE("Cannot decode to JPEG an XYB image"); + } + auto jpeg_c_map = JpegOrder(ColorTransform::kYCbCr, num_components == 1); + decoded->jpeg_data->width = frame_dim_.xsize; + decoded->jpeg_data->height = frame_dim_.ysize; + for (size_t c = 0; c < num_components; c++) { + auto& component = jpeg_data->components[jpeg_c_map[c]]; + component.width_in_blocks = + frame_dim_.xsize_blocks >> frame_header_.chroma_subsampling.HShift(c); + component.height_in_blocks = + frame_dim_.ysize_blocks >> frame_header_.chroma_subsampling.VShift(c); + component.h_samp_factor = + 1 << frame_header_.chroma_subsampling.RawHShift(c); + component.v_samp_factor = + 1 << frame_header_.chroma_subsampling.RawVShift(c); + component.coeffs.resize(component.width_in_blocks * + component.height_in_blocks * jxl::kDCTBlockSize); + } + } + + // Clear the state. + decoded_dc_global_ = false; + decoded_ac_global_ = false; + is_finalized_ = false; + finalized_dc_ = false; + decoded_dc_groups_.clear(); + decoded_dc_groups_.resize(frame_dim_.num_dc_groups); + decoded_passes_per_ac_group_.clear(); + decoded_passes_per_ac_group_.resize(frame_dim_.num_groups, 0); + processed_section_.clear(); + processed_section_.resize(section_offsets_.size()); + max_passes_ = frame_header_.passes.num_passes; + num_renders_ = 0; + allocated_ = false; + return true; +} + +Status FrameDecoder::ProcessDCGlobal(BitReader* br) { + PROFILER_FUNC; + PassesSharedState& shared = dec_state_->shared_storage; + if (shared.frame_header.flags & FrameHeader::kPatches) { + bool uses_extra_channels = false; + JXL_RETURN_IF_ERROR(shared.image_features.patches.Decode( + br, frame_dim_.xsize_padded, frame_dim_.ysize_padded, + &uses_extra_channels)); + if (uses_extra_channels && frame_header_.upsampling != 1) { + for (size_t ecups : frame_header_.extra_channel_upsampling) { + if (ecups != frame_header_.upsampling) { + return JXL_FAILURE( + "Cannot use extra channels in patches if color channels are " + "subsampled differently from extra channels"); + } + } + } + } else { + shared.image_features.patches.Clear(); + } + shared.image_features.splines.Clear(); + if (shared.frame_header.flags & FrameHeader::kSplines) { + JXL_RETURN_IF_ERROR(shared.image_features.splines.Decode( + br, frame_dim_.xsize * frame_dim_.ysize)); + } + if (shared.frame_header.flags & FrameHeader::kNoise) { + JXL_RETURN_IF_ERROR(DecodeNoise(br, &shared.image_features.noise_params)); + } + + JXL_RETURN_IF_ERROR(dec_state_->shared_storage.matrices.DecodeDC(br)); + if (frame_header_.encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR( + jxl::DecodeGlobalDCInfo(br, decoded_->IsJPEG(), dec_state_, pool_)); + } + // Splines' draw cache uses the color correlation map. + if (shared.frame_header.flags & FrameHeader::kSplines) { + JXL_RETURN_IF_ERROR(shared.image_features.splines.InitializeDrawCache( + frame_dim_.xsize_upsampled_padded, frame_dim_.ysize_upsampled_padded, + dec_state_->shared->cmap)); + } + Status dec_status = modular_frame_decoder_.DecodeGlobalInfo( + br, frame_header_, allow_partial_dc_global_); + if (dec_status.IsFatalError()) return dec_status; + if (dec_status) { + decoded_dc_global_ = true; + } + return dec_status; +} + +Status FrameDecoder::ProcessDCGroup(size_t dc_group_id, BitReader* br) { + PROFILER_FUNC; + const size_t gx = dc_group_id % frame_dim_.xsize_dc_groups; + const size_t gy = dc_group_id / frame_dim_.xsize_dc_groups; + const LoopFilter& lf = dec_state_->shared->frame_header.loop_filter; + if (frame_header_.encoding == FrameEncoding::kVarDCT && + !(frame_header_.flags & FrameHeader::kUseDcFrame)) { + JXL_RETURN_IF_ERROR( + modular_frame_decoder_.DecodeVarDCTDC(dc_group_id, br, dec_state_)); + } + const Rect mrect(gx * frame_dim_.dc_group_dim, gy * frame_dim_.dc_group_dim, + frame_dim_.dc_group_dim, frame_dim_.dc_group_dim); + JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( + mrect, br, 3, 1000, ModularStreamId::ModularDC(dc_group_id), + /*zerofill=*/false, nullptr, nullptr)); + if (frame_header_.encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR( + modular_frame_decoder_.DecodeAcMetadata(dc_group_id, br, dec_state_)); + } else if (lf.epf_iters > 0) { + FillImage(kInvSigmaNum / lf.epf_sigma_for_modular, + &dec_state_->filter_weights.sigma); + } + decoded_dc_groups_[dc_group_id] = true; + return true; +} + +void FrameDecoder::FinalizeDC() { + // Do Adaptive DC smoothing if enabled. This *must* happen between all the + // ProcessDCGroup and ProcessACGroup. + if (frame_header_.encoding == FrameEncoding::kVarDCT && + !(frame_header_.flags & FrameHeader::kSkipAdaptiveDCSmoothing) && + !(frame_header_.flags & FrameHeader::kUseDcFrame)) { + AdaptiveDCSmoothing(dec_state_->shared->quantizer.MulDC(), + &dec_state_->shared_storage.dc_storage, pool_); + } + + finalized_dc_ = true; +} + +void FrameDecoder::AllocateOutput() { + if (allocated_) return; + const CodecMetadata& metadata = *frame_header_.nonserialized_metadata; + if (dec_state_->rgb_output == nullptr && !dec_state_->pixel_callback) { + modular_frame_decoder_.MaybeDropFullImage(); + decoded_->SetFromImage(Image3F(frame_dim_.xsize_upsampled_padded, + frame_dim_.ysize_upsampled_padded), + dec_state_->output_encoding_info.color_encoding); + } + dec_state_->extra_channels.clear(); + if (metadata.m.num_extra_channels > 0) { + for (size_t i = 0; i < metadata.m.num_extra_channels; i++) { + uint32_t ecups = frame_header_.extra_channel_upsampling[i]; + dec_state_->extra_channels.emplace_back( + DivCeil(frame_dim_.xsize_upsampled_padded, ecups), + DivCeil(frame_dim_.ysize_upsampled_padded, ecups)); +#if JXL_MEMORY_SANITIZER + // Avoid errors due to loading vectors on the outermost padding. + // Upsample of extra channels requires this padding to be initialized. + // TODO(deymo): Remove this and use rects up to {x,y}size_upsampled + // instead of the padded one. + for (size_t y = 0; y < DivCeil(frame_dim_.ysize_upsampled_padded, ecups); + y++) { + for (size_t x = (y < DivCeil(frame_dim_.ysize_upsampled, ecups) + ? DivCeil(frame_dim_.xsize_upsampled, ecups) + : 0); + x < DivCeil(frame_dim_.xsize_upsampled_padded, ecups); x++) { + dec_state_->extra_channels.back().Row(y)[x] = + msan::kSanitizerSentinel; + } + } +#endif + } + } + decoded_->origin = dec_state_->shared->frame_header.frame_origin; + dec_state_->InitForAC(nullptr); + allocated_ = true; +} + +Status FrameDecoder::ProcessACGlobal(BitReader* br) { + JXL_CHECK(finalized_dc_); + JXL_CHECK(decoded_->HasColor() || dec_state_->rgb_output != nullptr || + !!dec_state_->pixel_callback); + + // Decode AC group. + if (frame_header_.encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR(dec_state_->shared_storage.matrices.Decode( + br, &modular_frame_decoder_)); + + size_t num_histo_bits = + CeilLog2Nonzero(dec_state_->shared->frame_dim.num_groups); + dec_state_->shared_storage.num_histograms = + 1 + br->ReadBits(num_histo_bits); + + dec_state_->code.resize(kMaxNumPasses); + dec_state_->context_map.resize(kMaxNumPasses); + // Read coefficient orders and histograms. + size_t max_num_bits_ac = 0; + for (size_t i = 0; + i < dec_state_->shared_storage.frame_header.passes.num_passes; i++) { + uint16_t used_orders = U32Coder::Read(kOrderEnc, br); + JXL_RETURN_IF_ERROR(DecodeCoeffOrders( + used_orders, dec_state_->used_acs, + &dec_state_->shared_storage + .coeff_orders[i * dec_state_->shared_storage.coeff_order_size], + br)); + size_t num_contexts = + dec_state_->shared->num_histograms * + dec_state_->shared_storage.block_ctx_map.NumACContexts(); + JXL_RETURN_IF_ERROR(DecodeHistograms( + br, num_contexts, &dec_state_->code[i], &dec_state_->context_map[i])); + // Add extra values to enable the cheat in hot loop of DecodeACVarBlock. + dec_state_->context_map[i].resize( + num_contexts + kZeroDensityContextLimit - kZeroDensityContextCount); + max_num_bits_ac = + std::max(max_num_bits_ac, dec_state_->code[i].max_num_bits); + } + max_num_bits_ac += CeilLog2Nonzero( + dec_state_->shared_storage.frame_header.passes.num_passes); + // 16-bit buffer for decoding to JPEG are not implemented. + // TODO(veluca): figure out the exact limit - 16 should still work with + // 16-bit buffers, but we are excluding it for safety. + bool use_16_bit = max_num_bits_ac < 16 && !decoded_->IsJPEG(); + bool store = frame_header_.passes.num_passes > 1; + size_t xs = store ? kGroupDim * kGroupDim : 0; + size_t ys = store ? frame_dim_.num_groups : 0; + if (use_16_bit) { + dec_state_->coefficients = make_unique>(xs, ys); + } else { + dec_state_->coefficients = make_unique>(xs, ys); + } + if (store) { + dec_state_->coefficients->ZeroFill(); + } + } + + // Set JPEG decoding data. + if (decoded_->IsJPEG()) { + decoded_->color_transform = frame_header_.color_transform; + decoded_->chroma_subsampling = frame_header_.chroma_subsampling; + const std::vector& qe = + dec_state_->shared_storage.matrices.encodings(); + if (qe.empty() || qe[0].mode != QuantEncoding::Mode::kQuantModeRAW || + std::abs(qe[0].qraw.qtable_den - 1.f / (8 * 255)) > 1e-8f) { + return JXL_FAILURE( + "Quantization table is not a JPEG quantization table."); + } + jpeg::JPEGData* jpeg_data = decoded_->jpeg_data.get(); + size_t num_components = jpeg_data->components.size(); + bool is_gray = (num_components == 1); + auto jpeg_c_map = JpegOrder(frame_header_.color_transform, is_gray); + for (size_t c = 0; c < num_components; c++) { + // TODO(eustas): why 1-st quant table for gray? + size_t quant_c = is_gray ? 1 : c; + size_t qpos = jpeg_data->components[jpeg_c_map[c]].quant_idx; + JXL_CHECK(qpos != jpeg_data->quant.size()); + for (size_t x = 0; x < 8; x++) { + for (size_t y = 0; y < 8; y++) { + jpeg_data->quant[qpos].values[x * 8 + y] = + (*qe[0].qraw.qtable)[quant_c * 64 + y * 8 + x]; + } + } + } + } + // Set memory buffer for pre-color-transform frame, if needed. + if (frame_header_.needs_color_transform() && + frame_header_.save_before_color_transform) { + dec_state_->pre_color_transform_frame = + Image3F(frame_dim_.xsize_upsampled, frame_dim_.ysize_upsampled); + } else { + // clear pre_color_transform_frame to ensure that previously moved-from + // images are not used. + dec_state_->pre_color_transform_frame = Image3F(); + } + decoded_ac_global_ = true; + return true; +} + +Status FrameDecoder::ProcessACGroup(size_t ac_group_id, + BitReader* JXL_RESTRICT* br, + size_t num_passes, size_t thread, + bool force_draw, bool dc_only) { + PROFILER_ZONE("process_group"); + 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; + + if (frame_header_.encoding == FrameEncoding::kVarDCT) { + group_dec_caches_[thread].InitOnce(frame_header_.passes.num_passes, + dec_state_->used_acs); + JXL_RETURN_IF_ERROR(DecodeGroup( + br, num_passes, ac_group_id, dec_state_, &group_dec_caches_[thread], + thread, decoded_, decoded_passes_per_ac_group_[ac_group_id], force_draw, + dc_only)); + } + + // don't limit to image dimensions here (is done in DecodeGroup) + const Rect mrect(x, y, frame_dim_.group_dim, frame_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); + if (i >= decoded_passes_per_ac_group_[ac_group_id] && + i < decoded_passes_per_ac_group_[ac_group_id] + num_passes) { + JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( + mrect, br[i - decoded_passes_per_ac_group_[ac_group_id]], minShift, + maxShift, ModularStreamId::ModularAC(ac_group_id, i), + /*zerofill=*/false, dec_state_, decoded_)); + } else if (i >= decoded_passes_per_ac_group_[ac_group_id] + num_passes && + force_draw) { + JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeGroup( + mrect, nullptr, minShift, maxShift, + ModularStreamId::ModularAC(ac_group_id, i), /*zerofill=*/true, + dec_state_, decoded_)); + } + } + decoded_passes_per_ac_group_[ac_group_id] += num_passes; + return true; +} + +Status FrameDecoder::ProcessSections(const SectionInfo* sections, size_t num, + SectionStatus* section_status) { + if (num == 0) return true; // Nothing to process + std::fill(section_status, section_status + num, SectionStatus::kSkipped); + size_t dc_global_sec = num; + size_t ac_global_sec = num; + std::vector dc_group_sec(frame_dim_.num_dc_groups, num); + std::vector> ac_group_sec( + frame_dim_.num_groups, + std::vector(frame_header_.passes.num_passes, num)); + std::vector num_ac_passes(frame_dim_.num_groups); + if (frame_dim_.num_groups == 1 && frame_header_.passes.num_passes == 1) { + JXL_ASSERT(num == 1); + JXL_ASSERT(sections[0].id == 0); + if (processed_section_[0] == false) { + processed_section_[0] = true; + ac_group_sec[0].resize(1); + dc_global_sec = ac_global_sec = dc_group_sec[0] = ac_group_sec[0][0] = 0; + num_ac_passes[0] = 1; + } else { + section_status[0] = SectionStatus::kDuplicate; + } + } else { + size_t ac_global_index = frame_dim_.num_dc_groups + 1; + for (size_t i = 0; i < num; i++) { + JXL_ASSERT(sections[i].id < processed_section_.size()); + if (processed_section_[sections[i].id]) { + section_status[i] = SectionStatus::kDuplicate; + continue; + } + if (sections[i].id == 0) { + dc_global_sec = i; + } else if (sections[i].id < ac_global_index) { + dc_group_sec[sections[i].id - 1] = i; + } else if (sections[i].id == ac_global_index) { + ac_global_sec = i; + } else { + size_t ac_idx = sections[i].id - ac_global_index - 1; + size_t acg = ac_idx % frame_dim_.num_groups; + size_t acp = ac_idx / frame_dim_.num_groups; + if (acp >= frame_header_.passes.num_passes) { + return JXL_FAILURE("Invalid section ID"); + } + if (acp >= max_passes_) { + continue; + } + ac_group_sec[acg][acp] = i; + } + processed_section_[sections[i].id] = true; + } + // Count number of new passes per group. + for (size_t g = 0; g < ac_group_sec.size(); g++) { + size_t j = 0; + for (; j + decoded_passes_per_ac_group_[g] < max_passes_; j++) { + if (ac_group_sec[g][j + decoded_passes_per_ac_group_[g]] == num) { + break; + } + } + num_ac_passes[g] = j; + } + } + if (dc_global_sec != num) { + Status dc_global_status = ProcessDCGlobal(sections[dc_global_sec].br); + if (dc_global_status.IsFatalError()) return dc_global_status; + if (dc_global_status) { + section_status[dc_global_sec] = SectionStatus::kDone; + } else { + section_status[dc_global_sec] = SectionStatus::kPartial; + } + } + + std::atomic has_error{false}; + if (decoded_dc_global_) { + RunOnPool( + pool_, 0, dc_group_sec.size(), ThreadPool::SkipInit(), + [this, &dc_group_sec, &num, §ions, §ion_status, &has_error]( + size_t i, size_t thread) { + if (dc_group_sec[i] != num) { + if (!ProcessDCGroup(i, sections[dc_group_sec[i]].br)) { + has_error = true; + } else { + section_status[dc_group_sec[i]] = SectionStatus::kDone; + } + } + }, + "DecodeDCGroup"); + } + if (has_error) return JXL_FAILURE("Error in DC group"); + + if (*std::min_element(decoded_dc_groups_.begin(), decoded_dc_groups_.end()) == + true && + !finalized_dc_) { + FinalizeDC(); + AllocateOutput(); + } + + if (finalized_dc_) dec_state_->EnsureBordersStorage(); + if (finalized_dc_ && ac_global_sec != num && !decoded_ac_global_) { + JXL_RETURN_IF_ERROR(ProcessACGlobal(sections[ac_global_sec].br)); + section_status[ac_global_sec] = SectionStatus::kDone; + } + + if (decoded_ac_global_) { + // The decoded image requires padding for filtering. ProcessACGlobal added + // the padding, however when Flush is used, the image is shrunk to the + // output size. Add the padding back here. This is a cheap operation + // since the image has the original allocated size. The memory and original + // size are already there, but for safety we require the indicated xsize and + // ysize dimensions match the working area, see PlaneRowBoundsCheck. + decoded_->ShrinkTo(frame_dim_.xsize_upsampled_padded, + frame_dim_.ysize_upsampled_padded); + + // Mark all the AC groups that we received as not complete yet. + for (size_t i = 0; i < ac_group_sec.size(); i++) { + if (num_ac_passes[i] == 0) continue; + dec_state_->group_border_assigner.ClearDone(i); + } + + RunOnPool( + pool_, 0, ac_group_sec.size(), + [this](size_t num_threads) { + PrepareStorage(num_threads, decoded_passes_per_ac_group_.size()); + return true; + }, + [this, &ac_group_sec, &num_ac_passes, &num, §ions, §ion_status, + &has_error](size_t g, size_t thread) { + if (num_ac_passes[g] == 0) { // no new AC pass, nothing to do. + return; + } + (void)num; + size_t first_pass = decoded_passes_per_ac_group_[g]; + BitReader* JXL_RESTRICT readers[kMaxNumPasses]; + for (size_t i = 0; i < num_ac_passes[g]; i++) { + JXL_ASSERT(ac_group_sec[g][first_pass + i] != num); + readers[i] = sections[ac_group_sec[g][first_pass + i]].br; + } + if (!ProcessACGroup(g, readers, num_ac_passes[g], + GetStorageLocation(thread, g), + /*force_draw=*/false, /*dc_only=*/false)) { + has_error = true; + } else { + for (size_t i = 0; i < num_ac_passes[g]; i++) { + section_status[ac_group_sec[g][first_pass + i]] = + SectionStatus::kDone; + } + } + }, + "DecodeGroup"); + } + if (has_error) return JXL_FAILURE("Error in AC group"); + + for (size_t i = 0; i < num; i++) { + if (section_status[i] == SectionStatus::kSkipped || + section_status[i] == SectionStatus::kPartial) { + processed_section_[sections[i].id] = false; + } + } + return true; +} + +Status FrameDecoder::Flush() { + bool has_blending = frame_header_.blending_info.mode != BlendMode::kReplace || + frame_header_.custom_size_or_origin; + for (const auto& blending_info_ec : + frame_header_.extra_channel_blending_info) { + if (blending_info_ec.mode != BlendMode::kReplace) has_blending = true; + } + // No early Flush() if blending is enabled. + if (has_blending && !is_finalized_) { + return false; + } + // No early Flush() - nothing to do - if the frame is a kSkipProgressive + // frame. + if (frame_header_.frame_type == FrameType::kSkipProgressive && + !is_finalized_) { + return true; + } + if (decoded_->IsJPEG()) { + // Nothing to do. + return true; + } + AllocateOutput(); + + uint32_t completely_decoded_ac_pass = *std::min_element( + decoded_passes_per_ac_group_.begin(), decoded_passes_per_ac_group_.end()); + if (completely_decoded_ac_pass < frame_header_.passes.num_passes) { + // We don't have all AC yet: force a draw of all the missing areas. + // Mark all sections as not complete. + for (size_t i = 0; i < decoded_passes_per_ac_group_.size(); i++) { + if (decoded_passes_per_ac_group_[i] == frame_header_.passes.num_passes) + continue; + dec_state_->group_border_assigner.ClearDone(i); + } + std::atomic has_error{false}; + RunOnPool( + pool_, 0, decoded_passes_per_ac_group_.size(), + [this](size_t num_threads) { + PrepareStorage(num_threads, decoded_passes_per_ac_group_.size()); + return true; + }, + [this, &has_error](size_t g, size_t thread) { + if (decoded_passes_per_ac_group_[g] == + frame_header_.passes.num_passes) { + // This group was drawn already, nothing to do. + return; + } + BitReader* JXL_RESTRICT readers[kMaxNumPasses] = {}; + bool ok = ProcessACGroup( + g, readers, /*num_passes=*/0, GetStorageLocation(thread, g), + /*force_draw=*/true, /*dc_only=*/!decoded_ac_global_); + if (!ok) has_error = true; + }, + "ForceDrawGroup"); + if (has_error) { + return JXL_FAILURE("Drawing groups failed"); + } + } + // TODO(veluca): the rest of this function should be removed once we have full + // support for per-group decoding. + + // undo global modular transforms and copy int pixel buffers to float ones + JXL_RETURN_IF_ERROR( + modular_frame_decoder_.FinalizeDecoding(dec_state_, pool_, decoded_)); + + JXL_RETURN_IF_ERROR(FinalizeFrameDecoding(decoded_, dec_state_, pool_, + /*force_fir=*/false, + /*skip_blending=*/false)); + + num_renders_++; + return true; +} + +int FrameDecoder::SavedAs(const FrameHeader& header) { + if (header.frame_type == FrameType::kDCFrame) { + // bits 16, 32, 64, 128 for DC level + return 16 << (header.dc_level - 1); + } else if (header.CanBeReferenced()) { + // bits 1, 2, 4 and 8 for the references + return 1 << header.save_as_reference; + } + + return 0; +} + +int FrameDecoder::References() const { + if (is_finalized_) { + return 0; + } + if ((!decoded_dc_global_ || !decoded_ac_global_ || + *std::min_element(decoded_dc_groups_.begin(), + decoded_dc_groups_.end()) != 1 || + *std::min_element(decoded_passes_per_ac_group_.begin(), + decoded_passes_per_ac_group_.end()) < max_passes_)) { + return 0; + } + + int result = 0; + + // Blending + if (frame_header_.frame_type == FrameType::kRegularFrame || + frame_header_.frame_type == FrameType::kSkipProgressive) { + bool cropped = frame_header_.custom_size_or_origin; + if (cropped || frame_header_.blending_info.mode != BlendMode::kReplace) { + result |= (1 << frame_header_.blending_info.source); + } + const auto& extra = frame_header_.extra_channel_blending_info; + for (size_t i = 0; i < extra.size(); ++i) { + if (cropped || extra[i].mode != BlendMode::kReplace) { + result |= (1 << extra[i].source); + } + } + } + + // Patches + if (frame_header_.flags & FrameHeader::kPatches) { + result |= dec_state_->shared->image_features.patches.GetReferences(); + } + + // DC Level + if (frame_header_.flags & FrameHeader::kUseDcFrame) { + // Reads from the next dc level + int dc_level = frame_header_.dc_level + 1; + // bits 16, 32, 64, 128 for DC level + result |= (16 << (dc_level - 1)); + } + + return result; +} + +Status FrameDecoder::FinalizeFrame() { + if (is_finalized_) { + return JXL_FAILURE("FinalizeFrame called multiple times"); + } + is_finalized_ = true; + if (decoded_->IsJPEG()) { + // Nothing to do. + return true; + } + if (!finalized_dc_) { + // We don't have all of DC: EPF might not behave correctly (and is not + // particularly useful anyway on upsampling results), so we disable it. + dec_state_->shared_storage.frame_header.loop_filter.epf_iters = 0; + } + if ((!decoded_dc_global_ || !decoded_ac_global_ || + *std::min_element(decoded_dc_groups_.begin(), + decoded_dc_groups_.end()) != 1 || + *std::min_element(decoded_passes_per_ac_group_.begin(), + decoded_passes_per_ac_group_.end()) < max_passes_) && + !allow_partial_frames_) { + return JXL_FAILURE( + "FinalizeFrame called before the frame was fully decoded"); + } + + if (!finalized_dc_) { + JXL_ASSERT(allow_partial_frames_); + AllocateOutput(); + } + + JXL_RETURN_IF_ERROR(Flush()); + + if (dec_state_->shared->frame_header.CanBeReferenced()) { + size_t id = dec_state_->shared->frame_header.save_as_reference; + auto& reference_frame = dec_state_->shared_storage.reference_frames[id]; + if (dec_state_->pre_color_transform_frame.xsize() == 0) { + reference_frame.storage = decoded_->Copy(); + } else { + reference_frame.storage = ImageBundle(decoded_->metadata()); + reference_frame.storage.SetFromImage( + std::move(dec_state_->pre_color_transform_frame), + decoded_->c_current()); + if (decoded_->HasExtraChannels()) { + const std::vector* ecs = &dec_state_->pre_color_transform_ec; + if (ecs->empty()) ecs = &decoded_->extra_channels(); + std::vector extra_channels; + for (const auto& ec : *ecs) { + extra_channels.push_back(CopyImage(ec)); + } + reference_frame.storage.SetExtraChannels(std::move(extra_channels)); + } + } + reference_frame.frame = &reference_frame.storage; + reference_frame.ib_is_in_xyb = + dec_state_->shared->frame_header.save_before_color_transform; + if (!dec_state_->shared->frame_header.save_before_color_transform) { + const CodecMetadata* metadata = + dec_state_->shared->frame_header.nonserialized_metadata; + if (reference_frame.frame->xsize() < metadata->xsize() || + reference_frame.frame->ysize() < metadata->ysize()) { + return JXL_FAILURE( + "trying to save a reference frame that is too small: %zux%zu " + "instead of %zux%zu", + reference_frame.frame->xsize(), reference_frame.frame->ysize(), + metadata->xsize(), metadata->ysize()); + } + reference_frame.storage.ShrinkTo(metadata->xsize(), metadata->ysize()); + } + } + if (frame_header_.nonserialized_is_preview) { + // Fix possible larger image size (multiple of kBlockDim) + // TODO(lode): verify if and when that happens. + decoded_->ShrinkTo(frame_dim_.xsize, frame_dim_.ysize); + } else if (!decoded_->IsJPEG()) { + // A kRegularFrame is blended with the other frames, and thus results in a + // coalesced frame of size equal to image dimensions. Other frames are not + // blended, thus their final size is the size that was defined in the + // frame_header. + if (frame_header_.frame_type == kRegularFrame || + frame_header_.frame_type == kSkipProgressive) { + decoded_->ShrinkTo( + dec_state_->shared->frame_header.nonserialized_metadata->xsize(), + dec_state_->shared->frame_header.nonserialized_metadata->ysize()); + } else { + // xsize_upsampled is the actual frame size, after any upsampling has been + // applied. + decoded_->ShrinkTo(frame_dim_.xsize_upsampled, + frame_dim_.ysize_upsampled); + } + } + + if (render_spotcolors_) { + for (size_t i = 0; i < decoded_->extra_channels().size(); i++) { + // Don't use Find() because there may be multiple spot color channels. + const ExtraChannelInfo& eci = decoded_->metadata()->extra_channel_info[i]; + if (eci.type == ExtraChannel::kOptional) { + continue; + } + if (eci.type == ExtraChannel::kUnknown || + (int(ExtraChannel::kReserved0) <= int(eci.type) && + int(eci.type) <= int(ExtraChannel::kReserved7))) { + return JXL_FAILURE( + "Unknown extra channel (bits %u, shift %u, name '%s')\n", + eci.bit_depth.bits_per_sample, eci.dim_shift, eci.name.c_str()); + } + if (eci.type == ExtraChannel::kSpotColor) { + float scale = eci.spot_color[3]; + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < decoded_->ysize(); y++) { + float* JXL_RESTRICT p = decoded_->color()->Plane(c).Row(y); + const float* JXL_RESTRICT s = + decoded_->extra_channels()[i].ConstRow(y); + for (size_t x = 0; x < decoded_->xsize(); x++) { + float mix = scale * s[x]; + p[x] = mix * eci.spot_color[c] + (1.0 - mix) * p[x]; + } + } + } + } + } + } + if (dec_state_->shared->frame_header.dc_level != 0) { + dec_state_->shared_storage + .dc_frames[dec_state_->shared->frame_header.dc_level - 1] = + std::move(*decoded_->color()); + decoded_->RemoveColor(); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/dec_frame.h b/lib/jxl/dec_frame.h new file mode 100644 index 0000000..db4e752 --- /dev/null +++ b/lib/jxl/dec_frame.h @@ -0,0 +1,282 @@ +// 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_DEC_FRAME_H_ +#define LIB_JXL_DEC_FRAME_H_ + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/blending.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_modular.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// TODO(veluca): remove DecodeFrameHeader once the API migrates to FrameDecoder. + +// `frame_header` must have nonserialized_metadata and +// nonserialized_is_preview set. +Status DecodeFrameHeader(BitReader* JXL_RESTRICT reader, + FrameHeader* JXL_RESTRICT frame_header); + +// Decodes a frame. Groups may be processed in parallel by `pool`. +// See DecodeFile for explanation of c_decoded. +// `io` is only used for reading maximum image size. Also updates +// `dec_state` with the new frame header. +// `metadata` is the metadata that applies to all frames of the codestream +// `decoded->metadata` must already be set and must match metadata.m. +Status DecodeFrame(const DecompressParams& dparams, + PassesDecoderState* dec_state, ThreadPool* JXL_RESTRICT pool, + BitReader* JXL_RESTRICT reader, ImageBundle* decoded, + const CodecMetadata& metadata, + const SizeConstraints* constraints, bool is_preview = false); + +// Leaves reader in the same state as DecodeFrame would. Used to skip preview. +// Also updates `dec_state` with the new frame header. +Status SkipFrame(const CodecMetadata& metadata, BitReader* JXL_RESTRICT reader, + bool is_preview = false); + +// TODO(veluca): implement "forced drawing". +class FrameDecoder { + public: + // All parameters must outlive the FrameDecoder. + FrameDecoder(PassesDecoderState* dec_state, const CodecMetadata& metadata, + ThreadPool* pool) + : dec_state_(dec_state), pool_(pool), frame_header_(&metadata) {} + + // `constraints` must outlive the FrameDecoder if not null, or stay alive + // until the next call to SetFrameSizeLimits. + void SetFrameSizeLimits(const SizeConstraints* constraints) { + constraints_ = constraints; + } + void SetRenderSpotcolors(bool rsc) { render_spotcolors_ = rsc; } + + // Read FrameHeader and table of contents from the given BitReader. + // Also checks frame dimensions for their limits, and sets the output + // image buffer. + // TODO(veluca): remove the `allow_partial_frames` flag - this should be moved + // on callers. + Status InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded, + bool is_preview, bool allow_partial_frames, + bool allow_partial_dc_global); + + struct SectionInfo { + BitReader* JXL_RESTRICT br; + size_t id; + }; + + enum SectionStatus { + // Processed correctly. + kDone = 0, + // Skipped because other required sections were not yet processed. + kSkipped = 1, + // Skipped because the section was already processed. + kDuplicate = 2, + // Only partially decoded: the section will need to be processed again. + kPartial = 3, + }; + + // Processes `num` sections; each SectionInfo contains the index + // of the section and a BitReader that only contains the data of the section. + // `section_status` should point to `num` elements, and will be filled with + // information about whether each section was processed or not. + // A section is a part of the encoded file that is indexed by the TOC. + Status ProcessSections(const SectionInfo* sections, size_t num, + SectionStatus* section_status); + + // Flushes all the data decoded so far to pixels. + Status Flush(); + + // Runs final operations once a frame data is decoded. + // Must be called exactly once per frame, after all calls to ProcessSections. + Status FinalizeFrame(); + + // Returns dependencies of this frame on reference ids as a bit mask: bits 0-3 + // indicate reference frame 0-3 for patches and blending, bits 4-7 indicate DC + // frames this frame depends on. Only returns a valid result after all calls + // to ProcessSections are finished and before FinalizeFrame. + int References() const; + + // Returns reference id of storage location where this frame is stored as a + // bit flag, or 0 if not stored. + // Matches the bit mask used for GetReferences: bits 0-3 indicate it is stored + // for patching or blending, bits 4-7 indicate DC frame. + // Unlike References, can be ran at any time as + // soon as the frame header is known. + static int SavedAs(const FrameHeader& header); + + // Returns offset of this section after the end of the TOC. The end of the TOC + // is the byte position of the bit reader after InitFrame was called. + const std::vector& SectionOffsets() const { + return section_offsets_; + } + const std::vector& SectionSizes() const { return section_sizes_; } + size_t NumSections() const { return section_sizes_.size(); } + + // TODO(veluca): remove once we remove --downsampling flag. + void SetMaxPasses(size_t max_passes) { max_passes_ = max_passes; } + const FrameHeader& GetFrameHeader() const { return frame_header_; } + + // Returns whether a DC image has been decoded, accessible at low resolution + // at passes.shared_storage.dc_storage + bool HasDecodedDC() const { + return frame_header_.encoding == FrameEncoding::kVarDCT && finalized_dc_; + } + + // Sets the buffer to which uint8 sRGB pixels will be decoded. This is not + // supported for all images. If it succeeds, HasRGBBuffer() will return true. + // If it does not succeed, the image is decoded to the ImageBundle passed to + // InitFrame instead. + // If an output callback is set, this function *may not* be called. + // + // @param undo_orientation: if true, indicates the frame decoder should apply + // the exif orientation to bring the image to the intended display + // 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 MaybeSetRGB8OutputBuffer(uint8_t* rgb_output, size_t stride, + bool is_rgba, bool undo_orientation) const { + if (!CanDoLowMemoryPath(undo_orientation)) return; + 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); +#if !JXL_HIGH_PRECISION + if (decoded_->metadata()->xyb_encoded && + dec_state_->output_encoding_info.color_encoding.IsSRGB() && + dec_state_->output_encoding_info.all_default_opsin && + HasFastXYBTosRGB8() && frame_header_.needs_color_transform()) { + dec_state_->fast_xyb_srgb8_conversion = true; + } +#endif + } + + // Same as MaybeSetRGB8OutputBuffer, but with a float callback. This is not + // supported for all images. If it succeeds, HasRGBBuffer() will return true. + // If it does not succeed, the image is decoded to the ImageBundle passed to + // InitFrame instead. + // If a RGB8 output buffer is set, this function *may not* be called. + // + // @param undo_orientation: if true, indicates the frame decoder should apply + // the exif orientation to bring the image to the intended display + // 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& cb, + bool is_rgba, bool undo_orientation) const { + if (!CanDoLowMemoryPath(undo_orientation)) return; + dec_state_->pixel_callback = cb; + dec_state_->rgb_output_is_rgba = is_rgba; + JXL_ASSERT(dec_state_->rgb_output == nullptr); + } + + // Returns true if the rgb output buffer passed by MaybeSetRGB8OutputBuffer + // has been/will be populated by Flush() / FinalizeFrame(), or if a pixel + // callback has been used. + bool HasRGBBuffer() const { + return dec_state_->rgb_output != nullptr || + dec_state_->pixel_callback != nullptr; + } + + private: + Status ProcessDCGlobal(BitReader* br); + Status ProcessDCGroup(size_t dc_group_id, BitReader* br); + void FinalizeDC(); + void AllocateOutput(); + Status ProcessACGlobal(BitReader* br); + Status ProcessACGroup(size_t ac_group_id, BitReader* JXL_RESTRICT* br, + size_t num_passes, size_t thread, bool force_draw, + bool dc_only); + + // Allocates storage for parallel decoding using up to `num_threads` threads + // of up to `num_tasks` tasks. The value of `thread` passed to + // `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) { + size_t storage_size = std::min(num_threads, num_tasks); + if (storage_size > group_dec_caches_.size()) { + group_dec_caches_.resize(storage_size); + } + dec_state_->EnsureStorage(storage_size); + use_task_id_ = num_threads > num_tasks; + } + + size_t GetStorageLocation(size_t thread, size_t task) { + if (use_task_id_) return task; + return thread; + } + + // If the image has default exif orientation (or has an orientation but should + // not be undone) and no blending, the current frame cannot be referenced by + // future frames, there are no spot colors to be rendered, and alpha is not + // premultiplied, then low memory options can be used + // (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 (ImageBlender::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; + } + + PassesDecoderState* dec_state_; + ThreadPool* pool_; + std::vector section_offsets_; + std::vector section_sizes_; + size_t max_passes_; + // TODO(veluca): figure out the duplication between these and dec_state_. + FrameHeader frame_header_; + FrameDimensions frame_dim_; + ImageBundle* decoded_; + ModularFrameDecoder modular_frame_decoder_; + bool allow_partial_frames_; + bool allow_partial_dc_global_; + bool render_spotcolors_ = true; + + std::vector processed_section_; + std::vector decoded_passes_per_ac_group_; + std::vector decoded_dc_groups_; + bool decoded_dc_global_; + bool decoded_ac_global_; + bool finalized_dc_ = true; + bool is_finalized_ = true; + size_t num_renders_ = 0; + bool allocated_ = false; + + std::vector group_dec_caches_; + + // Frame size limits. + const SizeConstraints* constraints_ = nullptr; + + // Whether or not the task id should be used for storage indexing, instead of + // the thread id. + bool use_task_id_ = false; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_FRAME_H_ diff --git a/lib/jxl/dec_group.cc b/lib/jxl/dec_group.cc new file mode 100644 index 0000000..0f51891 --- /dev/null +++ b/lib/jxl/dec_group.cc @@ -0,0 +1,786 @@ +// 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/dec_group.h" + +#include +#include + +#include +#include +#include + +#include "lib/jxl/frame_header.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_group.cc" +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/common.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_reconstruct.h" +#include "lib/jxl/dec_transforms-inl.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer-inl.h" +#include "lib/jxl/quantizer.h" + +#ifndef LIB_JXL_DEC_GROUP_CC +#define LIB_JXL_DEC_GROUP_CC +namespace jxl { + +// Interface for reading groups for DecodeGroupImpl. +class GetBlock { + public: + virtual void StartRow(size_t by) = 0; + virtual Status LoadBlock(size_t bx, size_t by, const AcStrategy& acs, + size_t size, size_t log2_covered_blocks, + ACPtr block[3], ACType ac_type) = 0; + virtual ~GetBlock() {} +}; + +// Controls whether DecodeGroupImpl renders to pixels or not. +enum DrawMode { + // Render to pixels. + kDraw = 0, + // Don't render to pixels. + kDontDraw = 1, + // Don't do IDCT or dequantization, but just postprocessing. Used for + // progressive DC. + kOnlyImageFeatures = 2, +}; + +} // namespace jxl +#endif // LIB_JXL_DEC_GROUP_CC + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::ShiftRight; + +using D = HWY_FULL(float); +using DU = HWY_FULL(uint32_t); +using DI = HWY_FULL(int32_t); +using DI16 = Rebind; +constexpr D d; +constexpr DI di; +constexpr DI16 di16; + +// TODO(veluca): consider SIMDfying. +void Transpose8x8InPlace(int32_t* JXL_RESTRICT block) { + for (size_t x = 0; x < 8; x++) { + for (size_t y = x + 1; y < 8; y++) { + std::swap(block[y * 8 + x], block[x * 8 + y]); + } + } +} + +template +void DequantLane(Vec scaled_dequant_x, Vec scaled_dequant_y, + Vec scaled_dequant_b, + const float* JXL_RESTRICT dequant_matrices, size_t dq_ofs, + size_t size, size_t k, Vec x_cc_mul, Vec b_cc_mul, + const float* JXL_RESTRICT biases, ACPtr qblock[3], + float* JXL_RESTRICT block) { + const auto x_mul = Load(d, dequant_matrices + dq_ofs + k) * scaled_dequant_x; + const auto y_mul = + Load(d, dequant_matrices + dq_ofs + size + k) * scaled_dequant_y; + const auto b_mul = + Load(d, dequant_matrices + dq_ofs + 2 * size + k) * scaled_dequant_b; + + Vec quantized_x_int; + Vec quantized_y_int; + Vec quantized_b_int; + if (ac_type == ACType::k16) { + Rebind di16; + quantized_x_int = PromoteTo(di, Load(di16, qblock[0].ptr16 + k)); + quantized_y_int = PromoteTo(di, Load(di16, qblock[1].ptr16 + k)); + quantized_b_int = PromoteTo(di, Load(di16, qblock[2].ptr16 + k)); + } else { + quantized_x_int = Load(di, qblock[0].ptr32 + k); + quantized_y_int = Load(di, qblock[1].ptr32 + k); + quantized_b_int = Load(di, qblock[2].ptr32 + k); + } + + const auto dequant_x_cc = + AdjustQuantBias(di, 0, quantized_x_int, biases) * x_mul; + const auto dequant_y = + AdjustQuantBias(di, 1, quantized_y_int, biases) * y_mul; + const auto dequant_b_cc = + AdjustQuantBias(di, 2, quantized_b_int, biases) * b_mul; + + const auto dequant_x = MulAdd(x_cc_mul, dequant_y, dequant_x_cc); + const auto dequant_b = MulAdd(b_cc_mul, dequant_y, dequant_b_cc); + Store(dequant_x, d, block + k); + Store(dequant_y, d, block + size + k); + Store(dequant_b, d, block + 2 * size + k); +} + +template +void DequantBlock(const AcStrategy& acs, float inv_global_scale, int quant, + float x_dm_multiplier, float b_dm_multiplier, Vec x_cc_mul, + Vec b_cc_mul, size_t kind, size_t size, + const Quantizer& quantizer, + const float* JXL_RESTRICT dequant_matrices, + size_t covered_blocks, const size_t* sbx, + const float* JXL_RESTRICT* JXL_RESTRICT dc_row, + size_t dc_stride, const float* JXL_RESTRICT biases, + ACPtr qblock[3], float* JXL_RESTRICT block) { + PROFILER_FUNC; + + const auto scaled_dequant_s = inv_global_scale / quant; + + const auto scaled_dequant_x = Set(d, scaled_dequant_s * x_dm_multiplier); + const auto scaled_dequant_y = Set(d, scaled_dequant_s); + const auto scaled_dequant_b = Set(d, scaled_dequant_s * b_dm_multiplier); + + const size_t dq_ofs = quantizer.DequantMatrixOffset(kind, 0); + + for (size_t k = 0; k < covered_blocks * kDCTBlockSize; k += Lanes(d)) { + DequantLane(scaled_dequant_x, scaled_dequant_y, scaled_dequant_b, + dequant_matrices, dq_ofs, size, k, x_cc_mul, b_cc_mul, + biases, qblock, block); + } + for (size_t c = 0; c < 3; c++) { + LowestFrequenciesFromDC(acs.Strategy(), dc_row[c] + sbx[c], dc_stride, + block + c * size); + } +} + +Status DecodeGroupImpl(GetBlock* JXL_RESTRICT get_block, + GroupDecCache* JXL_RESTRICT group_dec_cache, + PassesDecoderState* JXL_RESTRICT dec_state, + size_t thread, size_t group_idx, ImageBundle* decoded, + DrawMode draw) { + // TODO(veluca): investigate cache usage in this function. + PROFILER_FUNC; + constexpr size_t kGroupDataXBorder = PassesDecoderState::kGroupDataXBorder; + constexpr size_t kGroupDataYBorder = PassesDecoderState::kGroupDataYBorder; + + const Rect block_rect = dec_state->shared->BlockGroupRect(group_idx); + const AcStrategyImage& ac_strategy = dec_state->shared->ac_strategy; + + const size_t xsize_blocks = block_rect.xsize(); + const size_t ysize_blocks = block_rect.ysize(); + + const size_t dc_stride = dec_state->shared->dc->PixelsPerRow(); + + const float inv_global_scale = dec_state->shared->quantizer.InvGlobalScale(); + const float* JXL_RESTRICT dequant_matrices = + dec_state->shared->quantizer.DequantMatrix(0, 0); + + const YCbCrChromaSubsampling& cs = + dec_state->shared->frame_header.chroma_subsampling; + + const size_t idct_stride = dec_state->EagerFinalizeImageRect() + ? dec_state->group_data[thread].PixelsPerRow() + : dec_state->decoded.PixelsPerRow(); + + HWY_ALIGN int32_t scaled_qtable[64 * 3]; + + ACType ac_type = dec_state->coefficients->Type(); + auto dequant_block = ac_type == ACType::k16 ? DequantBlock + : DequantBlock; + // Whether or not coefficients should be stored for future usage, and/or read + // from past usage. + bool accumulate = !dec_state->coefficients->IsEmpty(); + // Offset of the current block in the group. + size_t offset = 0; + + std::array jpeg_c_map; + bool jpeg_is_gray = false; + std::array dcoff = {}; + + // TODO(veluca): all of this should be done only once per image. + if (decoded->IsJPEG()) { + if (!dec_state->shared->cmap.IsJPEGCompatible()) { + return JXL_FAILURE("The CfL map is not JPEG-compatible"); + } + jpeg_is_gray = (decoded->jpeg_data->components.size() == 1); + jpeg_c_map = JpegOrder(dec_state->shared->frame_header.color_transform, + jpeg_is_gray); + const std::vector& qe = + dec_state->shared->matrices.encodings(); + if (qe.empty() || qe[0].mode != QuantEncoding::Mode::kQuantModeRAW || + std::abs(qe[0].qraw.qtable_den - 1.f / (8 * 255)) > 1e-8f) { + return JXL_FAILURE( + "Quantization table is not a JPEG quantization table."); + } + for (size_t c = 0; c < 3; c++) { + if (dec_state->shared->frame_header.color_transform == + ColorTransform::kNone) { + dcoff[c] = 1024 / (*qe[0].qraw.qtable)[64 * c]; + } + for (size_t i = 0; i < 64; i++) { + // Transpose the matrix, as it will be used on the transposed block. + int n = qe[0].qraw.qtable->at(64 + i); + int d = qe[0].qraw.qtable->at(64 * c + i); + if (n <= 0 || d <= 0 || n >= 65536 || d >= 65536) { + return JXL_FAILURE("Invalid JPEG quantization table"); + } + scaled_qtable[64 * c + (i % 8) * 8 + (i / 8)] = + (1 << kCFLFixedPointPrecision) * n / d; + } + } + } + + size_t hshift[3] = {cs.HShift(0), cs.HShift(1), cs.HShift(2)}; + size_t vshift[3] = {cs.VShift(0), cs.VShift(1), cs.VShift(2)}; + Rect r[3]; + for (size_t i = 0; i < 3; i++) { + r[i] = + Rect(block_rect.x0() >> hshift[i], block_rect.y0() >> vshift[i], + block_rect.xsize() >> hshift[i], block_rect.ysize() >> vshift[i]); + } + + for (size_t by = 0; by < ysize_blocks; ++by) { + if (draw == kOnlyImageFeatures) break; + get_block->StartRow(by); + size_t sby[3] = {by >> vshift[0], by >> vshift[1], by >> vshift[2]}; + + const int32_t* JXL_RESTRICT row_quant = + block_rect.ConstRow(dec_state->shared->raw_quant_field, by); + + const float* JXL_RESTRICT dc_rows[3] = { + r[0].ConstPlaneRow(*dec_state->shared->dc, 0, sby[0]), + r[1].ConstPlaneRow(*dec_state->shared->dc, 1, sby[1]), + r[2].ConstPlaneRow(*dec_state->shared->dc, 2, sby[2]), + }; + + const size_t ty = (block_rect.y0() + by) / kColorTileDimInBlocks; + AcStrategyRow acs_row = ac_strategy.ConstRow(block_rect, by); + + const int8_t* JXL_RESTRICT row_cmap[3] = { + dec_state->shared->cmap.ytox_map.ConstRow(ty), + nullptr, + dec_state->shared->cmap.ytob_map.ConstRow(ty), + }; + + float* JXL_RESTRICT idct_row[3]; + int16_t* JXL_RESTRICT jpeg_row[3]; + for (size_t c = 0; c < 3; c++) { + if (dec_state->EagerFinalizeImageRect()) { + idct_row[c] = dec_state->group_data[thread].PlaneRow( + c, sby[c] * kBlockDim + kGroupDataYBorder) + + kGroupDataXBorder; + } else { + idct_row[c] = + dec_state->decoded.PlaneRow(c, (r[c].y0() + sby[c]) * kBlockDim) + + r[c].x0() * kBlockDim; + } + if (decoded->IsJPEG()) { + auto& component = decoded->jpeg_data->components[jpeg_c_map[c]]; + jpeg_row[c] = + component.coeffs.data() + + (component.width_in_blocks * (r[c].y0() + sby[c]) + r[c].x0()) * + kDCTBlockSize; + } + } + + size_t bx = 0; + for (size_t tx = 0; tx < DivCeil(xsize_blocks, kColorTileDimInBlocks); + tx++) { + size_t abs_tx = tx + block_rect.x0() / kColorTileDimInBlocks; + auto x_cc_mul = + Set(d, dec_state->shared->cmap.YtoXRatio(row_cmap[0][abs_tx])); + auto b_cc_mul = + Set(d, dec_state->shared->cmap.YtoBRatio(row_cmap[2][abs_tx])); + // Increment bx by llf_x because those iterations would otherwise + // immediately continue (!IsFirstBlock). Reduces mispredictions. + for (; bx < xsize_blocks && bx < (tx + 1) * kColorTileDimInBlocks;) { + size_t sbx[3] = {bx >> hshift[0], bx >> hshift[1], bx >> hshift[2]}; + AcStrategy acs = acs_row[bx]; + const size_t llf_x = acs.covered_blocks_x(); + + // Can only happen in the second or lower rows of a varblock. + if (JXL_UNLIKELY(!acs.IsFirstBlock())) { + bx += llf_x; + continue; + } + PROFILER_ZONE("DecodeGroupImpl inner"); + const size_t log2_covered_blocks = acs.log2_covered_blocks(); + + const size_t covered_blocks = 1 << log2_covered_blocks; + const size_t size = covered_blocks * kDCTBlockSize; + + ACPtr qblock[3]; + if (accumulate) { + for (size_t c = 0; c < 3; c++) { + qblock[c] = dec_state->coefficients->PlaneRow(c, group_idx, offset); + } + } else { + // No point in reading from bitstream without accumulating and not + // drawing. + JXL_ASSERT(draw == kDraw); + if (ac_type == ACType::k16) { + memset(group_dec_cache->dec_group_qblock16, 0, + size * 3 * sizeof(int16_t)); + for (size_t c = 0; c < 3; c++) { + qblock[c].ptr16 = group_dec_cache->dec_group_qblock16 + c * size; + } + } else { + memset(group_dec_cache->dec_group_qblock, 0, + size * 3 * sizeof(int32_t)); + for (size_t c = 0; c < 3; c++) { + qblock[c].ptr32 = group_dec_cache->dec_group_qblock + c * size; + } + } + } + JXL_RETURN_IF_ERROR(get_block->LoadBlock( + bx, by, acs, size, log2_covered_blocks, qblock, ac_type)); + offset += size; + if (draw == kDontDraw) { + bx += llf_x; + continue; + } + + if (JXL_UNLIKELY(decoded->IsJPEG())) { + if (acs.Strategy() != AcStrategy::Type::DCT) { + return JXL_FAILURE( + "Can only decode to JPEG if only DCT-8 is used."); + } + + HWY_ALIGN int32_t transposed_dct_y[64]; + for (size_t c : {1, 0, 2}) { + // Propagate only Y for grayscale. + if (jpeg_is_gray && c != 1) { + continue; + } + if ((sbx[c] << hshift[c] != bx) || (sby[c] << vshift[c] != by)) { + continue; + } + int16_t* JXL_RESTRICT jpeg_pos = + jpeg_row[c] + sbx[c] * kDCTBlockSize; + // JPEG XL is transposed, JPEG is not. + auto transposed_dct = qblock[c].ptr32; + Transpose8x8InPlace(transposed_dct); + // No CfL - no need to store the y block converted to integers. + if (!cs.Is444() || + (row_cmap[0][abs_tx] == 0 && row_cmap[2][abs_tx] == 0)) { + for (size_t i = 0; i < 64; i += Lanes(d)) { + const auto ini = Load(di, transposed_dct + i); + const auto ini16 = DemoteTo(di16, ini); + StoreU(ini16, di16, jpeg_pos + i); + } + } else if (c == 1) { + // Y channel: save for restoring X/B, but nothing else to do. + for (size_t i = 0; i < 64; i += Lanes(d)) { + const auto ini = Load(di, transposed_dct + i); + Store(ini, di, transposed_dct_y + i); + const auto ini16 = DemoteTo(di16, ini); + StoreU(ini16, di16, jpeg_pos + i); + } + } else { + // transposed_dct_y contains the y channel block, transposed. + const auto scale = Set( + di, dec_state->shared->cmap.RatioJPEG(row_cmap[c][abs_tx])); + const auto round = Set(di, 1 << (kCFLFixedPointPrecision - 1)); + for (int i = 0; i < 64; i += Lanes(d)) { + auto in = Load(di, transposed_dct + i); + auto in_y = Load(di, transposed_dct_y + i); + auto qt = Load(di, scaled_qtable + c * size + i); + auto coeff_scale = + ShiftRight(qt * scale + round); + auto cfl_factor = ShiftRight( + in_y * coeff_scale + round); + StoreU(DemoteTo(di16, in + cfl_factor), di16, jpeg_pos + i); + } + } + jpeg_pos[0] = + Clamp1(dc_rows[c][sbx[c]] - dcoff[c], -2047, 2047); + } + } else { + HWY_ALIGN float* const block = group_dec_cache->dec_group_block; + // Dequantize and add predictions. + dequant_block( + acs, inv_global_scale, row_quant[bx], dec_state->x_dm_multiplier, + dec_state->b_dm_multiplier, x_cc_mul, b_cc_mul, acs.RawStrategy(), + size, dec_state->shared->quantizer, dequant_matrices, + acs.covered_blocks_y() * acs.covered_blocks_x(), sbx, dc_rows, + dc_stride, + dec_state->output_encoding_info.opsin_params.quant_biases, qblock, + block); + + for (size_t c : {1, 0, 2}) { + if ((sbx[c] << hshift[c] != bx) || (sby[c] << vshift[c] != by)) { + continue; + } + // IDCT + float* JXL_RESTRICT idct_pos = idct_row[c] + sbx[c] * kBlockDim; + TransformToPixels(acs.Strategy(), block + c * size, idct_pos, + idct_stride, group_dec_cache->scratch_space); + } + } + bx += llf_x; + } + } + } + if (draw == kDontDraw) { + return true; + } + // No ApplyImageFeatures in JPEG mode or when we need to delay it. + if (!decoded->IsJPEG() && dec_state->EagerFinalizeImageRect()) { + JXL_RETURN_IF_ERROR(dec_state->FinalizeGroup( + group_idx, thread, &dec_state->group_data[thread], decoded)); + } + return true; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +namespace { +// Decode quantized AC coefficients of DCT blocks. +// LLF components in the output block will not be modified. +template +Status DecodeACVarBlock(size_t ctx_offset, size_t log2_covered_blocks, + int32_t* JXL_RESTRICT row_nzeros, + const int32_t* JXL_RESTRICT row_nzeros_top, + size_t nzeros_stride, size_t c, size_t bx, size_t by, + size_t lbx, AcStrategy acs, + const coeff_order_t* JXL_RESTRICT coeff_order, + BitReader* JXL_RESTRICT br, + ANSSymbolReader* JXL_RESTRICT decoder, + const std::vector& context_map, + const uint8_t* qdc_row, const int32_t* qf_row, + const BlockCtxMap& block_ctx_map, ACPtr block, + size_t shift = 0) { + PROFILER_FUNC; + // Equal to number of LLF coefficients. + const size_t covered_blocks = 1 << log2_covered_blocks; + const size_t size = covered_blocks * kDCTBlockSize; + int32_t predicted_nzeros = + PredictFromTopAndLeft(row_nzeros_top, row_nzeros, bx, 32); + + size_t ord = kStrategyOrder[acs.RawStrategy()]; + const coeff_order_t* JXL_RESTRICT order = + &coeff_order[CoeffOrderOffset(ord, c)]; + + size_t block_ctx = block_ctx_map.Context(qdc_row[lbx], qf_row[bx], ord, c); + const int32_t nzero_ctx = + block_ctx_map.NonZeroContext(predicted_nzeros, block_ctx) + ctx_offset; + + size_t nzeros = decoder->ReadHybridUint(nzero_ctx, br, context_map); + if (nzeros + covered_blocks > size) { + return JXL_FAILURE("Invalid AC: nzeros too large"); + } + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + row_nzeros[bx + x + y * nzeros_stride] = + (nzeros + covered_blocks - 1) >> log2_covered_blocks; + } + } + + const size_t histo_offset = + ctx_offset + block_ctx_map.ZeroDensityContextsOffset(block_ctx); + + // Skip LLF + { + PROFILER_ZONE("AcDecSkipLLF, reader"); + size_t prev = (nzeros > size / 16 ? 0 : 1); + for (size_t k = covered_blocks; k < size && nzeros != 0; ++k) { + const size_t ctx = + histo_offset + ZeroDensityContext(nzeros, k, covered_blocks, + log2_covered_blocks, prev); + const size_t u_coeff = decoder->ReadHybridUint(ctx, br, context_map); + // Hand-rolled version of UnpackSigned, shifting before the conversion to + // signed integer to avoid undefined behavior of shifting negative + // numbers. + const size_t magnitude = u_coeff >> 1; + const size_t neg_sign = (~u_coeff) & 1; + const intptr_t coeff = + static_cast((magnitude ^ (neg_sign - 1)) << shift); + if (ac_type == ACType::k16) { + block.ptr16[order[k]] += coeff; + } else { + block.ptr32[order[k]] += coeff; + } + prev = static_cast(u_coeff != 0); + nzeros -= prev; + } + if (JXL_UNLIKELY(nzeros != 0)) { + return JXL_FAILURE( + "Invalid AC: nzeros not 0. Block (%zu, %zu), channel %zu", bx, by, c); + } + } + return true; +} + +// Structs used by DecodeGroupImpl to get a quantized block. +// GetBlockFromBitstream uses ANS decoding (and thus keeps track of row +// pointers in row_nzeros), GetBlockFromEncoder simply reads the coefficient +// image provided by the encoder. + +struct GetBlockFromBitstream : public GetBlock { + void StartRow(size_t by) override { + qf_row = rect.ConstRow(*qf, by); + for (size_t c = 0; c < 3; c++) { + size_t sby = by >> vshift[c]; + quant_dc_row = quant_dc->ConstRow(rect.y0() + by) + rect.x0(); + for (size_t i = 0; i < num_passes; i++) { + row_nzeros[i][c] = group_dec_cache->num_nzeroes[i].PlaneRow(c, sby); + row_nzeros_top[i][c] = + sby == 0 + ? nullptr + : group_dec_cache->num_nzeroes[i].ConstPlaneRow(c, sby - 1); + } + } + } + + Status LoadBlock(size_t bx, size_t by, const AcStrategy& acs, size_t size, + size_t log2_covered_blocks, ACPtr block[3], + ACType ac_type) override { + auto decode_ac_varblock = ac_type == ACType::k16 + ? DecodeACVarBlock + : DecodeACVarBlock; + for (size_t c : {1, 0, 2}) { + size_t sbx = bx >> hshift[c]; + size_t sby = by >> vshift[c]; + if (JXL_UNLIKELY((sbx << hshift[c] != bx) || (sby << vshift[c] != by))) { + continue; + } + + for (size_t pass = 0; JXL_UNLIKELY(pass < num_passes); pass++) { + JXL_RETURN_IF_ERROR(decode_ac_varblock( + ctx_offset[pass], log2_covered_blocks, row_nzeros[pass][c], + row_nzeros_top[pass][c], nzeros_stride, c, sbx, sby, bx, acs, + &coeff_orders[pass * coeff_order_size], readers[pass], + &decoders[pass], context_map[pass], quant_dc_row, qf_row, + *block_ctx_map, block[c], shift_for_pass[pass])); + } + } + return true; + } + + Status Init(BitReader* JXL_RESTRICT* JXL_RESTRICT readers, size_t num_passes, + size_t group_idx, size_t histo_selector_bits, const Rect& rect, + GroupDecCache* JXL_RESTRICT group_dec_cache, + PassesDecoderState* dec_state, size_t first_pass) { + for (size_t i = 0; i < 3; i++) { + hshift[i] = dec_state->shared->frame_header.chroma_subsampling.HShift(i); + vshift[i] = dec_state->shared->frame_header.chroma_subsampling.VShift(i); + } + this->coeff_order_size = dec_state->shared->coeff_order_size; + this->coeff_orders = + dec_state->shared->coeff_orders.data() + first_pass * coeff_order_size; + this->context_map = dec_state->context_map.data() + first_pass; + this->readers = readers; + this->num_passes = num_passes; + this->shift_for_pass = + dec_state->shared->frame_header.passes.shift + first_pass; + this->group_dec_cache = group_dec_cache; + this->rect = rect; + block_ctx_map = &dec_state->shared->block_ctx_map; + qf = &dec_state->shared->raw_quant_field; + quant_dc = &dec_state->shared->quant_dc; + + for (size_t pass = 0; pass < num_passes; pass++) { + // Select which histogram set to use among those of the current pass. + size_t cur_histogram = 0; + if (histo_selector_bits != 0) { + cur_histogram = readers[pass]->ReadBits(histo_selector_bits); + } + if (cur_histogram >= dec_state->shared->num_histograms) { + return JXL_FAILURE("Invalid histogram selector"); + } + ctx_offset[pass] = cur_histogram * block_ctx_map->NumACContexts(); + + decoders[pass] = + ANSSymbolReader(&dec_state->code[pass + first_pass], readers[pass]); + } + nzeros_stride = group_dec_cache->num_nzeroes[0].PixelsPerRow(); + for (size_t i = 0; i < num_passes; i++) { + JXL_ASSERT( + nzeros_stride == + static_cast(group_dec_cache->num_nzeroes[i].PixelsPerRow())); + } + return true; + } + + const uint32_t* shift_for_pass = nullptr; // not owned + const coeff_order_t* JXL_RESTRICT coeff_orders; + size_t coeff_order_size; + const std::vector* JXL_RESTRICT context_map; + ANSSymbolReader decoders[kMaxNumPasses]; + BitReader* JXL_RESTRICT* JXL_RESTRICT readers; + size_t num_passes; + size_t ctx_offset[kMaxNumPasses]; + size_t nzeros_stride; + int32_t* JXL_RESTRICT row_nzeros[kMaxNumPasses][3]; + const int32_t* JXL_RESTRICT row_nzeros_top[kMaxNumPasses][3]; + GroupDecCache* JXL_RESTRICT group_dec_cache; + const BlockCtxMap* block_ctx_map; + const ImageI* qf; + const ImageB* quant_dc; + const int32_t* qf_row; + const uint8_t* quant_dc_row; + Rect rect; + size_t hshift[3], vshift[3]; +}; + +struct GetBlockFromEncoder : public GetBlock { + void StartRow(size_t by) override {} + + Status LoadBlock(size_t bx, size_t by, const AcStrategy& acs, size_t size, + size_t log2_covered_blocks, ACPtr block[3], + ACType ac_type) override { + JXL_DASSERT(ac_type == ACType::k32); + for (size_t c = 0; c < 3; c++) { + // for each pass + for (size_t i = 0; i < quantized_ac->size(); i++) { + for (size_t k = 0; k < size; k++) { + // TODO(veluca): SIMD. + block[c].ptr32[k] += + rows[i][c][offset + k] * (1 << shift_for_pass[i]); + } + } + } + offset += size; + return true; + } + + GetBlockFromEncoder(const std::vector>& ac, + size_t group_idx, const uint32_t* shift_for_pass) + : quantized_ac(&ac), shift_for_pass(shift_for_pass) { + // TODO(veluca): not supported with chroma subsampling. + for (size_t i = 0; i < quantized_ac->size(); i++) { + JXL_CHECK((*quantized_ac)[i]->Type() == ACType::k32); + for (size_t c = 0; c < 3; c++) { + rows[i][c] = (*quantized_ac)[i]->PlaneRow(c, group_idx, 0).ptr32; + } + } + } + + const std::vector>* JXL_RESTRICT quantized_ac; + size_t offset = 0; + const int32_t* JXL_RESTRICT rows[kMaxNumPasses][3]; + const uint32_t* shift_for_pass = nullptr; // not owned +}; + +HWY_EXPORT(DecodeGroupImpl); + +} // namespace + +Status DecodeGroup(BitReader* JXL_RESTRICT* JXL_RESTRICT readers, + size_t num_passes, size_t group_idx, + PassesDecoderState* JXL_RESTRICT dec_state, + GroupDecCache* JXL_RESTRICT group_dec_cache, size_t thread, + ImageBundle* JXL_RESTRICT decoded, size_t first_pass, + bool force_draw, bool dc_only) { + PROFILER_FUNC; + + DrawMode draw = (num_passes + first_pass == + dec_state->shared->frame_header.passes.num_passes) || + force_draw + ? kDraw + : kDontDraw; + + if (draw == kDraw && num_passes == 0 && first_pass == 0) { + const YCbCrChromaSubsampling& cs = + dec_state->shared->frame_header.chroma_subsampling; + for (size_t c : {0, 1, 2}) { + size_t hs = cs.HShift(c); + size_t vs = cs.VShift(c); + // We reuse filter_input_storage here as it is not currently in use. + const Rect src_rect_precs = dec_state->shared->BlockGroupRect(group_idx); + const Rect src_rect = + Rect(src_rect_precs.x0() >> hs, src_rect_precs.y0() >> vs, + src_rect_precs.xsize() >> hs, src_rect_precs.ysize() >> vs); + const Rect copy_rect(kBlockDim, 2, src_rect.xsize(), src_rect.ysize()); + CopyImageToWithPadding(src_rect, dec_state->shared->dc->Plane(c), 2, + copy_rect, + &dec_state->filter_input_storage[thread].Plane(c)); + EnsurePaddingInPlace( + &dec_state->filter_input_storage[thread].Plane(c), copy_rect, + src_rect, DivCeil(dec_state->shared->frame_dim.xsize_blocks, 1 << hs), + DivCeil(dec_state->shared->frame_dim.ysize_blocks, 1 << vs), 2, 2); + ImageF* upsampling_dst = &dec_state->decoded.Plane(c); + Rect dst_rect(src_rect.x0() * 8, src_rect.y0() * 8, src_rect.xsize() * 8, + src_rect.ysize() * 8); + if (dec_state->EagerFinalizeImageRect()) { + upsampling_dst = &dec_state->group_data[thread].Plane(c); + dst_rect = Rect(PassesDecoderState::kGroupDataXBorder, + PassesDecoderState::kGroupDataYBorder, dst_rect.xsize(), + dst_rect.ysize()); + } + JXL_ASSERT(dst_rect.IsInside(*upsampling_dst)); + dec_state->upsamplers[2].UpsampleRect( + dec_state->filter_input_storage[thread].Plane(c), copy_rect, + upsampling_dst, dst_rect, + static_cast(src_rect.y0()) - + static_cast(copy_rect.y0()), + dec_state->shared->frame_dim.ysize_blocks >> vs, + dec_state->upsampler_storage[thread].get()); + } + draw = kOnlyImageFeatures; + } + + size_t histo_selector_bits = 0; + if (dc_only) { + JXL_ASSERT(num_passes == 0); + } else { + JXL_ASSERT(dec_state->shared->num_histograms > 0); + histo_selector_bits = CeilLog2Nonzero(dec_state->shared->num_histograms); + } + + GetBlockFromBitstream get_block; + JXL_RETURN_IF_ERROR( + get_block.Init(readers, num_passes, group_idx, histo_selector_bits, + dec_state->shared->BlockGroupRect(group_idx), + group_dec_cache, dec_state, first_pass)); + + JXL_RETURN_IF_ERROR(HWY_DYNAMIC_DISPATCH(DecodeGroupImpl)( + &get_block, group_dec_cache, dec_state, thread, group_idx, decoded, + draw)); + + for (size_t pass = 0; pass < num_passes; pass++) { + if (!get_block.decoders[pass].CheckANSFinalState()) { + return JXL_FAILURE("ANS checksum failure."); + } + } + return true; +} + +Status DecodeGroupForRoundtrip(const std::vector>& ac, + size_t group_idx, + PassesDecoderState* JXL_RESTRICT dec_state, + GroupDecCache* JXL_RESTRICT group_dec_cache, + size_t thread, ImageBundle* JXL_RESTRICT decoded, + AuxOut* aux_out) { + PROFILER_FUNC; + + GetBlockFromEncoder get_block(ac, group_idx, + dec_state->shared->frame_header.passes.shift); + group_dec_cache->InitOnce( + /*num_passes=*/0, + /*used_acs=*/(1u << AcStrategy::kNumValidStrategies) - 1); + + return HWY_DYNAMIC_DISPATCH(DecodeGroupImpl)(&get_block, group_dec_cache, + dec_state, thread, group_idx, + decoded, kDraw); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_group.h b/lib/jxl/dec_group.h new file mode 100644 index 0000000..a7b868d --- /dev/null +++ b/lib/jxl/dec_group.h @@ -0,0 +1,47 @@ +// 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_DEC_GROUP_H_ +#define LIB_JXL_DEC_GROUP_H_ + +#include +#include + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/quantizer.h" + +namespace jxl { + +Status DecodeGroup(BitReader* JXL_RESTRICT* JXL_RESTRICT readers, + size_t num_passes, size_t group_idx, + PassesDecoderState* JXL_RESTRICT dec_state, + GroupDecCache* JXL_RESTRICT group_dec_cache, size_t thread, + ImageBundle* JXL_RESTRICT decoded, size_t first_pass, + bool force_draw, bool dc_only); + +Status DecodeGroupForRoundtrip(const std::vector>& ac, + size_t group_idx, + PassesDecoderState* JXL_RESTRICT dec_state, + GroupDecCache* JXL_RESTRICT group_dec_cache, + size_t thread, ImageBundle* JXL_RESTRICT decoded, + AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_DEC_GROUP_H_ diff --git a/lib/jxl/dec_group_border.cc b/lib/jxl/dec_group_border.cc new file mode 100644 index 0000000..2e08578 --- /dev/null +++ b/lib/jxl/dec_group_border.cc @@ -0,0 +1,183 @@ +// 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/dec_group_border.h" + +#include + +namespace jxl { + +void GroupBorderAssigner::Init(const FrameDimensions& frame_dim) { + frame_dim_ = frame_dim; + size_t num_corners = + (frame_dim_.xsize_groups + 1) * (frame_dim_.ysize_groups + 1); + counters_.reset(new std::atomic[num_corners]); + // Initialize counters. + for (size_t y = 0; y < frame_dim_.ysize_groups + 1; y++) { + for (size_t x = 0; x < frame_dim_.xsize_groups + 1; x++) { + // Counters at image borders don't have anything on the other side, we + // pre-fill their value to have more uniform handling afterwards. + uint8_t init_value = 0; + if (x == 0) { + init_value |= kTopLeft | kBottomLeft; + } + if (x == frame_dim_.xsize_groups) { + init_value |= kTopRight | kBottomRight; + } + if (y == 0) { + init_value |= kTopLeft | kTopRight; + } + if (y == frame_dim_.ysize_groups) { + init_value |= kBottomLeft | kBottomRight; + } + counters_[y * (frame_dim_.xsize_groups + 1) + x] = init_value; + } + } +} + +void GroupBorderAssigner::ClearDone(size_t group_id) { + size_t x = group_id % frame_dim_.xsize_groups; + size_t y = group_id / frame_dim_.xsize_groups; + size_t top_left_idx = y * (frame_dim_.xsize_groups + 1) + x; + size_t top_right_idx = y * (frame_dim_.xsize_groups + 1) + x + 1; + size_t bottom_right_idx = (y + 1) * (frame_dim_.xsize_groups + 1) + x + 1; + size_t bottom_left_idx = (y + 1) * (frame_dim_.xsize_groups + 1) + x; + counters_[top_left_idx].fetch_and(~kBottomRight); + counters_[top_right_idx].fetch_and(~kBottomLeft); + counters_[bottom_left_idx].fetch_and(~kTopRight); + counters_[bottom_right_idx].fetch_and(~kTopLeft); +} + +// Looking at each corner between groups, we can guarantee that the four +// involved groups will agree between each other regarding the order in which +// each of the four groups terminated. Thus, the last of the four groups +// gets the responsibility of handling the corner. For borders, every border +// is assigned to its top corner (for vertical borders) or to its left corner +// (for horizontal borders): the order as seen on those corners will decide who +// handles that border. + +void GroupBorderAssigner::GroupDone(size_t group_id, size_t padding, + Rect* rects_to_finalize, + size_t* num_to_finalize) { + size_t x = group_id % frame_dim_.xsize_groups; + size_t y = group_id / frame_dim_.xsize_groups; + Rect block_rect(x * frame_dim_.group_dim / kBlockDim, + y * frame_dim_.group_dim / kBlockDim, + frame_dim_.group_dim / kBlockDim, + frame_dim_.group_dim / kBlockDim, frame_dim_.xsize_blocks, + frame_dim_.ysize_blocks); + + size_t top_left_idx = y * (frame_dim_.xsize_groups + 1) + x; + size_t top_right_idx = y * (frame_dim_.xsize_groups + 1) + x + 1; + size_t bottom_right_idx = (y + 1) * (frame_dim_.xsize_groups + 1) + x + 1; + size_t bottom_left_idx = (y + 1) * (frame_dim_.xsize_groups + 1) + x; + + auto fetch_status = [this](size_t idx, uint8_t bit) { + // Note that the acq-rel semantics of this fetch are actually needed to + // ensure that the pixel data of the group is already written to memory. + size_t status = counters_[idx].fetch_or(bit); + JXL_DASSERT((bit & status) == 0); + return bit | status; + }; + + size_t top_left_status = fetch_status(top_left_idx, kBottomRight); + size_t top_right_status = fetch_status(top_right_idx, kBottomLeft); + size_t bottom_right_status = fetch_status(bottom_right_idx, kTopLeft); + size_t bottom_left_status = fetch_status(bottom_left_idx, kTopRight); + + size_t padx = PaddingX(padding); + size_t pady = padding; + + size_t x1 = block_rect.x0() + block_rect.xsize(); + size_t y1 = block_rect.y0() + block_rect.ysize(); + + bool is_last_group_x = frame_dim_.xsize_groups == x + 1; + bool is_last_group_y = frame_dim_.ysize_groups == y + 1; + + // Start of border of neighbouring group, end of border of this group, start + // of border of this group (on the other side), end of border of next group. + size_t xpos[4] = { + block_rect.x0() == 0 ? 0 : block_rect.x0() * kBlockDim - padx, + block_rect.x0() == 0 ? 0 : block_rect.x0() * kBlockDim + padx, + is_last_group_x ? frame_dim_.xsize_padded : x1 * kBlockDim - padx, + is_last_group_x ? frame_dim_.xsize_padded : x1 * kBlockDim + padx}; + size_t ypos[4] = { + block_rect.y0() == 0 ? 0 : block_rect.y0() * kBlockDim - pady, + block_rect.y0() == 0 ? 0 : block_rect.y0() * kBlockDim + pady, + is_last_group_y ? frame_dim_.ysize_padded : y1 * kBlockDim - pady, + is_last_group_y ? frame_dim_.ysize_padded : y1 * kBlockDim + pady}; + + *num_to_finalize = 0; + auto append_rect = [&](size_t x0, size_t x1, size_t y0, size_t y1) { + Rect rect(xpos[x0], ypos[y0], xpos[x1] - xpos[x0], ypos[y1] - ypos[y0]); + if (rect.xsize() == 0 || rect.ysize() == 0) return; + JXL_DASSERT(*num_to_finalize < kMaxToFinalize); + rects_to_finalize[(*num_to_finalize)++] = rect; + }; + + // Because of how group borders are assigned, it is impossible that we need to + // process the left and right side of some area but not the center area. Thus, + // we compute the first/last part to process in every horizontal strip and + // merge them together. We first collect a mask of what parts should be + // processed. + // We do this horizontally rather than vertically because horizontal borders + // are larger. + bool available_parts_mask[3][3] = {}; // [x][y] + // Center + available_parts_mask[1][1] = true; + // Corners + if (top_left_status == 0xF) available_parts_mask[0][0] = true; + if (top_right_status == 0xF) available_parts_mask[2][0] = true; + if (bottom_right_status == 0xF) available_parts_mask[2][2] = true; + if (bottom_left_status == 0xF) available_parts_mask[0][2] = true; + // Other borders + if (top_left_status & kTopRight) available_parts_mask[1][0] = true; + if (top_left_status & kBottomLeft) available_parts_mask[0][1] = true; + if (top_right_status & kBottomRight) available_parts_mask[2][1] = true; + if (bottom_left_status & kBottomRight) available_parts_mask[1][2] = true; + + // Collect horizontal ranges. + constexpr size_t kNoSegment = 3; + std::pair horizontal_segments[3] = {{kNoSegment, kNoSegment}, + {kNoSegment, kNoSegment}, + {kNoSegment, kNoSegment}}; + for (size_t y = 0; y < 3; y++) { + for (size_t x = 0; x < 3; x++) { + if (!available_parts_mask[x][y]) continue; + JXL_DASSERT(horizontal_segments[y].second == kNoSegment || + horizontal_segments[y].second == x); + JXL_DASSERT((horizontal_segments[y].first == kNoSegment) == + (horizontal_segments[y].second == kNoSegment)); + if (horizontal_segments[y].first == kNoSegment) { + horizontal_segments[y].first = x; + } + horizontal_segments[y].second = x + 1; + } + } + if (horizontal_segments[0] == horizontal_segments[1] && + horizontal_segments[0] == horizontal_segments[2]) { + append_rect(horizontal_segments[0].first, horizontal_segments[0].second, 0, + 3); + } else if (horizontal_segments[0] == horizontal_segments[1]) { + append_rect(horizontal_segments[0].first, horizontal_segments[0].second, 0, + 2); + append_rect(horizontal_segments[2].first, horizontal_segments[2].second, 2, + 3); + } else if (horizontal_segments[1] == horizontal_segments[2]) { + append_rect(horizontal_segments[0].first, horizontal_segments[0].second, 0, + 1); + append_rect(horizontal_segments[1].first, horizontal_segments[1].second, 1, + 3); + } else { + append_rect(horizontal_segments[0].first, horizontal_segments[0].second, 0, + 1); + append_rect(horizontal_segments[1].first, horizontal_segments[1].second, 1, + 2); + append_rect(horizontal_segments[2].first, horizontal_segments[2].second, 2, + 3); + } +} + +} // namespace jxl diff --git a/lib/jxl/dec_group_border.h b/lib/jxl/dec_group_border.h new file mode 100644 index 0000000..67af6af --- /dev/null +++ b/lib/jxl/dec_group_border.h @@ -0,0 +1,60 @@ +// 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_DEC_GROUP_BORDER_H_ +#define LIB_JXL_DEC_GROUP_BORDER_H_ + +#include + +#include + +#include "lib/jxl/base/arch_macros.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" + +namespace jxl { + +class GroupBorderAssigner { + public: + // Prepare the GroupBorderAssigner to handle a given frame. + void Init(const FrameDimensions& frame_dim); + // Marks a group as done, and returns the (at most 3) rects to run + // FinalizeImageRect on. `block_rect` must be the rect corresponding + // to the given `group_id`, measured in blocks. + void GroupDone(size_t group_id, size_t padding, Rect* rects_to_finalize, + size_t* num_to_finalize); + // Marks a group as not-done, for running re-paints. + void ClearDone(size_t group_id); + + static constexpr size_t kMaxToFinalize = 3; + + // Vectors on ARM NEON are never wider than 4 floats, so rounding to multiples + // of 4 is enough. +#if defined(__ARM_NEON) || defined(__ARM_NEON__) + static constexpr size_t kPaddingXRound = 4; +#else + static constexpr size_t kPaddingXRound = kBlockDim; +#endif + + // Returns the necessary amount of padding for the X axis. + static size_t PaddingX(size_t padding) { + return RoundUpTo(padding, kPaddingXRound); + } + + private: + FrameDimensions frame_dim_; + std::unique_ptr[]> counters_; + + // Constants to identify group positions relative to the corners. + static constexpr uint8_t kTopLeft = 0x01; + static constexpr uint8_t kTopRight = 0x02; + static constexpr uint8_t kBottomRight = 0x04; + static constexpr uint8_t kBottomLeft = 0x08; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_GROUP_BORDER_H_ diff --git a/lib/jxl/dec_huffman.cc b/lib/jxl/dec_huffman.cc new file mode 100644 index 0000000..05b2757 --- /dev/null +++ b/lib/jxl/dec_huffman.cc @@ -0,0 +1,255 @@ +// 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/dec_huffman.h" + +#include /* for memset */ + +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/huffman_table.h" + +namespace jxl { + +static const int kCodeLengthCodes = 18; +static const uint8_t kCodeLengthCodeOrder[kCodeLengthCodes] = { + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, +}; +static const uint8_t kDefaultCodeLength = 8; +static const uint8_t kCodeLengthRepeatCode = 16; + +int ReadHuffmanCodeLengths(const uint8_t* code_length_code_lengths, + int num_symbols, uint8_t* code_lengths, + BitReader* br) { + int symbol = 0; + uint8_t prev_code_len = kDefaultCodeLength; + int repeat = 0; + uint8_t repeat_code_len = 0; + int space = 32768; + HuffmanCode table[32]; + + uint16_t counts[16] = {0}; + for (int i = 0; i < kCodeLengthCodes; ++i) { + ++counts[code_length_code_lengths[i]]; + } + if (!BuildHuffmanTable(table, 5, code_length_code_lengths, kCodeLengthCodes, + &counts[0])) { + return 0; + } + + while (symbol < num_symbols && space > 0) { + const HuffmanCode* p = table; + uint8_t code_len; + br->Refill(); + p += br->PeekFixedBits<5>(); + br->Consume(p->bits); + code_len = (uint8_t)p->value; + if (code_len < kCodeLengthRepeatCode) { + repeat = 0; + code_lengths[symbol++] = code_len; + if (code_len != 0) { + prev_code_len = code_len; + space -= 32768u >> code_len; + } + } else { + const int extra_bits = code_len - 14; + int old_repeat; + int repeat_delta; + uint8_t new_len = 0; + if (code_len == kCodeLengthRepeatCode) { + new_len = prev_code_len; + } + if (repeat_code_len != new_len) { + repeat = 0; + repeat_code_len = new_len; + } + old_repeat = repeat; + if (repeat > 0) { + repeat -= 2; + repeat <<= extra_bits; + } + repeat += (int)br->ReadBits(extra_bits) + 3; + repeat_delta = repeat - old_repeat; + if (symbol + repeat_delta > num_symbols) { + return 0; + } + memset(&code_lengths[symbol], repeat_code_len, (size_t)repeat_delta); + symbol += repeat_delta; + if (repeat_code_len != 0) { + space -= repeat_delta << (15 - repeat_code_len); + } + } + } + if (space != 0) { + return 0; + } + memset(&code_lengths[symbol], 0, (size_t)(num_symbols - symbol)); + return true; +} + +static JXL_INLINE bool ReadSimpleCode(size_t alphabet_size, BitReader* br, + HuffmanCode* table) { + size_t max_bits = + (alphabet_size > 1u) ? FloorLog2Nonzero(alphabet_size - 1u) + 1 : 0; + + size_t num_symbols = br->ReadFixedBits<2>() + 1; + + uint16_t symbols[4] = {0}; + for (size_t i = 0; i < num_symbols; ++i) { + uint16_t symbol = br->ReadBits(max_bits); + if (symbol >= alphabet_size) { + return false; + } + symbols[i] = symbol; + } + + for (size_t i = 0; i < num_symbols - 1; ++i) { + for (size_t j = i + 1; j < num_symbols; ++j) { + if (symbols[i] == symbols[j]) return false; + } + } + + // 4 symbols have to option to encode. + if (num_symbols == 4) num_symbols += br->ReadFixedBits<1>(); + + const auto swap_symbols = [&symbols](size_t i, size_t j) { + uint16_t t = symbols[j]; + symbols[j] = symbols[i]; + symbols[i] = t; + }; + + size_t table_size = 1; + switch (num_symbols) { + case 1: + table[0] = {0, symbols[0]}; + break; + case 2: + if (symbols[0] > symbols[1]) swap_symbols(0, 1); + table[0] = {1, symbols[0]}; + table[1] = {1, symbols[1]}; + table_size = 2; + break; + case 3: + if (symbols[1] > symbols[2]) swap_symbols(1, 2); + table[0] = {1, symbols[0]}; + table[2] = {1, symbols[0]}; + table[1] = {2, symbols[1]}; + table[3] = {2, symbols[2]}; + table_size = 4; + break; + case 4: { + for (size_t i = 0; i < 3; ++i) { + for (size_t j = i + 1; j < 4; ++j) { + if (symbols[i] > symbols[j]) swap_symbols(i, j); + } + } + table[0] = {2, symbols[0]}; + table[2] = {2, symbols[1]}; + table[1] = {2, symbols[2]}; + table[3] = {2, symbols[3]}; + table_size = 4; + break; + } + case 5: { + if (symbols[2] > symbols[3]) swap_symbols(2, 3); + table[0] = {1, symbols[0]}; + table[1] = {2, symbols[1]}; + table[2] = {1, symbols[0]}; + table[3] = {3, symbols[2]}; + table[4] = {1, symbols[0]}; + table[5] = {2, symbols[1]}; + table[6] = {1, symbols[0]}; + table[7] = {3, symbols[3]}; + table_size = 8; + break; + } + default: { + // Unreachable. + return false; + } + } + + const uint32_t goal_size = 1u << kHuffmanTableBits; + while (table_size != goal_size) { + memcpy(&table[table_size], &table[0], + (size_t)table_size * sizeof(table[0])); + table_size <<= 1; + } + + return true; +} + +bool HuffmanDecodingData::ReadFromBitStream(size_t alphabet_size, + BitReader* br) { + if (alphabet_size > (1 << PREFIX_MAX_BITS)) return false; + + /* simple_code_or_skip is used as follows: + 1 for simple code; + 0 for no skipping, 2 skips 2 code lengths, 3 skips 3 code lengths */ + uint32_t simple_code_or_skip = br->ReadFixedBits<2>(); + if (simple_code_or_skip == 1u) { + table_.resize(1u << kHuffmanTableBits); + return ReadSimpleCode(alphabet_size, br, table_.data()); + } + + std::vector code_lengths(alphabet_size, 0); + uint8_t code_length_code_lengths[kCodeLengthCodes] = {0}; + int space = 32; + int num_codes = 0; + /* Static Huffman code for the code length code lengths */ + static const HuffmanCode huff[16] = { + {2, 0}, {2, 4}, {2, 3}, {3, 2}, {2, 0}, {2, 4}, {2, 3}, {4, 1}, + {2, 0}, {2, 4}, {2, 3}, {3, 2}, {2, 0}, {2, 4}, {2, 3}, {4, 5}, + }; + for (size_t i = simple_code_or_skip; i < kCodeLengthCodes && space > 0; ++i) { + const int code_len_idx = kCodeLengthCodeOrder[i]; + const HuffmanCode* p = huff; + uint8_t v; + br->Refill(); + p += br->PeekFixedBits<4>(); + br->Consume(p->bits); + v = (uint8_t)p->value; + code_length_code_lengths[code_len_idx] = v; + if (v != 0) { + space -= (32u >> v); + ++num_codes; + } + } + bool ok = (num_codes == 1 || space == 0) && + ReadHuffmanCodeLengths(code_length_code_lengths, alphabet_size, + &code_lengths[0], br); + + if (!ok) return false; + uint16_t counts[16] = {0}; + for (size_t i = 0; i < alphabet_size; ++i) { + ++counts[code_lengths[i]]; + } + table_.resize(alphabet_size + 376); + uint32_t table_size = + BuildHuffmanTable(table_.data(), kHuffmanTableBits, &code_lengths[0], + alphabet_size, &counts[0]); + table_.resize(table_size); + return (table_size > 0); +} + +// Decodes the next Huffman coded symbol from the bit-stream. +uint16_t HuffmanDecodingData::ReadSymbol(BitReader* br) const { + size_t n_bits; + const HuffmanCode* table = table_.data(); + table += br->PeekBits(kHuffmanTableBits); + n_bits = table->bits; + if (n_bits > kHuffmanTableBits) { + br->Consume(kHuffmanTableBits); + n_bits -= kHuffmanTableBits; + table += table->value; + table += br->PeekBits(n_bits); + } + br->Consume(table->bits); + return table->value; +} + +} // namespace jxl diff --git a/lib/jxl/dec_huffman.h b/lib/jxl/dec_huffman.h new file mode 100644 index 0000000..162c3e3 --- /dev/null +++ b/lib/jxl/dec_huffman.h @@ -0,0 +1,32 @@ +// 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_DEC_HUFFMAN_H_ +#define LIB_JXL_DEC_HUFFMAN_H_ + +#include +#include + +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/huffman_table.h" + +namespace jxl { + +static constexpr size_t kHuffmanTableBits = 8u; + +struct HuffmanDecodingData { + // Decodes the Huffman code lengths from the bit-stream and fills in the + // pre-allocated table with the corresponding 2-level Huffman decoding table. + // Returns false if the Huffman code lengths can not de decoded. + bool ReadFromBitStream(size_t alphabet_size, BitReader* br); + + uint16_t ReadSymbol(BitReader* br) const; + + std::vector table_; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_HUFFMAN_H_ diff --git a/lib/jxl/dec_modular.cc b/lib/jxl/dec_modular.cc new file mode 100644 index 0000000..f8b4c0a --- /dev/null +++ b/lib/jxl/dec_modular.cc @@ -0,0 +1,663 @@ +// 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/dec_modular.h" + +#include + +#include + +#include "lib/jxl/frame_header.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_modular.cc" +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/compressed_dc.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; + +void MultiplySum(const size_t xsize, + const pixel_type* const JXL_RESTRICT row_in, + const pixel_type* const JXL_RESTRICT row_in_Y, + const float factor, float* const JXL_RESTRICT row_out) { + const HWY_FULL(float) df; + const Rebind di; // assumes pixel_type <= float + const auto factor_v = Set(df, factor); + for (size_t x = 0; x < xsize; x += Lanes(di)) { + const auto in = Load(di, row_in + x) + Load(di, row_in_Y + x); + const auto out = ConvertTo(df, in) * factor_v; + Store(out, df, row_out + x); + } +} + +void RgbFromSingle(const size_t xsize, + const pixel_type* const JXL_RESTRICT row_in, + const float factor, Image3F* decoded, size_t /*c*/, size_t y, + Rect& rect) { + JXL_DASSERT(xsize <= rect.xsize()); + const HWY_FULL(float) df; + const Rebind di; // assumes pixel_type <= float + + float* const JXL_RESTRICT row_out_r = rect.PlaneRow(decoded, 0, y); + float* const JXL_RESTRICT row_out_g = rect.PlaneRow(decoded, 1, y); + float* const JXL_RESTRICT row_out_b = rect.PlaneRow(decoded, 2, y); + + const auto factor_v = Set(df, factor); + for (size_t x = 0; x < xsize; x += Lanes(di)) { + const auto in = Load(di, row_in + x); + const auto out = ConvertTo(df, in) * factor_v; + Store(out, df, row_out_r + x); + Store(out, df, row_out_g + x); + Store(out, df, row_out_b + x); + } +} + +// Same signature as RgbFromSingle so we can assign to the same pointer. +void SingleFromSingle(const size_t xsize, + const pixel_type* const JXL_RESTRICT row_in, + const float factor, Image3F* decoded, size_t c, size_t y, + Rect& rect) { + JXL_DASSERT(xsize <= rect.xsize()); + const HWY_FULL(float) df; + const Rebind di; // assumes pixel_type <= float + + float* const JXL_RESTRICT row_out = rect.PlaneRow(decoded, c, y); + + const auto factor_v = Set(df, factor); + for (size_t x = 0; x < xsize; x += Lanes(di)) { + const auto in = Load(di, row_in + x); + const auto out = ConvertTo(df, in) * factor_v; + Store(out, df, row_out + x); + } +} +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(MultiplySum); // Local function +HWY_EXPORT(RgbFromSingle); // Local function +HWY_EXPORT(SingleFromSingle); // Local function + +// 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, + float* const JXL_RESTRICT row_out, const size_t xsize, + const int bits, const int exp_bits) { + if (bits == 32) { + JXL_ASSERT(sizeof(pixel_type) == sizeof(float)); + JXL_ASSERT(exp_bits == 8); + memcpy(row_out, row_in, xsize * sizeof(float)); + return; + } + int exp_bias = (1 << (exp_bits - 1)) - 1; + int sign_shift = bits - 1; + int mant_bits = bits - exp_bits - 1; + int mant_shift = 23 - mant_bits; + for (size_t x = 0; x < xsize; ++x) { + uint32_t f; + memcpy(&f, &row_in[x], 4); + int signbit = (f >> sign_shift); + f &= (1 << sign_shift) - 1; + if (f == 0) { + row_out[x] = (signbit ? -0.f : 0.f); + continue; + } + int exp = (f >> mant_bits); + int mantissa = (f & ((1 << mant_bits) - 1)); + mantissa <<= mant_shift; + // Try to normalize only if there is space for maneuver. + if (exp == 0 && exp_bits < 8) { + // subnormal number + while ((mantissa & 0x800000) == 0) { + mantissa <<= 1; + exp--; + } + exp++; + // remove leading 1 because it is implicit now + mantissa &= 0x7fffff; + } + exp -= exp_bias; + // broke up the arbitrary float into its parts, now reassemble into + // binary32 + exp += 127; + JXL_ASSERT(exp >= 0); + f = (signbit ? 0x80000000 : 0); + f |= (exp << 23); + f |= mantissa; + memcpy(&row_out[x], &f, 4); + } +} + +Status ModularFrameDecoder::DecodeGlobalInfo(BitReader* reader, + const FrameHeader& frame_header, + bool allow_truncated_group) { + bool decode_color = frame_header.encoding == FrameEncoding::kModular; + const auto& metadata = frame_header.nonserialized_metadata->m; + bool is_gray = metadata.color_encoding.IsGray(); + size_t nb_chans = 3; + if (is_gray && frame_header.color_transform == ColorTransform::kNone) { + nb_chans = 1; + } + bool has_tree = reader->ReadBits(1); + if (has_tree) { + size_t tree_size_limit = + 1024 + frame_dim.xsize * frame_dim.ysize * nb_chans / 16; + JXL_RETURN_IF_ERROR(DecodeTree(reader, &tree, tree_size_limit)); + JXL_RETURN_IF_ERROR( + DecodeHistograms(reader, (tree.size() + 1) / 2, &code, &context_map)); + } + do_color = decode_color; + if (!do_color) nb_chans = 0; + size_t nb_extra = metadata.extra_channel_info.size(); + + bool fp = metadata.bit_depth.floating_point_sample; + + // bits_per_sample is just metadata for XYB images. + if (metadata.bit_depth.bits_per_sample >= 32 && do_color && + frame_header.color_transform != ColorTransform::kXYB) { + if (metadata.bit_depth.bits_per_sample == 32 && fp == false) { + return JXL_FAILURE("uint32_t not supported in dec_modular"); + } else if (metadata.bit_depth.bits_per_sample > 32) { + return JXL_FAILURE("bits_per_sample > 32 not supported"); + } + } + + Image gi(frame_dim.xsize, frame_dim.ysize, metadata.bit_depth.bits_per_sample, + nb_chans + nb_extra); + + all_same_shift = true; + if (frame_header.color_transform == ColorTransform::kYCbCr) { + for (size_t c = 0; c < nb_chans; c++) { + gi.channel[c].hshift = frame_header.chroma_subsampling.HShift(c); + gi.channel[c].vshift = frame_header.chroma_subsampling.VShift(c); + size_t xsize_shifted = + DivCeil(frame_dim.xsize, 1 << gi.channel[c].hshift); + size_t ysize_shifted = + DivCeil(frame_dim.ysize, 1 << gi.channel[c].vshift); + gi.channel[c].shrink(xsize_shifted, ysize_shifted); + if (gi.channel[c].hshift != gi.channel[0].hshift || + gi.channel[c].vshift != gi.channel[0].vshift) + all_same_shift = false; + } + } + + for (size_t ec = 0, c = nb_chans; ec < nb_extra; ec++, c++) { + size_t ecups = frame_header.extra_channel_upsampling[ec]; + gi.channel[c].shrink(DivCeil(frame_dim.xsize_upsampled, ecups), + DivCeil(frame_dim.ysize_upsampled, ecups)); + gi.channel[c].hshift = gi.channel[c].vshift = + CeilLog2Nonzero(ecups) - CeilLog2Nonzero(frame_header.upsampling); + if (gi.channel[c].hshift != gi.channel[0].hshift || + gi.channel[c].vshift != gi.channel[0].vshift) + all_same_shift = false; + } + + ModularOptions options; + options.max_chan_size = frame_dim.group_dim; + options.group_dim = frame_dim.group_dim; + Status dec_status = ModularGenericDecompress( + reader, gi, &global_header, ModularStreamId::Global().ID(frame_dim), + &options, + /*undo_transforms=*/-2, &tree, &code, &context_map, + allow_truncated_group); + if (!allow_truncated_group) JXL_RETURN_IF_ERROR(dec_status); + if (dec_status.IsFatalError()) { + return JXL_FAILURE("Failed to decode global modular info"); + } + + // TODO(eustas): are we sure this can be done after partial decode? + have_something = false; + for (size_t c = 0; c < gi.channel.size(); c++) { + Channel& gic = gi.channel[c]; + if (c >= gi.nb_meta_channels && gic.w <= frame_dim.group_dim && + gic.h <= frame_dim.group_dim) + have_something = true; + } + // move global transforms to groups if possible + if (!have_something && all_same_shift) { + if (gi.transform.size() == 1 && gi.transform[0].id == TransformId::kRCT) { + global_transform = gi.transform; + gi.transform.clear(); + // TODO(jon): also move no-delta-palette out (trickier though) + } + } + full_image = std::move(gi); + return dec_status; +} + +void ModularFrameDecoder::MaybeDropFullImage() { + if (full_image.transform.empty() && !have_something && all_same_shift) { + use_full_image = false; + for (auto& ch : full_image.channel) { + // keep metadata on channels around, but dealloc their planes + ch.plane = Plane(); + } + } +} + +Status ModularFrameDecoder::DecodeGroup(const Rect& rect, BitReader* reader, + int minShift, int maxShift, + const ModularStreamId& stream, + bool zerofill, + PassesDecoderState* dec_state, + ImageBundle* output) { + JXL_DASSERT(stream.kind == ModularStreamId::kModularDC || + stream.kind == ModularStreamId::kModularAC); + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + Image gi(xsize, ysize, full_image.bitdepth, 0); + // start at the first bigger-than-groupsize non-metachannel + size_t c = full_image.nb_meta_channels; + for (; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + if (fc.w > frame_dim.group_dim || fc.h > frame_dim.group_dim) break; + } + size_t beginc = c; + for (; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + int shift = std::min(fc.hshift, fc.vshift); + if (shift > maxShift) continue; + if (shift < minShift) continue; + Rect r(rect.x0() >> fc.hshift, rect.y0() >> fc.vshift, + rect.xsize() >> fc.hshift, rect.ysize() >> fc.vshift, fc.w, fc.h); + if (r.xsize() == 0 || r.ysize() == 0) continue; + if (zerofill && use_full_image) { + for (size_t y = 0; y < r.ysize(); ++y) { + pixel_type* const JXL_RESTRICT row_out = r.Row(&fc.plane, y); + memset(row_out, 0, r.xsize() * sizeof(*row_out)); + } + } else { + Channel gc(r.xsize(), r.ysize()); + if (zerofill) ZeroFillImage(&gc.plane); + gc.hshift = fc.hshift; + gc.vshift = fc.vshift; + gi.channel.emplace_back(std::move(gc)); + } + } + if (zerofill && use_full_image) return true; + // Return early if there's nothing to decode. Otherwise there might be + // problems later (in ModularImageToDecodedRect). + if (gi.channel.empty()) return true; + ModularOptions options; + if (!zerofill) { + if (!ModularGenericDecompress( + reader, gi, /*header=*/nullptr, stream.ID(frame_dim), &options, + /*undo_transforms=*/-1, &tree, &code, &context_map)) { + return JXL_FAILURE("Failed to decode modular group"); + } + } + // Undo global transforms that have been pushed to the group level + if (!use_full_image) { + for (auto t : global_transform) { + JXL_RETURN_IF_ERROR(t.Inverse(gi, global_header.wp_header)); + } + JXL_RETURN_IF_ERROR(ModularImageToDecodedRect( + gi, dec_state, nullptr, output, rect.Crop(dec_state->decoded))); + return true; + } + int gic = 0; + for (c = beginc; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + int shift = std::min(fc.hshift, fc.vshift); + if (shift > maxShift) continue; + if (shift < minShift) continue; + Rect r(rect.x0() >> fc.hshift, rect.y0() >> fc.vshift, + rect.xsize() >> fc.hshift, rect.ysize() >> fc.vshift, fc.w, fc.h); + if (r.xsize() == 0 || r.ysize() == 0) continue; + JXL_ASSERT(use_full_image); + CopyImageTo(/*rect_from=*/Rect(0, 0, r.xsize(), r.ysize()), + /*from=*/gi.channel[gic].plane, + /*rect_to=*/r, /*to=*/&fc.plane); + gic++; + } + return true; +} +Status ModularFrameDecoder::DecodeVarDCTDC(size_t group_id, BitReader* reader, + PassesDecoderState* dec_state) { + const Rect r = dec_state->shared->DCGroupRect(group_id); + // TODO(eustas): investigate if we could reduce the impact of + // EvalRationalPolynomial; generally speaking, the limit is + // 2**(128/(3*magic)), where 128 comes from IEEE 754 exponent, + // 3 comes from XybToRgb that cubes the values, and "magic" is + // the sum of all other contributions. 2**18 is known to lead + // to NaN on input found by fuzzing (see commit message). + Image image(r.xsize(), r.ysize(), full_image.bitdepth, 3); + size_t stream_id = ModularStreamId::VarDCTDC(group_id).ID(frame_dim); + reader->Refill(); + size_t extra_precision = reader->ReadFixedBits<2>(); + float mul = 1.0f / (1 << extra_precision); + ModularOptions options; + for (size_t c = 0; c < 3; c++) { + Channel& ch = image.channel[c < 2 ? c ^ 1 : c]; + ch.w >>= dec_state->shared->frame_header.chroma_subsampling.HShift(c); + ch.h >>= dec_state->shared->frame_header.chroma_subsampling.VShift(c); + ch.shrink(); + } + if (!ModularGenericDecompress( + reader, image, /*header=*/nullptr, stream_id, &options, + /*undo_transforms=*/-1, &tree, &code, &context_map)) { + return JXL_FAILURE("Failed to decode modular DC group"); + } + DequantDC(r, &dec_state->shared_storage.dc_storage, + &dec_state->shared_storage.quant_dc, image, + dec_state->shared->quantizer.MulDC(), mul, + dec_state->shared->cmap.DCFactors(), + dec_state->shared->frame_header.chroma_subsampling, + dec_state->shared->block_ctx_map); + return true; +} + +Status ModularFrameDecoder::DecodeAcMetadata(size_t group_id, BitReader* reader, + PassesDecoderState* dec_state) { + const Rect r = dec_state->shared->DCGroupRect(group_id); + size_t upper_bound = r.xsize() * r.ysize(); + reader->Refill(); + size_t count = reader->ReadBits(CeilLog2Nonzero(upper_bound)) + 1; + size_t stream_id = ModularStreamId::ACMetadata(group_id).ID(frame_dim); + // YToX, YToB, ACS + QF, EPF + Image image(r.xsize(), r.ysize(), full_image.bitdepth, 4); + static_assert(kColorTileDimInBlocks == 8, "Color tile size changed"); + Rect cr(r.x0() >> 3, r.y0() >> 3, (r.xsize() + 7) >> 3, (r.ysize() + 7) >> 3); + image.channel[0] = Channel(cr.xsize(), cr.ysize(), 3, 3); + image.channel[1] = Channel(cr.xsize(), cr.ysize(), 3, 3); + image.channel[2] = Channel(count, 2, 0, 0); + ModularOptions options; + if (!ModularGenericDecompress( + reader, image, /*header=*/nullptr, stream_id, &options, + /*undo_transforms=*/-1, &tree, &code, &context_map)) { + return JXL_FAILURE("Failed to decode AC metadata"); + } + ConvertPlaneAndClamp(Rect(image.channel[0].plane), image.channel[0].plane, cr, + &dec_state->shared_storage.cmap.ytox_map); + ConvertPlaneAndClamp(Rect(image.channel[1].plane), image.channel[1].plane, cr, + &dec_state->shared_storage.cmap.ytob_map); + size_t num = 0; + bool is444 = dec_state->shared->frame_header.chroma_subsampling.Is444(); + auto& ac_strategy = dec_state->shared_storage.ac_strategy; + size_t xlim = std::min(ac_strategy.xsize(), r.x0() + r.xsize()); + size_t ylim = std::min(ac_strategy.ysize(), r.y0() + r.ysize()); + uint32_t local_used_acs = 0; + for (size_t iy = 0; iy < r.ysize(); iy++) { + size_t y = r.y0() + iy; + int* row_qf = r.Row(&dec_state->shared_storage.raw_quant_field, iy); + uint8_t* row_epf = r.Row(&dec_state->shared_storage.epf_sharpness, iy); + int* row_in_1 = image.channel[2].plane.Row(0); + int* row_in_2 = image.channel[2].plane.Row(1); + int* row_in_3 = image.channel[3].plane.Row(iy); + for (size_t ix = 0; ix < r.xsize(); ix++) { + size_t x = r.x0() + ix; + int sharpness = row_in_3[ix]; + if (sharpness < 0 || sharpness >= LoopFilter::kEpfSharpEntries) { + return JXL_FAILURE("Corrupted sharpness field"); + } + row_epf[ix] = sharpness; + if (ac_strategy.IsValid(x, y)) { + continue; + } + + if (num >= count) return JXL_FAILURE("Corrupted stream"); + + if (!AcStrategy::IsRawStrategyValid(row_in_1[num])) { + return JXL_FAILURE("Invalid AC strategy"); + } + local_used_acs |= 1u << row_in_1[num]; + AcStrategy acs = AcStrategy::FromRawStrategy(row_in_1[num]); + if ((acs.covered_blocks_x() > 1 || acs.covered_blocks_y() > 1) && + !is444) { + return JXL_FAILURE( + "AC strategy not compatible with chroma subsampling"); + } + // Ensure that blocks do not overflow *AC* groups. + size_t next_x_ac_block = (x / kGroupDimInBlocks + 1) * kGroupDimInBlocks; + size_t next_y_ac_block = (y / kGroupDimInBlocks + 1) * kGroupDimInBlocks; + size_t next_x_dct_block = x + acs.covered_blocks_x(); + size_t next_y_dct_block = y + acs.covered_blocks_y(); + if (next_x_dct_block > next_x_ac_block || next_x_dct_block > xlim) { + return JXL_FAILURE("Invalid AC strategy, x overflow"); + } + if (next_y_dct_block > next_y_ac_block || next_y_dct_block > ylim) { + return JXL_FAILURE("Invalid AC strategy, y overflow"); + } + JXL_RETURN_IF_ERROR( + ac_strategy.SetNoBoundsCheck(x, y, AcStrategy::Type(row_in_1[num]))); + row_qf[ix] = + 1 + std::max(0, std::min(Quantizer::kQuantMax - 1, row_in_2[num])); + num++; + } + } + dec_state->used_acs |= local_used_acs; + if (dec_state->shared->frame_header.loop_filter.epf_iters > 0) { + ComputeSigma(r, dec_state); + } + return true; +} + +Status ModularFrameDecoder::ModularImageToDecodedRect( + Image& gi, PassesDecoderState* dec_state, jxl::ThreadPool* pool, + ImageBundle* output, Rect rect) { + auto& decoded = dec_state->decoded; + const auto& frame_header = dec_state->shared->frame_header; + const auto* metadata = frame_header.nonserialized_metadata; + size_t xsize = rect.xsize(); + size_t ysize = rect.ysize(); + if (!xsize || !ysize) { + return true; + } + JXL_DASSERT(rect.IsInside(decoded)); + + size_t c = 0; + if (do_color) { + const bool rgb_from_gray = + metadata->m.color_encoding.IsGray() && + frame_header.color_transform == ColorTransform::kNone; + 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; + size_t c_in = c; + if (frame_header.color_transform == ColorTransform::kXYB) { + factor = dec_state->shared->matrices.DCQuants()[c]; + // XYB is encoded as YX(B-Y) + if (c < 2) c_in = 1 - c; + } else if (rgb_from_gray) { + c_in = 0; + } + JXL_ASSERT(c_in < gi.channel.size()); + Channel& ch_in = gi.channel[c_in]; + // TODO(eustas): could we detect it on earlier stage? + if (ch_in.w == 0 || ch_in.h == 0) { + return JXL_FAILURE("Empty image"); + } + size_t xsize_shifted = DivCeil(xsize, 1 << ch_in.hshift); + size_t ysize_shifted = DivCeil(ysize, 1 << ch_in.vshift); + Rect r(rect.x0() >> ch_in.hshift, rect.y0() >> ch_in.vshift, + rect.xsize() >> ch_in.hshift, rect.ysize() >> ch_in.vshift, + DivCeil(decoded.xsize(), 1 << ch_in.hshift), + DivCeil(decoded.ysize(), 1 << ch_in.vshift)); + if (r.ysize() != ch_in.h || r.xsize() != ch_in.w) { + return JXL_FAILURE( + "Dimension mismatch: trying to fit a %zux%zu modular channel into " + "a %zux%zu rect", + ch_in.w, ch_in.h, r.xsize(), r.ysize()); + } + if (frame_header.color_transform == ColorTransform::kXYB && c == 2) { + JXL_ASSERT(!fp); + RunOnPool( + pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + const pixel_type* const JXL_RESTRICT row_in = ch_in.Row(y); + const pixel_type* const JXL_RESTRICT row_in_Y = + gi.channel[0].Row(y); + float* const JXL_RESTRICT row_out = r.PlaneRow(&decoded, c, y); + HWY_DYNAMIC_DISPATCH(MultiplySum) + (xsize_shifted, row_in, row_in_Y, factor, row_out); + }, + "ModularIntToFloat"); + } else if (fp) { + int bits = metadata->m.bit_depth.bits_per_sample; + int exp_bits = metadata->m.bit_depth.exponent_bits_per_sample; + RunOnPool( + pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + const pixel_type* const JXL_RESTRICT row_in = ch_in.Row(y); + float* const JXL_RESTRICT row_out = r.PlaneRow(&decoded, c, y); + int_to_float(row_in, row_out, xsize_shifted, bits, exp_bits); + }, + "ModularIntToFloat_losslessfloat"); + } else { + RunOnPool( + pool, 0, ysize_shifted, jxl::ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + const pixel_type* const JXL_RESTRICT row_in = ch_in.Row(y); + if (rgb_from_gray) { + HWY_DYNAMIC_DISPATCH(RgbFromSingle) + (xsize_shifted, row_in, factor, &decoded, c, y, r); + } else { + HWY_DYNAMIC_DISPATCH(SingleFromSingle) + (xsize_shifted, row_in, factor, &decoded, c, y, r); + } + }, + "ModularIntToFloat"); + } + if (rgb_from_gray) { + break; + } + } + if (rgb_from_gray) { + c = 1; + } + } + for (size_t ec = 0; ec < dec_state->extra_channels.size(); ec++, c++) { + const ExtraChannelInfo& eci = output->metadata()->extra_channel_info[ec]; + 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; + JXL_ASSERT(fp || bits < 32); + const float mul = fp ? 0 : (1.0f / ((1u << bits) - 1)); + size_t ecups = frame_header.extra_channel_upsampling[ec]; + const size_t ec_xsize = DivCeil(frame_dim.xsize_upsampled, ecups); + const size_t ec_ysize = DivCeil(frame_dim.ysize_upsampled, ecups); + JXL_ASSERT(c < gi.channel.size()); + Channel& ch_in = gi.channel[c]; + // For x0, y0 there's no need to do a DivCeil(). + JXL_DASSERT(rect.x0() % (1ul << ch_in.hshift) == 0); + JXL_DASSERT(rect.y0() % (1ul << ch_in.vshift) == 0); + Rect r(rect.x0() >> ch_in.hshift, rect.y0() >> ch_in.vshift, + DivCeil(rect.xsize(), 1lu << ch_in.hshift), + DivCeil(rect.ysize(), 1lu << ch_in.vshift), ec_xsize, ec_ysize); + + JXL_DASSERT(r.IsInside(dec_state->extra_channels[ec])); + JXL_DASSERT(Rect(0, 0, r.xsize(), r.ysize()).IsInside(ch_in.plane)); + for (size_t y = 0; y < r.ysize(); ++y) { + float* const JXL_RESTRICT row_out = + r.Row(&dec_state->extra_channels[ec], y); + const pixel_type* const JXL_RESTRICT row_in = ch_in.Row(y); + 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; + } + } + } + JXL_CHECK_IMAGE_INITIALIZED(dec_state->extra_channels[ec], r); + } + return true; +} + +Status ModularFrameDecoder::FinalizeDecoding(PassesDecoderState* dec_state, + jxl::ThreadPool* pool, + ImageBundle* output) { + if (!use_full_image) return true; + Image& gi = full_image; + size_t xsize = gi.w; + size_t ysize = gi.h; + + // Don't use threads if total image size is smaller than a group + if (xsize * ysize < frame_dim.group_dim * frame_dim.group_dim) pool = nullptr; + + // Undo the global transforms + gi.undo_transforms(global_header.wp_header, -1, pool); + for (auto t : global_transform) { + JXL_RETURN_IF_ERROR(t.Inverse(gi, global_header.wp_header)); + } + if (gi.error) return JXL_FAILURE("Undoing transforms failed"); + + auto& decoded = dec_state->decoded; + + JXL_RETURN_IF_ERROR( + ModularImageToDecodedRect(gi, dec_state, pool, output, Rect(decoded))); + return true; +} + +static constexpr const float kAlmostZero = 1e-8f; + +Status ModularFrameDecoder::DecodeQuantTable( + size_t required_size_x, size_t required_size_y, BitReader* br, + QuantEncoding* encoding, size_t idx, + ModularFrameDecoder* modular_frame_decoder) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &encoding->qraw.qtable_den)); + if (encoding->qraw.qtable_den < kAlmostZero) { + // qtable[] values are already checked for <= 0 so the denominator may not + // be negative. + return JXL_FAILURE("Invalid qtable_den: value too small"); + } + Image image(required_size_x, required_size_y, 8, 3); + ModularOptions options; + if (modular_frame_decoder) { + JXL_RETURN_IF_ERROR(ModularGenericDecompress( + br, image, /*header=*/nullptr, + ModularStreamId::QuantTable(idx).ID(modular_frame_decoder->frame_dim), + &options, /*undo_transforms=*/-1, &modular_frame_decoder->tree, + &modular_frame_decoder->code, &modular_frame_decoder->context_map)); + } else { + JXL_RETURN_IF_ERROR(ModularGenericDecompress(br, image, /*header=*/nullptr, + 0, &options, + /*undo_transforms=*/-1)); + } + if (!encoding->qraw.qtable) { + encoding->qraw.qtable = new std::vector(); + } + encoding->qraw.qtable->resize(required_size_x * required_size_y * 3); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < required_size_y; y++) { + int* JXL_RESTRICT row = image.channel[c].Row(y); + for (size_t x = 0; x < required_size_x; x++) { + (*encoding->qraw.qtable)[c * required_size_x * required_size_y + + y * required_size_x + x] = row[x]; + if (row[x] <= 0) { + return JXL_FAILURE("Invalid raw quantization table"); + } + } + } + } + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_modular.h b/lib/jxl/dec_modular.h new file mode 100644 index 0000000..5a3225e --- /dev/null +++ b/lib/jxl/dec_modular.h @@ -0,0 +1,133 @@ +// 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_DEC_MODULAR_H_ +#define LIB_JXL_DEC_MODULAR_H_ + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +struct ModularStreamId { + enum Kind { + kGlobalData, + kVarDCTDC, + kModularDC, + kACMetadata, + kQuantTable, + kModularAC + }; + Kind kind; + size_t quant_table_id; + size_t group_id; // DC or AC group id. + size_t pass_id; // Only for kModularAC. + size_t ID(const FrameDimensions& frame_dim) const { + size_t id = 0; + switch (kind) { + case kGlobalData: + id = 0; + break; + case kVarDCTDC: + id = 1 + group_id; + break; + case kModularDC: + id = 1 + frame_dim.num_dc_groups + group_id; + break; + case kACMetadata: + id = 1 + 2 * frame_dim.num_dc_groups + group_id; + break; + case kQuantTable: + id = 1 + 3 * frame_dim.num_dc_groups + quant_table_id; + break; + case kModularAC: + id = 1 + 3 * frame_dim.num_dc_groups + DequantMatrices::kNum + + frame_dim.num_groups * pass_id + group_id; + break; + }; + return id; + } + static ModularStreamId Global() { + return ModularStreamId{kGlobalData, 0, 0, 0}; + } + static ModularStreamId VarDCTDC(size_t group_id) { + return ModularStreamId{kVarDCTDC, 0, group_id, 0}; + } + static ModularStreamId ModularDC(size_t group_id) { + return ModularStreamId{kModularDC, 0, group_id, 0}; + } + static ModularStreamId ACMetadata(size_t group_id) { + return ModularStreamId{kACMetadata, 0, group_id, 0}; + } + static ModularStreamId QuantTable(size_t quant_table_id) { + JXL_ASSERT(quant_table_id < DequantMatrices::kNum); + return ModularStreamId{kQuantTable, quant_table_id, 0, 0}; + } + static ModularStreamId ModularAC(size_t group_id, size_t pass_id) { + return ModularStreamId{kModularAC, 0, group_id, pass_id}; + } + static size_t Num(const FrameDimensions& frame_dim, size_t passes) { + return ModularAC(0, passes).ID(frame_dim); + } +}; + +class ModularFrameDecoder { + public: + void Init(const FrameDimensions& frame_dim) { this->frame_dim = frame_dim; } + Status DecodeGlobalInfo(BitReader* reader, const FrameHeader& frame_header, + bool allow_truncated_group); + Status DecodeGroup(const Rect& rect, BitReader* reader, int minShift, + int maxShift, const ModularStreamId& stream, bool zerofill, + PassesDecoderState* dec_state, ImageBundle* output); + // Decodes a VarDCT DC group (`group_id`) from the given `reader`. + Status DecodeVarDCTDC(size_t group_id, BitReader* reader, + PassesDecoderState* dec_state); + // Decodes a VarDCT AC Metadata group (`group_id`) from the given `reader`. + Status DecodeAcMetadata(size_t group_id, BitReader* reader, + PassesDecoderState* dec_state); + // Decodes a RAW quant table from `br` into the given `encoding`, of size + // `required_size_x x required_size_y`. If `modular_frame_decoder` is passed, + // its global tree is used, otherwise no global tree is used. + static Status DecodeQuantTable(size_t required_size_x, size_t required_size_y, + BitReader* br, QuantEncoding* encoding, + size_t idx, + ModularFrameDecoder* modular_frame_decoder); + Status FinalizeDecoding(PassesDecoderState* dec_state, jxl::ThreadPool* pool, + ImageBundle* output); + bool have_dc() const { return have_something; } + void MaybeDropFullImage(); + + private: + Status ModularImageToDecodedRect(Image& gi, PassesDecoderState* dec_state, + jxl::ThreadPool* pool, ImageBundle* output, + Rect rect); + + Image full_image; + std::vector global_transform; + FrameDimensions frame_dim; + bool do_color; + bool have_something; + bool use_full_image = true; + bool all_same_shift; + Tree tree; + ANSCode code; + std::vector context_map; + GroupHeader global_header; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_MODULAR_H_ diff --git a/lib/jxl/dec_noise.cc b/lib/jxl/dec_noise.cc new file mode 100644 index 0000000..240b8af --- /dev/null +++ b/lib/jxl/dec_noise.cc @@ -0,0 +1,295 @@ +// 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/dec_noise.h" + +#include +#include +#include + +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_noise.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/xorshift128plus-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::ShiftRight; +using hwy::HWY_NAMESPACE::Vec; + +using D = HWY_CAPPED(float, kBlockDim); +using DI = hwy::HWY_NAMESPACE::Rebind; +using DI8 = hwy::HWY_NAMESPACE::Repartition; + +// Converts one vector's worth of random bits to floats in [1, 2). +// NOTE: as the convolution kernel sums to 0, it doesn't matter if inputs are in +// [0, 1) or in [1, 2). +void BitsToFloat(const uint32_t* JXL_RESTRICT random_bits, + float* JXL_RESTRICT floats) { + const HWY_FULL(float) df; + const HWY_FULL(uint32_t) du; + + const auto bits = Load(du, random_bits); + // 1.0 + 23 random mantissa bits = [1, 2) + const auto rand12 = BitCast(df, ShiftRight<9>(bits) | Set(du, 0x3F800000)); + Store(rand12, df, floats); +} + +void RandomImage(Xorshift128Plus* rng, const Rect& rect, + ImageF* JXL_RESTRICT noise) { + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + + // May exceed the vector size, hence we have two loops over x below. + constexpr size_t kFloatsPerBatch = + Xorshift128Plus::N * sizeof(uint64_t) / sizeof(float); + HWY_ALIGN uint64_t batch[Xorshift128Plus::N]; + + const HWY_FULL(float) df; + const size_t N = Lanes(df); + + for (size_t y = 0; y < ysize; ++y) { + float* JXL_RESTRICT row = rect.Row(noise, y); + + size_t x = 0; + // Only entire batches (avoids exceeding the image padding). + for (; x + kFloatsPerBatch <= xsize; x += kFloatsPerBatch) { + rng->Fill(batch); + for (size_t i = 0; i < kFloatsPerBatch; i += Lanes(df)) { + BitsToFloat(reinterpret_cast(batch) + i, row + x + i); + } + } + + // Any remaining pixels, rounded up to vectors (safe due to padding). + rng->Fill(batch); + size_t batch_pos = 0; // < kFloatsPerBatch + for (; x < xsize; x += N) { + BitsToFloat(reinterpret_cast(batch) + batch_pos, + row + x); + batch_pos += N; + } + } +} + +// [0, max_value] +template +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 +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; + + 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), Set(D(), kScale - 1), + floor_x); + frac_x = IfThenElse(scaled_vx >= Set(D(), kScale), 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 +void AddNoiseToRGB(const D d, const Vec rnd_noise_r, + const Vec rnd_noise_g, const Vec rnd_noise_cor, + const Vec noise_strength_g, const Vec 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 seed, const Rect& rect, Image3F* JXL_RESTRICT noise) { + HWY_ALIGN Xorshift128Plus rng(seed); + RandomImage(&rng, rect, &noise->Plane(0)); + RandomImage(&rng, rect, &noise->Plane(1)); + RandomImage(&rng, rect, &noise->Plane(2)); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +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 seed, const Rect& rect, Image3F* JXL_RESTRICT noise) { + return HWY_DYNAMIC_DISPATCH(RandomImage3)(seed, rect, noise); +} + +void DecodeFloatParam(float precision, float* val, BitReader* br) { + const int absval_quant = br->ReadFixedBits<10>(); + *val = absval_quant / precision; +} + +Status DecodeNoise(BitReader* br, NoiseParams* noise_params) { + for (float& i : noise_params->lut) { + DecodeFloatParam(kNoisePrecision, &i, br); + } + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_noise.h b/lib/jxl/dec_noise.h new file mode 100644 index 0000000..f7135e7 --- /dev/null +++ b/lib/jxl/dec_noise.h @@ -0,0 +1,36 @@ +// 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_DEC_NOISE_H_ +#define LIB_JXL_DEC_NOISE_H_ + +// Noise synthesis. Currently disabled. + +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/image.h" +#include "lib/jxl/noise.h" + +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 seed, const Rect& rect, Image3F* JXL_RESTRICT noise); + +// Must only call if FrameHeader.flags.kNoise. +Status DecodeNoise(BitReader* br, NoiseParams* noise_params); + +} // namespace jxl + +#endif // LIB_JXL_DEC_NOISE_H_ diff --git a/lib/jxl/dec_params.h b/lib/jxl/dec_params.h new file mode 100644 index 0000000..e3131e6 --- /dev/null +++ b/lib/jxl/dec_params.h @@ -0,0 +1,62 @@ +// 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_DEC_PARAMS_H_ +#define LIB_JXL_DEC_PARAMS_H_ + +// Parameters and flags that govern JXL decompression. + +#include +#include + +#include + +#include "lib/jxl/base/override.h" + +namespace jxl { + +struct DecompressParams { + // If true, checks at the end of decoding that all of the compressed data + // was consumed by the decoder. + bool check_decompressed_size = true; + + // If true, skip dequant and iDCT and decode to JPEG (only if possible) + bool keep_dct = false; + // If true, render spot colors (otherwise only returned as extra channels) + bool render_spotcolors = true; + + // These cannot be kOn because they need encoder support. + Override preview = Override::kDefault; + + // How many passes to decode at most. By default, decode everything. + uint32_t max_passes = std::numeric_limits::max(); + // Alternatively, one can specify the maximum tolerable downscaling factor + // with respect to the full size of the image. By default, nothing less than + // the full size is requested. + size_t max_downsampling = 1; + + // Try to decode as much as possible of a truncated codestream, but only whole + // sections at a time. + bool allow_partial_files = false; + // Allow even more progression. + bool allow_more_progressive_steps = false; + + bool operator==(const DecompressParams other) const { + return check_decompressed_size == other.check_decompressed_size && + keep_dct == other.keep_dct && + render_spotcolors == other.render_spotcolors && + preview == other.preview && max_passes == other.max_passes && + max_downsampling == other.max_downsampling && + allow_partial_files == other.allow_partial_files && + allow_more_progressive_steps == other.allow_more_progressive_steps; + } + bool operator!=(const DecompressParams& other) const { + return !(*this == other); + } +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_PARAMS_H_ diff --git a/lib/jxl/dec_patch_dictionary.cc b/lib/jxl/dec_patch_dictionary.cc new file mode 100644 index 0000000..7e0af7d --- /dev/null +++ b/lib/jxl/dec_patch_dictionary.cc @@ -0,0 +1,248 @@ +// 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/dec_patch_dictionary.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/blending.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_frame.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/patch_dictionary_internal.h" + +namespace jxl { + +constexpr size_t kMaxPatches = 1 << 24; + +Status PatchDictionary::Decode(BitReader* br, size_t xsize, size_t ysize, + bool* uses_extra_channels) { + positions_.clear(); + std::vector context_map; + ANSCode code; + JXL_RETURN_IF_ERROR( + DecodeHistograms(br, kNumPatchDictionaryContexts, &code, &context_map)); + ANSSymbolReader decoder(&code, br); + + auto read_num = [&](size_t context) { + size_t r = decoder.ReadHybridUint(context, br, context_map); + return r; + }; + + size_t num_ref_patch = read_num(kNumRefPatchContext); + // TODO(veluca): does this make sense? + if (num_ref_patch > kMaxPatches) { + return JXL_FAILURE("Too many patches in dictionary"); + } + + size_t total_patches = 0; + size_t next_size = 1; + + for (size_t id = 0; id < num_ref_patch; id++) { + PatchReferencePosition ref_pos; + ref_pos.ref = read_num(kReferenceFrameContext); + if (ref_pos.ref >= kMaxNumReferenceFrames || + shared_->reference_frames[ref_pos.ref].frame->xsize() == 0) { + return JXL_FAILURE("Invalid reference frame ID"); + } + if (!shared_->reference_frames[ref_pos.ref].ib_is_in_xyb) { + return JXL_FAILURE( + "Patches cannot use frames saved post color transforms"); + } + const ImageBundle& ib = *shared_->reference_frames[ref_pos.ref].frame; + ref_pos.x0 = read_num(kPatchReferencePositionContext); + ref_pos.y0 = read_num(kPatchReferencePositionContext); + ref_pos.xsize = read_num(kPatchSizeContext) + 1; + ref_pos.ysize = read_num(kPatchSizeContext) + 1; + if (ref_pos.x0 + ref_pos.xsize > ib.xsize()) { + return JXL_FAILURE("Invalid position specified in reference frame"); + } + if (ref_pos.y0 + ref_pos.ysize > ib.ysize()) { + return JXL_FAILURE("Invalid position specified in reference frame"); + } + size_t id_count = read_num(kPatchCountContext) + 1; + total_patches += id_count; + if (total_patches > kMaxPatches) { + return JXL_FAILURE("Too many patches in dictionary"); + } + if (next_size < total_patches) { + next_size *= 2; + next_size = std::min(next_size, kMaxPatches); + } + positions_.reserve(next_size); + for (size_t i = 0; i < id_count; i++) { + PatchPosition pos; + pos.ref_pos = ref_pos; + if (i == 0) { + pos.x = read_num(kPatchPositionContext); + pos.y = read_num(kPatchPositionContext); + } else { + pos.x = + positions_.back().x + UnpackSigned(read_num(kPatchOffsetContext)); + pos.y = + positions_.back().y + UnpackSigned(read_num(kPatchOffsetContext)); + } + if (pos.x + ref_pos.xsize > xsize) { + return JXL_FAILURE("Invalid patch x: at %zu + %zu > %zu", pos.x, + ref_pos.xsize, xsize); + } + if (pos.y + ref_pos.ysize > ysize) { + return JXL_FAILURE("Invalid patch y: at %zu + %zu > %zu", pos.y, + ref_pos.ysize, ysize); + } + for (size_t i = 0; i < shared_->metadata->m.extra_channel_info.size() + 1; + i++) { + uint32_t blend_mode = read_num(kPatchBlendModeContext); + if (blend_mode >= uint32_t(PatchBlendMode::kNumBlendModes)) { + return JXL_FAILURE("Invalid patch blend mode: %u", blend_mode); + } + PatchBlending info; + info.mode = static_cast(blend_mode); + if (UsesAlpha(info.mode)) { + *uses_extra_channels = true; + } + if (info.mode != PatchBlendMode::kNone && i > 0) { + *uses_extra_channels = true; + } + if (UsesAlpha(info.mode) && + shared_->metadata->m.extra_channel_info.size() > 1) { + info.alpha_channel = read_num(kPatchAlphaChannelContext); + if (info.alpha_channel >= + shared_->metadata->m.extra_channel_info.size()) { + return JXL_FAILURE( + "Invalid alpha channel for blending: %u out of %u\n", + info.alpha_channel, + (uint32_t)shared_->metadata->m.extra_channel_info.size()); + } + } else { + info.alpha_channel = 0; + } + if (UsesClamp(info.mode)) { + info.clamp = read_num(kPatchClampContext); + } else { + info.clamp = false; + } + pos.blending.push_back(info); + } + positions_.push_back(std::move(pos)); + } + } + positions_.shrink_to_fit(); + + if (!decoder.CheckANSFinalState()) { + return JXL_FAILURE("ANS checksum failure."); + } + if (!HasAny()) { + return JXL_FAILURE("Decoded patch dictionary but got none"); + } + + ComputePatchCache(); + return true; +} + +int PatchDictionary::GetReferences() const { + int result = 0; + for (size_t i = 0; i < positions_.size(); ++i) { + result |= (1 << static_cast(positions_[i].ref_pos.ref)); + } + return result; +} + +void PatchDictionary::ComputePatchCache() { + patch_starts_.clear(); + sorted_patches_.clear(); + if (positions_.empty()) return; + std::vector> sorted_patches_y; + for (size_t i = 0; i < positions_.size(); i++) { + const PatchPosition& pos = positions_[i]; + for (size_t y = pos.y; y < pos.y + pos.ref_pos.ysize; y++) { + sorted_patches_y.emplace_back(y, i); + } + } + // The relative order of patches that affect the same pixels is preserved. + // This is important for patches that have a blend mode different from kAdd. + std::sort(sorted_patches_y.begin(), sorted_patches_y.end()); + patch_starts_.resize(sorted_patches_y.back().first + 2, + sorted_patches_y.size()); + sorted_patches_.resize(sorted_patches_y.size()); + for (size_t i = 0; i < sorted_patches_y.size(); i++) { + sorted_patches_[i] = sorted_patches_y[i].second; + patch_starts_[sorted_patches_y[i].first] = + std::min(patch_starts_[sorted_patches_y[i].first], i); + } + for (size_t i = patch_starts_.size() - 1; i > 0; i--) { + patch_starts_[i - 1] = std::min(patch_starts_[i], patch_starts_[i - 1]); + } +} + +Status 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 true; + size_t num_ec = shared_->metadata->m.num_extra_channels; + std::vector fg_ptrs(3 + num_ec); + std::vector 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(); + } + JXL_RETURN_IF_ERROR( + PerformBlending(bg_ptrs.data(), fg_ptrs.data(), bg_ptrs.data(), + x1 - x0, pos.blending[0], pos.blending.data() + 1, + shared_->metadata->m.extra_channel_info)); + } + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/dec_patch_dictionary.h b/lib/jxl/dec_patch_dictionary.h new file mode 100644 index 0000000..8e3c4d0 --- /dev/null +++ b/lib/jxl/dec_patch_dictionary.h @@ -0,0 +1,200 @@ +// 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_DEC_PATCH_DICTIONARY_H_ +#define LIB_JXL_DEC_PATCH_DICTIONARY_H_ + +// Chooses reference patches, and avoids encoding them once per occurrence. + +#include +#include +#include + +#include +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/image.h" +#include "lib/jxl/opsin_params.h" + +namespace jxl { + +constexpr size_t kMaxPatchSize = 32; + +enum class PatchBlendMode : uint8_t { + // The new values are the old ones. Useful to skip some channels. + kNone = 0, + // The new values (in the crop) replace the old ones: sample = new + kReplace = 1, + // The new values (in the crop) get added to the old ones: sample = old + new + kAdd = 2, + // The new values (in the crop) get multiplied by the old ones: + // sample = old * new + // This blend mode is only supported if BlendColorSpace is kEncoded. The + // range of the new value matters for multiplication purposes, and its + // nominal range of 0..1 is computed the same way as this is done for the + // alpha values in kBlend and kAlphaWeightedAdd. + kMul = 3, + // The new values (in the crop) replace the old ones if alpha>0: + // For first alpha channel: + // alpha = old + new * (1 - old) + // For other channels if !alpha_associated: + // sample = ((1 - new_alpha) * old * old_alpha + new_alpha * new) / alpha + // For other channels if alpha_associated: + // sample = (1 - new_alpha) * old + new + // The alpha formula applies to the alpha used for the division in the other + // channels formula, and applies to the alpha channel itself if its + // blend_channel value matches itself. + // If using kBlendAbove, new is the patch and old is the original image; if + // using kBlendBelow, the meaning is inverted. + kBlendAbove = 4, + kBlendBelow = 5, + // The new values (in the crop) are added to the old ones if alpha>0: + // For first alpha channel: sample = sample = old + new * (1 - old) + // For other channels: sample = old + alpha * new + kAlphaWeightedAddAbove = 6, + kAlphaWeightedAddBelow = 7, + kNumBlendModes, +}; + +inline bool UsesAlpha(PatchBlendMode mode) { + return mode == PatchBlendMode::kBlendAbove || + mode == PatchBlendMode::kBlendBelow || + mode == PatchBlendMode::kAlphaWeightedAddAbove || + mode == PatchBlendMode::kAlphaWeightedAddBelow; +} +inline bool UsesClamp(PatchBlendMode mode) { + return UsesAlpha(mode) || mode == PatchBlendMode::kMul; +} + +struct PatchBlending { + PatchBlendMode mode; + uint32_t alpha_channel; + bool clamp; +}; + +struct QuantizedPatch { + size_t xsize; + size_t ysize; + QuantizedPatch() { + for (size_t i = 0; i < 3; i++) { + pixels[i].resize(kMaxPatchSize * kMaxPatchSize); + fpixels[i].resize(kMaxPatchSize * kMaxPatchSize); + } + } + std::vector pixels[3] = {}; + // Not compared. Used only to retrieve original pixels to construct the + // reference image. + std::vector fpixels[3] = {}; + bool operator==(const QuantizedPatch& other) const { + if (xsize != other.xsize) return false; + if (ysize != other.ysize) return false; + for (size_t c = 0; c < 3; c++) { + if (memcmp(pixels[c].data(), other.pixels[c].data(), + sizeof(int8_t) * xsize * ysize) != 0) + return false; + } + return true; + } + + bool operator<(const QuantizedPatch& other) const { + if (xsize != other.xsize) return xsize < other.xsize; + if (ysize != other.ysize) return ysize < other.ysize; + for (size_t c = 0; c < 3; c++) { + int cmp = memcmp(pixels[c].data(), other.pixels[c].data(), + sizeof(int8_t) * xsize * ysize); + if (cmp > 0) return false; + if (cmp < 0) return true; + } + return false; + } +}; + +// Pair (patch, vector of occurrences). +using PatchInfo = + std::pair>>; + +// Position and size of the patch in the reference frame. +struct PatchReferencePosition { + size_t ref, x0, y0, xsize, ysize; + bool operator<(const PatchReferencePosition& oth) const { + return std::make_tuple(ref, x0, y0, xsize, ysize) < + std::make_tuple(oth.ref, oth.x0, oth.y0, oth.xsize, oth.ysize); + } + bool operator==(const PatchReferencePosition& oth) const { + return !(*this < oth) && !(oth < *this); + } +}; + +struct PatchPosition { + // Position of top-left corner of the patch in the image. + size_t x, y; + // Different blend mode for color and extra channels. + std::vector blending; + PatchReferencePosition ref_pos; + bool operator<(const PatchPosition& oth) const { + return std::make_tuple(ref_pos, x, y) < + std::make_tuple(oth.ref_pos, oth.x, oth.y); + } +}; + +struct PassesSharedState; + +// Encoder-side helper class to encode the PatchesDictionary. +class PatchDictionaryEncoder; + +class PatchDictionary { + public: + PatchDictionary() = default; + + void SetPassesSharedState(const PassesSharedState* shared) { + shared_ = shared; + } + + bool HasAny() const { return !positions_.empty(); } + + Status Decode(BitReader* br, size_t xsize, size_t ysize, + bool* uses_extra_channels); + + void Clear() { + positions_.clear(); + ComputePatchCache(); + } + + // Only adds patches that belong to the `image_rect` area of the decoded + // image, writing them to the `opsin_rect` area of `opsin`. + Status 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; + + private: + friend class PatchDictionaryEncoder; + + const PassesSharedState* shared_; + std::vector positions_; + + // Patch occurrences sorted by y. + std::vector sorted_patches_; + // Index of the first patch for each y value. + std::vector patch_starts_; + + // Patch IDs in position [patch_starts_[y], patch_start_[y+1]) of + // sorted_patches_ are all the patches that intersect the horizontal line at + // y. + // The relative order of patches that affect the same pixels is the same - + // important when applying patches is noncommutative. + + // Compute patches_by_y_ after updating positions_. + void ComputePatchCache(); +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_PATCH_DICTIONARY_H_ diff --git a/lib/jxl/dec_reconstruct.cc b/lib/jxl/dec_reconstruct.cc new file mode 100644 index 0000000..a1baef4 --- /dev/null +++ b/lib/jxl/dec_reconstruct.cc @@ -0,0 +1,1269 @@ +// 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/dec_reconstruct.h" + +#include +#include + +#include "lib/jxl/filters.h" +#include "lib/jxl/image_ops.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_reconstruct.cc" +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/blending.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_noise.h" +#include "lib/jxl/dec_upsample.h" +#include "lib/jxl/dec_xyb-inl.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/sanitizers.h" +#include "lib/jxl/transfer_functions-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +template +void DoUndoXYBInPlace(Image3F* idct, const Rect& rect, Op op, + const OutputEncodingInfo& output_encoding_info) { + // TODO(eustas): should it still be capped? + const HWY_CAPPED(float, GroupBorderAssigner::kPaddingXRound) d; + const size_t xsize = rect.xsize(); + const size_t xsize_v = RoundUpTo(xsize, Lanes(d)); + // The size of `rect` might not be a multiple of Lanes(d), but is guaranteed + // to be a multiple of kBlockDim or at the margin of the image. + for (size_t y = 0; y < rect.ysize(); y++) { + float* JXL_RESTRICT row0 = rect.PlaneRow(idct, 0, y); + float* JXL_RESTRICT row1 = rect.PlaneRow(idct, 1, y); + float* JXL_RESTRICT row2 = rect.PlaneRow(idct, 2, y); + // All calculations are lane-wise, still some might require value-dependent + // behaviour (e.g. NearestInt). Temporary unposion last vector tail. + msan::UnpoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + for (size_t x = 0; x < rect.xsize(); x += Lanes(d)) { + const auto in_opsin_x = Load(d, row0 + x); + const auto in_opsin_y = Load(d, row1 + x); + const auto in_opsin_b = Load(d, row2 + x); + JXL_COMPILER_FENCE; + auto linear_r = Undefined(d); + auto linear_g = Undefined(d); + auto linear_b = Undefined(d); + XybToRgb(d, in_opsin_x, in_opsin_y, in_opsin_b, + output_encoding_info.opsin_params, &linear_r, &linear_g, + &linear_b); + Store(op.Transform(d, linear_r), d, row0 + x); + Store(op.Transform(d, linear_g), d, row1 + x); + Store(op.Transform(d, linear_b), d, row2 + x); + } + msan::PoisonMemory(row0 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row1 + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row2 + xsize, sizeof(float) * (xsize_v - xsize)); + } +} + +struct OpLinear { + template + T Transform(D d, const T& linear) { + return linear; + } +}; + +struct OpRgb { + template + T Transform(D d, const T& linear) { +#if JXL_HIGH_PRECISION + return TF_SRGB().EncodedFromDisplay(d, linear); +#else + return FastLinearToSRGB(d, linear); +#endif + } +}; + +struct OpPq { + template + T Transform(D d, const T& linear) { + return TF_PQ().EncodedFromDisplay(d, linear); + } +}; + +struct OpHlg { + template + T Transform(D d, const T& linear) { + return TF_HLG().EncodedFromDisplay(d, linear); + } +}; + +struct Op709 { + template + T Transform(D d, const T& linear) { + return TF_709().EncodedFromDisplay(d, linear); + } +}; + +struct OpGamma { + const float inverse_gamma; + template + T Transform(D d, const T& linear) { + return IfThenZeroElse(linear <= Set(d, 1e-5f), + FastPowf(d, linear, Set(d, inverse_gamma))); + } +}; + +Status UndoXYBInPlace(Image3F* idct, const Rect& rect, + const OutputEncodingInfo& output_encoding_info) { + PROFILER_ZONE("UndoXYB"); + + if (output_encoding_info.color_encoding.tf.IsLinear()) { + DoUndoXYBInPlace(idct, rect, OpLinear(), output_encoding_info); + } else if (output_encoding_info.color_encoding.tf.IsSRGB()) { + DoUndoXYBInPlace(idct, rect, OpRgb(), output_encoding_info); + } else if (output_encoding_info.color_encoding.tf.IsPQ()) { + DoUndoXYBInPlace(idct, rect, OpPq(), output_encoding_info); + } else if (output_encoding_info.color_encoding.tf.IsHLG()) { + DoUndoXYBInPlace(idct, rect, OpHlg(), output_encoding_info); + } else if (output_encoding_info.color_encoding.tf.Is709()) { + DoUndoXYBInPlace(idct, rect, Op709(), output_encoding_info); + } else if (output_encoding_info.color_encoding.tf.IsGamma() || + output_encoding_info.color_encoding.tf.IsDCI()) { + OpGamma op = {output_encoding_info.inverse_gamma}; + DoUndoXYBInPlace(idct, rect, op, output_encoding_info); + } else { + // This is a programming error. + JXL_ABORT("Invalid target encoding"); + } + return true; +} + +template +void StoreRGBA(D d, V r, V g, V b, V a, bool alpha, size_t n, size_t extra, + uint8_t* buf) { +#if HWY_TARGET == HWY_SCALAR + buf[0] = r.raw; + buf[1] = g.raw; + buf[2] = b.raw; + if (alpha) { + buf[3] = a.raw; + } +#elif HWY_TARGET == HWY_NEON + if (alpha) { + uint8x8x4_t data = {r.raw, g.raw, b.raw, a.raw}; + if (extra >= 8) { + vst4_u8(buf, data); + } else { + uint8_t tmp[8 * 4]; + vst4_u8(tmp, data); + memcpy(buf, tmp, n * 4); + } + } else { + uint8x8x3_t data = {r.raw, g.raw, b.raw}; + if (extra >= 8) { + vst3_u8(buf, data); + } else { + uint8_t tmp[8 * 3]; + vst3_u8(tmp, data); + memcpy(buf, tmp, n * 3); + } + } +#else + // TODO(veluca): implement this for x86. + size_t mul = alpha ? 4 : 3; + HWY_ALIGN uint8_t bytes[16]; + Store(r, d, bytes); + for (size_t i = 0; i < n; i++) { + buf[mul * i] = bytes[i]; + } + Store(g, d, bytes); + for (size_t i = 0; i < n; i++) { + buf[mul * i + 1] = bytes[i]; + } + Store(b, d, bytes); + for (size_t i = 0; i < n; i++) { + buf[mul * i + 2] = bytes[i]; + } + if (alpha) { + Store(a, d, bytes); + for (size_t i = 0; i < n; i++) { + buf[4 * i + 3] = bytes[i]; + } + } +#endif +} + +// Outputs floating point image to RGBA 8-bit buffer. Does not support alpha +// channel in the input, but outputs opaque alpha channel for the case where the +// output buffer to write to is in the 4-byte per pixel RGBA format. +void FloatToRGBA8(const Image3F& input, const Rect& input_rect, bool is_rgba, + const ImageF* alpha_in, const Rect& alpha_rect, + const Rect& output_buf_rect, uint8_t* JXL_RESTRICT output_buf, + size_t stride) { + size_t bytes = is_rgba ? 4 : 3; + for (size_t y = 0; y < output_buf_rect.ysize(); y++) { + const float* JXL_RESTRICT row_in_r = input_rect.ConstPlaneRow(input, 0, y); + const float* JXL_RESTRICT row_in_g = input_rect.ConstPlaneRow(input, 1, y); + const float* JXL_RESTRICT row_in_b = input_rect.ConstPlaneRow(input, 2, y); + const float* JXL_RESTRICT row_in_a = + alpha_in ? alpha_rect.ConstRow(*alpha_in, y) : nullptr; + size_t base_ptr = + (y + output_buf_rect.y0()) * stride + bytes * output_buf_rect.x0(); + using D = HWY_CAPPED(float, 4); + const D d; + D::Rebind du; + auto zero = Zero(d); + auto one = Set(d, 1.0f); + auto mul = Set(d, 255.0f); + + // All calculations are lane-wise, still some might require value-dependent + // behaviour (e.g. NearestInt). Temporary unposion last vector tail. + size_t xsize = output_buf_rect.xsize(); + size_t xsize_v = RoundUpTo(xsize, Lanes(d)); + msan::UnpoisonMemory(row_in_r + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row_in_g + xsize, sizeof(float) * (xsize_v - xsize)); + msan::UnpoisonMemory(row_in_b + xsize, sizeof(float) * (xsize_v - xsize)); + if (row_in_a) + msan::UnpoisonMemory(row_in_a + xsize, sizeof(float) * (xsize_v - xsize)); + for (size_t x = 0; x < xsize; x += Lanes(d)) { + auto rf = Clamp(zero, Load(d, row_in_r + x), one) * mul; + auto gf = Clamp(zero, Load(d, row_in_g + x), one) * mul; + auto bf = Clamp(zero, Load(d, row_in_b + x), one) * mul; + auto af = row_in_a ? Clamp(zero, Load(d, row_in_a + x), one) * mul + : Set(d, 255.0f); + auto r8 = U8FromU32(BitCast(du, NearestInt(rf))); + auto g8 = U8FromU32(BitCast(du, NearestInt(gf))); + auto b8 = U8FromU32(BitCast(du, NearestInt(bf))); + auto a8 = U8FromU32(BitCast(du, NearestInt(af))); + size_t n = output_buf_rect.xsize() - x; + if (JXL_LIKELY(n >= Lanes(d))) { + StoreRGBA(D::Rebind(), r8, g8, b8, a8, is_rgba, Lanes(d), n, + output_buf + base_ptr + bytes * x); + } else { + StoreRGBA(D::Rebind(), r8, g8, b8, a8, is_rgba, n, n, + output_buf + base_ptr + bytes * x); + } + } + msan::PoisonMemory(row_in_r + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row_in_g + xsize, sizeof(float) * (xsize_v - xsize)); + msan::PoisonMemory(row_in_b + xsize, sizeof(float) * (xsize_v - xsize)); + if (row_in_a) + msan::PoisonMemory(row_in_a + xsize, sizeof(float) * (xsize_v - xsize)); + } +} + +// Upsample in horizonal (if hs=1) and vertical (if vs=1) the plane_in image +// to the output plane_out image. +// The output region "rect" in plane_out and a border around it of lf.Padding() +// will be generated, as long as those pixels fall inside the image frame. +// Otherwise the border pixels that fall outside the image frame in plane_out +// are undefined. +// "rect" is an area inside the plane_out image which corresponds to the +// "frame_rect" area in the frame. plane_in and plane_out both are expected to +// have a padding of kGroupDataXBorder and kGroupDataYBorder on either side of +// X and Y coordinates. This means that when upsampling vertically the plane_out +// row `kGroupDataXBorder + N` will be generated from the plane_in row +// `kGroupDataXBorder + N / 2` (and a previous or next row). +void DoYCbCrUpsampling(size_t hs, size_t vs, ImageF* plane_in, const Rect& rect, + const Rect& frame_rect, const FrameDimensions& frame_dim, + ImageF* plane_out, const LoopFilter& lf, ImageF* temp) { + JXL_DASSERT(SameSize(rect, frame_rect)); + JXL_DASSERT(hs <= 1 && vs <= 1); + // The pixel in (xoff, yoff) is the origin of the downsampling coordinate + // system. + size_t xoff = PassesDecoderState::kGroupDataXBorder; + size_t yoff = PassesDecoderState::kGroupDataYBorder; + + // This X,Y range is the intersection between the requested "rect" expanded + // with a lf.Padding() all around and the image frame translated to the + // coordinate system used by plane_out. + // All the pixels in the [x0, x1) x [y0, y1) range must be defined in the + // plane_out output at the end. + const size_t y0 = rect.y0() - std::min(lf.Padding(), frame_rect.y0()); + const size_t y1 = rect.y0() + + std::min(frame_rect.y0() + rect.ysize() + lf.Padding(), + frame_dim.ysize_padded) - + frame_rect.y0(); + + const size_t x0 = rect.x0() - std::min(lf.Padding(), frame_rect.x0()); + const size_t x1 = rect.x0() + + std::min(frame_rect.x0() + rect.xsize() + lf.Padding(), + frame_dim.xsize_padded) - + frame_rect.x0(); + + if (hs == 0 && vs == 0) { + Rect r(x0, y0, x1 - x0, y1 - y0); + JXL_CHECK_IMAGE_INITIALIZED(*plane_in, r); + CopyImageTo(r, *plane_in, r, plane_out); + return; + } + // Prepare padding if we are on a border. + // Copy the whole row/column here: it is likely similarly fast and ensures + // that we don't forget some parts of padding. + if (frame_rect.x0() == 0) { + for (size_t y = 0; y < plane_in->ysize(); y++) { + plane_in->Row(y)[rect.x0() - 1] = plane_in->Row(y)[rect.x0()]; + } + } + if (frame_rect.x0() + x1 - rect.x0() >= frame_dim.xsize_padded) { + ssize_t borderx = static_cast(x1 - xoff + hs) / (1 << hs) + xoff; + for (size_t y = 0; y < plane_in->ysize(); y++) { + plane_in->Row(y)[borderx] = plane_in->Row(y)[borderx - 1]; + } + } + if (frame_rect.y0() == 0) { + memcpy(plane_in->Row(rect.y0() - 1), plane_in->Row(rect.y0()), + plane_in->xsize() * sizeof(float)); + } + if (frame_rect.y0() + y1 - rect.y0() >= frame_dim.ysize_padded) { + ssize_t bordery = static_cast(y1 - yoff + vs) / (1 << vs) + yoff; + memcpy(plane_in->Row(bordery), plane_in->Row(bordery - 1), + plane_in->xsize() * sizeof(float)); + } + if (hs == 1) { + // Limited to 4 for Interleave*. + HWY_CAPPED(float, 4) d; + auto threefour = Set(d, 0.75f); + auto onefour = Set(d, 0.25f); + size_t orig_y0 = y0; + size_t orig_y1 = y1; + if (vs != 0) { + orig_y0 = (y0 >> 1) + (yoff >> 1) - 1; + orig_y1 = (y1 >> 1) + (yoff >> 1) + 1; + } + for (size_t y = orig_y0; y < orig_y1; y++) { + const float* in = plane_in->Row(y); + float* out = temp->Row(y); + for (size_t x = x0 / (2 * Lanes(d)) * 2 * Lanes(d); + x < RoundUpTo(x1, 2 * Lanes(d)); x += 2 * Lanes(d)) { + size_t ox = (x >> 1) + (xoff >> 1); + auto current = Load(d, in + ox) * threefour; + auto prev = LoadU(d, in + ox - 1); + auto next = LoadU(d, in + ox + 1); + auto left = MulAdd(onefour, prev, current); + auto right = MulAdd(onefour, next, current); +#if HWY_TARGET == HWY_SCALAR + Store(left, d, out + x); + Store(right, d, out + x + 1); +#else + Store(InterleaveLower(left, right), d, out + x); + Store(InterleaveUpper(left, right), d, out + x + Lanes(d)); +#endif + } + } + } else { + CopyImageTo(*plane_in, temp); + } + if (vs == 1) { + HWY_FULL(float) d; + auto threefour = Set(d, 0.75f); + auto onefour = Set(d, 0.25f); + for (size_t y = y0; y < y1; y++) { + size_t oy1 = (y >> 1) + (yoff >> 1); + if ((y & 1) == 1) oy1++; + size_t oy0 = oy1 - 1; + const float* in0 = temp->Row(oy0); + const float* in1 = temp->Row(oy1); + float* out = plane_out->Row(y); + if ((y & 1) == 1) { + for (size_t x = x0 / Lanes(d) * Lanes(d); x < RoundUpTo(x1, Lanes(d)); + x += Lanes(d)) { + auto i0 = Load(d, in0 + x); + auto i1 = Load(d, in1 + x); + auto o = MulAdd(i0, threefour, i1 * onefour); + Store(o, d, out + x); + } + } else { + for (size_t x = x0 / Lanes(d) * Lanes(d); x < RoundUpTo(x1, Lanes(d)); + x += Lanes(d)) { + auto i0 = Load(d, in0 + x); + auto i1 = Load(d, in1 + x); + auto o = MulAdd(i0, onefour, i1 * threefour); + Store(o, d, out + x); + } + } + } + } else { + CopyImageTo(*temp, plane_out); + } + + // The output must be initialized including the lf.Padding() around the image + // for all the pixels that fall inside the image frame. + JXL_CHECK_IMAGE_INITIALIZED(*plane_out, Rect(x0, y0, x1 - x0, y1 - y0)); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(UndoXYBInPlace); +HWY_EXPORT(FloatToRGBA8); +HWY_EXPORT(DoYCbCrUpsampling); + +void UndoXYB(const Image3F& src, Image3F* dst, + const OutputEncodingInfo& output_info, ThreadPool* pool) { + CopyImageTo(src, dst); + pool->Run(0, src.ysize(), ThreadPool::SkipInit(), [&](int y, int /*thread*/) { + JXL_CHECK(HWY_DYNAMIC_DISPATCH(UndoXYBInPlace)(dst, Rect(*dst).Line(y), + output_info)); + }); +} + +namespace { +Rect ScaleRectForEC(Rect in, const FrameHeader& frame_header, size_t ec) { + auto s = [&](size_t x) { + return DivCeil(x * frame_header.upsampling, + frame_header.extra_channel_upsampling[ec]); + }; + // For x0 and y0 the DivCeil is actually an exact division. + return Rect(s(in.x0()), s(in.y0()), s(in.xsize()), s(in.ysize())); +} + +// Implements EnsurePaddingInPlace, but allows processing data one row at a +// time. +class EnsurePaddingInPlaceRowByRow { + void Init(const Rect& rect, const Rect& image_rect, size_t image_xsize, + size_t image_ysize, size_t xpadding, size_t ypadding, ssize_t* y0, + ssize_t* y1) { + // coordinates relative to rect. + JXL_ASSERT(SameSize(rect, image_rect)); + JXL_ASSERT(image_rect.x0() + image_rect.xsize() <= image_xsize); + JXL_ASSERT(image_rect.y0() + image_rect.ysize() <= image_ysize); + *y0 = -std::min(image_rect.y0(), ypadding); + *y1 = rect.ysize() + std::min(ypadding, image_ysize - image_rect.ysize() - + image_rect.y0()); + if (image_rect.x0() >= xpadding && + image_rect.x0() + image_rect.xsize() + xpadding <= image_xsize) { + // Nothing to do. + strategy_ = kSkip; + } else if (image_xsize >= 2 * xpadding) { + strategy_ = kFast; + } else { + strategy_ = kSlow; + } + y0_ = rect.y0(); + JXL_ASSERT(rect.x0() >= xpadding); + x0_ = x1_ = rect.x0() - xpadding; + // If close to the left border - do mirroring. + if (image_rect.x0() < xpadding) x1_ = rect.x0() - image_rect.x0(); + x2_ = x3_ = rect.x0() + rect.xsize() + xpadding; + // If close to the right border - do mirroring. + if (image_rect.x0() + image_rect.xsize() + xpadding > image_xsize) { + x2_ = rect.x0() + image_xsize - image_rect.x0(); + } + JXL_ASSERT(x0_ <= x1_); + JXL_ASSERT(x1_ <= x2_); + JXL_ASSERT(x2_ <= x3_); + JXL_ASSERT(image_xsize == (x2_ - x1_) || + (x1_ - x0_ <= x2_ - x1_ && x3_ - x2_ <= x2_ - x1_)); + } + + public: + void Init(Image3F* img, const Rect& rect, const Rect& image_rect, + size_t image_xsize, size_t image_ysize, size_t xpadding, + size_t ypadding, ssize_t* y0, ssize_t* y1) { + Init(rect, image_rect, image_xsize, image_ysize, xpadding, ypadding, y0, + y1); + img3_ = img; + JXL_DASSERT(x3_ <= img->xsize()); + } + void Init(ImageF* img, const Rect& rect, const Rect& image_rect, + size_t image_xsize, size_t image_ysize, size_t xpadding, + size_t ypadding, ssize_t* y0, ssize_t* y1) { + Init(rect, image_rect, image_xsize, image_ysize, xpadding, ypadding, y0, + y1); + img_ = img; + JXL_DASSERT(x3_ <= img->xsize()); + } + // To be called when row `y` of the input is available, for all the values in + // [*y0, *y1). + void Process3(ssize_t y) { + JXL_DASSERT(img3_); + for (size_t c = 0; c < 3; c++) { + img_ = &img3_->Plane(c); + Process(y); + } + } + void Process(ssize_t y) { + JXL_DASSERT(img_); + switch (strategy_) { + case kSkip: + break; + case kFast: { + // Image is wide enough that a single Mirror() step is sufficient. + float* JXL_RESTRICT row = img_->Row(y + y0_); + for (size_t x = x0_; x < x1_; x++) { + row[x] = row[2 * x1_ - x - 1]; + } + for (size_t x = x2_; x < x3_; x++) { + row[x] = row[2 * x2_ - x - 1]; + } + break; + } + case kSlow: { + // Slow case for small images. + float* JXL_RESTRICT row = img_->Row(y + y0_) + x1_; + for (ssize_t x = x0_ - x1_; x < 0; x++) { + *(row + x) = row[Mirror(x, x2_ - x1_)]; + } + for (size_t x = x2_ - x1_; x < x3_ - x1_; x++) { + *(row + x) = row[Mirror(x, x2_ - x1_)]; + } + break; + } + } + } + + private: + // Initialized to silence spurious compiler warnings. + Image3F* img3_ = nullptr; + ImageF* img_ = nullptr; + // Will fill [x0_, x1_) and [x2_, x3_) on every row. + // The [x1_, x2_) range contains valid image pixels. We guarantee that either + // x1_ - x0_ <= x2_ - x1_, (and similarly for x2_, x3_), or that the [x1_, + // x2_) contains a full horizontal line of the original image. + size_t x0_ = 0, x1_ = 0, x2_ = 0, x3_ = 0; + size_t y0_ = 0; + // kSlow: use calls to Mirror(), for the case where the border might be larger + // than the image. + // kFast: directly use the result of Mirror() when it can be computed in a + // single iteration. + // kSkip: do nothing. + enum Strategy { kFast, kSlow, kSkip }; + Strategy strategy_ = kSkip; +}; +} // namespace + +void EnsurePaddingInPlace(Image3F* img, const Rect& rect, + const Rect& image_rect, size_t image_xsize, + size_t image_ysize, size_t xpadding, + size_t ypadding) { + ssize_t y0, y1; + EnsurePaddingInPlaceRowByRow impl; + impl.Init(img, rect, image_rect, image_xsize, image_ysize, xpadding, ypadding, + &y0, &y1); + for (ssize_t y = y0; y < y1; y++) { + impl.Process3(y); + } +} + +void EnsurePaddingInPlace(ImageF* img, const Rect& rect, const Rect& image_rect, + size_t image_xsize, size_t image_ysize, + size_t xpadding, size_t ypadding) { + ssize_t y0, y1; + EnsurePaddingInPlaceRowByRow impl; + impl.Init(img, rect, image_rect, image_xsize, image_ysize, xpadding, ypadding, + &y0, &y1); + for (ssize_t y = y0; y < y1; y++) { + impl.Process(y); + } +} + +Status FinalizeImageRect( + Image3F* input_image, const Rect& input_rect, + const std::vector>& extra_channels, + PassesDecoderState* dec_state, size_t thread, + ImageBundle* JXL_RESTRICT output_image, const Rect& frame_rect) { + const ImageFeatures& image_features = dec_state->shared->image_features; + const FrameHeader& frame_header = dec_state->shared->frame_header; + const ImageMetadata& metadata = frame_header.nonserialized_metadata->m; + const LoopFilter& lf = frame_header.loop_filter; + const FrameDimensions& frame_dim = dec_state->shared->frame_dim; + JXL_DASSERT(frame_rect.xsize() <= kApplyImageFeaturesTileDim); + JXL_DASSERT(frame_rect.ysize() <= kApplyImageFeaturesTileDim); + JXL_DASSERT(input_rect.xsize() == frame_rect.xsize()); + JXL_DASSERT(input_rect.ysize() == frame_rect.ysize()); + JXL_DASSERT(frame_rect.x0() % GroupBorderAssigner::kPaddingXRound == 0); + JXL_DASSERT(frame_rect.xsize() % GroupBorderAssigner::kPaddingXRound == 0 || + frame_rect.xsize() + frame_rect.x0() == frame_dim.xsize || + frame_rect.xsize() + frame_rect.x0() == frame_dim.xsize_padded); + + // +----------------------------- STEP 1 ------------------------------+ + // | Compute the rects on which patches and splines will be applied. | + // | In case we are applying upsampling, we need to apply patches on a | + // | slightly larger image. | + // +-------------------------------------------------------------------+ + + // If we are applying upsampling, we need 2 more pixels around the actual rect + // for border. Thus, we also need to apply patches and splines to those + // pixels. We compute here + // - The portion of image that corresponds to the area we are applying IF. + // (rect_for_if) + // - The rect where that pixel data is stored in upsampling_input_storage. + // (rect_for_if_storage) + // - The rect where the pixel data that we need to upsample is stored. + // (rect_for_upsampling) + // - The source rect for the pixel data in `input_image`. It is assumed that, + // if `frame_rect` is not on an image border, `input_image:input_rect` has + // enough border available. (rect_for_if_input) + + Image3F* output_color = + dec_state->rgb_output == nullptr && dec_state->pixel_callback == nullptr + ? output_image->color() + : nullptr; + + Image3F* storage_for_if = output_color; + Rect rect_for_if = frame_rect; + Rect rect_for_if_storage = frame_rect; + Rect rect_for_upsampling = frame_rect; + Rect rect_for_if_input = input_rect; + // The same as rect_for_if_input but in the frame coordinates. + Rect frame_rect_for_ycbcr_upsampling = frame_rect; + size_t extra_rows_t = 0; + size_t extra_rows_b = 0; + if (frame_header.upsampling != 1) { + size_t ifbx0 = 0; + size_t ifbx1 = 0; + size_t ifby0 = 0; + size_t ifby1 = 0; + if (frame_rect.x0() >= 2) { + JXL_DASSERT(input_rect.x0() >= 2); + ifbx0 = 2; + } + if (frame_rect.y0() >= 2) { + JXL_DASSERT(input_rect.y0() >= 2); + extra_rows_t = ifby0 = 2; + } + for (size_t extra : {1, 2}) { + if (frame_rect.x0() + frame_rect.xsize() + extra <= + dec_state->shared->frame_dim.xsize_padded) { + JXL_DASSERT(input_rect.x0() + input_rect.xsize() + extra <= + input_image->xsize()); + ifbx1 = extra; + } + if (frame_rect.y0() + frame_rect.ysize() + extra <= + dec_state->shared->frame_dim.ysize_padded) { + JXL_DASSERT(input_rect.y0() + input_rect.ysize() + extra <= + input_image->ysize()); + extra_rows_b = ifby1 = extra; + } + } + rect_for_if = Rect(frame_rect.x0() - ifbx0, frame_rect.y0() - ifby0, + frame_rect.xsize() + ifbx0 + ifbx1, + frame_rect.ysize() + ifby0 + ifby1); + // Storage for pixel data does not necessarily start at (0, 0) as we need to + // have the left border of upsampling_rect aligned to a multiple of + // GroupBorderAssigner::kPaddingXRound. + rect_for_if_storage = + Rect(kBlockDim + RoundUpTo(ifbx0, GroupBorderAssigner::kPaddingXRound) - + ifbx0, + kBlockDim, rect_for_if.xsize(), rect_for_if.ysize()); + rect_for_upsampling = + Rect(kBlockDim + RoundUpTo(ifbx0, GroupBorderAssigner::kPaddingXRound), + kBlockDim + ifby0, frame_rect.xsize(), frame_rect.ysize()); + rect_for_if_input = + Rect(input_rect.x0() - ifbx0, input_rect.y0() - ifby0, + rect_for_if_storage.xsize(), rect_for_if_storage.ysize()); + frame_rect_for_ycbcr_upsampling = + Rect(frame_rect.x0() - ifbx0, frame_rect.y0() - ifby0, + rect_for_if_input.xsize(), rect_for_if_input.ysize()); + storage_for_if = &dec_state->upsampling_input_storage[thread]; + } + + // +--------------------------- STEP 1.5 ------------------------------+ + // | Perform YCbCr upsampling if needed. | + // +-------------------------------------------------------------------+ + + Image3F* input = input_image; + if (!frame_header.chroma_subsampling.Is444()) { + for (size_t c = 0; c < 3; c++) { + size_t vs = frame_header.chroma_subsampling.VShift(c); + size_t hs = frame_header.chroma_subsampling.HShift(c); + // The per-thread output is used for the first time here. Poison the temp + // image on this thread to prevent leaking initialized data from a + // previous run in this thread in msan builds. + msan::PoisonImage(dec_state->ycbcr_out_images[thread].Plane(c)); + HWY_DYNAMIC_DISPATCH(DoYCbCrUpsampling) + (hs, vs, &input_image->Plane(c), rect_for_if_input, + frame_rect_for_ycbcr_upsampling, frame_dim, + &dec_state->ycbcr_out_images[thread].Plane(c), lf, + &dec_state->ycbcr_temp_images[thread]); + } + input = &dec_state->ycbcr_out_images[thread]; + } + + // Variables for upsampling and filtering. + Rect upsampled_frame_rect(frame_rect.x0() * frame_header.upsampling, + frame_rect.y0() * frame_header.upsampling, + frame_rect.xsize() * frame_header.upsampling, + frame_rect.ysize() * frame_header.upsampling); + Rect full_frame_rect(0, 0, frame_dim.xsize_upsampled, + frame_dim.ysize_upsampled); + upsampled_frame_rect = upsampled_frame_rect.Crop(full_frame_rect); + EnsurePaddingInPlaceRowByRow ensure_padding_upsampling; + ssize_t ensure_padding_upsampling_y0 = 0; + ssize_t ensure_padding_upsampling_y1 = 0; + + EnsurePaddingInPlaceRowByRow ensure_padding_filter; + FilterPipeline* fp = nullptr; + ssize_t ensure_padding_filter_y0 = 0; + ssize_t ensure_padding_filter_y1 = 0; + if (lf.epf_iters != 0 || lf.gab) { + fp = &dec_state->filter_pipelines[thread]; + } + + // +----------------------------- STEP 2 ------------------------------+ + // | Change rects and buffer to not use `output_image` if direct | + // | output to rgb8 is requested. | + // +-------------------------------------------------------------------+ + Image3F* output_pixel_data_storage = output_color; + Rect upsampled_frame_rect_for_storage = upsampled_frame_rect; + if (dec_state->rgb_output || dec_state->pixel_callback) { + size_t log2_upsampling = CeilLog2Nonzero(frame_header.upsampling); + if (storage_for_if == output_color) { + storage_for_if = + &dec_state->output_pixel_data_storage[log2_upsampling][thread]; + rect_for_if_storage = + Rect(0, 0, rect_for_if_storage.xsize(), rect_for_if_storage.ysize()); + } + output_pixel_data_storage = + &dec_state->output_pixel_data_storage[log2_upsampling][thread]; + upsampled_frame_rect_for_storage = + Rect(0, 0, upsampled_frame_rect.xsize(), upsampled_frame_rect.ysize()); + if (frame_header.upsampling == 1 && fp == nullptr) { + upsampled_frame_rect_for_storage = rect_for_if_storage = + rect_for_if_input; + output_pixel_data_storage = storage_for_if = input; + } + } + // Set up alpha channel. + const size_t ec = + metadata.Find(ExtraChannel::kAlpha) - metadata.extra_channel_info.data(); + const ImageF* alpha = nullptr; + Rect alpha_rect = upsampled_frame_rect; + if (ec < metadata.extra_channel_info.size()) { + JXL_ASSERT(ec < extra_channels.size()); + if (frame_header.extra_channel_upsampling[ec] == 1) { + alpha = extra_channels[ec].first; + alpha_rect = extra_channels[ec].second; + } else { + alpha = &output_image->extra_channels()[ec]; + alpha_rect = upsampled_frame_rect; + } + } + + // +----------------------------- STEP 3 ------------------------------+ + // | Set up upsampling and upsample extra channels. | + // +-------------------------------------------------------------------+ + Upsampler* color_upsampler = nullptr; + if (frame_header.upsampling != 1) { + color_upsampler = + &dec_state->upsamplers[CeilLog2Nonzero(frame_header.upsampling) - 1]; + ensure_padding_upsampling.Init( + storage_for_if, rect_for_upsampling, frame_rect, frame_dim.xsize_padded, + frame_dim.ysize_padded, 2, 2, &ensure_padding_upsampling_y0, + &ensure_padding_upsampling_y1); + } + + std::vector> extra_channels_for_patches; + std::vector ec_padding; + + bool late_ec_upsample = frame_header.upsampling != 1; + for (auto ecups : frame_header.extra_channel_upsampling) { + if (ecups != frame_header.upsampling) { + // If patches are applied, either frame_header.upsampling == 1 or + // late_ec_upsample is true. + late_ec_upsample = false; + } + } + + ssize_t ensure_padding_upsampling_ec_y0 = 0; + ssize_t ensure_padding_upsampling_ec_y1 = 0; + + // TODO(veluca) do not upsample extra channels to a full-image-sized buffer if + // we are not outputting to an ImageBundle. + if (!late_ec_upsample) { + // Upsample extra channels first if not all channels have the same + // upsampling factor. + for (size_t ec = 0; ec < extra_channels.size(); ec++) { + size_t ecups = frame_header.extra_channel_upsampling[ec]; + if (ecups == 1) { + extra_channels_for_patches.push_back(extra_channels[ec]); + continue; + } + ssize_t ensure_padding_y0, ensure_padding_y1; + EnsurePaddingInPlaceRowByRow ensure_padding; + // frame_rect can go up to frame_dim.xsize_padded, in VarDCT mode. + Rect ec_image_rect = ScaleRectForEC( + frame_rect.Crop(frame_dim.xsize, frame_dim.ysize), frame_header, ec); + size_t ecxs = DivCeil(frame_dim.xsize_upsampled, + frame_header.extra_channel_upsampling[ec]); + size_t ecys = DivCeil(frame_dim.ysize_upsampled, + frame_header.extra_channel_upsampling[ec]); + ensure_padding.Init(extra_channels[ec].first, extra_channels[ec].second, + ec_image_rect, ecxs, ecys, 2, 2, &ensure_padding_y0, + &ensure_padding_y1); + for (ssize_t y = ensure_padding_y0; y < ensure_padding_y1; y++) { + ensure_padding.Process(y); + } + Upsampler& upsampler = + dec_state->upsamplers[CeilLog2Nonzero( + frame_header.extra_channel_upsampling[ec]) - + 1]; + upsampler.UpsampleRect( + *extra_channels[ec].first, extra_channels[ec].second, + &output_image->extra_channels()[ec], upsampled_frame_rect, + static_cast(ec_image_rect.y0()) - + static_cast(extra_channels[ec].second.y0()), + ecys, dec_state->upsampler_storage[thread].get()); + extra_channels_for_patches.emplace_back( + &output_image->extra_channels()[ec], upsampled_frame_rect); + } + } else { + // Upsample extra channels last if color channels are upsampled and all the + // extra channels have the same upsampling as them. + ec_padding.resize(extra_channels.size()); + for (size_t ec = 0; ec < extra_channels.size(); ec++) { + // Add a border to the extra channel rect for when patches are applied. + // This ensures that the correct row is accessed (y values for patches are + // relative to rect_for_if, not to input_rect). + // As the rect is extended by 0 or 2 pixels, and the patches input has, + // accordingly, the same padding, this is safe. + Rect r(extra_channels[ec].second.x0() + rect_for_upsampling.x0() - + rect_for_if_storage.x0(), + extra_channels[ec].second.y0() + rect_for_upsampling.y0() - + rect_for_if_storage.y0(), + extra_channels[ec].second.xsize() + rect_for_if_storage.xsize() - + rect_for_upsampling.xsize(), + extra_channels[ec].second.ysize() + rect_for_if_storage.ysize() - + rect_for_upsampling.ysize()); + extra_channels_for_patches.emplace_back(extra_channels[ec].first, r); + // frame_rect can go up to frame_dim.xsize_padded, in VarDCT mode. + ec_padding[ec].Init(extra_channels[ec].first, extra_channels[ec].second, + frame_rect.Crop(frame_dim.xsize, frame_dim.ysize), + frame_dim.xsize, frame_dim.ysize, 2, 2, + &ensure_padding_upsampling_ec_y0, + &ensure_padding_upsampling_ec_y1); + } + } + + // Initialized to a valid non-null ptr to avoid UB if arithmetic is done with + // the pointer value (which would then not be used). + std::vector ec_ptrs_for_patches(extra_channels.size(), + input->PlaneRow(0, 0)); + + // +----------------------------- STEP 4 ------------------------------+ + // | Set up the filter pipeline. | + // +-------------------------------------------------------------------+ + if (fp) { + ensure_padding_filter.Init( + input, rect_for_if_input, rect_for_if, frame_dim.xsize_padded, + frame_dim.ysize_padded, lf.Padding(), lf.Padding(), + &ensure_padding_filter_y0, &ensure_padding_filter_y1); + + fp = PrepareFilterPipeline(dec_state, rect_for_if, *input, + rect_for_if_input, frame_dim.ysize_padded, + thread, storage_for_if, rect_for_if_storage); + } + + // +----------------------------- STEP 5 ------------------------------+ + // | Run the prepared pipeline of operations. | + // +-------------------------------------------------------------------+ + + // y values are relative to rect_for_if. + // Automatic mirroring in fp->ApplyFiltersRow() implies that we should ensure + // that padding for the first lines of the image is already present before + // calling ApplyFiltersRow() with "virtual" rows. + // Here we rely on the fact that virtual rows at the beginning of the image + // are only present if input_rect.y0() == 0. + ssize_t first_ensure_padding_y = ensure_padding_filter_y0; + if (frame_rect.y0() == 0) { + JXL_DASSERT(ensure_padding_filter_y0 == 0); + first_ensure_padding_y = + std::min(lf.Padding(), ensure_padding_filter_y1); + for (ssize_t y = 0; y < first_ensure_padding_y; y++) { + ensure_padding_filter.Process3(y); + } + } + + for (ssize_t y = -lf.Padding(); + y < static_cast(lf.Padding() + rect_for_if.ysize()); y++) { + if (fp) { + if (y >= first_ensure_padding_y && y < ensure_padding_filter_y1) { + ensure_padding_filter.Process3(y); + } + fp->ApplyFiltersRow(lf, dec_state->filter_weights, y); + } else if (output_pixel_data_storage != input) { + for (size_t c = 0; c < 3; c++) { + memcpy(rect_for_if_storage.PlaneRow(storage_for_if, c, y), + rect_for_if_input.ConstPlaneRow(*input, c, y), + rect_for_if_input.xsize() * sizeof(float)); + } + } + if (y < static_cast(lf.Padding())) continue; + // At this point, row `y - lf.Padding()` of `rect_for_if` has been produced + // by the filters. + ssize_t available_y = y - lf.Padding(); + if (frame_header.upsampling == 1) { + for (size_t i = 0; i < extra_channels.size(); i++) { + ec_ptrs_for_patches[i] = extra_channels_for_patches[i].second.Row( + extra_channels_for_patches[i].first, available_y); + } + } + JXL_RETURN_IF_ERROR(image_features.patches.AddTo( + storage_for_if, rect_for_if_storage.Line(available_y), + ec_ptrs_for_patches.data(), rect_for_if.Line(available_y))); + image_features.splines.AddTo(storage_for_if, + rect_for_if_storage.Line(available_y), + rect_for_if.Line(available_y)); + size_t num_ys = 1; + if (frame_header.upsampling != 1) { + // Upsampling `y` values are relative to `rect_for_upsampling`, not to + // `rect_for_if`. + ssize_t shifted_y = available_y - extra_rows_t; + if (shifted_y >= ensure_padding_upsampling_y0 && + shifted_y < ensure_padding_upsampling_y1) { + ensure_padding_upsampling.Process3(shifted_y); + } + if (late_ec_upsample && shifted_y >= ensure_padding_upsampling_ec_y0 && + shifted_y < ensure_padding_upsampling_ec_y1) { + for (size_t ec = 0; ec < extra_channels.size(); ec++) { + ec_padding[ec].Process(shifted_y); + } + } + // Upsampling will access two rows of border, so the first upsampling + // output will be available after shifted_y is at least 2, *unless* image + // height is <= 2. + if (shifted_y < 2 && + shifted_y + 1 != static_cast(frame_rect.ysize())) { + continue; + } + // Value relative to upsampled_frame_rect. + size_t input_y = std::max(shifted_y - 2, 0); + size_t upsampled_available_y = frame_header.upsampling * input_y; + size_t num_input_rows = 1; + // If we are going to mirror the last output rows, then we already have 3 + // input lines ready. This happens iff we did not extend rect_for_if on + // the bottom *and* we are at the last `y` value. + if (extra_rows_b != 2 && + static_cast(y) + 1 == lf.Padding() + rect_for_if.ysize()) { + num_input_rows = 3; + } + num_input_rows = std::min(num_input_rows, frame_dim.ysize_padded); + num_ys = num_input_rows * frame_header.upsampling; + + if (static_cast(upsampled_available_y) >= + upsampled_frame_rect.ysize()) { + continue; + } + + if (upsampled_available_y + num_ys >= upsampled_frame_rect.ysize()) { + num_ys = upsampled_frame_rect.ysize() - upsampled_available_y; + } + + // Upsampler takes care of mirroring, and checks "physical" boundaries. + Rect upsample_input_rect = rect_for_upsampling.Lines(input_y, 1); + color_upsampler->UpsampleRect( + *storage_for_if, upsample_input_rect, output_pixel_data_storage, + upsampled_frame_rect_for_storage.Lines(upsampled_available_y, num_ys), + static_cast(frame_rect.y0()) - + static_cast(rect_for_upsampling.y0()), + frame_dim.ysize_padded, dec_state->upsampler_storage[thread].get()); + if (late_ec_upsample) { + for (size_t ec = 0; ec < extra_channels.size(); ec++) { + // Upsampler takes care of mirroring, and checks "physical" + // boundaries. + Rect upsample_ec_input_rect = + extra_channels[ec].second.Lines(input_y, 1); + color_upsampler->UpsampleRect( + *extra_channels[ec].first, upsample_ec_input_rect, + &output_image->extra_channels()[ec], + upsampled_frame_rect.Lines(upsampled_available_y, num_ys), + static_cast(frame_rect.y0()) - + static_cast(extra_channels[ec].second.y0()), + frame_dim.ysize, dec_state->upsampler_storage[thread].get()); + } + } + available_y = upsampled_available_y; + } + + if (static_cast(available_y) >= upsampled_frame_rect.ysize()) { + continue; + } + + // The image data is now unconditionally in + // `output_image_storage:upsampled_frame_rect_for_storage`. + if (frame_header.flags & FrameHeader::kNoise) { + PROFILER_ZONE("AddNoise"); + AddNoise(image_features.noise_params, + upsampled_frame_rect.Lines(available_y, num_ys), + dec_state->noise, + upsampled_frame_rect_for_storage.Lines(available_y, num_ys), + dec_state->shared_storage.cmap, output_pixel_data_storage); + } + + if (dec_state->pre_color_transform_frame.xsize() != 0) { + for (size_t c = 0; c < 3; c++) { + for (size_t y = available_y; y < available_y + num_ys; y++) { + float* JXL_RESTRICT row_out = upsampled_frame_rect.PlaneRow( + &dec_state->pre_color_transform_frame, c, y); + const float* JXL_RESTRICT row_in = + upsampled_frame_rect_for_storage.ConstPlaneRow( + *output_pixel_data_storage, c, y); + memcpy(row_out, row_in, + upsampled_frame_rect.xsize() * sizeof(*row_in)); + } + } + } + + // We skip the color transform entirely if save_before_color_transform and + // the frame is not supposed to be displayed. + + if (dec_state->fast_xyb_srgb8_conversion) { + FastXYBTosRGB8( + *output_pixel_data_storage, + upsampled_frame_rect_for_storage.Lines(available_y, num_ys), + upsampled_frame_rect.Lines(available_y, num_ys) + .Crop(Rect(0, 0, frame_dim.xsize_upsampled, + frame_dim.ysize_upsampled)), + alpha, alpha_rect.Lines(available_y, num_ys), + dec_state->rgb_output_is_rgba, dec_state->rgb_output, frame_dim.xsize, + dec_state->rgb_stride); + } else { + if (frame_header.needs_color_transform()) { + if (frame_header.color_transform == ColorTransform::kXYB) { + JXL_RETURN_IF_ERROR(HWY_DYNAMIC_DISPATCH(UndoXYBInPlace)( + output_pixel_data_storage, + upsampled_frame_rect_for_storage.Lines(available_y, num_ys), + dec_state->output_encoding_info)); + } else if (frame_header.color_transform == ColorTransform::kYCbCr) { + YcbcrToRgb( + *output_pixel_data_storage, output_pixel_data_storage, + upsampled_frame_rect_for_storage.Lines(available_y, num_ys)); + } + } + + // TODO(veluca): all blending should happen here. + + if (dec_state->rgb_output != nullptr) { + HWY_DYNAMIC_DISPATCH(FloatToRGBA8) + (*output_pixel_data_storage, + upsampled_frame_rect_for_storage.Lines(available_y, num_ys), + dec_state->rgb_output_is_rgba, alpha, + alpha_rect.Lines(available_y, num_ys), + upsampled_frame_rect.Lines(available_y, num_ys) + .Crop(Rect(0, 0, frame_dim.xsize, frame_dim.ysize)), + dec_state->rgb_output, dec_state->rgb_stride); + } + if (dec_state->pixel_callback != nullptr) { + Rect alpha_line_rect = alpha_rect.Lines(available_y, num_ys); + Rect color_input_line_rect = + upsampled_frame_rect_for_storage.Lines(available_y, num_ys); + Rect image_line_rect = upsampled_frame_rect.Lines(available_y, num_ys) + .Crop(Rect(0, 0, frame_dim.xsize_upsampled, + frame_dim.ysize_upsampled)); + const float* line_buffers[4]; + for (size_t iy = 0; iy < image_line_rect.ysize(); iy++) { + for (size_t c = 0; c < 3; c++) { + line_buffers[c] = color_input_line_rect.ConstPlaneRow( + *output_pixel_data_storage, c, iy); + } + if (alpha) { + line_buffers[3] = alpha_line_rect.ConstRow(*alpha, iy); + } else { + line_buffers[3] = dec_state->opaque_alpha.data(); + } + std::vector& interleaved = + dec_state->pixel_callback_rows[thread]; + size_t j = 0; + for (size_t i = 0; i < image_line_rect.xsize(); i++) { + interleaved[j++] = line_buffers[0][i]; + interleaved[j++] = line_buffers[1][i]; + interleaved[j++] = line_buffers[2][i]; + if (dec_state->rgb_output_is_rgba) { + interleaved[j++] = line_buffers[3][i]; + } + } + dec_state->pixel_callback(interleaved.data(), image_line_rect.x0(), + image_line_rect.y0() + iy, + image_line_rect.xsize()); + } + } + } + } + + return true; +} + +Status FinalizeFrameDecoding(ImageBundle* decoded, + PassesDecoderState* dec_state, ThreadPool* pool, + bool force_fir, bool skip_blending) { + const FrameHeader& frame_header = dec_state->shared->frame_header; + const FrameDimensions& frame_dim = dec_state->shared->frame_dim; + + // FinalizeImageRect was not yet run, or we are forcing a run. + if (!dec_state->EagerFinalizeImageRect() || force_fir) { + std::vector rects_to_process; + for (size_t y = 0; y < frame_dim.ysize_padded; y += kGroupDim) { + for (size_t x = 0; x < frame_dim.xsize_padded; x += kGroupDim) { + Rect rect(x, y, kGroupDim, kGroupDim, frame_dim.xsize_padded, + frame_dim.ysize_padded); + if (rect.xsize() == 0 || rect.ysize() == 0) continue; + rects_to_process.push_back(rect); + } + } + const auto allocate_storage = [&](size_t num_threads) { + dec_state->EnsureStorage(num_threads); + return true; + }; + + { + std::vector ecs; + const ImageMetadata& metadata = frame_header.nonserialized_metadata->m; + for (size_t i = 0; i < metadata.num_extra_channels; i++) { + if (frame_header.extra_channel_upsampling[i] == 1) { + ecs.push_back(std::move(dec_state->extra_channels[i])); + } else { + ecs.emplace_back(frame_dim.xsize_upsampled_padded, + frame_dim.ysize_upsampled_padded); + } + } + decoded->SetExtraChannels(std::move(ecs)); + } + + std::atomic apply_features_ok{true}; + auto run_apply_features = [&](size_t rect_id, size_t thread) { + size_t xstart = PassesDecoderState::kGroupDataXBorder; + size_t ystart = PassesDecoderState::kGroupDataYBorder; + for (size_t c = 0; c < 3; c++) { + Rect rh(rects_to_process[rect_id].x0() >> + frame_header.chroma_subsampling.HShift(c), + rects_to_process[rect_id].y0() >> + frame_header.chroma_subsampling.VShift(c), + rects_to_process[rect_id].xsize() >> + frame_header.chroma_subsampling.HShift(c), + rects_to_process[rect_id].ysize() >> + frame_header.chroma_subsampling.VShift(c)); + Rect group_data_rect(xstart, ystart, rh.xsize(), rh.ysize()); + // Poison the image in this thread to prevent leaking initialized data + // from a previous run in this thread in msan builds. + msan::PoisonImage(dec_state->group_data[thread].Plane(c)); + CopyImageToWithPadding( + rh, dec_state->decoded.Plane(c), dec_state->FinalizeRectPadding(), + group_data_rect, &dec_state->group_data[thread].Plane(c)); + } + Rect group_data_rect(xstart, ystart, rects_to_process[rect_id].xsize(), + rects_to_process[rect_id].ysize()); + std::vector> ec_rects; + ec_rects.reserve(decoded->extra_channels().size()); + for (size_t i = 0; i < decoded->extra_channels().size(); i++) { + Rect r = ScaleRectForEC( + rects_to_process[rect_id].Crop(frame_dim.xsize, frame_dim.ysize), + frame_header, i); + if (frame_header.extra_channel_upsampling[i] != 1) { + Rect ec_input_rect(kBlockDim, 2, r.xsize(), r.ysize()); + auto eti = + &dec_state + ->ec_temp_images[thread * decoded->extra_channels().size() + + i]; + // Poison the temp image on this thread to prevent leaking initialized + // data from a previous run in this thread in msan builds. + msan::PoisonImage(*eti); + JXL_CHECK_IMAGE_INITIALIZED(dec_state->extra_channels[i], r); + CopyImageToWithPadding(r, dec_state->extra_channels[i], + /*padding=*/2, ec_input_rect, eti); + ec_rects.emplace_back(eti, ec_input_rect); + } else { + JXL_CHECK_IMAGE_INITIALIZED(decoded->extra_channels()[i], r); + ec_rects.emplace_back(&decoded->extra_channels()[i], r); + } + } + if (!FinalizeImageRect(&dec_state->group_data[thread], group_data_rect, + ec_rects, dec_state, thread, decoded, + rects_to_process[rect_id])) { + apply_features_ok = false; + } + }; + + RunOnPool(pool, 0, rects_to_process.size(), allocate_storage, + run_apply_features, "ApplyFeatures"); + + if (!apply_features_ok) { + return JXL_FAILURE("FinalizeImageRect failed"); + } + } + + const size_t xsize = frame_dim.xsize_upsampled; + const size_t ysize = frame_dim.ysize_upsampled; + + decoded->ShrinkTo(xsize, ysize); + if (dec_state->pre_color_transform_frame.xsize() != 0) { + dec_state->pre_color_transform_frame.ShrinkTo(xsize, ysize); + } + + if (!skip_blending && ImageBlender::NeedsBlending(dec_state)) { + if (dec_state->pre_color_transform_frame.xsize() != 0) { + // Extra channels are going to be modified. Make a copy. + dec_state->pre_color_transform_ec.clear(); + for (const auto& ec : decoded->extra_channels()) { + dec_state->pre_color_transform_ec.emplace_back(CopyImage(ec)); + } + } + ImageBlender blender; + ImageBundle foreground = std::move(*decoded); + decoded->SetFromImage(Image3F(frame_header.nonserialized_metadata->xsize(), + frame_header.nonserialized_metadata->ysize()), + foreground.c_current()); + std::vector extra_channels_rects; + decoded->extra_channels().reserve(foreground.extra_channels().size()); + extra_channels_rects.reserve(foreground.extra_channels().size()); + for (size_t i = 0; i < foreground.extra_channels().size(); ++i) { + decoded->extra_channels().emplace_back( + frame_header.nonserialized_metadata->xsize(), + frame_header.nonserialized_metadata->ysize()); + extra_channels_rects.emplace_back(decoded->extra_channels().back()); + } + JXL_RETURN_IF_ERROR(blender.PrepareBlending( + dec_state, foreground.origin, foreground.xsize(), foreground.ysize(), + &frame_header.nonserialized_metadata->m.extra_channel_info, + foreground.c_current(), Rect(*decoded->color()), + /*output=*/decoded->color(), Rect(*decoded->color()), + &decoded->extra_channels(), std::move(extra_channels_rects))); + + std::vector rects_to_process; + for (size_t y = 0; y < frame_dim.ysize; y += kGroupDim) { + for (size_t x = 0; x < frame_dim.xsize; x += kGroupDim) { + Rect rect(x, y, kGroupDim, kGroupDim, frame_dim.xsize, frame_dim.ysize); + if (rect.xsize() == 0 || rect.ysize() == 0) continue; + rects_to_process.push_back(rect); + } + } + + std::atomic blending_ok{true}; + JXL_RETURN_IF_ERROR(RunOnPool( + pool, 0, rects_to_process.size(), ThreadPool::SkipInit(), + [&](size_t i, size_t /*thread*/) { + const Rect& rect = rects_to_process[i]; + auto rect_blender = blender.PrepareRect( + rect, *foreground.color(), foreground.extra_channels(), rect); + for (size_t y = 0; y < rect.ysize(); ++y) { + if (!rect_blender.DoBlending(y)) { + blending_ok = false; + return; + } + } + }, + "Blend")); + JXL_RETURN_IF_ERROR(blending_ok.load()); + } + + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_reconstruct.h b/lib/jxl/dec_reconstruct.h new file mode 100644 index 0000000..3e1ae50 --- /dev/null +++ b/lib/jxl/dec_reconstruct.h @@ -0,0 +1,72 @@ +// 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_DEC_RECONSTRUCT_H_ +#define LIB_JXL_DEC_RECONSTRUCT_H_ + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/splines.h" + +namespace jxl { + +// Finalizes the decoding of a frame by applying image features if necessary, +// doing color transforms (unless the frame header specifies +// `SaveBeforeColorTransform()`) and applying upsampling. +// +// Writes pixels in the appropriate colorspace to `idct`, shrinking it if +// necessary. +// `skip_blending` is necessary because the encoder butteraugli loop does not +// (yet) handle blending. +// TODO(veluca): remove the "force_fir" parameter, and call EPF directly in +// those use cases where this is needed. +Status FinalizeFrameDecoding(ImageBundle* JXL_RESTRICT decoded, + PassesDecoderState* dec_state, ThreadPool* pool, + bool force_fir, bool skip_blending); + +// Renders the `frame_rect` portion of the final image to `output_image` +// (unless the frame is upsampled - in which case, `frame_rect` is scaled +// accordingly). `input_rect` should have the same shape. `input_rect` always +// refers to the non-padded pixels. `frame_rect.x0()` is guaranteed to be a +// multiple of GroupBorderAssigner::kPaddingRoundX. `frame_rect.xsize()` is +// either a multiple of GroupBorderAssigner::kPaddingRoundX, or is such that +// `frame_rect.x0() + frame_rect.xsize() == frame_dim.xsize`. `input_image` +// may be mutated by adding padding. If `frame_rect` is on an image border, the +// input will be padded. Otherwise, appropriate padding must already be present. +Status FinalizeImageRect( + Image3F* input_image, const Rect& input_rect, + const std::vector>& extra_channels, + PassesDecoderState* dec_state, size_t thread, + ImageBundle* JXL_RESTRICT output_image, const Rect& frame_rect); + +// Fills padding around `img:rect` in the x direction by mirroring. Padding is +// applied so that a full border of xpadding and ypadding is available, except +// if `image_rect` points to an area of the full image that touches the top or +// the bottom. It is expected that padding is already in place for inputs such +// that the corresponding image_rect is not at an image border. +void EnsurePaddingInPlace(Image3F* img, const Rect& rect, + const Rect& image_rect, size_t image_xsize, + size_t image_ysize, size_t xpadding, size_t ypadding); +void EnsurePaddingInPlace(ImageF* img, const Rect& rect, const Rect& image_rect, + size_t image_xsize, size_t image_ysize, + size_t xpadding, size_t ypadding); + +// For DC in the API. +void UndoXYB(const Image3F& src, Image3F* dst, + const OutputEncodingInfo& output_info, ThreadPool* pool); + +} // namespace jxl + +#endif // LIB_JXL_DEC_RECONSTRUCT_H_ diff --git a/lib/jxl/dec_render_pipeline.h b/lib/jxl/dec_render_pipeline.h new file mode 100644 index 0000000..9496770 --- /dev/null +++ b/lib/jxl/dec_render_pipeline.h @@ -0,0 +1,91 @@ +// 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_DEC_RENDER_PIPELINE_H_ +#define LIB_JXL_DEC_RENDER_PIPELINE_H_ + +#include + +#include "lib/jxl/filters.h" + +namespace jxl { + +// The first pixel in the input to RenderPipelineStage will be located at +// this position. Pixels before this position may be accessed as padding. +constexpr size_t kRenderPipelineXOffset = 16; + +enum class RenderPipelineChannelMode { + kIgnored = 0, + kInPlace = 1, + kInOut = 2, +}; + +class RenderPipelineStage { + public: + // `input` points to `2*MaxPaddingY() + 1` pointers, each of which points to + // `3+num_non_color_channels` pointer-to-row. So, `input[MaxPaddingY()][0]` is + // the pointer to the center row of the first color channel. + // `MaxPaddingY()` is the maximum value returned by `GetPaddingX()`; + // typically, this is a constant. + // `output` points to `1<>& channel_shifts) { + JXL_ABORT("Not implemented"); + } + + // Adds a stage to the pipeline. The shifts for all the channels that are not + // kIgnored by the stage must be identical at this point. + void AddStage(std::unique_ptr stage) { + JXL_ABORT("Not implemented"); + } + + // Finalizes setup of the pipeline. Shifts for all channels should be 0 at + // this point. + void Finalize() { JXL_ABORT("Not implemented"); } + + // Allocates storage to run with `num` threads. + void PrepareForThreads(size_t num) { JXL_ABORT("Not implemented"); } + + // TBD: run the pipeline for a given input, on a given thread. + // void Run(Image3F* color_data, ImageF* ec_data, const Rect& input_rect, + // size_t thread, size_t xpos, size_t ypos) {} +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_RENDER_PIPELINE_H_ diff --git a/lib/jxl/dec_transforms-inl.h b/lib/jxl/dec_transforms-inl.h new file mode 100644 index 0000000..c9aebc6 --- /dev/null +++ b/lib/jxl/dec_transforms-inl.h @@ -0,0 +1,867 @@ +// 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. + +#if defined(LIB_JXL_DEC_TRANSFORMS_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_DEC_TRANSFORMS_INL_H_ +#undef LIB_JXL_DEC_TRANSFORMS_INL_H_ +#else +#define LIB_JXL_DEC_TRANSFORMS_INL_H_ +#endif + +#include + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dct-inl.h" +#include "lib/jxl/dct_scales.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +template +struct DoDCT { + template + void operator()(const From& from, float* JXL_RESTRICT to, + float* JXL_RESTRICT scratch_space) { + ComputeScaledDCT()(from, to, scratch_space); + } +}; + +template +struct DoDCT { + template + void operator()(const From& from, float* JXL_RESTRICT to, + float* JXL_RESTRICT scratch_space) { + ComputeTransposedScaledDCT()(from, to, scratch_space); + } +}; + +// Computes the lowest-frequency LF_ROWSxLF_COLS-sized square in output, which +// is a DCT_ROWS*DCT_COLS-sized DCT block, by doing a ROWS*COLS DCT on the +// input block. +template +JXL_INLINE void ReinterpretingDCT(const float* input, const size_t input_stride, + float* output, const size_t output_stride) { + static_assert(LF_ROWS == ROWS, + "ReinterpretingDCT should only be called with LF == N"); + static_assert(LF_COLS == COLS, + "ReinterpretingDCT should only be called with LF == N"); + HWY_ALIGN float block[ROWS * COLS]; + + // ROWS, COLS <= 8, so we can put scratch space on the stack. + HWY_ALIGN float scratch_space[ROWS * COLS]; + DoDCT()(DCTFrom(input, input_stride), block, scratch_space); + if (ROWS < COLS) { + for (size_t y = 0; y < LF_ROWS; y++) { + for (size_t x = 0; x < LF_COLS; x++) { + output[y * output_stride + x] = + block[y * COLS + x] * DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } else { + for (size_t y = 0; y < LF_COLS; y++) { + for (size_t x = 0; x < LF_ROWS; x++) { + output[y * output_stride + x] = + block[y * ROWS + x] * DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } +} + +template +void IDCT2TopBlock(const float* block, size_t stride_out, float* out) { + static_assert(kBlockDim % S == 0, "S should be a divisor of kBlockDim"); + static_assert(S % 2 == 0, "S should be even"); + float temp[kDCTBlockSize]; + constexpr size_t num_2x2 = S / 2; + for (size_t y = 0; y < num_2x2; y++) { + for (size_t x = 0; x < num_2x2; x++) { + float c00 = block[y * kBlockDim + x]; + float c01 = block[y * kBlockDim + num_2x2 + x]; + float c10 = block[(y + num_2x2) * kBlockDim + x]; + float c11 = block[(y + num_2x2) * kBlockDim + num_2x2 + x]; + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + temp[y * 2 * kBlockDim + x * 2] = r00; + temp[y * 2 * kBlockDim + x * 2 + 1] = r01; + temp[(y * 2 + 1) * kBlockDim + x * 2] = r10; + temp[(y * 2 + 1) * kBlockDim + x * 2 + 1] = r11; + } + } + for (size_t y = 0; y < S; y++) { + for (size_t x = 0; x < S; x++) { + out[y * stride_out + x] = temp[y * kBlockDim + x]; + } + } +} + +void AFVIDCT4x4(const float* JXL_RESTRICT coeffs, float* JXL_RESTRICT pixels) { + HWY_ALIGN static constexpr float k4x4AFVBasis[16][16] = { + { + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + }, + { + 0.876902929799142f, + 0.2206518106944235f, + -0.10140050393753763f, + -0.1014005039375375f, + 0.2206518106944236f, + -0.10140050393753777f, + -0.10140050393753772f, + -0.10140050393753763f, + -0.10140050393753758f, + -0.10140050393753769f, + -0.1014005039375375f, + -0.10140050393753768f, + -0.10140050393753768f, + -0.10140050393753759f, + -0.10140050393753763f, + -0.10140050393753741f, + }, + { + 0.0, + 0.0, + 0.40670075830260755f, + 0.44444816619734445f, + 0.0, + 0.0, + 0.19574399372042936f, + 0.2929100136981264f, + -0.40670075830260716f, + -0.19574399372042872f, + 0.0, + 0.11379074460448091f, + -0.44444816619734384f, + -0.29291001369812636f, + -0.1137907446044814f, + 0.0, + }, + { + 0.0, + 0.0, + -0.21255748058288748f, + 0.3085497062849767f, + 0.0, + 0.4706702258572536f, + -0.1621205195722993f, + 0.0, + -0.21255748058287047f, + -0.16212051957228327f, + -0.47067022585725277f, + -0.1464291867126764f, + 0.3085497062849487f, + 0.0, + -0.14642918671266536f, + 0.4251149611657548f, + }, + { + 0.0, + -0.7071067811865474f, + 0.0, + 0.0, + 0.7071067811865476f, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + }, + { + -0.4105377591765233f, + 0.6235485373547691f, + -0.06435071657946274f, + -0.06435071657946266f, + 0.6235485373547694f, + -0.06435071657946284f, + -0.0643507165794628f, + -0.06435071657946274f, + -0.06435071657946272f, + -0.06435071657946279f, + -0.06435071657946266f, + -0.06435071657946277f, + -0.06435071657946277f, + -0.06435071657946273f, + -0.06435071657946274f, + -0.0643507165794626f, + }, + { + 0.0, + 0.0, + -0.4517556589999482f, + 0.15854503551840063f, + 0.0, + -0.04038515160822202f, + 0.0074182263792423875f, + 0.39351034269210167f, + -0.45175565899994635f, + 0.007418226379244351f, + 0.1107416575309343f, + 0.08298163094882051f, + 0.15854503551839705f, + 0.3935103426921022f, + 0.0829816309488214f, + -0.45175565899994796f, + }, + { + 0.0, + 0.0, + -0.304684750724869f, + 0.5112616136591823f, + 0.0, + 0.0, + -0.290480129728998f, + -0.06578701549142804f, + 0.304684750724884f, + 0.2904801297290076f, + 0.0, + -0.23889773523344604f, + -0.5112616136592012f, + 0.06578701549142545f, + 0.23889773523345467f, + 0.0, + }, + { + 0.0, + 0.0, + 0.3017929516615495f, + 0.25792362796341184f, + 0.0, + 0.16272340142866204f, + 0.09520022653475037f, + 0.0, + 0.3017929516615503f, + 0.09520022653475055f, + -0.16272340142866173f, + -0.35312385449816297f, + 0.25792362796341295f, + 0.0, + -0.3531238544981624f, + -0.6035859033230976f, + }, + { + 0.0, + 0.0, + 0.40824829046386274f, + 0.0, + 0.0, + 0.0, + 0.0, + -0.4082482904638628f, + -0.4082482904638635f, + 0.0, + 0.0, + -0.40824829046386296f, + 0.0, + 0.4082482904638634f, + 0.408248290463863f, + 0.0, + }, + { + 0.0, + 0.0, + 0.1747866975480809f, + 0.0812611176717539f, + 0.0, + 0.0, + -0.3675398009862027f, + -0.307882213957909f, + -0.17478669754808135f, + 0.3675398009862011f, + 0.0, + 0.4826689115059883f, + -0.08126111767175039f, + 0.30788221395790305f, + -0.48266891150598584f, + 0.0, + }, + { + 0.0, + 0.0, + -0.21105601049335784f, + 0.18567180916109802f, + 0.0, + 0.0, + 0.49215859013738733f, + -0.38525013709251915f, + 0.21105601049335806f, + -0.49215859013738905f, + 0.0, + 0.17419412659916217f, + -0.18567180916109904f, + 0.3852501370925211f, + -0.1741941265991621f, + 0.0, + }, + { + 0.0, + 0.0, + -0.14266084808807264f, + -0.3416446842253372f, + 0.0, + 0.7367497537172237f, + 0.24627107722075148f, + -0.08574019035519306f, + -0.14266084808807344f, + 0.24627107722075137f, + 0.14883399227113567f, + -0.04768680350229251f, + -0.3416446842253373f, + -0.08574019035519267f, + -0.047686803502292804f, + -0.14266084808807242f, + }, + { + 0.0, + 0.0, + -0.13813540350758585f, + 0.3302282550303788f, + 0.0, + 0.08755115000587084f, + -0.07946706605909573f, + -0.4613374887461511f, + -0.13813540350758294f, + -0.07946706605910261f, + 0.49724647109535086f, + 0.12538059448563663f, + 0.3302282550303805f, + -0.4613374887461554f, + 0.12538059448564315f, + -0.13813540350758452f, + }, + { + 0.0, + 0.0, + -0.17437602599651067f, + 0.0702790691196284f, + 0.0, + -0.2921026642334881f, + 0.3623817333531167f, + 0.0, + -0.1743760259965108f, + 0.36238173335311646f, + 0.29210266423348785f, + -0.4326608024727445f, + 0.07027906911962818f, + 0.0, + -0.4326608024727457f, + 0.34875205199302267f, + }, + { + 0.0, + 0.0, + 0.11354987314994337f, + -0.07417504595810355f, + 0.0, + 0.19402893032594343f, + -0.435190496523228f, + 0.21918684838857466f, + 0.11354987314994257f, + -0.4351904965232251f, + 0.5550443808910661f, + -0.25468277124066463f, + -0.07417504595810233f, + 0.2191868483885728f, + -0.25468277124066413f, + 0.1135498731499429f, + }, + }; + + const HWY_CAPPED(float, 16) d; + for (size_t i = 0; i < 16; i += Lanes(d)) { + auto pixel = Zero(d); + for (size_t j = 0; j < 16; j++) { + auto cf = Set(d, coeffs[j]); + auto basis = Load(d, k4x4AFVBasis[j] + i); + pixel = MulAdd(cf, basis, pixel); + } + Store(pixel, d, pixels + i); + } +} + +template +void AFVTransformToPixels(const float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, size_t pixels_stride) { + HWY_ALIGN float scratch_space[4 * 8]; + size_t afv_x = afv_kind & 1; + size_t afv_y = afv_kind / 2; + float dcs[3] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + dcs[0] = (block00 + block10 + block01) * 4.0f; + dcs[1] = (block00 + block10 - block01); + dcs[2] = block00 - block10; + // IAFV: (even, even) positions. + HWY_ALIGN float coeff[4 * 4]; + coeff[0] = dcs[0]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + coeff[iy * 4 + ix] = coefficients[iy * 2 * 8 + ix * 2]; + } + } + HWY_ALIGN float block[4 * 8]; + AFVIDCT4x4(coeff, block); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + pixels[(iy + afv_y * 4) * pixels_stride + afv_x * 4 + ix] = + block[(afv_y == 1 ? 3 - iy : iy) * 4 + (afv_x == 1 ? 3 - ix : ix)]; + } + } + // IDCT4x4 in (odd, even) positions. + block[0] = dcs[1]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 4 + ix] = coefficients[iy * 2 * 8 + ix * 2 + 1]; + } + } + ComputeTransposedScaledIDCT<4>()( + block, + DCTTo(pixels + afv_y * 4 * pixels_stride + (afv_x == 1 ? 0 : 4), + pixels_stride), + scratch_space); + // IDCT4x8. + block[0] = dcs[2]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(1 + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<4, 8>()( + block, + DCTTo(pixels + (afv_y == 1 ? 0 : 4) * pixels_stride, pixels_stride), + scratch_space); +} + +HWY_MAYBE_UNUSED void TransformToPixels(const AcStrategy::Type strategy, + float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, + size_t pixels_stride, + float* scratch_space) { + using Type = AcStrategy::Type; + switch (strategy) { + case Type::IDENTITY: { + PROFILER_ZONE("IDCT Identity"); + float dcs[4] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + float block_dc = dcs[y * 2 + x]; + float residual_sum = 0; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + residual_sum += coefficients[(y + iy * 2) * 8 + x + ix * 2]; + } + } + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1] = + block_dc - residual_sum * (1.0f / 16); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 1 && iy == 1) continue; + pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix] = + coefficients[(y + iy * 2) * 8 + x + ix * 2] + + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1]; + } + } + pixels[y * 4 * pixels_stride + x * 4] = + coefficients[(y + 2) * 8 + x + 2] + + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1]; + } + } + break; + } + case Type::DCT8X4: { + PROFILER_ZONE("IDCT 8x4"); + float dcs[2] = {}; + float block0 = coefficients[0]; + float block1 = coefficients[8]; + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 8]; + block[0] = dcs[x]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(x + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<8, 4>()(block, DCTTo(pixels + x * 4, pixels_stride), + scratch_space); + } + break; + } + case Type::DCT4X8: { + PROFILER_ZONE("IDCT 4x8"); + float dcs[2] = {}; + float block0 = coefficients[0]; + float block1 = coefficients[8]; + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + for (size_t y = 0; y < 2; y++) { + HWY_ALIGN float block[4 * 8]; + block[0] = dcs[y]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(y + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<4, 8>()( + block, DCTTo(pixels + y * 4 * pixels_stride, pixels_stride), + scratch_space); + } + break; + } + case Type::DCT4X4: { + PROFILER_ZONE("IDCT 4"); + float dcs[4] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 4]; + block[0] = dcs[y * 2 + x]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 4 + ix] = coefficients[(y + iy * 2) * 8 + x + ix * 2]; + } + } + ComputeTransposedScaledIDCT<4>()( + block, + DCTTo(pixels + y * 4 * pixels_stride + x * 4, pixels_stride), + scratch_space); + } + } + break; + } + case Type::DCT2X2: { + PROFILER_ZONE("IDCT 2"); + HWY_ALIGN float coeffs[kDCTBlockSize]; + memcpy(coeffs, coefficients, sizeof(float) * kDCTBlockSize); + IDCT2TopBlock<2>(coeffs, kBlockDim, coeffs); + IDCT2TopBlock<4>(coeffs, kBlockDim, coeffs); + IDCT2TopBlock<8>(coeffs, kBlockDim, coeffs); + for (size_t y = 0; y < kBlockDim; y++) { + for (size_t x = 0; x < kBlockDim; x++) { + pixels[y * pixels_stride + x] = coeffs[y * kBlockDim + x]; + } + } + break; + } + case Type::DCT16X16: { + PROFILER_ZONE("IDCT 16"); + ComputeTransposedScaledIDCT<16>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::DCT16X8: { + PROFILER_ZONE("IDCT 16x8"); + ComputeScaledIDCT<16, 8>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT8X16: { + PROFILER_ZONE("IDCT 8x16"); + ComputeScaledIDCT<8, 16>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT32X8: { + PROFILER_ZONE("IDCT 32x8"); + ComputeScaledIDCT<32, 8>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT8X32: { + PROFILER_ZONE("IDCT 8x32"); + ComputeScaledIDCT<8, 32>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT32X16: { + PROFILER_ZONE("IDCT 32x16"); + ComputeScaledIDCT<32, 16>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT16X32: { + PROFILER_ZONE("IDCT 16x32"); + ComputeScaledIDCT<16, 32>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT32X32: { + PROFILER_ZONE("IDCT 32"); + ComputeTransposedScaledIDCT<32>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::DCT: { + PROFILER_ZONE("IDCT 8"); + ComputeTransposedScaledIDCT<8>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::AFV0: { + PROFILER_ZONE("IAFV0"); + AFVTransformToPixels<0>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV1: { + PROFILER_ZONE("IAFV1"); + AFVTransformToPixels<1>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV2: { + PROFILER_ZONE("IAFV2"); + AFVTransformToPixels<2>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV3: { + PROFILER_ZONE("IAFV3"); + AFVTransformToPixels<3>(coefficients, pixels, pixels_stride); + break; + } + case Type::DCT64X32: { + PROFILER_ZONE("IDCT 64x32"); + ComputeScaledIDCT<64, 32>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT32X64: { + PROFILER_ZONE("IDCT 32x64"); + ComputeScaledIDCT<32, 64>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT64X64: { + PROFILER_ZONE("IDCT 64"); + ComputeTransposedScaledIDCT<64>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::DCT128X64: { + PROFILER_ZONE("IDCT 128x64"); + ComputeScaledIDCT<128, 64>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT64X128: { + PROFILER_ZONE("IDCT 64x128"); + ComputeScaledIDCT<64, 128>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT128X128: { + PROFILER_ZONE("IDCT 128"); + ComputeTransposedScaledIDCT<128>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::DCT256X128: { + PROFILER_ZONE("IDCT 256x128"); + ComputeScaledIDCT<256, 128>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT128X256: { + PROFILER_ZONE("IDCT 128x256"); + ComputeScaledIDCT<128, 256>()(coefficients, DCTTo(pixels, pixels_stride), + scratch_space); + break; + } + case Type::DCT256X256: { + PROFILER_ZONE("IDCT 256"); + ComputeTransposedScaledIDCT<256>()( + coefficients, DCTTo(pixels, pixels_stride), scratch_space); + break; + } + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +HWY_MAYBE_UNUSED void LowestFrequenciesFromDC(const AcStrategy::Type strategy, + const float* dc, size_t dc_stride, + float* llf) { + using Type = AcStrategy::Type; + switch (strategy) { + case Type::DCT16X8: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT8X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT16X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT32X8: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT8X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT32X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT16X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT32X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT64X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 8 * kBlockDim); + break; + } + case Type::DCT32X64: { + ReinterpretingDCT( + dc, dc_stride, llf, 8 * kBlockDim); + break; + } + case Type::DCT64X64: { + ReinterpretingDCT( + dc, dc_stride, llf, 8 * kBlockDim); + break; + } + case Type::DCT128X64: { + ReinterpretingDCT( + dc, dc_stride, llf, 16 * kBlockDim); + break; + } + case Type::DCT64X128: { + ReinterpretingDCT( + dc, dc_stride, llf, 16 * kBlockDim); + break; + } + case Type::DCT128X128: { + ReinterpretingDCT< + /*DCT_ROWS=*/16 * kBlockDim, /*DCT_COLS=*/16 * kBlockDim, + /*LF_ROWS=*/16, /*LF_COLS=*/16, /*ROWS=*/16, /*COLS=*/16>( + dc, dc_stride, llf, 16 * kBlockDim); + break; + } + case Type::DCT256X128: { + ReinterpretingDCT< + /*DCT_ROWS=*/32 * kBlockDim, /*DCT_COLS=*/16 * kBlockDim, + /*LF_ROWS=*/32, /*LF_COLS=*/16, /*ROWS=*/32, /*COLS=*/16>( + dc, dc_stride, llf, 32 * kBlockDim); + break; + } + case Type::DCT128X256: { + ReinterpretingDCT< + /*DCT_ROWS=*/16 * kBlockDim, /*DCT_COLS=*/32 * kBlockDim, + /*LF_ROWS=*/16, /*LF_COLS=*/32, /*ROWS=*/16, /*COLS=*/32>( + dc, dc_stride, llf, 32 * kBlockDim); + break; + } + case Type::DCT256X256: { + ReinterpretingDCT< + /*DCT_ROWS=*/32 * kBlockDim, /*DCT_COLS=*/32 * kBlockDim, + /*LF_ROWS=*/32, /*LF_COLS=*/32, /*ROWS=*/32, /*COLS=*/32>( + dc, dc_stride, llf, 32 * kBlockDim); + break; + } + case Type::DCT: + case Type::DCT2X2: + case Type::DCT4X4: + case Type::DCT4X8: + case Type::DCT8X4: + case Type::AFV0: + case Type::AFV1: + case Type::AFV2: + case Type::AFV3: + case Type::IDENTITY: + llf[0] = dc[0]; + break; + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + }; +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_DEC_TRANSFORMS_INL_H_ diff --git a/lib/jxl/dec_transforms_testonly.cc b/lib/jxl/dec_transforms_testonly.cc new file mode 100644 index 0000000..9ee80c5 --- /dev/null +++ b/lib/jxl/dec_transforms_testonly.cc @@ -0,0 +1,41 @@ +// 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/dec_transforms_testonly.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_transforms_testonly.cc" +#include +#include + +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/dec_transforms-inl.h" + +namespace jxl { + +#if HWY_ONCE +HWY_EXPORT(TransformToPixels); +void TransformToPixels(AcStrategy::Type strategy, + float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, size_t pixels_stride, + float* scratch_space) { + return HWY_DYNAMIC_DISPATCH(TransformToPixels)(strategy, coefficients, pixels, + pixels_stride, scratch_space); +} + +HWY_EXPORT(LowestFrequenciesFromDC); +void LowestFrequenciesFromDC(const jxl::AcStrategy::Type strategy, + const float* dc, size_t dc_stride, float* llf) { + return HWY_DYNAMIC_DISPATCH(LowestFrequenciesFromDC)(strategy, dc, dc_stride, + llf); +} + +HWY_EXPORT(AFVIDCT4x4); +void AFVIDCT4x4(const float* JXL_RESTRICT coeffs, float* JXL_RESTRICT pixels) { + return HWY_DYNAMIC_DISPATCH(AFVIDCT4x4)(coeffs, pixels); +} +#endif // HWY_ONCE + +} // namespace jxl diff --git a/lib/jxl/dec_transforms_testonly.h b/lib/jxl/dec_transforms_testonly.h new file mode 100644 index 0000000..97c4ca5 --- /dev/null +++ b/lib/jxl/dec_transforms_testonly.h @@ -0,0 +1,32 @@ +// 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_DEC_TRANSFORMS_TESTONLY_H_ +#define LIB_JXL_DEC_TRANSFORMS_TESTONLY_H_ + +// Facade for (non-inlined) inverse integral transforms. + +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +void TransformToPixels(AcStrategy::Type strategy, + float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, size_t pixels_stride, + float* JXL_RESTRICT scratch_space); + +// Equivalent of the above for DC image. +void LowestFrequenciesFromDC(const jxl::AcStrategy::Type strategy, + const float* dc, size_t dc_stride, float* llf); + +void AFVIDCT4x4(const float* JXL_RESTRICT coeffs, float* JXL_RESTRICT pixels); + +} // namespace jxl + +#endif // LIB_JXL_DEC_TRANSFORMS_TESTONLY_H_ diff --git a/lib/jxl/dec_upsample.cc b/lib/jxl/dec_upsample.cc new file mode 100644 index 0000000..7277e4f --- /dev/null +++ b/lib/jxl/dec_upsample.cc @@ -0,0 +1,381 @@ +// 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/dec_upsample.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_upsample.cc" +#include +#include + +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/sanitizers.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +void InitKernel(const float* weights, CacheAlignedUniquePtr* kernel_storage, + size_t N, size_t x_repeat) { + const size_t NX = N * x_repeat; + const size_t N2 = N / 2; + HWY_FULL(float) df; + const size_t V = Lanes(df); + const size_t num_kernels = N * NX; + + constexpr const size_t M = 2 * Upsampler::filter_radius() + 1; + const size_t MX = M + x_repeat - 1; + const size_t num_coeffs = M * MX; + + // Pad kernel slices to vector size. + const size_t stride = RoundUpTo(num_kernels, V); + *kernel_storage = AllocateArray(stride * sizeof(float) * num_coeffs); + float* kernels = reinterpret_cast(kernel_storage->get()); + memset(kernels, 0, stride * sizeof(float) * num_coeffs); + + for (size_t offset = 0; offset < num_coeffs; ++offset) { + size_t iy = offset / MX; + size_t ix = offset % MX; + for (size_t kernel = 0; kernel < num_kernels; ++kernel) { + size_t ky = kernel / NX; + size_t kx_ = kernel % NX; + size_t kx = kx_ % N; + size_t shift = kx_ / N; + if ((ix < shift) || (ix - shift >= M)) continue; // 0 weight from memset. + // Only weights for top-left 1 / 4 of kernels are specified; other 3 / 4 + // kernels are produced by vertical and horizontal mirroring. + size_t j = (ky < N2) ? (iy + M * ky) : ((M - 1 - iy) + M * (N - 1 - ky)); + size_t i = (kx < N2) ? (ix - shift + M * kx) + : ((M - 1 - (ix - shift)) + M * (N - 1 - kx)); + // (y, x) = sorted(i, j) + // the matrix built of kernel matrices as blocks is symmetric. + size_t y = std::min(i, j); + size_t x = std::max(i, j); + // Take the weight from "triangle" coordinates. + float weight = + weights[M * N2 * y - y * (static_cast(y) - 1) / 2 + x - y]; + kernels[offset * stride + kernel] = weight; + } + } +} + +template +void Upsample(const ImageF& src, const Rect& src_rect, ImageF* dst, + const Rect& dst_rect, const float* kernels, + ssize_t image_y_offset, size_t image_ysize, float* arena) { + constexpr const size_t M = 2 * Upsampler::filter_radius() + 1; + constexpr const size_t M2 = M / 2; + JXL_DASSERT(src_rect.x0() >= M2); + const size_t src_x_limit = src_rect.x0() + src_rect.xsize() + M2; + JXL_DASSERT(src_x_limit <= src.xsize()); + JXL_ASSERT(DivCeil(dst_rect.xsize(), N) <= src_rect.xsize()); + // TODO(eustas): add proper (src|dst) ysize check that accounts for mirroring. + + constexpr const size_t MX = M + x_repeat - 1; + constexpr const size_t num_coeffs = M * MX; + + constexpr const size_t NX = N * x_repeat; + + HWY_FULL(float) df; + const size_t V = Lanes(df); + const size_t num_kernels = N * NX; + const size_t stride = RoundUpTo(num_kernels, V); + + const size_t rsx = DivCeil(dst_rect.xsize(), N); + const size_t dsx = rsx + 2 * M2; + // Round-down to complete vectors. + const size_t dsx_v = V * (dsx / V); + + float* JXL_RESTRICT in = arena; + arena += RoundUpTo(num_coeffs, V); + float* JXL_RESTRICT out = arena; + arena += stride; + float* JXL_RESTRICT raw_min_row = arena; + arena += RoundUpTo(dsx + V, V); + float* JXL_RESTRICT raw_max_row = arena; + arena += RoundUpTo(dsx + V, V); + float* JXL_RESTRICT min_row = arena; + arena += RoundUpTo(rsx * N + V, V); + float* JXL_RESTRICT max_row = arena; + arena += RoundUpTo(rsx * N + V, V); + + memset(raw_min_row + dsx_v, 0, sizeof(float) * (V + dsx - dsx_v)); + memset(raw_max_row + dsx_v, 0, sizeof(float) * (V + dsx - dsx_v)); + memset(min_row + dst_rect.xsize(), 0, sizeof(float) * V); + memset(max_row + dst_rect.xsize(), 0, sizeof(float) * V); + + // For min/max reduction. + const size_t span_tail_len = M % V; + const bool has_span_tail = (span_tail_len != 0); + JXL_ASSERT(has_span_tail || V <= M); + const size_t span_start = has_span_tail ? 0 : V; + const size_t span_tail_start = M - span_tail_len; + const auto span_tail_mask = Iota(df, 0) < Set(df, span_tail_len); + + // sx and sy correspond to offset in source image. + // x and y correspond to top-left pixel offset in upsampled output image. + for (size_t y = 0; y < dst_rect.ysize(); y += N) { + const float* src_rows[M]; + const size_t sy = y / N; + const ssize_t top = static_cast(sy + src_rect.y0() - M2); + for (size_t iy = 0; iy < M; iy++) { + const ssize_t image_y = top + iy + image_y_offset; + src_rows[iy] = src.Row(Mirror(image_y, image_ysize) - image_y_offset); + } + const size_t sx0 = src_rect.x0() - M2; + for (size_t sx = 0; sx < dsx_v; sx += V) { + static_assert(M == 5, "Filter diameter is expected to be 5"); + const auto r0 = LoadU(df, src_rows[0] + sx0 + sx); + const auto r1 = LoadU(df, src_rows[1] + sx0 + sx); + const auto r2 = LoadU(df, src_rows[2] + sx0 + sx); + const auto r3 = LoadU(df, src_rows[3] + sx0 + sx); + const auto r4 = LoadU(df, src_rows[4] + sx0 + sx); + const auto min0 = Min(r0, r1); + const auto max0 = Max(r0, r1); + const auto min1 = Min(r2, r3); + const auto max1 = Max(r2, r3); + const auto min2 = Min(min0, r4); + const auto max2 = Max(max0, r4); + Store(Min(min1, min2), df, raw_min_row + sx); + Store(Max(max1, max2), df, raw_max_row + sx); + } + for (size_t sx = dsx_v; sx < dsx; sx++) { + static_assert(M == 5, "Filter diameter is expected to be 5"); + const auto r0 = src_rows[0][sx0 + sx]; + const auto r1 = src_rows[1][sx0 + sx]; + const auto r2 = src_rows[2][sx0 + sx]; + const auto r3 = src_rows[3][sx0 + sx]; + const auto r4 = src_rows[4][sx0 + sx]; + const auto min0 = std::min(r0, r1); + const auto max0 = std::max(r0, r1); + const auto min1 = std::min(r2, r3); + const auto max1 = std::max(r2, r3); + const auto min2 = std::min(min0, r4); + const auto max2 = std::max(max0, r4); + raw_min_row[sx] = std::min(min1, min2); + raw_max_row[sx] = std::max(max1, max2); + } + + for (size_t sx = 0; sx < rsx; sx++) { + decltype(Zero(df)) min, max; + if (has_span_tail) { + auto dummy = Set(df, raw_min_row[sx]); + min = IfThenElse(span_tail_mask, + LoadU(df, raw_min_row + sx + span_tail_start), dummy); + max = IfThenElse(span_tail_mask, + LoadU(df, raw_max_row + sx + span_tail_start), dummy); + } else { + min = LoadU(df, raw_min_row + sx); + max = LoadU(df, raw_max_row + sx); + } + for (size_t fx = span_start; fx < span_tail_start; fx += V) { + min = Min(LoadU(df, raw_min_row + sx + fx), min); + max = Max(LoadU(df, raw_max_row + sx + fx), max); + } + min = MinOfLanes(min); + max = MaxOfLanes(max); + for (size_t lx = 0; lx < N; lx += V) { + StoreU(min, df, min_row + N * sx + lx); + StoreU(max, df, max_row + N * sx + lx); + } + } + + for (size_t x = 0; x < dst_rect.xsize(); x += NX) { + const size_t sx = x / N; + const size_t xbase = sx + sx0; + // Copy input pixels for "linearization". + for (size_t iy = 0; iy < M; iy++) { + memcpy(in + MX * iy, src_rows[iy] + xbase, MX * sizeof(float)); + } + if (x_repeat > 1) { + // Even if filter coeffs contain 0 at "undefined" values, the result + // might be undefined, because NaN will poison the sum. + if (JXL_UNLIKELY(xbase + MX > src_x_limit)) { + for (size_t iy = 0; iy < M; iy++) { + for (size_t ix = src_x_limit - xbase; ix < MX; ++ix) { + in[MX * iy + ix] = 0.0f; + } + } + } + } + constexpr size_t U = 4; // Unroll factor. + constexpr size_t tail = num_coeffs & ~(U - 1); + constexpr size_t tail_length = num_coeffs - tail; + for (size_t kernel_idx = 0; kernel_idx < num_kernels; kernel_idx += V) { + const float* JXL_RESTRICT kernel_base = kernels + kernel_idx; + decltype(Zero(df)) results[U]; + for (size_t i = 0; i < U; i++) { + results[i] = Set(df, in[i]) * Load(df, kernel_base + i * stride); + } + for (size_t i = U; i < tail; i += U) { + for (size_t j = 0; j < U; ++j) { + results[j] = + MulAdd(Set(df, in[i + j]), + Load(df, kernel_base + (i + j) * stride), results[j]); + } + } + for (size_t i = 0; i < tail_length; ++i) { + results[i] = + MulAdd(Set(df, in[tail + i]), + Load(df, kernel_base + (tail + i) * stride), results[i]); + } + auto result = results[0]; + for (size_t i = 1; i < U; ++i) result += results[i]; + Store(result, df, out + kernel_idx); + } + const size_t oy_max = std::min(dst_rect.ysize(), y + N); + const size_t ox_max = std::min(dst_rect.xsize(), x + NX); + const size_t copy_len = ox_max - x; + const size_t copy_last = RoundUpTo(copy_len, V); + if (JXL_LIKELY(x + copy_last <= dst_rect.xsize())) { + for (size_t dx = 0; dx < copy_len; dx += V) { + auto min = LoadU(df, min_row + x + dx); + auto max = LoadU(df, max_row + x + dx); + float* pixels = out; + for (size_t oy = sy * N; oy < oy_max; ++oy, pixels += NX) { + StoreU(Clamp(LoadU(df, pixels + dx), min, max), df, + dst_rect.Row(dst, oy) + x + dx); + } + } + } else { + for (size_t dx = 0; dx < copy_len; dx++) { + auto min = min_row[x + dx]; + auto max = max_row[x + dx]; + float* pixels = out; + for (size_t oy = sy * N; oy < oy_max; ++oy, pixels += NX) { + dst_rect.Row(dst, oy)[x + dx] = Clamp1(pixels[dx], min, max); + } + } + } + } + } +} + +} // namespace + +void UpsampleRect(size_t upsampling, const float* kernels, const ImageF& src, + const Rect& src_rect, ImageF* dst, const Rect& dst_rect, + ssize_t image_y_offset, size_t image_ysize, float* arena, + size_t x_repeat) { + if (upsampling == 1) return; + if (upsampling == 2) { + if (x_repeat == 1) { + Upsample(src, src_rect, dst, dst_rect, kernels, + image_y_offset, image_ysize, arena); + } else if (x_repeat == 2) { + Upsample(src, src_rect, dst, dst_rect, kernels, + image_y_offset, image_ysize, arena); + } else if (x_repeat == 4) { + Upsample(src, src_rect, dst, dst_rect, kernels, + image_y_offset, image_ysize, arena); + } else { + JXL_ABORT("Not implemented"); + } + } else if (upsampling == 4) { + JXL_ASSERT(x_repeat == 1); + Upsample(src, src_rect, dst, dst_rect, kernels, + image_y_offset, image_ysize, arena); + } else if (upsampling == 8) { + JXL_ASSERT(x_repeat == 1); + Upsample(src, src_rect, dst, dst_rect, kernels, + image_y_offset, image_ysize, arena); + } else { + JXL_ABORT("Not implemented"); + } +} + +size_t NumLanes() { + HWY_FULL(float) df; + return Lanes(df); +} + +void Init(size_t upsampling, CacheAlignedUniquePtr* kernel_storage, + const CustomTransformData& data, size_t x_repeat) { + if ((upsampling & (upsampling - 1)) != 0 || + upsampling > Upsampler::max_upsampling()) { + JXL_ABORT("Invalid upsample"); + } + if ((x_repeat & (x_repeat - 1)) != 0 || + x_repeat > Upsampler::max_x_repeat()) { + JXL_ABORT("Invalid x_repeat"); + } + + // No-op upsampling. + if (upsampling == 1) return; + const float* weights = (upsampling == 2) ? data.upsampling2_weights + : (upsampling == 4) ? data.upsampling4_weights + : data.upsampling8_weights; + InitKernel(weights, kernel_storage, upsampling, x_repeat); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +namespace { +HWY_EXPORT(NumLanes); +HWY_EXPORT(Init); +HWY_EXPORT(UpsampleRect); +} // namespace + +void Upsampler::Init(size_t upsampling, const CustomTransformData& data) { + upsampling_ = upsampling; + size_t V = HWY_DYNAMIC_DISPATCH(NumLanes)(); + x_repeat_ = 1; + if (upsampling_ == 2) { + // 2 * 2 = 4 kernels; repeat cell, if there is more lanes available + if (V >= 8) x_repeat_ = 2; + if (V >= 16) x_repeat_ = 4; + } + HWY_DYNAMIC_DISPATCH(Init)(upsampling, &kernel_storage_, data, x_repeat_); +} + +size_t Upsampler::GetArenaSize(size_t max_dst_xsize) { + size_t V = HWY_DYNAMIC_DISPATCH(NumLanes)(); + constexpr const size_t M2 = Upsampler::filter_radius(); + constexpr const size_t M = 2 * M2 + 1; + constexpr size_t X = max_x_repeat(); + constexpr const size_t MX = M + X - 1; + constexpr const size_t N = max_upsampling(); + // TODO(eustas): raw_(min|max)_row and (min|max)_row could overlap almost + // completely. + return RoundUpTo(N * N * X, V) + RoundUpTo(M * MX, V) + + 2 * RoundUpTo(DivCeil(max_dst_xsize, 8) * 4 + 2 * M2 + V, V) + + 2 * RoundUpTo(max_dst_xsize + V, V); +} + +void Upsampler::UpsampleRect(const ImageF& src, const Rect& src_rect, + ImageF* dst, const Rect& dst_rect, + ssize_t image_y_offset, size_t image_ysize, + float* arena) const { + JXL_CHECK(arena); + JXL_CHECK_IMAGE_INITIALIZED(src, src_rect); + HWY_DYNAMIC_DISPATCH(UpsampleRect) + (upsampling_, reinterpret_cast(kernel_storage_.get()), src, src_rect, + dst, dst_rect, image_y_offset, image_ysize, arena, x_repeat_); + JXL_CHECK_IMAGE_INITIALIZED(*dst, dst_rect); +} + +void Upsampler::UpsampleRect(const Image3F& src, const Rect& src_rect, + Image3F* dst, const Rect& dst_rect, + ssize_t image_y_offset, size_t image_ysize, + float* arena) const { + PROFILER_FUNC; + JXL_CHECK_IMAGE_INITIALIZED(src, src_rect); + for (size_t c = 0; c < 3; c++) { + UpsampleRect(src.Plane(c), src_rect, &dst->Plane(c), dst_rect, + image_y_offset, image_ysize, arena); + } + JXL_CHECK_IMAGE_INITIALIZED(*dst, dst_rect); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_upsample.h b/lib/jxl/dec_upsample.h new file mode 100644 index 0000000..036acdf --- /dev/null +++ b/lib/jxl/dec_upsample.h @@ -0,0 +1,57 @@ +// 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_DEC_UPSAMPLE_H_ +#define LIB_JXL_DEC_UPSAMPLE_H_ + +#include "lib/jxl/image.h" +#include "lib/jxl/image_metadata.h" + +namespace jxl { + +struct Upsampler { + void Init(size_t upsampling, const CustomTransformData& data); + + // Only 1x, 2x, 4x and 8x upsampling is supported. + static constexpr size_t max_upsampling() { return 8; } + + // To produce N x N upsampled pixels the [-2..2]x[-2..2] neighborhood of + // input pixel is taken and dot-multiplied with N x N corresponding "kernels". + // Thus the "kernel" is a 5 x 5 matrix of weights. + static constexpr size_t filter_radius() { return 2; } + + // Calculate multiple upsampled cells at the same time. + // Kernels are transposed - several kernels are multiplied by input + // at the same time. In case of 2x upsampling there are only 4 kernels. + // If current target supports SIMD vectors longer than 4 floats, to reduce + // the wasted multiplications we increase the effective kernel count. + static constexpr size_t max_x_repeat() { return 4; } + + // Get the size of "arena" required for UpsampleRect; + // "arena" should be an aligned piece of memory with at least `GetArenaSize()` + // float values accessible. + static size_t GetArenaSize(size_t max_dst_xsize); + + // The caller must guarantee that `src:src_rect` has two pixels of padding + // available on each side of the x dimension. `image_ysize` is the total + // height of the frame that the source area belongs to (not the buffer); + // `image_y_offset` is the difference between `src.y0()` and the corresponding + // y value in the full frame. + void UpsampleRect(const Image3F& src, const Rect& src_rect, Image3F* dst, + const Rect& dst_rect, ssize_t image_y_offset, + size_t image_ysize, float* arena) const; + void UpsampleRect(const ImageF& src, const Rect& src_rect, ImageF* dst, + const Rect& dst_rect, ssize_t image_y_offset, + size_t image_ysize, float* arena) const; + + private: + size_t upsampling_ = 1; + size_t x_repeat_ = 1; + CacheAlignedUniquePtr kernel_storage_ = {nullptr}; +}; + +} // namespace jxl + +#endif // LIB_JXL_DEC_UPSAMPLE_H_ diff --git a/lib/jxl/dec_xyb-inl.h b/lib/jxl/dec_xyb-inl.h new file mode 100644 index 0000000..df16ce8 --- /dev/null +++ b/lib/jxl/dec_xyb-inl.h @@ -0,0 +1,351 @@ +// 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. + +// XYB -> linear sRGB helper function. + +#if defined(LIB_JXL_DEC_XYB_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_DEC_XYB_INL_H_ +#undef LIB_JXL_DEC_XYB_INL_H_ +#else +#define LIB_JXL_DEC_XYB_INL_H_ +#endif + +#include + +#include "lib/jxl/dec_xyb.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Broadcast; + +// Inverts the pixel-wise RGB->XYB conversion in OpsinDynamicsImage() (including +// the gamma mixing and simple gamma). Avoids clamping to [0, 1] - out of (sRGB) +// gamut values may be in-gamut after transforming to a wider space. +// "inverse_matrix" points to 9 broadcasted vectors, which are the 3x3 entries +// of the (row-major) opsin absorbance matrix inverse. Pre-multiplying its +// entries by c is equivalent to multiplying linear_* by c afterwards. +template +HWY_INLINE HWY_MAYBE_UNUSED void XybToRgb(D d, const V opsin_x, const V opsin_y, + const V opsin_b, + const OpsinParams& opsin_params, + V* const HWY_RESTRICT linear_r, + V* const HWY_RESTRICT linear_g, + V* const HWY_RESTRICT linear_b) { +#if HWY_TARGET == HWY_SCALAR + const auto neg_bias_r = Set(d, opsin_params.opsin_biases[0]); + const auto neg_bias_g = Set(d, opsin_params.opsin_biases[1]); + const auto neg_bias_b = Set(d, opsin_params.opsin_biases[2]); +#else + const auto neg_bias_rgb = LoadDup128(d, opsin_params.opsin_biases); + const auto neg_bias_r = Broadcast<0>(neg_bias_rgb); + const auto neg_bias_g = Broadcast<1>(neg_bias_rgb); + const auto neg_bias_b = Broadcast<2>(neg_bias_rgb); +#endif + + // Color space: XYB -> RGB + auto gamma_r = opsin_y + opsin_x; + auto gamma_g = opsin_y - opsin_x; + auto gamma_b = opsin_b; + + gamma_r -= Set(d, opsin_params.opsin_biases_cbrt[0]); + gamma_g -= Set(d, opsin_params.opsin_biases_cbrt[1]); + gamma_b -= Set(d, opsin_params.opsin_biases_cbrt[2]); + + // Undo gamma compression: linear = gamma^3 for efficiency. + const auto gamma_r2 = gamma_r * gamma_r; + const auto gamma_g2 = gamma_g * gamma_g; + const auto gamma_b2 = gamma_b * gamma_b; + const auto mixed_r = MulAdd(gamma_r2, gamma_r, neg_bias_r); + const auto mixed_g = MulAdd(gamma_g2, gamma_g, neg_bias_g); + const auto mixed_b = MulAdd(gamma_b2, gamma_b, neg_bias_b); + + const float* HWY_RESTRICT inverse_matrix = opsin_params.inverse_opsin_matrix; + + // Unmix (multiply by 3x3 inverse_matrix) + *linear_r = LoadDup128(d, &inverse_matrix[0 * 4]) * mixed_r; + *linear_g = LoadDup128(d, &inverse_matrix[3 * 4]) * mixed_r; + *linear_b = LoadDup128(d, &inverse_matrix[6 * 4]) * mixed_r; + *linear_r = MulAdd(LoadDup128(d, &inverse_matrix[1 * 4]), mixed_g, *linear_r); + *linear_g = MulAdd(LoadDup128(d, &inverse_matrix[4 * 4]), mixed_g, *linear_g); + *linear_b = MulAdd(LoadDup128(d, &inverse_matrix[7 * 4]), mixed_g, *linear_b); + *linear_r = MulAdd(LoadDup128(d, &inverse_matrix[2 * 4]), mixed_b, *linear_r); + *linear_g = MulAdd(LoadDup128(d, &inverse_matrix[5 * 4]), mixed_b, *linear_g); + *linear_b = MulAdd(LoadDup128(d, &inverse_matrix[8 * 4]), mixed_b, *linear_b); +} + +static inline HWY_MAYBE_UNUSED bool HasFastXYBTosRGB8() { +#if HWY_TARGET == HWY_NEON + return true; +#else + return false; +#endif +} + +static inline HWY_MAYBE_UNUSED void FastXYBTosRGB8( + const Image3F& input, const Rect& input_rect, const Rect& output_buf_rect, + const ImageF* alpha, const Rect& alpha_rect, bool is_rgba, + uint8_t* JXL_RESTRICT output_buf, size_t xsize, size_t output_stride) { + // This function is very NEON-specific. As such, it uses intrinsics directly. +#if HWY_TARGET == HWY_NEON + // WARNING: doing fixed point arithmetic correctly is very complicated. + // Changes to this function should be thoroughly tested. + + // Note that the input is assumed to have 13 bits of mantissa, and the output + // will have 14 bits. + auto srgb_tf = [&](int16x8_t v16) { + int16x8_t clz = vclzq_s16(v16); + // Convert to [0.25, 0.5) range. + int16x8_t v025_05_16 = vqshlq_s16(v16, vqsubq_s16(clz, vdupq_n_s16(2))); + + // third degree polynomial approximation between 0.25 and 0.5 + // of 1.055/2^(7/2.4) * x^(1/2.4) / 32. + // poly ~ ((0.95x-1.75)*x+1.72)*x+0.29 + // We actually compute ~ ((0.47x-0.87)*x+0.86)*(2x)+0.29 as 1.75 and 1.72 + // overflow our fixed point representation. + + int16x8_t twov = vqaddq_s16(v025_05_16, v025_05_16); + + // 0.47 * x + int16x8_t step1 = vqrdmulhq_n_s16(v025_05_16, 15706); + // - 0.87 + int16x8_t step2 = vsubq_s16(step1, vdupq_n_s16(28546)); + // * x + int16x8_t step3 = vqrdmulhq_s16(step2, v025_05_16); + // + 0.86 + int16x8_t step4 = vaddq_s16(step3, vdupq_n_s16(28302)); + // * 2x + int16x8_t step5 = vqrdmulhq_s16(step4, twov); + // + 0.29 + int16x8_t mul16 = vaddq_s16(step5, vdupq_n_s16(9485)); + + int16x8_t exp16 = vsubq_s16(vdupq_n_s16(11), clz); + // Compute 2**(1/2.4*exp16)/32. Values of exp16 that would overflow are + // capped to 1. + // Generated with the following Python script: + // a = [] + // b = [] + // + // for i in range(0, 16): + // v = 2**(5/12.*i) + // v /= 16 + // v *= 256 * 128 + // v = int(v) + // a.append(v // 256) + // b.append(v % 256) + // + // print(", ".join("0x%02x" % x for x in a)) + // + // print(", ".join("0x%02x" % x for x in b)) + + HWY_ALIGN constexpr uint8_t k2to512powersm1div32_high[16] = { + 0x08, 0x0a, 0x0e, 0x13, 0x19, 0x21, 0x2d, 0x3c, + 0x50, 0x6b, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + }; + HWY_ALIGN constexpr uint8_t k2to512powersm1div32_low[16] = { + 0x00, 0xad, 0x41, 0x06, 0x65, 0xe7, 0x41, 0x68, + 0xa2, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }; + // Using the highway implementation here since vqtbl1q is aarch64-only. + using hwy::HWY_NAMESPACE::Vec128; + uint8x16_t pow_low = + TableLookupBytes( + Vec128(vld1q_u8(k2to512powersm1div32_low)), + Vec128(vreinterpretq_u8_s16(exp16))) + .raw; + uint8x16_t pow_high = + TableLookupBytes( + Vec128(vld1q_u8(k2to512powersm1div32_high)), + Vec128(vreinterpretq_u8_s16(exp16))) + .raw; + int16x8_t pow16 = vreinterpretq_s16_u16(vsliq_n_u16( + vreinterpretq_u16_u8(pow_low), vreinterpretq_u16_u8(pow_high), 8)); + + // approximation of v * 12.92, divided by 2 + // Note that our input is using 13 mantissa bits instead of 15. + int16x8_t v16_linear = vrshrq_n_s16(vmulq_n_s16(v16, 826), 5); + // 1.055*pow(v, 1/2.4) - 0.055, divided by 2 + auto v16_pow = vsubq_s16(vqrdmulhq_s16(mul16, pow16), vdupq_n_s16(901)); + // > 0.0031308f (note that v16 has 13 mantissa bits) + return vbslq_s16(vcgeq_s16(v16, vdupq_n_s16(26)), v16_pow, v16_linear); + }; + for (size_t y = 0; y < output_buf_rect.ysize(); y++) { + const float* JXL_RESTRICT row_in_x = input_rect.ConstPlaneRow(input, 0, y); + const float* JXL_RESTRICT row_in_y = input_rect.ConstPlaneRow(input, 1, y); + const float* JXL_RESTRICT row_in_b = input_rect.ConstPlaneRow(input, 2, y); + const float* JXL_RESTRICT row_in_a = + alpha == nullptr ? nullptr : alpha_rect.ConstRow(*alpha, y); + size_t cnt = !is_rgba ? 3 : 4; + size_t base_ptr = + (y + output_buf_rect.y0()) * output_stride + output_buf_rect.x0() * cnt; + for (size_t x = 0; x < output_buf_rect.xsize(); x += 8) { + // Normal ranges for xyb for in-gamut sRGB colors: + // x: -0.015386 0.028100 + // y: 0.000000 0.845308 + // b: 0.000000 0.845308 + + // We actually want x * 8 to have some extra precision. + // TODO(veluca): consider different approaches here, like vld1q_f32_x2. + float32x4_t opsin_x_left = vld1q_f32(row_in_x + x); + int16x4_t opsin_x16_times8_left = + vqmovn_s32(vcvtq_n_s32_f32(opsin_x_left, 18)); + float32x4_t opsin_x_right = + vld1q_f32(row_in_x + x + (x + 4 < output_buf_rect.xsize() ? 4 : 0)); + int16x4_t opsin_x16_times8_right = + vqmovn_s32(vcvtq_n_s32_f32(opsin_x_right, 18)); + int16x8_t opsin_x16_times8 = + vcombine_s16(opsin_x16_times8_left, opsin_x16_times8_right); + + float32x4_t opsin_y_left = vld1q_f32(row_in_y + x); + int16x4_t opsin_y16_left = vqmovn_s32(vcvtq_n_s32_f32(opsin_y_left, 15)); + float32x4_t opsin_y_right = + vld1q_f32(row_in_y + x + (x + 4 < output_buf_rect.xsize() ? 4 : 0)); + int16x4_t opsin_y16_right = + vqmovn_s32(vcvtq_n_s32_f32(opsin_y_right, 15)); + int16x8_t opsin_y16 = vcombine_s16(opsin_y16_left, opsin_y16_right); + + float32x4_t opsin_b_left = vld1q_f32(row_in_b + x); + int16x4_t opsin_b16_left = vqmovn_s32(vcvtq_n_s32_f32(opsin_b_left, 15)); + float32x4_t opsin_b_right = + vld1q_f32(row_in_b + x + (x + 4 < output_buf_rect.xsize() ? 4 : 0)); + int16x4_t opsin_b16_right = + vqmovn_s32(vcvtq_n_s32_f32(opsin_b_right, 15)); + int16x8_t opsin_b16 = vcombine_s16(opsin_b16_left, opsin_b16_right); + + int16x8_t neg_bias16 = vdupq_n_s16(-124); // -0.0037930732552754493 + int16x8_t neg_bias_cbrt16 = vdupq_n_s16(-5110); // -0.155954201 + int16x8_t neg_bias_half16 = vdupq_n_s16(-62); + + // Color space: XYB -> RGB + // Compute ((y+x-bias_cbrt)^3-(y-x-bias_cbrt)^3)/2, + // ((y+x-bias_cbrt)^3+(y-x-bias_cbrt)^3)/2+bias, (b-bias_cbrt)^3+bias. + // Note that ignoring x2 in the formulas below (as x << y) results in + // errors of at least 3 in the final sRGB values. + int16x8_t opsin_yp16 = vqsubq_s16(opsin_y16, neg_bias_cbrt16); + int16x8_t ysq16 = vqrdmulhq_s16(opsin_yp16, opsin_yp16); + int16x8_t twentyfourx16 = vmulq_n_s16(opsin_x16_times8, 3); + int16x8_t twentyfourxy16 = vqrdmulhq_s16(opsin_yp16, twentyfourx16); + int16x8_t threexsq16 = + vrshrq_n_s16(vqrdmulhq_s16(opsin_x16_times8, twentyfourx16), 6); + + // We can ignore x^3 here. Note that this is multiplied by 8. + int16x8_t mixed_rmg16 = vqrdmulhq_s16(twentyfourxy16, opsin_yp16); + + int16x8_t mixed_rpg_sos_half = vhaddq_s16(ysq16, threexsq16); + int16x8_t mixed_rpg16 = vhaddq_s16( + vqrdmulhq_s16(opsin_yp16, mixed_rpg_sos_half), neg_bias_half16); + + int16x8_t gamma_b16 = vqsubq_s16(opsin_b16, neg_bias_cbrt16); + int16x8_t gamma_bsq16 = vqrdmulhq_s16(gamma_b16, gamma_b16); + int16x8_t gamma_bcb16 = vqrdmulhq_s16(gamma_bsq16, gamma_b16); + int16x8_t mixed_b16 = vqaddq_s16(gamma_bcb16, neg_bias16); + // mixed_rpg and mixed_b are in 0-1 range. + // mixed_rmg has a smaller range (-0.035 to 0.035 for valid sRGB). Note + // that at this point it is already multiplied by 8. + + // We multiply all the mixed values by 1/4 (i.e. shift them to 13-bit + // fixed point) to ensure intermediate quantities are in range. Note that + // r-g is not shifted, and was x8 before here; this corresponds to a x32 + // overall multiplicative factor and ensures that all the matrix constants + // are in 0-1 range. + // Similarly, mixed_rpg16 is already multiplied by 1/4 because of the two + // vhadd + using neg_bias_half. + mixed_b16 = vshrq_n_s16(mixed_b16, 2); + + // Unmix (multiply by 3x3 inverse_matrix) + // For increased precision, we use a matrix for converting from + // ((mixed_r - mixed_g)/2, (mixed_r + mixed_g)/2, mixed_b) to rgb. This + // avoids cancellation effects when computing (y+x)^3-(y-x)^3. + // We compute mixed_rpg - mixed_b because the (1+c)*mixed_rpg - c * + // mixed_b pattern is repeated frequently in the code below. This allows + // us to save a multiply per channel, and removes the presence of + // some constants above 1. Moreover, mixed_rmg - mixed_b is in (-1, 1) + // range, so the subtraction is safe. + // All the magic-looking constants here are derived by computing the + // inverse opsin matrix for the transformation modified as described + // above. + + // Precomputation common to multiple color values. + int16x8_t mixed_rpgmb16 = vqsubq_s16(mixed_rpg16, mixed_b16); + int16x8_t mixed_rpgmb_times_016 = vqrdmulhq_n_s16(mixed_rpgmb16, 5394); + int16x8_t mixed_rg16 = vqaddq_s16(mixed_rpgmb_times_016, mixed_rpg16); + + // R + int16x8_t linear_r16 = + vqaddq_s16(mixed_rg16, vqrdmulhq_n_s16(mixed_rmg16, 21400)); + + // G + int16x8_t linear_g16 = + vqaddq_s16(mixed_rg16, vqrdmulhq_n_s16(mixed_rmg16, -7857)); + + // B + int16x8_t linear_b16 = vqrdmulhq_n_s16(mixed_rpgmb16, -30996); + linear_b16 = vqaddq_s16(linear_b16, mixed_b16); + linear_b16 = vqaddq_s16(linear_b16, vqrdmulhq_n_s16(mixed_rmg16, -6525)); + + // Apply SRGB transfer function. + int16x8_t r = srgb_tf(linear_r16); + int16x8_t g = srgb_tf(linear_g16); + int16x8_t b = srgb_tf(linear_b16); + + uint8x8_t r8 = + vqmovun_s16(vrshrq_n_s16(vsubq_s16(r, vshrq_n_s16(r, 8)), 6)); + uint8x8_t g8 = + vqmovun_s16(vrshrq_n_s16(vsubq_s16(g, vshrq_n_s16(g, 8)), 6)); + uint8x8_t b8 = + vqmovun_s16(vrshrq_n_s16(vsubq_s16(b, vshrq_n_s16(b, 8)), 6)); + + size_t n = output_buf_rect.xsize() - x; + if (is_rgba) { + float32x4_t a_f32_left = + row_in_a ? vld1q_f32(row_in_a + x) : vdupq_n_f32(1.0f); + float32x4_t a_f32_right = + row_in_a ? vld1q_f32(row_in_a + x + + (x + 4 < output_buf_rect.xsize() ? 4 : 0)) + : vdupq_n_f32(1.0f); + int16x4_t a16_left = vqmovn_s32(vcvtq_n_s32_f32(a_f32_left, 8)); + int16x4_t a16_right = vqmovn_s32(vcvtq_n_s32_f32(a_f32_right, 8)); + uint8x8_t a8 = vqmovun_s16(vcombine_s16(a16_left, a16_right)); + uint8_t* buf = output_buf + base_ptr + 4 * x; + uint8x8x4_t data = {r8, g8, b8, a8}; + if (n >= 8) { + vst4_u8(buf, data); + } else { + uint8_t tmp[8 * 4]; + vst4_u8(tmp, data); + memcpy(buf, tmp, n * 4); + } + } else { + uint8_t* buf = output_buf + base_ptr + 3 * x; + uint8x8x3_t data = {r8, g8, b8}; + if (n >= 8) { + vst3_u8(buf, data); + } else { + uint8_t tmp[8 * 3]; + vst3_u8(tmp, data); + memcpy(buf, tmp, n * 3); + } + } + } + } +#else + (void)input; + (void)input_rect; + (void)output_buf_rect; + (void)output_buf; + (void)xsize; + JXL_ABORT("Unreachable"); +#endif +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_DEC_XYB_INL_H_ diff --git a/lib/jxl/dec_xyb.cc b/lib/jxl/dec_xyb.cc new file mode 100644 index 0000000..26e1003 --- /dev/null +++ b/lib/jxl/dec_xyb.cc @@ -0,0 +1,290 @@ +// 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/dec_xyb.h" + +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/dec_xyb.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_group_border.h" +#include "lib/jxl/dec_xyb-inl.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/sanitizers.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Broadcast; + +void OpsinToLinearInplace(Image3F* JXL_RESTRICT inout, ThreadPool* pool, + const OpsinParams& opsin_params) { + PROFILER_FUNC; + JXL_CHECK_IMAGE_INITIALIZED(*inout, Rect(*inout)); + + const size_t xsize = inout->xsize(); // not padded + RunOnPool( + pool, 0, inout->ysize(), ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + + // Faster than adding via ByteOffset at end of loop. + float* JXL_RESTRICT row0 = inout->PlaneRow(0, y); + float* JXL_RESTRICT row1 = inout->PlaneRow(1, y); + float* JXL_RESTRICT row2 = inout->PlaneRow(2, y); + + const HWY_FULL(float) d; + + for (size_t x = 0; x < xsize; x += Lanes(d)) { + const auto in_opsin_x = Load(d, row0 + x); + const auto in_opsin_y = Load(d, row1 + x); + const auto in_opsin_b = Load(d, row2 + x); + JXL_COMPILER_FENCE; + auto linear_r = Undefined(d); + auto linear_g = Undefined(d); + auto linear_b = Undefined(d); + XybToRgb(d, in_opsin_x, in_opsin_y, in_opsin_b, opsin_params, + &linear_r, &linear_g, &linear_b); + + Store(linear_r, d, row0 + x); + Store(linear_g, d, row1 + x); + Store(linear_b, d, row2 + x); + } + }, + "OpsinToLinear"); +} + +// Same, but not in-place. +void OpsinToLinear(const Image3F& opsin, const Rect& rect, ThreadPool* pool, + Image3F* JXL_RESTRICT linear, + const OpsinParams& opsin_params) { + PROFILER_FUNC; + + JXL_ASSERT(SameSize(rect, *linear)); + JXL_CHECK_IMAGE_INITIALIZED(opsin, rect); + + RunOnPool( + pool, 0, static_cast(rect.ysize()), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t y = static_cast(task); + + // Faster than adding via ByteOffset at end of loop. + const float* JXL_RESTRICT row_opsin_0 = rect.ConstPlaneRow(opsin, 0, y); + const float* JXL_RESTRICT row_opsin_1 = rect.ConstPlaneRow(opsin, 1, y); + const float* JXL_RESTRICT row_opsin_2 = rect.ConstPlaneRow(opsin, 2, y); + float* JXL_RESTRICT row_linear_0 = linear->PlaneRow(0, y); + float* JXL_RESTRICT row_linear_1 = linear->PlaneRow(1, y); + float* JXL_RESTRICT row_linear_2 = linear->PlaneRow(2, y); + + const HWY_FULL(float) d; + + for (size_t x = 0; x < rect.xsize(); x += Lanes(d)) { + const auto in_opsin_x = Load(d, row_opsin_0 + x); + const auto in_opsin_y = Load(d, row_opsin_1 + x); + const auto in_opsin_b = Load(d, row_opsin_2 + x); + JXL_COMPILER_FENCE; + auto linear_r = Undefined(d); + auto linear_g = Undefined(d); + auto linear_b = Undefined(d); + XybToRgb(d, in_opsin_x, in_opsin_y, in_opsin_b, opsin_params, + &linear_r, &linear_g, &linear_b); + + Store(linear_r, d, row_linear_0 + x); + Store(linear_g, d, row_linear_1 + x); + Store(linear_b, d, row_linear_2 + x); + } + }, + "OpsinToLinear(Rect)"); + JXL_CHECK_IMAGE_INITIALIZED(*linear, rect); +} + +// Transform YCbCr to RGB. +// Could be performed in-place (i.e. Y, Cb and Cr could alias R, B and B). +void YcbcrToRgb(const Image3F& ycbcr, Image3F* rgb, const Rect& rect) { + JXL_CHECK_IMAGE_INITIALIZED(ycbcr, rect); + const HWY_CAPPED(float, GroupBorderAssigner::kPaddingXRound) df; + const size_t S = Lanes(df); // Step. + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + if ((xsize == 0) || (ysize == 0)) return; + + // Full-range BT.601 as defined by JFIF Clause 7: + // https://www.itu.int/rec/T-REC-T.871-201105-I/en + const auto c128 = Set(df, 128.0f / 255); + const auto crcr = Set(df, 1.402f); + const auto cgcb = Set(df, -0.114f * 1.772f / 0.587f); + const auto cgcr = Set(df, -0.299f * 1.402f / 0.587f); + const auto cbcb = Set(df, 1.772f); + + for (size_t y = 0; y < ysize; y++) { + const float* y_row = rect.ConstPlaneRow(ycbcr, 1, y); + const float* cb_row = rect.ConstPlaneRow(ycbcr, 0, y); + const float* cr_row = rect.ConstPlaneRow(ycbcr, 2, y); + float* r_row = rect.PlaneRow(rgb, 0, y); + float* g_row = rect.PlaneRow(rgb, 1, y); + float* b_row = rect.PlaneRow(rgb, 2, y); + for (size_t x = 0; x < xsize; x += S) { + const auto y_vec = Load(df, y_row + x) + c128; + const auto cb_vec = Load(df, cb_row + x); + const auto cr_vec = Load(df, cr_row + x); + const auto r_vec = crcr * cr_vec + y_vec; + const auto g_vec = cgcr * cr_vec + cgcb * cb_vec + y_vec; + const auto b_vec = cbcb * cb_vec + y_vec; + Store(r_vec, df, r_row + x); + Store(g_vec, df, g_row + x); + Store(b_vec, df, b_row + x); + } + } + JXL_CHECK_IMAGE_INITIALIZED(*rgb, rect); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(OpsinToLinearInplace); +void OpsinToLinearInplace(Image3F* JXL_RESTRICT inout, ThreadPool* pool, + const OpsinParams& opsin_params) { + return HWY_DYNAMIC_DISPATCH(OpsinToLinearInplace)(inout, pool, opsin_params); +} + +HWY_EXPORT(OpsinToLinear); +void OpsinToLinear(const Image3F& opsin, const Rect& rect, ThreadPool* pool, + Image3F* JXL_RESTRICT linear, + const OpsinParams& opsin_params) { + return HWY_DYNAMIC_DISPATCH(OpsinToLinear)(opsin, rect, pool, linear, + opsin_params); +} + +HWY_EXPORT(YcbcrToRgb); +void YcbcrToRgb(const Image3F& ycbcr, Image3F* rgb, const Rect& rect) { + return HWY_DYNAMIC_DISPATCH(YcbcrToRgb)(ycbcr, rgb, rect); +} + +HWY_EXPORT(HasFastXYBTosRGB8); +bool HasFastXYBTosRGB8() { return HWY_DYNAMIC_DISPATCH(HasFastXYBTosRGB8)(); } + +HWY_EXPORT(FastXYBTosRGB8); +void FastXYBTosRGB8(const Image3F& input, const Rect& input_rect, + const Rect& output_buf_rect, const ImageF* alpha, + const Rect& alpha_rect, bool is_rgba, + uint8_t* JXL_RESTRICT output_buf, size_t xsize, + size_t output_stride) { + return HWY_DYNAMIC_DISPATCH(FastXYBTosRGB8)( + input, input_rect, output_buf_rect, alpha, alpha_rect, is_rgba, + output_buf, xsize, output_stride); +} + +void OpsinParams::Init(float intensity_target) { + InitSIMDInverseMatrix(GetOpsinAbsorbanceInverseMatrix(), inverse_opsin_matrix, + intensity_target); + memcpy(opsin_biases, kNegOpsinAbsorbanceBiasRGB, + sizeof(kNegOpsinAbsorbanceBiasRGB)); + memcpy(quant_biases, kDefaultQuantBias, sizeof(kDefaultQuantBias)); + for (size_t c = 0; c < 4; c++) { + opsin_biases_cbrt[c] = cbrtf(opsin_biases[c]); + } +} + +Status OutputEncodingInfo::Set(const CodecMetadata& metadata, + const ColorEncoding& default_enc) { + const auto& im = metadata.transform_data.opsin_inverse_matrix; + float inverse_matrix[9]; + memcpy(inverse_matrix, im.inverse_matrix, sizeof(inverse_matrix)); + float intensity_target = metadata.m.IntensityTarget(); + if (metadata.m.xyb_encoded) { + const auto& orig_color_encoding = metadata.m.color_encoding; + color_encoding = default_enc; + // Figure out if we can output to this color encoding. + do { + if (!orig_color_encoding.HaveFields()) break; + // TODO(veluca): keep in sync with dec_reconstruct.cc + if (!orig_color_encoding.tf.IsPQ() && !orig_color_encoding.tf.IsSRGB() && + !orig_color_encoding.tf.IsGamma() && + !orig_color_encoding.tf.IsLinear() && + !orig_color_encoding.tf.IsHLG() && !orig_color_encoding.tf.IsDCI() && + !orig_color_encoding.tf.Is709()) { + break; + } + if (orig_color_encoding.tf.IsGamma()) { + inverse_gamma = orig_color_encoding.tf.GetGamma(); + } + if (orig_color_encoding.tf.IsDCI()) { + inverse_gamma = 1.0f / 2.6f; + } + if (orig_color_encoding.IsGray() && + orig_color_encoding.white_point != WhitePoint::kD65) { + // TODO(veluca): figure out what should happen here. + break; + } + + if ((orig_color_encoding.primaries != Primaries::kSRGB || + orig_color_encoding.white_point != WhitePoint::kD65) && + !orig_color_encoding.IsGray()) { + all_default_opsin = false; + float srgb_to_xyzd50[9]; + const auto& srgb = ColorEncoding::SRGB(/*is_gray=*/false); + JXL_CHECK(PrimariesToXYZD50( + srgb.GetPrimaries().r.x, srgb.GetPrimaries().r.y, + srgb.GetPrimaries().g.x, srgb.GetPrimaries().g.y, + srgb.GetPrimaries().b.x, srgb.GetPrimaries().b.y, + srgb.GetWhitePoint().x, srgb.GetWhitePoint().y, srgb_to_xyzd50)); + float xyzd50_to_original[9]; + JXL_RETURN_IF_ERROR(PrimariesToXYZD50( + orig_color_encoding.GetPrimaries().r.x, + orig_color_encoding.GetPrimaries().r.y, + orig_color_encoding.GetPrimaries().g.x, + orig_color_encoding.GetPrimaries().g.y, + orig_color_encoding.GetPrimaries().b.x, + orig_color_encoding.GetPrimaries().b.y, + orig_color_encoding.GetWhitePoint().x, + orig_color_encoding.GetWhitePoint().y, xyzd50_to_original)); + JXL_RETURN_IF_ERROR(Inv3x3Matrix(xyzd50_to_original)); + float srgb_to_original[9]; + MatMul(xyzd50_to_original, srgb_to_xyzd50, 3, 3, 3, srgb_to_original); + MatMul(srgb_to_original, im.inverse_matrix, 3, 3, 3, inverse_matrix); + } + color_encoding = orig_color_encoding; + color_encoding_is_original = true; + if (color_encoding.tf.IsPQ()) { + intensity_target = 10000; + } + } while (false); + } else { + color_encoding = metadata.m.color_encoding; + } + if (std::abs(intensity_target - 255.0) > 0.1f || !im.all_default) { + all_default_opsin = false; + } + InitSIMDInverseMatrix(inverse_matrix, opsin_params.inverse_opsin_matrix, + intensity_target); + std::copy(std::begin(im.opsin_biases), std::end(im.opsin_biases), + opsin_params.opsin_biases); + for (int i = 0; i < 3; ++i) { + opsin_params.opsin_biases_cbrt[i] = cbrtf(opsin_params.opsin_biases[i]); + } + opsin_params.opsin_biases_cbrt[3] = opsin_params.opsin_biases[3] = 1; + std::copy(std::begin(im.quant_biases), std::end(im.quant_biases), + opsin_params.quant_biases); + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/dec_xyb.h b/lib/jxl/dec_xyb.h new file mode 100644 index 0000000..affdef1 --- /dev/null +++ b/lib/jxl/dec_xyb.h @@ -0,0 +1,71 @@ +// 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_DEC_XYB_H_ +#define LIB_JXL_DEC_XYB_H_ + +// XYB -> linear sRGB. + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_metadata.h" +#include "lib/jxl/opsin_params.h" + +namespace jxl { + +// Parameters for XYB->sRGB conversion. +struct OpsinParams { + float inverse_opsin_matrix[9 * 4]; + float opsin_biases[4]; + float opsin_biases_cbrt[4]; + float quant_biases[4]; + void Init(float intensity_target); +}; + +struct OutputEncodingInfo { + ColorEncoding color_encoding; + // Used for Gamma and DCI transfer functions. + float inverse_gamma; + // Contains an opsin matrix that converts to the primaries of the output + // encoding. + OpsinParams opsin_params; + // default_enc is used for xyb encoded image with ICC profile, in other + // cases it has no effect. Use linear sRGB or grayscale if ICC profile is + // not matched (not parsed or no matching ColorEncoding exists) + Status Set(const CodecMetadata& metadata, const ColorEncoding& default_enc); + bool all_default_opsin = true; + bool color_encoding_is_original = false; +}; + +// Converts `inout` (not padded) from opsin to linear sRGB in-place. Called from +// per-pass postprocessing, hence parallelized. +void OpsinToLinearInplace(Image3F* JXL_RESTRICT inout, ThreadPool* pool, + const OpsinParams& opsin_params); + +// Converts `opsin:rect` (opsin may be padded, rect.x0 must be vector-aligned) +// to linear sRGB. Called from whole-frame encoder, hence parallelized. +void OpsinToLinear(const Image3F& opsin, const Rect& rect, ThreadPool* pool, + Image3F* JXL_RESTRICT linear, + const OpsinParams& opsin_params); + +// Bt.601 to match JPEG/JFIF. Inputs are _signed_ YCbCr values suitable for DCT, +// see F.1.1.3 of T.81 (because our data type is float, there is no need to add +// a bias to make the values unsigned). +void YcbcrToRgb(const Image3F& ycbcr, Image3F* rgb, const Rect& rect); + +bool HasFastXYBTosRGB8(); +void FastXYBTosRGB8(const Image3F& input, const Rect& input_rect, + const Rect& output_buf_rect, const ImageF* alpha, + const Rect& alpha_rect, bool is_rgba, + uint8_t* JXL_RESTRICT output_buf, size_t xsize, + size_t output_stride); + +} // namespace jxl + +#endif // LIB_JXL_DEC_XYB_H_ diff --git a/lib/jxl/decode.cc b/lib/jxl/decode.cc new file mode 100644 index 0000000..ef8adf0 --- /dev/null +++ b/lib/jxl/decode.cc @@ -0,0 +1,2325 @@ +// 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 "jxl/decode.h" + +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/dec_frame.h" +#include "lib/jxl/dec_modular.h" +#include "lib/jxl/dec_reconstruct.h" +#include "lib/jxl/decode_to_jpeg.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/icc_codec.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/memory_manager_internal.h" +#include "lib/jxl/toc.h" + +namespace { + +// If set (by fuzzer) then some operations will fail, if those would require +// allocating large objects. Actual memory usage might be two orders of +// magnitude bigger. +// TODO(eustas): this is a poor-mans replacement for memory-manager approach; +// remove, once memory-manager actually works. +size_t memory_limit_base_ = 0; +size_t cpu_limit_base_ = 0; +size_t used_cpu_base_ = 0; + +bool CheckSizeLimit(size_t xsize, size_t ysize) { + if (!memory_limit_base_) return true; + if (xsize == 0 || ysize == 0) return true; + size_t num_pixels = xsize * ysize; + if (num_pixels / xsize != ysize) return false; // overflow + if (num_pixels > memory_limit_base_) return false; + return true; +} + +// Checks if a + b > size, taking possible integer overflow into account. +bool OutOfBounds(size_t a, size_t b, size_t size) { + size_t pos = a + b; + if (pos > size) return true; + if (pos < a) return true; // overflow happened + return false; +} + +// Checks if a + b + c > size, taking possible integer overflow into account. +bool OutOfBounds(size_t a, size_t b, size_t c, size_t size) { + size_t pos = a + b; + if (pos < b) return true; // overflow happened + pos += c; + if (pos < c) return true; // overflow happened + if (pos > size) return true; + return false; +} + +bool SumOverflows(size_t a, size_t b, size_t c) { + size_t sum = a + b; + if (sum < b) return true; + sum += c; + if (sum < c) return true; + return false; +} + +JXL_INLINE size_t InitialBasicInfoSizeHint() { + // Amount of bytes before the start of the codestream in the container format, + // assuming that the codestream is the first box after the signature and + // filetype boxes. 12 bytes signature box + 20 bytes filetype box + 16 bytes + // codestream box length + name + optional XLBox length. + const size_t container_header_size = 48; + + // Worst-case amount of bytes for basic info of the JPEG XL codestream header, + // that is all information up to and including extra_channel_bits. Up to + // around 2 bytes signature + 8 bytes SizeHeader + 31 bytes ColorEncoding + 4 + // bytes rest of ImageMetadata + 5 bytes part of ImageMetadata2. + // TODO(lode): recompute and update this value when alpha_bits is moved to + // extra channels info. + const size_t max_codestream_basic_info_size = 50; + + return container_header_size + max_codestream_basic_info_size; +} + +// Debug-printing failure macro similar to JXL_FAILURE, but for the status code +// JXL_DEC_ERROR +#ifdef JXL_CRASH_ON_ERROR +#define JXL_API_ERROR(format, ...) \ + (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ + ::jxl::Abort(), JXL_DEC_ERROR) +#else // JXL_CRASH_ON_ERROR +#define JXL_API_ERROR(format, ...) \ + (((JXL_DEBUG_ON_ERROR) && \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__)), \ + JXL_DEC_ERROR) +#endif // JXL_CRASH_ON_ERROR + +JxlDecoderStatus ConvertStatus(JxlDecoderStatus status) { return status; } + +JxlDecoderStatus ConvertStatus(jxl::Status status) { + return status ? JXL_DEC_SUCCESS : JXL_DEC_ERROR; +} + +JxlSignature ReadSignature(const uint8_t* buf, size_t len, size_t* pos) { + if (*pos >= len) return JXL_SIG_NOT_ENOUGH_BYTES; + + buf += *pos; + len -= *pos; + + // JPEG XL codestream: 0xff 0x0a + if (len >= 1 && buf[0] == 0xff) { + if (len < 2) { + return JXL_SIG_NOT_ENOUGH_BYTES; + } else if (buf[1] == jxl::kCodestreamMarker) { + *pos += 2; + return JXL_SIG_CODESTREAM; + } else { + return JXL_SIG_INVALID; + } + } + + // JPEG XL container + if (len >= 1 && buf[0] == 0) { + if (len < 12) { + return JXL_SIG_NOT_ENOUGH_BYTES; + } else if (buf[1] == 0 && buf[2] == 0 && buf[3] == 0xC && buf[4] == 'J' && + buf[5] == 'X' && buf[6] == 'L' && buf[7] == ' ' && + buf[8] == 0xD && buf[9] == 0xA && buf[10] == 0x87 && + buf[11] == 0xA) { + *pos += 12; + return JXL_SIG_CONTAINER; + } else { + return JXL_SIG_INVALID; + } + } + + return JXL_SIG_INVALID; +} + +} // namespace + +uint32_t JxlDecoderVersion(void) { + return JPEGXL_MAJOR_VERSION * 1000000 + JPEGXL_MINOR_VERSION * 1000 + + JPEGXL_PATCH_VERSION; +} + +JxlSignature JxlSignatureCheck(const uint8_t* buf, size_t len) { + size_t pos = 0; + return ReadSignature(buf, len, &pos); +} + +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. + } + return 0; // Indicate invalid data type. +} + +enum class DecoderStage : uint32_t { + kInited, // Decoder created, no JxlDecoderProcessInput called yet + kStarted, // Running JxlDecoderProcessInput calls + kFinished, // Everything done, nothing left to process + kError, // Error occurred, decoder object no longer usable +}; + +enum class FrameStage : uint32_t { + kHeader, // Must parse frame header. dec->frame_start must be set up + // correctly already. + kTOC, // Must parse TOC + kFull, // Must parse full pixels + kFullOutput, // Must output full pixels +}; + +// Manages the sections for the FrameDecoder based on input bytes received. +struct Sections { + // sections_begin = position in the frame where the sections begin, after + // the frame header and TOC, so sections_begin = sum of frame header size and + // TOC size. + Sections(jxl::FrameDecoder* frame_dec, size_t frame_size, + size_t sections_begin) + : frame_dec_(frame_dec), + frame_size_(frame_size), + sections_begin_(sections_begin) {} + + Sections(const Sections&) = delete; + Sections& operator=(const Sections&) = delete; + Sections(Sections&&) = delete; + Sections& operator=(Sections&&) = delete; + + ~Sections() { + // Avoid memory leaks if the JXL decoder quits early and doesn't end up + // calling CloseInput(). + CloseInput(); + } + + // frame_dec_ must have been Inited already, but not yet done ProcessSections. + JxlDecoderStatus Init() { + section_received.resize(frame_dec_->NumSections(), 0); + + const auto& offsets = frame_dec_->SectionOffsets(); + const auto& sizes = frame_dec_->SectionSizes(); + + // Ensure none of the sums of section offset and size overflow. + for (size_t i = 0; i < frame_dec_->NumSections(); i++) { + if (OutOfBounds(sections_begin_, offsets[i], sizes[i], frame_size_)) { + return JXL_API_ERROR("section out of bounds"); + } + } + + return JXL_DEC_SUCCESS; + } + + // Sets the input data for the frame. The frame pointer must point to the + // beginning of the frame, size is the amount of bytes gotten so far and + // should increase with next calls until the full frame is loaded. + // TODO(lode): allow caller to provide only later chunks of memory when + // earlier sections are fully processed already. + void SetInput(const uint8_t* frame, size_t size) { + const auto& offsets = frame_dec_->SectionOffsets(); + const auto& sizes = frame_dec_->SectionSizes(); + + for (size_t i = 0; i < frame_dec_->NumSections(); i++) { + if (section_received[i]) continue; + if (!OutOfBounds(sections_begin_, offsets[i], sizes[i], size)) { + section_received[i] = 1; + section_info.emplace_back(jxl::FrameDecoder::SectionInfo{nullptr, i}); + section_status.emplace_back(); + } + } + // Reset all the bitreaders, because the address of the frame pointer may + // change, even if it always represents the same frame start. + for (size_t i = 0; i < section_info.size(); i++) { + size_t id = section_info[i].id; + JXL_ASSERT(section_info[i].br == nullptr); + section_info[i].br = new jxl::BitReader(jxl::Span( + frame + sections_begin_ + offsets[id], sizes[id])); + } + } + + JxlDecoderStatus CloseInput() { + bool out_of_bounds = false; + for (size_t i = 0; i < section_info.size(); i++) { + if (!section_info[i].br) continue; + if (!section_info[i].br->AllReadsWithinBounds()) { + // Mark out of bounds section, but keep closing and deleting the next + // ones as well. + out_of_bounds = true; + } + JXL_ASSERT(section_info[i].br->Close()); + delete section_info[i].br; + section_info[i].br = nullptr; + } + if (out_of_bounds) { + // If any bit reader indicates out of bounds, it's an error, not just + // needing more input, since we ensure only bit readers containing + // a complete section are provided to the FrameDecoder. + return JXL_API_ERROR("frame out of bounds"); + } + return JXL_DEC_SUCCESS; + } + + // Not managed by us. + jxl::FrameDecoder* frame_dec_; + + size_t frame_size_; + size_t sections_begin_; + + std::vector section_info; + std::vector section_status; + std::vector section_received; +}; + +/* +Given list of frame references to storage slots, and storage slots in which this +frame is saved, computes which frames are required to decode the frame at the +given index and any frames after it. The frames on which this depends are +returned as a vector of their indices, in no particular order. The given index +must be smaller than saved_as.size(), and references.size() must equal +saved_as.size(). Any frames beyond saved_as and references are considered +unknown future frames and must be treated as if something depends on them. +*/ +std::vector GetFrameDependencies(size_t index, + const std::vector& saved_as, + const std::vector& references) { + JXL_ASSERT(references.size() == saved_as.size()); + JXL_ASSERT(index < references.size()); + + std::vector result; + + constexpr size_t kNumStorage = 8; + + // value which indicates nothing is stored in this storage slot + const size_t invalid = references.size(); + // for each of the 8 storage slots, a vector that translates frame index to + // frame stored in this storage slot at this point, that is, the last + // frame that was stored in this slot before or at this index. + std::array, kNumStorage> storage; + for (size_t s = 0; s < kNumStorage; ++s) { + storage[s].resize(saved_as.size()); + int mask = 1 << s; + size_t id = invalid; + for (size_t i = 0; i < saved_as.size(); ++i) { + if (saved_as[i] & mask) { + id = i; + } + storage[s][i] = id; + } + } + + std::vector seen(index + 1, 0); + std::vector stack; + stack.push_back(index); + seen[index] = 1; + + // For frames after index, assume they can depend on any of the 8 storage + // slots, so push the frame for each stored reference to the stack and result. + // All frames after index are treated as having unknown references and with + // the possibility that there are more frames after the last known. + // TODO(lode): take values of saved_as and references after index, and a + // input flag indicating if they are all frames of the image, to further + // optimize this. + for (size_t s = 0; s < kNumStorage; ++s) { + size_t frame_ref = storage[s][index]; + if (frame_ref == invalid) continue; + if (seen[frame_ref]) continue; + stack.push_back(frame_ref); + seen[frame_ref] = 1; + result.push_back(frame_ref); + } + + while (!stack.empty()) { + size_t frame_index = stack.back(); + stack.pop_back(); + if (frame_index == 0) continue; // first frame cannot have references + for (size_t s = 0; s < kNumStorage; ++s) { + int mask = 1 << s; + if (!(references[frame_index] & mask)) continue; + size_t frame_ref = storage[s][frame_index - 1]; + if (frame_ref == invalid) continue; + if (seen[frame_ref]) continue; + stack.push_back(frame_ref); + seen[frame_ref] = 1; + result.push_back(frame_ref); + } + } + + return result; +} + +// Parameters for user-requested extra channel output. +struct ExtraChannelOutput { + JxlPixelFormat format; + void* buffer; + size_t buffer_size; +}; + +} // namespace + +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +struct JxlDecoderStruct { + JxlDecoderStruct() = default; + + JxlMemoryManager memory_manager; + std::unique_ptr thread_pool; + + DecoderStage stage; + + // Status of progression, internal. + bool got_signature; + bool first_codestream_seen; + // Indicates we know that we've seen the last codestream, however this is not + // guaranteed to be true for the last box because a jxl file may have multiple + // "jxlp" boxes and it is possible (and permitted) that the last one is not a + // final box that uses size 0 to indicate the end. + bool last_codestream_seen; + bool got_basic_info; + size_t header_except_icc_bits = 0; // To skip everything before ICC. + bool got_all_headers; // Codestream metadata headers. + bool post_headers; // Already decoding pixels. + jxl::ICCReader icc_reader; + + // This means either we actually got the preview image, or determined we + // cannot get it or there is none. + bool got_preview_image; + + // Position of next_in in the original file including box format if present + // (as opposed to position in the codestream) + size_t file_pos; + size_t box_begin; + size_t box_end; + bool skip_box; + // Begin and end of the content of the current codestream box. This could be + // a partial codestream box. + // codestream_begin 0 is used to indicate the begin is not yet known. + // codestream_end 0 is used to indicate uncapped (until end of file, for the + // last box if this box doesn't indicate its actual size). + // Not used if the file is a direct codestream. + size_t codestream_begin; + size_t codestream_end; + + // Settings + bool keep_orientation; + + // Bitfield, for which informative events (JXL_DEC_BASIC_INFO, etc...) the + // decoder returns a status. By default, do not return for any of the events, + // only return when the decoder cannot continue because it needs more input or + // output data. + int events_wanted; + int orig_events_wanted; + + // Fields for reading the basic info from the header. + size_t basic_info_size_hint; + bool have_container; + + // Whether the preview out buffer was set. It is possible for the buffer to + // be nullptr and buffer_set to be true, indicating it was deliberately + // set to nullptr. + bool preview_out_buffer_set; + // Idem for the image buffer. + bool image_out_buffer_set; + + // 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; + + size_t preview_out_size; + size_t image_out_size; + + JxlPixelFormat preview_out_format; + JxlPixelFormat image_out_format; + + // For extra channels. Empty if no extra channels are requested, and they are + // reset each frame + std::vector extra_channel_output; + + jxl::CodecMetadata metadata; + std::unique_ptr ib; + // ColorEncoding to use for xyb encoded image with ICC profile. + jxl::ColorEncoding default_enc; + + std::unique_ptr passes_state; + std::unique_ptr frame_dec; + std::unique_ptr sections; + // The FrameDecoder is initialized, and not yet finalized + bool frame_dec_in_progress; + + // headers and TOC for the current frame. When got_toc is true, this is + // always the frame header of the last frame of the current still series, + // that is, the displayed frame. + std::unique_ptr frame_header; + + // Start of the current frame being processed, as offset from the beginning of + // the codestream. + size_t frame_start; + size_t frame_size; + FrameStage frame_stage; + // The currently processed frame is the last of the current composite still, + // and so must be returned as pixels + bool is_last_of_still; + // The currently processed frame is the last of the codestream + bool is_last_total; + // How many frames to skip. + size_t skip_frames; + // Skipping the current frame. May be false if skip_frames was just set to + // a positive value while already processing a current frame, then + // skipping_frame will be enabled only for the next frame. + bool skipping_frame; + + // Amount of internal frames and external frames started. External frames are + // user-visible frames, internal frames includes all external frames and + // also invisible frames such as patches, blending-only and dc_level frames. + size_t internal_frames; + size_t external_frames; + + // For each internal frame, which storage locations it references, and which + // storage locations it is stored in, using the bit mask as defined in + // FrameDecoder::References and FrameDecoder::SaveAs. + std::vector frame_references; + std::vector frame_saved_as; + + // Translates external frame index to internal frame index. The external + // index is the index of user-visible frames. The internal index can be larger + // since non-visible frames (such as frames with patches, ...) are included. + std::vector frame_external_to_internal; + + // Whether the frame with internal index is required to decode the frame + // being skipped to or any frames after that. If no skipping is active, + // this vector is ignored. If the current internal frame index is beyond this + // vector, it must be treated as a required frame. + std::vector frame_required; + + // Codestream input data is stored here, when the decoder takes in and stores + // the user input bytes. If the decoder does not do that (e.g. in one-shot + // case), this field is unused. + // TODO(lode): avoid needing this field once the C++ decoder doesn't need + // all bytes at once, to save memory. Find alternative to std::vector doubling + // strategy to prevent some memory usage. + std::vector codestream; + + jxl::JxlToJpegDecoder jpeg_decoder; + + // Position in the actual codestream, which codestream.begin() points to. + // Non-zero once earlier parts of the codestream vector have been erased. + size_t codestream_pos; + + // Statistics which CodecInOut can keep + uint64_t dec_pixels; + + const uint8_t* next_in; + size_t avail_in; +}; + +// TODO(zond): Make this depend on the data loaded into the decoder. +JxlDecoderStatus JxlDecoderDefaultPixelFormat(const JxlDecoder* dec, + JxlPixelFormat* format) { + if (!dec->got_basic_info) return JXL_DEC_NEED_MORE_INPUT; + *format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + return JXL_DEC_SUCCESS; +} + +void JxlDecoderReset(JxlDecoder* dec) { + dec->thread_pool.reset(); + dec->stage = DecoderStage::kInited; + dec->got_signature = false; + dec->first_codestream_seen = false; + dec->last_codestream_seen = false; + dec->got_basic_info = false; + dec->header_except_icc_bits = 0; + dec->got_all_headers = false; + dec->post_headers = false; + dec->icc_reader.Reset(); + dec->got_preview_image = false; + dec->file_pos = 0; + dec->box_begin = 0; + dec->box_end = 0; + dec->skip_box = false; + dec->codestream_pos = 0; + dec->codestream_begin = 0; + dec->codestream_end = 0; + dec->keep_orientation = false; + dec->events_wanted = 0; + dec->orig_events_wanted = 0; + dec->basic_info_size_hint = InitialBasicInfoSizeHint(); + dec->have_container = 0; + dec->preview_out_buffer_set = false; + 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->preview_out_size = 0; + dec->image_out_size = 0; + dec->extra_channel_output.clear(); + dec->dec_pixels = 0; + dec->next_in = 0; + dec->avail_in = 0; + + dec->passes_state.reset(nullptr); + dec->frame_dec.reset(nullptr); + dec->sections.reset(nullptr); + dec->frame_dec_in_progress = false; + + dec->ib.reset(); + dec->metadata = jxl::CodecMetadata(); + dec->frame_header.reset(new jxl::FrameHeader(&dec->metadata)); + dec->codestream.clear(); + + dec->frame_stage = FrameStage::kHeader; + dec->frame_start = 0; + dec->frame_size = 0; + dec->is_last_of_still = false; + dec->is_last_total = false; + dec->skip_frames = 0; + dec->skipping_frame = false; + dec->internal_frames = 0; + dec->external_frames = 0; + dec->frame_references.clear(); + dec->frame_saved_as.clear(); + dec->frame_external_to_internal.clear(); + dec->frame_required.clear(); +} + +JxlDecoder* JxlDecoderCreate(const JxlMemoryManager* memory_manager) { + JxlMemoryManager local_memory_manager; + if (!jxl::MemoryManagerInit(&local_memory_manager, memory_manager)) + return nullptr; + + void* alloc = + jxl::MemoryManagerAlloc(&local_memory_manager, sizeof(JxlDecoder)); + if (!alloc) return nullptr; + // Placement new constructor on allocated memory + JxlDecoder* dec = new (alloc) JxlDecoder(); + dec->memory_manager = local_memory_manager; + + JxlDecoderReset(dec); + + return dec; +} + +void JxlDecoderDestroy(JxlDecoder* dec) { + if (dec) { + // Call destructor directly since custom free function is used. + dec->~JxlDecoder(); + jxl::MemoryManagerFree(&dec->memory_manager, dec); + } +} + +void JxlDecoderRewind(JxlDecoder* dec) { + int keep_orientation = dec->keep_orientation; + int events_wanted = dec->orig_events_wanted; + std::vector frame_references; + std::vector frame_saved_as; + std::vector frame_external_to_internal; + std::vector frame_required; + frame_references.swap(dec->frame_references); + frame_saved_as.swap(dec->frame_saved_as); + frame_external_to_internal.swap(dec->frame_external_to_internal); + frame_required.swap(dec->frame_required); + + JxlDecoderReset(dec); + dec->keep_orientation = keep_orientation; + dec->events_wanted = events_wanted; + dec->orig_events_wanted = events_wanted; + frame_references.swap(dec->frame_references); + frame_saved_as.swap(dec->frame_saved_as); + frame_external_to_internal.swap(dec->frame_external_to_internal); + frame_required.swap(dec->frame_required); +} + +void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount) { + // Increment amount, rather than set it: making the amount smaller is + // impossible because the decoder may already have skipped frames required to + // decode earlier frames, and making the amount larger compared to an existing + // amount is impossible because if JxlDecoderSkipFrames is called in the + // middle of already skipping frames, the user cannot know how many frames + // have already been skipped internally so far so an absolute value cannot + // be defined. + dec->skip_frames += amount; + + dec->frame_required.clear(); + size_t next_frame = dec->external_frames + dec->skip_frames; + + // A frame that has been seen before a rewind + if (next_frame < dec->frame_external_to_internal.size()) { + size_t internal_index = dec->frame_external_to_internal[next_frame]; + if (internal_index < dec->frame_saved_as.size()) { + std::vector deps = GetFrameDependencies( + internal_index, dec->frame_saved_as, dec->frame_references); + + dec->frame_required.resize(internal_index + 1, 0); + for (size_t i = 0; i < deps.size(); i++) { + JXL_ASSERT(deps[i] < dec->frame_required.size()); + dec->frame_required[deps[i]] = 1; + } + } + } +} + +JXL_EXPORT JxlDecoderStatus +JxlDecoderSetParallelRunner(JxlDecoder* dec, JxlParallelRunner parallel_runner, + void* parallel_runner_opaque) { + if (dec->thread_pool) return JXL_API_ERROR("parallel runner already set"); + dec->thread_pool.reset( + new jxl::ThreadPool(parallel_runner, parallel_runner_opaque)); + return JXL_DEC_SUCCESS; +} + +size_t JxlDecoderSizeHintBasicInfo(const JxlDecoder* dec) { + if (dec->got_basic_info) return 0; + return dec->basic_info_size_hint; +} + +JxlDecoderStatus JxlDecoderSubscribeEvents(JxlDecoder* dec, int events_wanted) { + if (dec->stage != DecoderStage::kInited) { + return JXL_DEC_ERROR; // Cannot subscribe to events after having started. + } + if (events_wanted & 63) { + return JXL_DEC_ERROR; // Can only subscribe to informative events. + } + dec->events_wanted = events_wanted; + dec->orig_events_wanted = events_wanted; + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetKeepOrientation(JxlDecoder* dec, + JXL_BOOL keep_orientation) { + if (dec->stage != DecoderStage::kInited) { + return JXL_API_ERROR("Must set keep_orientation option before starting"); + } + dec->keep_orientation = !!keep_orientation; + return JXL_DEC_SUCCESS; +} + +namespace jxl { +namespace { + +template +bool CanRead(Span data, BitReader* reader, T* JXL_RESTRICT t) { + // Use a copy of the bit reader because CanRead advances bits. + BitReader reader2(data); + reader2.SkipBits(reader->TotalBitsConsumed()); + bool result = Bundle::CanRead(&reader2, t); + JXL_ASSERT(reader2.Close()); + return result; +} + +// Returns JXL_DEC_SUCCESS if the full bundle was successfully read, status +// indicating either error or need more input otherwise. +template +JxlDecoderStatus ReadBundle(Span data, BitReader* reader, + T* JXL_RESTRICT t) { + if (!CanRead(data, reader, t)) { + return JXL_DEC_NEED_MORE_INPUT; + } + if (!Bundle::Read(reader, t)) { + return JXL_DEC_ERROR; + } + return JXL_DEC_SUCCESS; +} + +#define JXL_API_RETURN_IF_ERROR(expr) \ + { \ + JxlDecoderStatus status_ = ConvertStatus(expr); \ + if (status_ != JXL_DEC_SUCCESS) return status_; \ + } + +std::unique_ptr> GetBitReader( + Span span) { + BitReader* reader = new BitReader(span); + return std::unique_ptr>( + reader, [](BitReader* reader) { + // We can't allow Close to abort the program if the reader is out of + // bounds, or all return paths in the code, even those that already + // return failure, would have to manually call AllReadsWithinBounds(). + // Invalid JXL codestream should not cause program to quit. + (void)reader->AllReadsWithinBounds(); + (void)reader->Close(); + delete reader; + }); +} + +JxlDecoderStatus JxlDecoderReadBasicInfo(JxlDecoder* dec, const uint8_t* in, + size_t size) { + size_t pos = 0; + + // Check and skip the codestream signature + JxlSignature signature = ReadSignature(in, size, &pos); + if (signature == JXL_SIG_NOT_ENOUGH_BYTES) { + return JXL_DEC_NEED_MORE_INPUT; + } + if (signature == JXL_SIG_CONTAINER) { + // There is a container signature where we expect a codestream, container + // is handled at a higher level already. + return JXL_API_ERROR("invalid: nested container"); + } + if (signature != JXL_SIG_CODESTREAM) { + return JXL_API_ERROR("invalid signature"); + } + + Span span(in + pos, size - pos); + auto reader = GetBitReader(span); + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dec->metadata.size)); + + dec->metadata.m.nonserialized_only_parse_basic_info = true; + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dec->metadata.m)); + dec->metadata.m.nonserialized_only_parse_basic_info = false; + dec->got_basic_info = true; + dec->basic_info_size_hint = 0; + + if (!CheckSizeLimit(dec->metadata.size.xsize(), dec->metadata.size.ysize())) { + return JXL_API_ERROR("image is too large"); + } + + return JXL_DEC_SUCCESS; +} + +// Reads all codestream headers (but not frame headers) +JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec, const uint8_t* in, + size_t size) { + size_t pos = 0; + + // Check and skip the codestream signature + JxlSignature signature = ReadSignature(in, size, &pos); + if (signature == JXL_SIG_CONTAINER) { + return JXL_API_ERROR("invalid: nested container"); + } + if (signature != JXL_SIG_CODESTREAM) { + return JXL_API_ERROR("invalid signature"); + } + + Span span(in + pos, size - pos); + auto reader = GetBitReader(span); + + if (dec->header_except_icc_bits != 0) { + // Headers were decoded already. + reader->SkipBits(dec->header_except_icc_bits); + } else { + SizeHeader dummy_size_header; + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_size_header)); + + // We already decoded the metadata to dec->metadata.m, no reason to + // overwrite it, use a dummy metadata instead. + ImageMetadata dummy_metadata; + JXL_API_RETURN_IF_ERROR(ReadBundle(span, reader.get(), &dummy_metadata)); + + JXL_API_RETURN_IF_ERROR( + ReadBundle(span, reader.get(), &dec->metadata.transform_data)); + } + + dec->header_except_icc_bits = reader->TotalBitsConsumed(); + + if (dec->metadata.m.color_encoding.WantICC()) { + jxl::Status status = dec->icc_reader.Init(reader.get(), memory_limit_base_); + // Always check AllReadsWithinBounds, not all the C++ decoder implementation + // handles reader out of bounds correctly yet (e.g. context map). Not + // checking AllReadsWithinBounds can cause reader->Close() to trigger an + // assert, but we don't want library to quit program for invalid codestream. + if (!reader->AllReadsWithinBounds()) { + return JXL_DEC_NEED_MORE_INPUT; + } + if (!status) { + if (status.code() == StatusCode::kNotEnoughBytes) { + return JXL_DEC_NEED_MORE_INPUT; + } + // Other non-successful status is an error + return JXL_DEC_ERROR; + } + PaddedBytes icc; + status = dec->icc_reader.Process(reader.get(), &icc); + if (!status) { + if (status.code() == StatusCode::kNotEnoughBytes) { + return JXL_DEC_NEED_MORE_INPUT; + } + // Other non-successful status is an error + return JXL_DEC_ERROR; + } + if (!dec->metadata.m.color_encoding.SetICCRaw(std::move(icc))) { + return JXL_DEC_ERROR; + } + } + + dec->got_all_headers = true; + JXL_API_RETURN_IF_ERROR(reader->JumpToByteBoundary()); + + dec->frame_start = pos + reader->TotalBitsConsumed() / jxl::kBitsPerByte; + + if (!dec->passes_state) { + dec->passes_state.reset(new jxl::PassesDecoderState()); + } + + dec->default_enc = + ColorEncoding::LinearSRGB(dec->metadata.m.color_encoding.IsGray()); + + JXL_API_RETURN_IF_ERROR(dec->passes_state->output_encoding_info.Set( + dec->metadata, dec->default_enc)); + + return JXL_DEC_SUCCESS; +} + +static size_t GetStride(const JxlDecoder* dec, const JxlPixelFormat& format, + const jxl::ImageBundle* frame = nullptr) { + size_t xsize = dec->metadata.xsize(); + if (!dec->keep_orientation && dec->metadata.m.orientation > 4) { + xsize = dec->metadata.ysize(); + } + if (frame) { + xsize = dec->keep_orientation ? frame->xsize() : frame->oriented_xsize(); + } + size_t stride = xsize * (BitsPerChannel(format.data_type) * + format.num_channels / jxl::kBitsPerByte); + if (format.align > 1) { + stride = jxl::DivCeil(stride, format.align) * format.align; + } + return stride; +} + +// Internal wrapper around jxl::ConvertToExternal which converts the stride, +// format and orientation and allows to choose whether to get all RGB(A) +// channels or alternatively get a single extra channel. +// If want_extra_channel, a valid index to a single extra channel must be +// given, the output must be single-channel, and format.num_channels is ignored +// and treated as if it is 1. +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) { + // TODO(lode): handle mismatch of RGB/grayscale color profiles and pixel data + // color/grayscale format + const size_t stride = GetStride(dec, format, &frame); + + bool float_format = format.data_type == JXL_TYPE_FLOAT || + format.data_type == JXL_TYPE_FLOAT16; + + jxl::Orientation undo_orientation = dec->keep_orientation + ? jxl::Orientation::kIdentity + : dec->metadata.m.GetOrientation(); + + 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); + } 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); + } + + return status ? JXL_DEC_SUCCESS : JXL_DEC_ERROR; +} + +// Parses the FrameHeader and the total frame_size, given the initial bytes +// of the frame up to and including the TOC. +// TODO(lode): merge this with FrameDecoder +JxlDecoderStatus ParseFrameHeader(jxl::FrameHeader* frame_header, + const uint8_t* in, size_t size, size_t pos, + bool is_preview, size_t* frame_size, + int* saved_as) { + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } + Span span(in + pos, size - pos); + auto reader = GetBitReader(span); + + frame_header->nonserialized_is_preview = is_preview; + jxl::Status status = DecodeFrameHeader(reader.get(), frame_header); + jxl::FrameDimensions frame_dim = frame_header->ToFrameDimensions(); + if (!CheckSizeLimit(frame_dim.xsize_upsampled_padded, + frame_dim.ysize_upsampled_padded)) { + return JXL_API_ERROR("frame is too large"); + } + + if (status.code() == StatusCode::kNotEnoughBytes) { + // TODO(lode): prevent asking for way too much input bytes in case of + // invalid header that the decoder thinks is a very long user extension + // instead. Example: fields can currently print something like this: + // "../lib/jxl/fields.cc:416: Skipping 71467322-bit extension(s)" + // Maybe fields.cc should return error in the above case rather than + // print a message. + return JXL_DEC_NEED_MORE_INPUT; + } else if (!status) { + return JXL_API_ERROR("invalid frame header"); + } + + // Read TOC. + uint64_t groups_total_size; + const bool has_ac_global = true; + const size_t toc_entries = + NumTocEntries(frame_dim.num_groups, frame_dim.num_dc_groups, + frame_header->passes.num_passes, has_ac_global); + + std::vector group_offsets; + std::vector group_sizes; + status = ReadGroupOffsets(toc_entries, reader.get(), &group_offsets, + &group_sizes, &groups_total_size); + + // TODO(lode): we're actually relying on AllReadsWithinBounds() here + // instead of on status.code(), change the internal TOC C++ code to + // correctly set the status.code() instead so we can rely on that one. + if (!reader->AllReadsWithinBounds() || + status.code() == StatusCode::kNotEnoughBytes) { + return JXL_DEC_NEED_MORE_INPUT; + } else if (!status) { + return JXL_API_ERROR("invalid toc entries"); + } + + JXL_DASSERT((reader->TotalBitsConsumed() % kBitsPerByte) == 0); + JXL_API_RETURN_IF_ERROR(reader->JumpToByteBoundary()); + size_t header_size = (reader->TotalBitsConsumed() >> 3); + *frame_size = header_size + groups_total_size; + + if (saved_as != nullptr) { + *saved_as = FrameDecoder::SavedAs(*frame_header); + } + + return JXL_DEC_SUCCESS; +} + +// TODO(eustas): no CodecInOut -> no image size reinforcement -> possible OOM. +JxlDecoderStatus JxlDecoderProcessInternal(JxlDecoder* dec, const uint8_t* in, + size_t size) { + // If no parallel runner is set, use the default + // TODO(lode): move this initialization to an appropriate location once the + // runner is used to decode pixels. + if (!dec->thread_pool) { + dec->thread_pool.reset(new jxl::ThreadPool(nullptr, nullptr)); + } + + // No matter what events are wanted, the basic info is always required. + if (!dec->got_basic_info) { + JxlDecoderStatus status = JxlDecoderReadBasicInfo(dec, in, size); + if (status != JXL_DEC_SUCCESS) return status; + } + + if (dec->events_wanted & JXL_DEC_BASIC_INFO) { + dec->events_wanted &= ~JXL_DEC_BASIC_INFO; + return JXL_DEC_BASIC_INFO; + } + + if (!dec->got_all_headers) { + JxlDecoderStatus status = JxlDecoderReadAllHeaders(dec, in, size); + if (status != JXL_DEC_SUCCESS) return status; + } + + if (dec->events_wanted & JXL_DEC_EXTENSIONS) { + dec->events_wanted &= ~JXL_DEC_EXTENSIONS; + if (dec->metadata.m.extensions != 0) { + return JXL_DEC_EXTENSIONS; + } + } + + if (dec->events_wanted & JXL_DEC_COLOR_ENCODING) { + dec->events_wanted &= ~JXL_DEC_COLOR_ENCODING; + return JXL_DEC_COLOR_ENCODING; + } + + dec->post_headers = true; + + // Decode to pixels, only if required for the events the user wants. + if (!dec->got_preview_image) { + // Parse the preview, or at least its TOC to be able to skip the frame, if + // any frame or image decoding is desired. + bool parse_preview = + (dec->events_wanted & + (JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + + if (!dec->metadata.m.have_preview) { + // There is no preview, mark this as done and go to next step + dec->got_preview_image = true; + } else if (!parse_preview) { + // No preview parsing needed, mark this step as done + dec->got_preview_image = true; + } else { + // Want to decode the preview, not just skip the frame + bool want_preview = (dec->events_wanted & JXL_DEC_PREVIEW_IMAGE); + size_t frame_size; + size_t pos = dec->frame_start; + dec->frame_header.reset(new FrameHeader(&dec->metadata)); + JxlDecoderStatus status = ParseFrameHeader(dec->frame_header.get(), in, + size, pos, true, &frame_size, + /*saved_as=*/nullptr); + if (status != JXL_DEC_SUCCESS) return status; + if (OutOfBounds(pos, frame_size, size)) { + return JXL_DEC_NEED_MORE_INPUT; + } + + if (want_preview && !dec->preview_out_buffer_set) { + return JXL_DEC_NEED_PREVIEW_OUT_BUFFER; + } + + jxl::Span compressed(in + dec->frame_start, + size - dec->frame_start); + auto reader = GetBitReader(compressed); + jxl::DecompressParams dparams; + dparams.preview = want_preview ? jxl::Override::kOn : jxl::Override::kOff; + jxl::ImageBundle ib(&dec->metadata.m); + PassesDecoderState preview_dec_state; + JXL_API_RETURN_IF_ERROR(preview_dec_state.output_encoding_info.Set( + dec->metadata, + ColorEncoding::LinearSRGB(dec->metadata.m.color_encoding.IsGray()))); + if (!DecodeFrame(dparams, &preview_dec_state, dec->thread_pool.get(), + reader.get(), &ib, dec->metadata, + /*constraints=*/nullptr, + /*is_preview=*/true)) { + return JXL_API_ERROR("decoding preview failed"); + } + + // Set frame_start to the first non-preview frame. + dec->frame_start += DivCeil(reader->TotalBitsConsumed(), kBitsPerByte); + dec->got_preview_image = true; + + if (want_preview) { + if (dec->preview_out_buffer) { + 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); + if (status != JXL_DEC_SUCCESS) return status; + } + return JXL_DEC_PREVIEW_IMAGE; + } + } + } + + // Handle frames + for (;;) { + if (!(dec->events_wanted & (JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME))) { + break; + } + if (dec->frame_stage == FrameStage::kHeader && dec->is_last_total) { + break; + } + + if (dec->frame_stage == FrameStage::kHeader) { + size_t pos = dec->frame_start - dec->codestream_pos; + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } + dec->frame_header.reset(new FrameHeader(&dec->metadata)); + int saved_as = 0; + JxlDecoderStatus status = + ParseFrameHeader(dec->frame_header.get(), in, size, pos, + /*is_preview=*/false, &dec->frame_size, &saved_as); + if (status != JXL_DEC_SUCCESS) return status; + + // is last in entire codestream + dec->is_last_total = dec->frame_header->is_last; + // is last of current still + dec->is_last_of_still = + dec->is_last_total || dec->frame_header->animation_frame.duration > 0; + + const size_t internal_frame_index = dec->internal_frames; + const size_t external_frame_index = dec->external_frames; + if (dec->is_last_of_still) dec->external_frames++; + dec->internal_frames++; + + dec->frame_stage = FrameStage::kTOC; + + if (dec->skip_frames > 0) { + dec->skipping_frame = true; + if (dec->is_last_of_still) { + dec->skip_frames--; + } + } else { + dec->skipping_frame = false; + } + + if (external_frame_index >= dec->frame_external_to_internal.size()) { + dec->frame_external_to_internal.push_back(internal_frame_index); + JXL_ASSERT(dec->frame_external_to_internal.size() == + external_frame_index + 1); + } + + if (internal_frame_index >= dec->frame_saved_as.size()) { + dec->frame_saved_as.push_back(saved_as); + JXL_ASSERT(dec->frame_saved_as.size() == internal_frame_index + 1); + + // add the value 0xff (which means all references) to new slots: we only + // know the references of the frame at FinalizeFrame, and fill in the + // correct values there. As long as this information is not known, the + // worst case where the frame depends on all storage slots is assumed. + dec->frame_references.push_back(0xff); + JXL_ASSERT(dec->frame_references.size() == internal_frame_index + 1); + } + + if (dec->skipping_frame) { + // Whether this frame could be referenced by any future frame: either + // because it's a frame saved for blending or patches, or because it's + // a DC frame. + bool referenceable = + dec->frame_header->CanBeReferenced() || + dec->frame_header->frame_type == FrameType::kDCFrame; + if (internal_frame_index < dec->frame_required.size() && + !dec->frame_required[internal_frame_index]) { + referenceable = false; + } + if (!referenceable) { + // Skip all decoding for this frame, since the user is skipping this + // frame and no future frames can reference it. + dec->frame_stage = FrameStage::kHeader; + dec->frame_start += dec->frame_size; + continue; + } + } + + if ((dec->events_wanted & JXL_DEC_FRAME) && dec->is_last_of_still) { + // Only return this for the last of a series of stills: patches frames + // etc... before this one do not contain the correct information such + // as animation timing, ... + if (!dec->skipping_frame) { + return JXL_DEC_FRAME; + } + } + } + + if (dec->frame_stage == FrameStage::kTOC) { + size_t pos = dec->frame_start - dec->codestream_pos; + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } + Span span(in + pos, size - pos); + auto reader = GetBitReader(span); + + if (!dec->passes_state) { + dec->passes_state.reset(new jxl::PassesDecoderState()); + } + if (!dec->ib) { + dec->ib.reset(new jxl::ImageBundle(&dec->metadata.m)); + } + + dec->frame_dec.reset(new FrameDecoder( + dec->passes_state.get(), dec->metadata, dec->thread_pool.get())); + + // If JPEG reconstruction is wanted and possible, set the jpeg_data of + // the ImageBundle. + if (!dec->jpeg_decoder.SetImageBundleJpegData(dec->ib.get())) + return JXL_DEC_ERROR; + + jxl::Status status = dec->frame_dec->InitFrame( + reader.get(), dec->ib.get(), /*is_preview=*/false, + /*allow_partial_frames=*/false, /*allow_partial_dc_global=*/false); + if (!status) JXL_API_RETURN_IF_ERROR(status); + + size_t sections_begin = + DivCeil(reader->TotalBitsConsumed(), kBitsPerByte); + + dec->sections.reset( + new Sections(dec->frame_dec.get(), dec->frame_size, sections_begin)); + JXL_API_RETURN_IF_ERROR(dec->sections->Init()); + + // If we don't need pixels, we can skip actually decoding the frames + // (kFull / kFullOut). By not updating frame_stage, none of + // these stages will execute, and the loop will continue from the next + // frame. + if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { + dec->frame_dec_in_progress = true; + dec->frame_stage = FrameStage::kFull; + } + } + + bool return_full_image = false; + + if (dec->frame_stage == FrameStage::kFull) { + if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { + if (!dec->image_out_buffer_set && + (!dec->jpeg_decoder.IsOutputSet() || + dec->ib->jpeg_data == nullptr) && + dec->is_last_of_still) { + // TODO(lode): remove the dec->is_last_of_still condition if the + // frame decoder needs the image buffer as working space for decoding + // non-visible or blending frames too + if (!dec->skipping_frame) { + return JXL_DEC_NEED_IMAGE_OUT_BUFFER; + } + } + } + + if (dec->image_out_buffer_set && !!dec->image_out_buffer && + dec->image_out_format.data_type == JXL_TYPE_UINT8 && + dec->image_out_format.num_channels >= 3 && + dec->extra_channel_output.empty()) { + bool is_rgba = dec->image_out_format.num_channels == 4; + dec->frame_dec->MaybeSetRGB8OutputBuffer( + reinterpret_cast(dec->image_out_buffer), + GetStride(dec, dec->image_out_format), is_rgba, + !dec->keep_orientation); + } + + const bool little_endian = + dec->image_out_format.endianness == JXL_LITTLE_ENDIAN || + (dec->image_out_format.endianness == JXL_NATIVE_ENDIAN && + IsLittleEndian()); + bool swap_endianness = little_endian != IsLittleEndian(); + + // 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 && + 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) { + dec->image_out_callback(dec->image_out_opaque, x, y, num_pixels, + pixels); + }, + is_rgba, !dec->keep_orientation); + } + + size_t pos = dec->frame_start - dec->codestream_pos; + if (pos >= size) { + return JXL_DEC_NEED_MORE_INPUT; + } + dec->sections->SetInput(in + pos, size - pos); + + if (cpu_limit_base_ != 0) { + FrameDimensions frame_dim = dec->frame_header->ToFrameDimensions(); + // No overflow, checked in ParseHeader. + size_t num_pixels = frame_dim.xsize * frame_dim.ysize; + if (used_cpu_base_ + num_pixels < used_cpu_base_) { + return JXL_API_ERROR("used too much CPU"); + } + used_cpu_base_ += num_pixels; + if (used_cpu_base_ > cpu_limit_base_) { + return JXL_API_ERROR("used too much CPU"); + } + } + + jxl::Status status = + dec->frame_dec->ProcessSections(dec->sections->section_info.data(), + dec->sections->section_info.size(), + dec->sections->section_status.data()); + JXL_API_RETURN_IF_ERROR(dec->sections->CloseInput()); + if (status.IsFatalError()) { + return JXL_API_ERROR("decoding frame failed"); + } + + // TODO(lode): allow next_in to move forward if sections from the + // beginning of the stream have been processed + + if (status.code() == StatusCode::kNotEnoughBytes || + dec->sections->section_info.size() < dec->frame_dec->NumSections()) { + // Not all sections have been processed yet + return JXL_DEC_NEED_MORE_INPUT; + } + + size_t internal_index = dec->internal_frames - 1; + JXL_ASSERT(dec->frame_references.size() > internal_index); + // Always fill this in, even if it was already written, it could be that + // this frame was skipped before and set to 255, while only now we know + // the true value. + dec->frame_references[internal_index] = dec->frame_dec->References(); + if (!dec->frame_dec->FinalizeFrame()) { + return JXL_API_ERROR("decoding frame failed"); + } + dec->frame_dec_in_progress = false; + dec->frame_stage = FrameStage::kFullOutput; + } + + if (dec->frame_stage == FrameStage::kFullOutput) { + if (dec->is_last_of_still) { + if (dec->events_wanted & JXL_DEC_FULL_IMAGE) { + dec->events_wanted &= ~JXL_DEC_FULL_IMAGE; + return_full_image = true; + } + + // Frame finished, restore the events_wanted with the per-frame events + // from orig_events_wanted, in case there is a next frame. + dec->events_wanted |= + (dec->orig_events_wanted & (JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)); + + // If no output buffer was set, we merely return the JXL_DEC_FULL_IMAGE + // status without outputting pixels. + if (dec->jpeg_decoder.IsOutputSet() && dec->ib->jpeg_data != nullptr) { + JxlDecoderStatus status = + dec->jpeg_decoder.WriteOutput(*dec->ib->jpeg_data); + if (status != JXL_DEC_SUCCESS) return status; + } else if (return_full_image && dec->image_out_buffer_set) { + if (!dec->frame_dec->HasRGBBuffer()) { + // Copy pixels if desired. + JxlDecoderStatus status = ConvertImageInternal( + 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); + if (status != JXL_DEC_SUCCESS) return status; + } + dec->image_out_buffer_set = false; + + 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; + 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); + if (status != JXL_DEC_SUCCESS) return status; + } + + dec->extra_channel_output.clear(); + } + } + } + + // The pixels have been output or are not needed, do not keep them in + // memory here. + dec->ib.reset(); + dec->frame_stage = FrameStage::kHeader; + dec->frame_start += dec->frame_size; + if (return_full_image && !dec->skipping_frame) { + return JXL_DEC_FULL_IMAGE; + } + } + + dec->stage = DecoderStage::kFinished; + // Return success, this means there is nothing more to do. + return JXL_DEC_SUCCESS; +} + +} // namespace +} // namespace jxl + +JxlDecoderStatus JxlDecoderSetInput(JxlDecoder* dec, const uint8_t* data, + size_t size) { + if (dec->next_in) return JXL_DEC_ERROR; + + dec->next_in = data; + dec->avail_in = size; + return JXL_DEC_SUCCESS; +} + +size_t JxlDecoderReleaseInput(JxlDecoder* dec) { + size_t result = dec->avail_in; + dec->next_in = nullptr; + dec->avail_in = 0; + return result; +} + +JxlDecoderStatus JxlDecoderSetJPEGBuffer(JxlDecoder* dec, uint8_t* data, + size_t size) { + return dec->jpeg_decoder.SetOutputBuffer(data, size); +} + +size_t JxlDecoderReleaseJPEGBuffer(JxlDecoder* dec) { + return dec->jpeg_decoder.ReleaseOutputBuffer(); +} + +JxlDecoderStatus JxlDecoderProcessInput(JxlDecoder* dec) { + const uint8_t** next_in = &dec->next_in; + size_t* avail_in = &dec->avail_in; + if (dec->stage == DecoderStage::kInited) { + dec->stage = DecoderStage::kStarted; + } + if (dec->stage == DecoderStage::kError) { + return JXL_API_ERROR( + "Cannot keep using decoder after it encountered an error, use " + "JxlDecoderReset to reset it"); + } + if (dec->stage == DecoderStage::kFinished) { + return JXL_API_ERROR( + "Cannot keep using decoder after it finished, use JxlDecoderReset to " + "reset it"); + } + + if (!dec->got_signature) { + JxlSignature sig = JxlSignatureCheck(*next_in, *avail_in); + if (sig == JXL_SIG_INVALID) return JXL_API_ERROR("invalid signature"); + if (sig == JXL_SIG_NOT_ENOUGH_BYTES) return JXL_DEC_NEED_MORE_INPUT; + + dec->got_signature = true; + + if (sig == JXL_SIG_CONTAINER) { + dec->have_container = 1; + } + } + + // Available codestream bytes, may differ from *avail_in if there is another + // box behind the current position, in the dec->have_container case. + size_t csize = *avail_in; + + if (dec->have_container) { + /* + Process bytes as follows: + *) find the box(es) containing the codestream + *) support codestream split over multiple partial boxes + *) avoid copying bytes to the codestream vector if the decoding will be + one-shot, when the user already provided everything contiguously in + memory + *) copy to codestream vector, and update next_in so user can delete the data + on their side, once we know it's not oneshot. This relieves the user from + continuing to store the data. + *) also copy to codestream if one-shot but the codestream is split across + multiple boxes: this copying can be avoided in the future if the C++ + decoder is updated for streaming, but for now it requires all consecutive + data at once. + */ + + if (dec->skip_box) { + // Amount of remaining bytes in the box that is being skipped. + size_t remaining = dec->box_end - dec->file_pos; + if (*avail_in < remaining) { + // Don't have the full box yet, skip all we have so far + dec->file_pos += *avail_in; + *next_in += *avail_in; + *avail_in -= *avail_in; + return JXL_DEC_NEED_MORE_INPUT; + } else { + // Full box available, skip all its remaining bytes + dec->file_pos += remaining; + *next_in += remaining; + *avail_in -= remaining; + dec->skip_box = false; + } + } + + if (dec->first_codestream_seen && !dec->last_codestream_seen && + dec->codestream_end != 0 && dec->file_pos < dec->codestream_end && + dec->file_pos + *avail_in >= dec->codestream_end && + !dec->codestream.empty()) { + // dec->file_pos in a codestream, not in surrounding box format bytes, but + // the end of the current codestream part is in the current input, and + // boxes that can contain a next part of the codestream could be present. + // Therefore, store the known codestream part, and ensure processing of + // boxes below will trigger. This is only done if + // !dec->codestream.empty(), that is, we're already streaming. + + // Size of the codestream, excluding potential boxes that come after it. + csize = *avail_in; + if (dec->codestream_end && csize > dec->codestream_end - dec->file_pos) { + csize = dec->codestream_end - dec->file_pos; + } + dec->codestream.insert(dec->codestream.end(), *next_in, *next_in + csize); + dec->file_pos += csize; + *next_in += csize; + *avail_in -= csize; + } + + if (dec->jpeg_decoder.IsParsingBox()) { + // We are inside a JPEG reconstruction box. + JxlDecoderStatus recon_result = + dec->jpeg_decoder.Process(next_in, avail_in); + if (recon_result == JXL_DEC_JPEG_RECONSTRUCTION) { + // If successful JPEG reconstruction, return the success if the user + // cares about it, otherwise continue. + if (dec->events_wanted & recon_result) { + dec->events_wanted &= ~recon_result; + return recon_result; + } + } else { + // If anything else, return the result. + return recon_result; + } + } + + if (!dec->last_codestream_seen && + (dec->codestream_begin == 0 || + (dec->codestream_end != 0 && dec->file_pos >= dec->codestream_end))) { + size_t pos = 0; + // after this for loop, either we should be in a part of the data that is + // codestream (not boxes), or have returned that we need more input. + for (;;) { + const uint8_t* in = *next_in; + size_t size = *avail_in; + if (size == pos) { + // If the remaining size is 0, we are exactly after a full box. We + // can't know for sure if this is the last box or not since more bytes + // can follow, but do not return NEED_MORE_INPUT, instead break and + // let the codestream-handling code determine if we need more. + break; + } + if (OutOfBounds(pos, 8, size)) { + dec->basic_info_size_hint = + InitialBasicInfoSizeHint() + pos + 8 - dec->file_pos; + return JXL_DEC_NEED_MORE_INPUT; + } + size_t box_start = pos; + // Box size, including this header itself. + uint64_t box_size = LoadBE32(in + pos); + char type[5] = {0}; + memcpy(type, in + pos + 4, 4); + pos += 8; + if (box_size == 1) { + if (OutOfBounds(pos, 8, size)) return JXL_DEC_NEED_MORE_INPUT; + box_size = LoadBE64(in + pos); + pos += 8; + } + size_t header_size = pos - box_start; + if (box_size > 0 && box_size < header_size) { + return JXL_API_ERROR("invalid box size"); + } + if (SumOverflows(dec->file_pos, pos, box_size)) { + return JXL_API_ERROR("Box size overflow"); + } + size_t contents_size = + (box_size == 0) ? 0 : (box_size - pos + box_start); + + dec->box_begin = box_start; + dec->box_end = dec->file_pos + box_start + box_size; + if (strcmp(type, "jxlc") == 0 || strcmp(type, "jxlp") == 0) { + size_t codestream_size = contents_size; + // Whether this is the last codestream box, either when it is a jxlc + // box, or when it is a jxlp box that has the final bit set. + // The codestream is either contained within a single jxlc box, or + // within one or more jxlp boxes. The final jxlp box is marked as last + // by setting the high bit of its 4-byte box-index value. + bool last_codestream = false; + if (strcmp(type, "jxlp") == 0) { + if (OutOfBounds(pos, 4, size)) return JXL_DEC_NEED_MORE_INPUT; + if (box_size != 0 && contents_size < 4) { + return JXL_API_ERROR("jxlp box too small to contain index"); + } + codestream_size -= 4; + size_t jxlp_index = LoadBE32(in + pos); + pos += 4; + // The high bit of jxlp_index indicates whether this is the last + // jxlp box. + if (jxlp_index & 0x80000000) last_codestream = true; + } else if (strcmp(type, "jxlc") == 0) { + last_codestream = true; + } + if (!last_codestream && box_size == 0) { + return JXL_API_ERROR( + "final box has unbounded size, but is a non-final codestream " + "box"); + } + dec->first_codestream_seen = true; + if (last_codestream) dec->last_codestream_seen = true; + if (dec->codestream_begin != 0 && dec->codestream.empty()) { + // We've already seen a codestream part, so it's a stream spanning + // multiple boxes. + // We have no choice but to copy contents to the codestream + // vector to make it a contiguous stream for the C++ decoder. + // This appends the previous codestream box that we had seen to + // dec->codestream. + if (dec->codestream_begin < dec->file_pos) { + return JXL_API_ERROR("earlier codestream box out of range"); + } + size_t begin = dec->codestream_begin - dec->file_pos; + size_t end = dec->codestream_end - dec->file_pos; + JXL_ASSERT(end <= *avail_in); + dec->codestream.insert(dec->codestream.end(), *next_in + begin, + *next_in + end); + } + dec->codestream_begin = dec->file_pos + pos; + dec->codestream_end = + (box_size == 0) ? 0 : (dec->codestream_begin + codestream_size); + size_t avail_codestream_size = + (box_size == 0) + ? (size - pos) + : std::min(size - pos, box_size - pos + box_start); + // If already appending codestream, append what we have here too + if (!dec->codestream.empty()) { + size_t begin = pos; + size_t end = + std::min(*avail_in, begin + avail_codestream_size); + dec->codestream.insert(dec->codestream.end(), *next_in + begin, + *next_in + end); + pos += (end - begin); + dec->file_pos += pos; + *next_in += pos; + *avail_in -= pos; + pos = 0; + // TODO(lode): check if this should break always instead, and + // process what we have of the codestream so far, to support + // progressive decoding, and get events such as basic info faster. + // The user could have given 1.5 boxes here, and the first one could + // contain useful parts of codestream that can already be processed. + // Similar to several other exact avail_size checks. This may not + // need to be changed here, but instead at the point in this for + // loop where it returns "NEED_MORE_INPUT", it could instead break + // and allow decoding what we have of the codestream so far. + if (*avail_in == 0) break; + } else { + // skip only the header, so next_in points to the start of this new + // codestream part, for the one-shot case where user data is not + // (yet) copied to dec->codestream. + dec->file_pos += pos; + *next_in += pos; + *avail_in -= pos; + pos = 0; + // Update pos to be after the box contents with codestream + if (avail_codestream_size == *avail_in) { + break; // the rest is codestream, this loop is done + } + pos += avail_codestream_size; + } + } else if ((JPEGXL_ENABLE_TRANSCODE_JPEG) && + (dec->orig_events_wanted & JXL_DEC_JPEG_RECONSTRUCTION) && + strcmp(type, "jbrd") == 0) { + // This is a new JPEG reconstruction metadata box. + dec->jpeg_decoder.StartBox(box_size, contents_size); + dec->file_pos += pos; + *next_in += pos; + *avail_in -= pos; + pos = 0; + JxlDecoderStatus recon_result = + dec->jpeg_decoder.Process(next_in, avail_in); + if (recon_result == JXL_DEC_JPEG_RECONSTRUCTION) { + // If successful JPEG reconstruction, return the success if the user + // cares about it, otherwise continue. + if (dec->events_wanted & recon_result) { + dec->events_wanted &= ~recon_result; + return recon_result; + } + } else { + // If anything else, return the result. + return recon_result; + } + } else { + if (box_size == 0) { + // Final box with unknown size, but it's not a codestream box, so + // nothing more to do. + if (!dec->first_codestream_seen) { + return JXL_API_ERROR("didn't find any codestream box"); + } + break; + } + if (OutOfBounds(pos, contents_size, size)) { + dec->skip_box = true; + dec->file_pos += pos; + *next_in += pos; + *avail_in -= pos; + // Indicate how many more bytes needed starting from *next_in. + dec->basic_info_size_hint = InitialBasicInfoSizeHint() + pos + + contents_size - dec->file_pos; + return JXL_DEC_NEED_MORE_INPUT; + } + pos += contents_size; + if (!(dec->codestream.empty() && dec->first_codestream_seen)) { + // Last box no longer needed since we have copied the codestream + // buffer, remove from input so user can release memory. + dec->file_pos += pos; + *next_in += pos; + *avail_in -= pos; + pos = 0; + } + } + } + } + + // Size of the codestream, excluding potential boxes that come after it. + csize = *avail_in; + if (dec->codestream_end && csize > dec->codestream_end - dec->file_pos) { + csize = dec->codestream_end - dec->file_pos; + } + } + + // Whether we are taking the input directly from the user (oneshot case, + // without copying bytes), or appending parts of input to dec->codestream + // (streaming) + bool detected_streaming = !dec->codestream.empty(); + JxlDecoderStatus result; + JXL_DASSERT(csize <= *avail_in); + + if (detected_streaming) { + dec->codestream.insert(dec->codestream.end(), *next_in, *next_in + csize); + dec->file_pos += csize; + *next_in += csize; + *avail_in -= csize; + result = jxl::JxlDecoderProcessInternal(dec, dec->codestream.data(), + dec->codestream.size()); + } else { + // No data copied to codestream buffer yet, the user input may contain the + // full codestream. + result = jxl::JxlDecoderProcessInternal(dec, *next_in, csize); + // Copy the user's input bytes to the codestream once we are able to and + // it is needed. Before we got the basic info, we're still parsing the box + // format instead. If the result is not JXL_DEC_NEED_MORE_INPUT, then + // there is no reason yet to copy since the user may have a full buffer + // allowing one-shot. Once JXL_DEC_NEED_MORE_INPUT occurred at least once, + // start copying over the codestream bytes and allow user to free them + // instead. Next call, detected_streaming will be true. + if (dec->got_basic_info && result == JXL_DEC_NEED_MORE_INPUT) { + dec->codestream.insert(dec->codestream.end(), *next_in, *next_in + csize); + dec->file_pos += csize; + *next_in += csize; + *avail_in -= csize; + } + } + + return result; +} + +// To ensure ABI forward-compatibility, this struct has a constant size. +static_assert(sizeof(JxlBasicInfo) == 204, + "JxlBasicInfo struct size should remain constant"); + +JxlDecoderStatus JxlDecoderGetBasicInfo(const JxlDecoder* dec, + JxlBasicInfo* info) { + if (!dec->got_basic_info) return JXL_DEC_NEED_MORE_INPUT; + + if (info) { + const jxl::ImageMetadata& meta = dec->metadata.m; + + info->have_container = dec->have_container; + info->xsize = dec->metadata.size.xsize(); + info->ysize = dec->metadata.size.ysize(); + info->uses_original_profile = !meta.xyb_encoded; + + info->bits_per_sample = meta.bit_depth.bits_per_sample; + info->exponent_bits_per_sample = meta.bit_depth.exponent_bits_per_sample; + + info->have_preview = meta.have_preview; + info->have_animation = meta.have_animation; + // TODO(janwas): intrinsic_size + info->orientation = static_cast(meta.orientation); + + if (!dec->keep_orientation) { + if (info->orientation >= JXL_ORIENT_TRANSPOSE) { + std::swap(info->xsize, info->ysize); + } + info->orientation = JXL_ORIENT_IDENTITY; + } + + info->intensity_target = meta.IntensityTarget(); + info->min_nits = meta.tone_mapping.min_nits; + info->relative_to_max_display = meta.tone_mapping.relative_to_max_display; + info->linear_below = meta.tone_mapping.linear_below; + + const jxl::ExtraChannelInfo* alpha = meta.Find(jxl::ExtraChannel::kAlpha); + if (alpha != nullptr) { + info->alpha_bits = alpha->bit_depth.bits_per_sample; + info->alpha_exponent_bits = alpha->bit_depth.exponent_bits_per_sample; + info->alpha_premultiplied = alpha->alpha_associated; + } else { + info->alpha_bits = 0; + info->alpha_exponent_bits = 0; + info->alpha_premultiplied = 0; + } + + info->num_color_channels = + meta.color_encoding.GetColorSpace() == jxl::ColorSpace::kGray ? 1 : 3; + + info->num_extra_channels = meta.num_extra_channels; + + if (info->have_preview) { + info->preview.xsize = dec->metadata.m.preview_size.xsize(); + info->preview.ysize = dec->metadata.m.preview_size.ysize(); + } + + if (info->have_animation) { + info->animation.tps_numerator = dec->metadata.m.animation.tps_numerator; + info->animation.tps_denominator = + dec->metadata.m.animation.tps_denominator; + info->animation.num_loops = dec->metadata.m.animation.num_loops; + info->animation.have_timecodes = dec->metadata.m.animation.have_timecodes; + } + } + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetExtraChannelInfo(const JxlDecoder* dec, + size_t index, + JxlExtraChannelInfo* info) { + if (!dec->got_basic_info) return JXL_DEC_NEED_MORE_INPUT; + + const std::vector& channels = + dec->metadata.m.extra_channel_info; + + if (index >= channels.size()) return JXL_DEC_ERROR; // out of bounds + const jxl::ExtraChannelInfo& channel = channels[index]; + + info->type = static_cast(channel.type); + info->bits_per_sample = channel.bit_depth.bits_per_sample; + info->exponent_bits_per_sample = + channel.bit_depth.floating_point_sample + ? channel.bit_depth.exponent_bits_per_sample + : 0; + info->dim_shift = channel.dim_shift; + info->name_length = channel.name.size(); + info->alpha_premultiplied = channel.alpha_associated; + info->spot_color[0] = channel.spot_color[0]; + info->spot_color[1] = channel.spot_color[1]; + info->spot_color[2] = channel.spot_color[2]; + info->spot_color[3] = channel.spot_color[3]; + info->cfa_channel = channel.cfa_channel; + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetExtraChannelName(const JxlDecoder* dec, + size_t index, char* name, + size_t size) { + if (!dec->got_basic_info) return JXL_DEC_NEED_MORE_INPUT; + + const std::vector& channels = + dec->metadata.m.extra_channel_info; + + if (index >= channels.size()) return JXL_DEC_ERROR; // out of bounds + const jxl::ExtraChannelInfo& channel = channels[index]; + + // Also need null-termination character + if (channel.name.size() + 1 > size) return JXL_DEC_ERROR; + + memcpy(name, channel.name.c_str(), channel.name.size() + 1); + + return JXL_DEC_SUCCESS; +} + +namespace { + +// Gets the jxl::ColorEncoding for the desired target, and checks errors. +// Returns the object regardless of whether the actual color space is in ICC, +// but ensures that if the color encoding is not the encoding from the +// codestream header metadata, it cannot require ICC profile. +JxlDecoderStatus GetColorEncodingForTarget( + const JxlDecoder* dec, const JxlPixelFormat* format, + JxlColorProfileTarget target, const jxl::ColorEncoding** encoding) { + if (!dec->got_all_headers) return JXL_DEC_NEED_MORE_INPUT; + *encoding = nullptr; + if (target == JXL_COLOR_PROFILE_TARGET_DATA && dec->metadata.m.xyb_encoded) { + *encoding = &dec->passes_state->output_encoding_info.color_encoding; + } else { + *encoding = &dec->metadata.m.color_encoding; + } + return JXL_DEC_SUCCESS; +} +} // namespace + +JxlDecoderStatus JxlDecoderGetColorAsEncodedProfile( + const JxlDecoder* dec, const JxlPixelFormat* format, + JxlColorProfileTarget target, JxlColorEncoding* color_encoding) { + const jxl::ColorEncoding* jxl_color_encoding = nullptr; + JxlDecoderStatus status = + GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + if (status) return status; + + if (jxl_color_encoding->WantICC()) + return JXL_DEC_ERROR; // Indicate no encoded profile available. + + if (color_encoding) { + ConvertInternalToExternalColorEncoding(*jxl_color_encoding, color_encoding); + } + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetICCProfileSize(const JxlDecoder* dec, + const JxlPixelFormat* format, + JxlColorProfileTarget target, + size_t* size) { + const jxl::ColorEncoding* jxl_color_encoding = nullptr; + JxlDecoderStatus status = + GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + if (status != JXL_DEC_SUCCESS) return status; + + if (jxl_color_encoding->WantICC()) { + jxl::ColorSpace color_space = + dec->metadata.m.color_encoding.GetColorSpace(); + if (color_space == jxl::ColorSpace::kUnknown || + color_space == jxl::ColorSpace::kXYB) { + // This indicates there's no ICC profile available + // TODO(lode): for the XYB case, do we want to craft an ICC profile that + // represents XYB as an RGB profile? It may be possible, but not with + // only 1D transfer functions. + return JXL_DEC_ERROR; + } + } + + if (size) { + *size = jxl_color_encoding->ICC().size(); + } + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetColorAsICCProfile(const JxlDecoder* dec, + const JxlPixelFormat* format, + JxlColorProfileTarget target, + uint8_t* icc_profile, + size_t size) { + size_t wanted_size; + // This also checks the NEED_MORE_INPUT and the unknown/xyb cases + JxlDecoderStatus status = + JxlDecoderGetICCProfileSize(dec, format, target, &wanted_size); + if (status != JXL_DEC_SUCCESS) return status; + if (size < wanted_size) return JXL_API_ERROR("ICC profile output too small"); + + const jxl::ColorEncoding* jxl_color_encoding = nullptr; + status = GetColorEncodingForTarget(dec, format, target, &jxl_color_encoding); + if (status != JXL_DEC_SUCCESS) return status; + + memcpy(icc_profile, jxl_color_encoding->ICC().data(), + jxl_color_encoding->ICC().size()); + + return JXL_DEC_SUCCESS; +} + +namespace { + +// Returns the amount of bits needed for getting memory buffer size, and does +// all error checking required for size checking and format validity. +JxlDecoderStatus PrepareSizeCheck(const JxlDecoder* dec, + const JxlPixelFormat* format, size_t* bits) { + if (!dec->got_basic_info) { + // Don't know image dimensions yet, cannot check for valid size. + return JXL_DEC_NEED_MORE_INPUT; + } + 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_DEC_SUCCESS; +} +} // namespace + +JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) { + if (!dec->image_out_buffer) return JXL_DEC_ERROR; + if (!dec->sections || dec->sections->section_info.empty()) { + return JXL_DEC_ERROR; + } + if (!dec->frame_dec || !dec->frame_dec_in_progress) { + return JXL_DEC_ERROR; + } + if (!dec->frame_dec->HasDecodedDC()) { + // FrameDecoder::Fush currently requires DC to have been decoded already + // to work correctly. + return JXL_DEC_ERROR; + } + if (dec->frame_header->encoding != jxl::FrameEncoding::kVarDCT) { + // Flushing does not yet work correctly if the frame uses modular encoding. + return JXL_DEC_ERROR; + } + if (dec->metadata.m.num_extra_channels > 0) { + // Flushing does not yet work correctly if there are extra channels, which + // use modular + return JXL_DEC_ERROR; + } + + if (!dec->frame_dec->Flush()) { + return JXL_DEC_ERROR; + } + + if (dec->frame_dec->HasRGBBuffer()) { + return JXL_DEC_SUCCESS; + } + + // Temporarily shrink `dec->ib` to the actual size of the full image to call + // ConvertImageInternal. + size_t xsize = dec->ib->xsize(); + size_t ysize = dec->ib->ysize(); + dec->ib->ShrinkTo(dec->metadata.size.xsize(), dec->metadata.size.ysize()); + JxlDecoderStatus status = jxl::ConvertImageInternal( + 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); + dec->ib->ShrinkTo(xsize, ysize); + if (status != JXL_DEC_SUCCESS) return status; + return JXL_DEC_SUCCESS; +} + +JXL_EXPORT JxlDecoderStatus JxlDecoderPreviewOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size) { + size_t bits; + JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); + if (status != JXL_DEC_SUCCESS) return status; + if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { + return JXL_API_ERROR("Grayscale output not possible for color image"); + } + + size_t xsize = dec->metadata.oriented_preview_xsize(dec->keep_orientation); + size_t ysize = dec->metadata.oriented_preview_ysize(dec->keep_orientation); + + size_t row_size = + jxl::DivCeil(xsize * format->num_channels * bits, jxl::kBitsPerByte); + if (format->align > 1) { + row_size = jxl::DivCeil(row_size, format->align) * format->align; + } + *size = row_size * ysize; + return JXL_DEC_SUCCESS; +} + +JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer( + JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size) { + if (!dec->got_basic_info || !dec->metadata.m.have_preview || + !(dec->orig_events_wanted & JXL_DEC_PREVIEW_IMAGE)) { + return JXL_API_ERROR("No preview out buffer needed at this time"); + } + if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { + return JXL_API_ERROR("Grayscale output not possible for color image"); + } + + size_t min_size; + // This also checks whether the format is valid and supported and basic info + // is available. + JxlDecoderStatus status = + JxlDecoderPreviewOutBufferSize(dec, format, &min_size); + if (status != JXL_DEC_SUCCESS) return status; + + if (size < min_size) return JXL_DEC_ERROR; + + dec->preview_out_buffer_set = true; + dec->preview_out_buffer = buffer; + dec->preview_out_size = size; + dec->preview_out_format = *format; + + return JXL_DEC_SUCCESS; +} + +JXL_EXPORT JxlDecoderStatus JxlDecoderDCOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size) { + size_t bits; + JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); + if (status != JXL_DEC_SUCCESS) return status; + + size_t xsize = jxl::DivCeil( + dec->metadata.oriented_xsize(dec->keep_orientation), jxl::kBlockDim); + size_t ysize = jxl::DivCeil( + dec->metadata.oriented_ysize(dec->keep_orientation), jxl::kBlockDim); + + size_t row_size = + jxl::DivCeil(xsize * format->num_channels * bits, jxl::kBitsPerByte); + if (format->align > 1) { + row_size = jxl::DivCeil(row_size, format->align) * format->align; + } + *size = row_size * ysize; + return JXL_DEC_SUCCESS; +} + +JXL_EXPORT JxlDecoderStatus JxlDecoderSetDCOutBuffer( + JxlDecoder* dec, const JxlPixelFormat* format, void* buffer, size_t size) { + // No buffer set: this feature is deprecated + return JXL_DEC_SUCCESS; +} + +JXL_EXPORT JxlDecoderStatus JxlDecoderImageOutBufferSize( + const JxlDecoder* dec, const JxlPixelFormat* format, size_t* size) { + size_t bits; + JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); + if (status != JXL_DEC_SUCCESS) return status; + if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { + return JXL_API_ERROR("Grayscale output not possible for color image"); + } + + size_t row_size = + jxl::DivCeil(dec->metadata.oriented_xsize(dec->keep_orientation) * + format->num_channels * bits, + jxl::kBitsPerByte); + if (format->align > 1) { + row_size = jxl::DivCeil(row_size, format->align) * format->align; + } + *size = row_size * dec->metadata.oriented_ysize(dec->keep_orientation); + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetImageOutBuffer(JxlDecoder* dec, + const JxlPixelFormat* format, + void* buffer, size_t size) { + 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) { + return JXL_API_ERROR( + "Cannot change from image out callback to image out buffer"); + } + if (format->num_channels < 3 && !dec->metadata.m.color_encoding.IsGray()) { + return JXL_API_ERROR("Grayscale output not possible for color image"); + } + size_t min_size; + // This also checks whether the format is valid and supported and basic info + // is available. + JxlDecoderStatus status = + JxlDecoderImageOutBufferSize(dec, format, &min_size); + if (status != JXL_DEC_SUCCESS) return status; + + if (size < min_size) return JXL_DEC_ERROR; + + dec->image_out_buffer_set = true; + dec->image_out_buffer = buffer; + dec->image_out_size = size; + dec->image_out_format = *format; + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderExtraChannelBufferSize(const JxlDecoder* dec, + const JxlPixelFormat* format, + size_t* size, + uint32_t index) { + if (!dec->got_basic_info || !(dec->orig_events_wanted & JXL_DEC_FULL_IMAGE)) { + return JXL_API_ERROR("No extra channel buffer needed at this time"); + } + + if (index >= dec->metadata.m.num_extra_channels) { + return JXL_API_ERROR("Invalid extra channel index"); + } + + size_t num_channels = 1; // Do not use format's num_channels + + size_t bits; + JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits); + if (status != JXL_DEC_SUCCESS) return status; + + size_t row_size = jxl::DivCeil( + dec->metadata.oriented_xsize(dec->keep_orientation) * num_channels * bits, + jxl::kBitsPerByte); + if (format->align > 1) { + row_size = jxl::DivCeil(row_size, format->align) * format->align; + } + *size = row_size * dec->metadata.oriented_ysize(dec->keep_orientation); + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetExtraChannelBuffer(JxlDecoder* dec, + const JxlPixelFormat* format, + void* buffer, size_t size, + uint32_t index) { + size_t min_size; + // This also checks whether the format and index are valid and supported and + // basic info is available. + JxlDecoderStatus status = + JxlDecoderExtraChannelBufferSize(dec, format, &min_size, index); + if (status != JXL_DEC_SUCCESS) return status; + + if (size < min_size) return JXL_DEC_ERROR; + + if (dec->extra_channel_output.size() <= index) { + dec->extra_channel_output.resize(dec->metadata.m.num_extra_channels, + {{}, nullptr, 0}); + } + // Guaranteed correct thanks to check in JxlDecoderExtraChannelBufferSize. + JXL_ASSERT(index < dec->extra_channel_output.size()); + + dec->extra_channel_output[index].format = *format; + dec->extra_channel_output[index].format.num_channels = 1; + dec->extra_channel_output[index].buffer = buffer; + dec->extra_channel_output[index].buffer_size = size; + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetImageOutCallback(JxlDecoder* dec, + const JxlPixelFormat* format, + JxlImageOutCallback callback, + void* 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"); + } + + // 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_format = *format; + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetFrameHeader(const JxlDecoder* dec, + JxlFrameHeader* header) { + if (!dec->frame_header || dec->frame_stage == FrameStage::kHeader) { + return JXL_API_ERROR("no frame header available"); + } + const auto& metadata = dec->metadata.m; + if (metadata.have_animation) { + header->duration = dec->frame_header->animation_frame.duration; + if (metadata.animation.have_timecodes) { + header->timecode = dec->frame_header->animation_frame.timecode; + } + } + header->name_length = dec->frame_header->name.size(); + header->is_last = dec->frame_header->is_last; + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderGetFrameName(const JxlDecoder* dec, char* name, + size_t size) { + if (!dec->frame_header || dec->frame_stage == FrameStage::kHeader) { + return JXL_API_ERROR("no frame header available"); + } + if (size < dec->frame_header->name.size() + 1) { + return JXL_API_ERROR("too small frame name output buffer"); + } + memcpy(name, dec->frame_header->name.c_str(), + dec->frame_header->name.size() + 1); + + return JXL_DEC_SUCCESS; +} + +JxlDecoderStatus JxlDecoderSetPreferredColorProfile( + JxlDecoder* dec, const JxlColorEncoding* color_encoding) { + if (!dec->got_all_headers) { + return JXL_API_ERROR("color info not yet available"); + } + if (dec->post_headers) { + return JXL_API_ERROR("too late to set the color encoding"); + } + if (dec->metadata.m.color_encoding.IsGray() != + (color_encoding->color_space == JXL_COLOR_SPACE_GRAY)) { + return JXL_API_ERROR("grayscale mismatch"); + } + if (color_encoding->color_space == JXL_COLOR_SPACE_UNKNOWN || + color_encoding->color_space == JXL_COLOR_SPACE_XYB) { + return JXL_API_ERROR("only RGB or grayscale output supported"); + } + + JXL_API_RETURN_IF_ERROR(ConvertExternalToInternalColorEncoding( + *color_encoding, &dec->default_enc)); + JXL_API_RETURN_IF_ERROR(dec->passes_state->output_encoding_info.Set( + dec->metadata, dec->default_enc)); + return JXL_DEC_SUCCESS; +} + +// This function is "package-private". It is only used by fuzzer to avoid +// running cases that are too memory / CPU hungry. Limitations are applied +// at mid-level API. In the future high-level API would also include the +// means of limiting / throttling memory / CPU usage. +void SetDecoderMemoryLimitBase_(size_t memory_limit_base) { + memory_limit_base_ = memory_limit_base; + // 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. + cpu_limit_base_ = 5 * memory_limit_base; +} diff --git a/lib/jxl/decode_test.cc b/lib/jxl/decode_test.cc new file mode 100644 index 0000000..17a4a90 --- /dev/null +++ b/lib/jxl/decode_test.cc @@ -0,0 +1,3017 @@ +// 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 "jxl/decode.h" + +#include +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/decode_cxx.h" +#include "jxl/resizable_parallel_runner_cxx.h" +#include "jxl/thread_parallel_runner_cxx.h" +#include "lib/extras/codec.h" +#include "lib/extras/codec_jpg.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_gamma_correct.h" +#include "lib/jxl/enc_icc_codec.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/icc_codec.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" +#include "tools/box/box.h" + +//////////////////////////////////////////////////////////////////////////////// + +namespace { +void AppendU32BE(uint32_t u32, jxl::PaddedBytes* bytes) { + bytes->push_back(u32 >> 24); + bytes->push_back(u32 >> 16); + bytes->push_back(u32 >> 8); + bytes->push_back(u32 >> 0); +} + +bool Near(double expected, double value, double max_dist) { + double dist = expected > value ? expected - value : value - expected; + return dist <= max_dist; +} + +// Loads a Big-Endian float +float LoadBEFloat(const uint8_t* p) { + uint32_t u = LoadBE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +// Loads a Little-Endian float +float LoadLEFloat(const uint8_t* p) { + uint32_t u = LoadLE32(p); + float result; + memcpy(&result, &u, 4); + return result; +} + +// Based on highway scalar implementation, for testing +float LoadFloat16(uint16_t bits16) { + const uint32_t sign = bits16 >> 15; + const uint32_t biased_exp = (bits16 >> 10) & 0x1F; + const uint32_t mantissa = bits16 & 0x3FF; + + // Subnormal or zero + if (biased_exp == 0) { + const float subnormal = (1.0f / 16384) * (mantissa * (1.0f / 1024)); + return sign ? -subnormal : subnormal; + } + + // Normalized: convert the representation directly (faster than ldexp/tables). + const uint32_t biased_exp32 = biased_exp + (127 - 15); + const uint32_t mantissa32 = mantissa << (23 - 10); + const uint32_t bits32 = (sign << 31) | (biased_exp32 << 23) | mantissa32; + + float result; + memcpy(&result, &bits32, 4); + return result; +} + +float LoadLEFloat16(const uint8_t* p) { + uint16_t bits16 = LoadLE16(p); + return LoadFloat16(bits16); +} + +float LoadBEFloat16(const uint8_t* p) { + uint16_t bits16 = LoadBE16(p); + return LoadFloat16(bits16); +} + +size_t GetPrecision(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: + // Floating point mantissa precision + return 24; + case JXL_TYPE_FLOAT16: + return 11; + } + JXL_ASSERT(false); // unknown type +} + +size_t GetDataBits(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; + } + JXL_ASSERT(false); // unknown type +} + +// What type of codestream format in the boxes to use for testing +enum CodeStreamBoxFormat { + // Do not use box format at all, only pure codestream + kCSBF_None, + // Have a single codestream box, with its actual size given in the box + kCSBF_Single, + // Have a single codestream box, with box size 0 (final box running to end) + kCSBF_Single_Zero_Terminated, + // Single codestream box, with another unknown box behind it + kCSBF_Single_other, + // Have multiple partial codestream boxes + kCSBF_Multi, + // Have multiple partial codestream boxes, with final box size 0 (running + // to end) + kCSBF_Multi_Zero_Terminated, + // Have multiple partial codestream boxes, terminated by non-codestream box + kCSBF_Multi_Other_Terminated, + // Have multiple partial codestream boxes, terminated by non-codestream box + // that has its size set to 0 (running to end) + kCSBF_Multi_Other_Zero_Terminated, + // Have multiple partial codestream boxes, and the first one has a content + // of zero length + kCSBF_Multi_First_Empty, + // Not a value but used for counting amount of enum entries + kCSBF_NUM_ENTRIES, +}; + +// Returns an ICC profile output by the JPEG XL decoder for RGB_D65_SRG_Rel_Lin, +// but with, on purpose, rXYZ, bXYZ and gXYZ (the RGB primaries) switched to a +// different order to ensure the profile does not match any known profile, so +// the encoder cannot encode it in a compact struct instead. +jxl::PaddedBytes GetIccTestProfile() { + const uint8_t* profile = reinterpret_cast( + "\0\0\3\200lcms\0040\0\0mntrRGB XYZ " + "\a\344\0\a\0\27\0\21\0$" + "\0\37acspAPPL\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\0\0\366" + "\326\0\1\0\0\0\0\323-lcms\372c\207\36\227\200{" + "\2\232s\255\327\340\0\n\26\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0\rdesc\0\0\1 " + "\0\0\0Bcprt\0\0\1d\0\0\1\0wtpt\0\0\2d\0\0\0\24chad\0\0\2x\0\0\0," + "bXYZ\0\0\2\244\0\0\0\24gXYZ\0\0\2\270\0\0\0\24rXYZ\0\0\2\314\0\0\0\24rTR" + "C\0\0\2\340\0\0\0 gTRC\0\0\2\340\0\0\0 bTRC\0\0\2\340\0\0\0 " + "chrm\0\0\3\0\0\0\0$dmnd\0\0\3$\0\0\0(" + "dmdd\0\0\3L\0\0\0002mluc\0\0\0\0\0\0\0\1\0\0\0\fenUS\0\0\0&" + "\0\0\0\34\0R\0G\0B\0_\0D\0006\0005\0_\0S\0R\0G\0_\0R\0e\0l\0_" + "\0L\0i\0n\0\0mluc\0\0\0\0\0\0\0\1\0\0\0\fenUS\0\0\0\344\0\0\0\34\0C\0o\0" + "p\0y\0r\0i\0g\0h\0t\0 \0002\0000\0001\08\0 \0G\0o\0o\0g\0l\0e\0 " + "\0L\0L\0C\0,\0 \0C\0C\0-\0B\0Y\0-\0S\0A\0 \0003\0.\0000\0 " + "\0U\0n\0p\0o\0r\0t\0e\0d\0 " + "\0l\0i\0c\0e\0n\0s\0e\0(\0h\0t\0t\0p\0s\0:\0/\0/" + "\0c\0r\0e\0a\0t\0i\0v\0e\0c\0o\0m\0m\0o\0n\0s\0.\0o\0r\0g\0/" + "\0l\0i\0c\0e\0n\0s\0e\0s\0/\0b\0y\0-\0s\0a\0/\0003\0.\0000\0/" + "\0l\0e\0g\0a\0l\0c\0o\0d\0e\0)XYZ " + "\0\0\0\0\0\0\366\326\0\1\0\0\0\0\323-" + "sf32\0\0\0\0\0\1\fB\0\0\5\336\377\377\363%" + "\0\0\a\223\0\0\375\220\377\377\373\241\377\377\375\242\0\0\3\334\0\0\300" + "nXYZ \0\0\0\0\0\0o\240\0\08\365\0\0\3\220XYZ " + "\0\0\0\0\0\0$\237\0\0\17\204\0\0\266\304XYZ " + "\0\0\0\0\0\0b\227\0\0\267\207\0\0\30\331para\0\0\0\0\0\3\0\0\0\1\0\0\0\1" + "\0\0\0\0\0\0\0\1\0\0\0\0\0\0chrm\0\0\0\0\0\3\0\0\0\0\243\327\0\0T|" + "\0\0L\315\0\0\231\232\0\0&" + "g\0\0\17\\mluc\0\0\0\0\0\0\0\1\0\0\0\fenUS\0\0\0\f\0\0\0\34\0G\0o\0o\0g" + "\0l\0emluc\0\0\0\0\0\0\0\1\0\0\0\fenUS\0\0\0\26\0\0\0\34\0I\0m\0a\0g\0e" + "\0 \0c\0o\0d\0e\0c\0\0"); + size_t profile_size = 896; + jxl::PaddedBytes icc_profile; + icc_profile.assign(profile, profile + profile_size); + return icc_profile; +} + +} // namespace + +namespace jxl { +namespace { + +// Input pixels always given as 16-bit RGBA, 8 bytes per pixel. +// include_alpha determines if the encoded image should contain the alpha +// channel. +// add_icc_profile: if false, encodes the image as sRGB using the JXL fields, +// for grayscale or RGB images. If true, encodes the image using the ICC profile +// returned by GetIccTestProfile, without the JXL fields, this requires the +// image is RGB, not grayscale. +// Providing jpeg_codestream will populate the jpeg_codestream with compressed +// JPEG bytes, and make it possible to reconstruct those exact JPEG bytes using +// the return value _if_ add_container indicates a box format. +PaddedBytes CreateTestJXLCodestream( + Span pixels, size_t xsize, size_t ysize, size_t num_channels, + const CompressParams& cparams, CodeStreamBoxFormat add_container, + JxlOrientation orientation, bool add_preview, bool add_icc_profile = false, + PaddedBytes* jpeg_codestream = nullptr) { + // Compress the pixels with JPEG XL. + bool grayscale = (num_channels <= 2); + bool include_alpha = !(num_channels & 1) && jpeg_codestream == nullptr; + size_t bitdepth = jpeg_codestream == nullptr ? 16 : 8; + CodecInOut io; + io.SetSize(xsize, ysize); + ColorEncoding color_encoding = + jxl::ColorEncoding::SRGB(/*is_gray=*/grayscale); + if (add_icc_profile) { + // the hardcoded ICC profile we attach requires RGB. + EXPECT_EQ(false, grayscale); + EXPECT_TRUE(color_encoding.SetICC(GetIccTestProfile())); + } + ThreadPool pool(nullptr, nullptr); + io.metadata.m.SetUintSamples(bitdepth); + if (include_alpha) { + io.metadata.m.SetAlphaBits(bitdepth); + } + // Make the grayscale-ness of the io metadata color_encoding and the packed + // image match. + io.metadata.m.color_encoding = color_encoding; + EXPECT_TRUE(ConvertFromExternal( + pixels, xsize, ysize, color_encoding, /*has_alpha=*/include_alpha, + /*alpha_is_premultiplied=*/false, bitdepth, JXL_BIG_ENDIAN, + /*flipped_y=*/false, &pool, &io.Main(), /*float_in=*/false)); + jxl::PaddedBytes jpeg_data; + if (jpeg_codestream != nullptr) { +#if JPEGXL_ENABLE_JPEG + jxl::PaddedBytes jpeg_bytes; + EXPECT_TRUE(EncodeImageJPG(&io, jxl::extras::JpegEncoder::kLibJpeg, + /*quality=*/70, jxl::YCbCrChromaSubsampling(), + &pool, &jpeg_bytes)); + jpeg_codestream->append(jpeg_bytes.data(), + jpeg_bytes.data() + jpeg_bytes.size()); + EXPECT_TRUE(jxl::jpeg::DecodeImageJPG( + jxl::Span(jpeg_bytes.data(), jpeg_bytes.size()), &io)); + EXPECT_TRUE(EncodeJPEGData(*io.Main().jpeg_data, &jpeg_data)); + io.metadata.m.xyb_encoded = false; +#else // JPEGXL_ENABLE_JPEG + JXL_ABORT( + "unable to create reconstructible JPEG without JPEG support enabled"); +#endif // JPEGXL_ENABLE_JPEG + } + if (add_preview) { + io.preview_frame = io.Main().Copy(); + io.preview_frame.ShrinkTo(xsize / 7, ysize / 7); + io.metadata.m.have_preview = true; + EXPECT_TRUE(io.metadata.m.preview_size.Set(io.preview_frame.xsize(), + io.preview_frame.ysize())); + } + io.metadata.m.orientation = orientation; + AuxOut aux_out; + PaddedBytes compressed; + PassesEncoderState enc_state; + EXPECT_TRUE( + EncodeFile(cparams, &io, &enc_state, &compressed, &aux_out, &pool)); + if (add_container != kCSBF_None) { + // Header with signature box and ftyp box. + const uint8_t header[] = {0, 0, 0, 0xc, 0x4a, 0x58, 0x4c, 0x20, + 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14, + 0x66, 0x74, 0x79, 0x70, 0x6a, 0x78, 0x6c, 0x20, + 0, 0, 0, 0, 0x6a, 0x78, 0x6c, 0x20}; + // Unknown box, could be a box added by user, decoder must be able to skip + // over it. Type is set to 'unkn', size to 24, contents to 16 0's. + const uint8_t unknown[] = {0, 0, 0, 0x18, 0x75, 0x6e, 0x6b, 0x6e, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + // same as the unknown box, but with size set to 0, this can only be a final + // box + const uint8_t unknown_end[] = {0, 0, 0, 0, 0x75, 0x6e, 0x6b, 0x6e, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + + bool is_multi = add_container == kCSBF_Multi || + add_container == kCSBF_Multi_Zero_Terminated || + add_container == kCSBF_Multi_Other_Terminated || + add_container == kCSBF_Multi_Other_Zero_Terminated || + add_container == kCSBF_Multi_First_Empty; + + if (is_multi) { + size_t third = compressed.size() / 3; + std::vector compressed0(compressed.data(), + compressed.data() + third); + std::vector compressed1(compressed.data() + third, + compressed.data() + 2 * third); + std::vector compressed2(compressed.data() + 2 * third, + compressed.data() + compressed.size()); + + PaddedBytes c; + c.append(header, header + sizeof(header)); + if (jpeg_codestream != nullptr) { + jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, + &c); + c.append(jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); + } + uint32_t jxlp_index = 0; + if (add_container == kCSBF_Multi_First_Empty) { + // Dummy (empty) codestream part + AppendU32BE(12, &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++, &c); + } + // First codestream part + AppendU32BE(compressed0.size() + 12, &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++, &c); + c.append(compressed0.data(), compressed0.data() + compressed0.size()); + // A few non-codestream boxes in between + c.append(unknown, unknown + sizeof(unknown)); + c.append(unknown, unknown + sizeof(unknown)); + // Dummy (empty) codestream part + AppendU32BE(12, &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++, &c); + // Second codestream part + AppendU32BE(compressed1.size() + 12, &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++, &c); + c.append(compressed1.data(), compressed1.data() + compressed1.size()); + // Third codestream part + AppendU32BE(add_container == kCSBF_Multi ? (compressed2.size() + 12) : 0, + &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('p'); + AppendU32BE(jxlp_index++ | 0x80000000, &c); + c.append(compressed2.data(), compressed2.data() + compressed2.size()); + if (add_container == kCSBF_Multi_Other_Terminated) { + c.append(unknown, unknown + sizeof(unknown)); + } + if (add_container == kCSBF_Multi_Other_Zero_Terminated) { + c.append(unknown_end, unknown_end + sizeof(unknown_end)); + } + compressed.swap(c); + } else { + PaddedBytes c; + c.append(header, header + sizeof(header)); + if (jpeg_codestream != nullptr) { + jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, + &c); + c.append(jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); + } + AppendU32BE(add_container == kCSBF_Single_Zero_Terminated + ? 0 + : (compressed.size() + 8), + &c); + c.push_back('j'); + c.push_back('x'); + c.push_back('l'); + c.push_back('c'); + c.append(compressed.data(), compressed.data() + compressed.size()); + if (add_container == kCSBF_Single_other) { + c.append(unknown, unknown + sizeof(unknown)); + } + compressed.swap(c); + } + } + + return compressed; +} + +// Decodes one-shot with the API for non-streaming decoding tests. +std::vector DecodeWithAPI(JxlDecoder* dec, + Span compressed, + const JxlPixelFormat& format, + bool use_callback, bool set_buffer_early, + bool use_resizable_runner) { + JxlThreadParallelRunnerPtr runner_fixed; + JxlResizableParallelRunnerPtr runner_resizable; + JxlParallelRunner runner_fn; + void* runner; + + if (use_resizable_runner) { + runner_resizable = JxlResizableParallelRunnerMake(nullptr); + runner = runner_resizable.get(); + runner_fn = JxlResizableParallelRunner; + } else { + runner_fixed = JxlThreadParallelRunnerMake( + nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + runner = runner_fixed.get(); + runner_fn = JxlThreadParallelRunner; + } + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, runner_fn, runner)); + + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | (set_buffer_early ? JXL_DEC_FRAME : 0) | + JXL_DEC_PREVIEW_IMAGE | JXL_DEC_FULL_IMAGE)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, compressed.data(), compressed.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + if (use_resizable_runner) { + JxlResizableParallelRunnerSetThreads( + runner, + JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); + } + + std::vector pixels(buffer_size); + size_t bytes_per_pixel = + format.num_channels * GetDataBits(format.data_type) / jxl::kBitsPerByte; + size_t stride = bytes_per_pixel * info.xsize; + if (format.align > 1) { + stride = jxl::DivCeil(stride, format.align) * format.align; + } + auto callback = [&](size_t x, size_t y, size_t num_pixels, + const void* pixels_row) { + memcpy(pixels.data() + stride * y + bytes_per_pixel * x, pixels_row, + num_pixels * bytes_per_pixel); + }; + + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + + std::vector preview; + if (status == JXL_DEC_NEED_PREVIEW_OUT_BUFFER) { + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)); + preview.resize(buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreviewOutBuffer(dec, &format, preview.data(), + preview.size())); + EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, JxlDecoderProcessInput(dec)); + + status = JxlDecoderProcessInput(dec); + } + + if (set_buffer_early) { + EXPECT_EQ(JXL_DEC_FRAME, status); + } else { + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, status); + } + + if (use_callback) { + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutCallback( + dec, &format, + [](void* opaque, size_t x, size_t y, size_t xsize, + const void* pixels_row) { + auto cb = static_cast(opaque); + (*cb)(x, y, xsize, pixels_row); + }, + /*opaque=*/&callback)); + } else { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + } + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + // After the full image was output, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + return pixels; +} + +// Decodes one-shot with the API for non-streaming decoding tests. +std::vector DecodeWithAPI(Span compressed, + const JxlPixelFormat& format, + bool use_callback, bool set_buffer_early, + bool use_resizable_runner) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + std::vector pixels = + DecodeWithAPI(dec, compressed, format, use_callback, set_buffer_early, + use_resizable_runner); + JxlDecoderDestroy(dec); + return pixels; +} + +} // namespace +} // namespace jxl + +namespace { + +// Procedure to convert pixels to double precision, not efficient, but +// well-controlled for testing. It uses double, to be able to represent all +// precisions needed for the maximum data types the API supports: uint32_t +// integers, and, single precision float. The values are in range 0-1 for SDR. +std::vector ConvertToRGBA32(const uint8_t* pixels, size_t xsize, + size_t ysize, + const JxlPixelFormat& format) { + std::vector result(xsize * ysize * 4); + size_t num_channels = format.num_channels; + bool gray = num_channels == 1 || num_channels == 2; + bool alpha = num_channels == 2 || num_channels == 4; + + size_t stride = + xsize * jxl::DivCeil(GetDataBits(format.data_type) * num_channels, + jxl::kBitsPerByte); + if (format.align > 1) stride = jxl::RoundUpTo(stride, format.align); + + if (format.data_type == JXL_TYPE_BOOLEAN) { + for (size_t y = 0; y < ysize; ++y) { + jxl::BitReader br(jxl::Span(pixels + stride * y, stride)); + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + double r = br.ReadBits(1); + double g = gray ? r : br.ReadBits(1); + double b = gray ? r : br.ReadBits(1); + double a = alpha ? br.ReadBits(1) : 1; + result[j + 0] = r; + result[j + 1] = g; + result[j + 2] = b; + result[j + 3] = a; + } + JXL_CHECK(br.Close()); + } + } else if (format.data_type == JXL_TYPE_UINT8) { + double mul = 1.0 / 255.0; // Multiplier to bring to 0-1.0 range + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + size_t i = y * stride + x * num_channels; + double r = pixels[i]; + double g = gray ? r : pixels[i + 1]; + double b = gray ? r : pixels[i + 2]; + double a = alpha ? pixels[i + num_channels - 1] : 255; + result[j + 0] = r * mul; + result[j + 1] = g * mul; + result[j + 2] = b * mul; + result[j + 3] = a * mul; + } + } + } else if (format.data_type == JXL_TYPE_UINT16) { + double mul = 1.0 / 65535.0; // Multiplier to bring to 0-1.0 range + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + size_t i = y * stride + x * num_channels * 2; + double r, g, b, a; + if (format.endianness == JXL_BIG_ENDIAN) { + r = (pixels[i + 0] << 8) + pixels[i + 1]; + g = gray ? r : (pixels[i + 2] << 8) + pixels[i + 3]; + b = gray ? r : (pixels[i + 4] << 8) + pixels[i + 5]; + a = alpha ? (pixels[i + num_channels * 2 - 2] << 8) + + pixels[i + num_channels * 2 - 1] + : 65535; + } else { + r = (pixels[i + 1] << 8) + pixels[i + 0]; + g = gray ? r : (pixels[i + 3] << 8) + pixels[i + 2]; + b = gray ? r : (pixels[i + 5] << 8) + pixels[i + 4]; + a = alpha ? (pixels[i + num_channels * 2 - 1] << 8) + + pixels[i + num_channels * 2 - 2] + : 65535; + } + result[j + 0] = r * mul; + result[j + 1] = g * mul; + result[j + 2] = b * mul; + result[j + 3] = a * mul; + } + } + } else if (format.data_type == JXL_TYPE_UINT32) { + double mul = 1.0 / 4294967295.0; // Multiplier to bring to 0-1.0 range + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + size_t i = y * stride + x * num_channels * 4; + double r, g, b, a; + if (format.endianness == JXL_BIG_ENDIAN) { + r = LoadBE32(pixels + i); + g = gray ? r : LoadBE32(pixels + i + 4); + b = gray ? r : LoadBE32(pixels + i + 8); + a = alpha ? LoadBE32(pixels + i + num_channels * 2 - 4) : 4294967295; + + } else { + r = LoadLE32(pixels + i); + g = gray ? r : LoadLE32(pixels + i + 4); + b = gray ? r : LoadLE32(pixels + i + 8); + a = alpha ? LoadLE32(pixels + i + num_channels * 2 - 4) : 4294967295; + } + result[j + 0] = r * mul; + result[j + 1] = g * mul; + result[j + 2] = b * mul; + result[j + 3] = a * mul; + } + } + } else if (format.data_type == JXL_TYPE_FLOAT) { + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + size_t i = y * stride + x * num_channels * 4; + double r, g, b, a; + if (format.endianness == JXL_BIG_ENDIAN) { + r = LoadBEFloat(pixels + i); + g = gray ? r : LoadBEFloat(pixels + i + 4); + b = gray ? r : LoadBEFloat(pixels + i + 8); + a = alpha ? LoadBEFloat(pixels + i + num_channels * 4 - 4) : 1.0; + } else { + r = LoadLEFloat(pixels + i); + g = gray ? r : LoadLEFloat(pixels + i + 4); + b = gray ? r : LoadLEFloat(pixels + i + 8); + a = alpha ? LoadLEFloat(pixels + i + num_channels * 4 - 4) : 1.0; + } + result[j + 0] = r; + result[j + 1] = g; + result[j + 2] = b; + result[j + 3] = a; + } + } + } else if (format.data_type == JXL_TYPE_FLOAT16) { + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t j = (y * xsize + x) * 4; + size_t i = y * stride + x * num_channels * 2; + double r, g, b, a; + if (format.endianness == JXL_BIG_ENDIAN) { + r = LoadBEFloat16(pixels + i); + g = gray ? r : LoadBEFloat16(pixels + i + 2); + b = gray ? r : LoadBEFloat16(pixels + i + 4); + a = alpha ? LoadBEFloat16(pixels + i + num_channels * 2 - 2) : 1.0; + } else { + r = LoadLEFloat16(pixels + i); + g = gray ? r : LoadLEFloat16(pixels + i + 2); + b = gray ? r : LoadLEFloat16(pixels + i + 4); + a = alpha ? LoadLEFloat16(pixels + i + num_channels * 2 - 2) : 1.0; + } + result[j + 0] = r; + result[j + 1] = g; + result[j + 2] = b; + result[j + 3] = a; + } + } + } else { + JXL_ASSERT(false); // Unsupported type + } + return result; +} + +// Returns amount of pixels which differ between the two pictures. Image b is +// the image after roundtrip after roundtrip, image a before roundtrip. There +// are more strict requirements for the alpha channel and grayscale values of +// the output image. +size_t ComparePixels(const uint8_t* a, const uint8_t* b, size_t xsize, + size_t ysize, const JxlPixelFormat& format_a, + const JxlPixelFormat& format_b) { + // Convert both images to equal full precision for comparison. + std::vector a_full = ConvertToRGBA32(a, xsize, ysize, format_a); + std::vector b_full = ConvertToRGBA32(b, xsize, ysize, format_b); + bool gray_a = format_a.num_channels < 3; + bool gray_b = format_b.num_channels < 3; + bool alpha_a = !(format_a.num_channels & 1); + bool alpha_b = !(format_b.num_channels & 1); + size_t bits_a = GetPrecision(format_a.data_type); + size_t bits_b = GetPrecision(format_b.data_type); + size_t bits = std::min(bits_a, bits_b); + // How much distance is allowed in case of pixels with lower bit depths, given + // that the double precision float images use range 0-1.0. + // E.g. in case of 1-bit this is 0.5 since 0.499 must map to 0 and 0.501 must + // map to 1. + double precision = 0.5 / ((1ull << bits) - 1ull); + if (format_a.data_type == JXL_TYPE_FLOAT16 || + format_b.data_type == JXL_TYPE_FLOAT16) { + // Lower the precision for float16, because it currently looks like the + // scalar and wasm implementations of hwy have 1 less bit of precision + // than the x86 implementations. + // TODO(lode): Set the required precision back to 11 bits when possible. + precision = 0.5 / ((1ull << (bits - 1)) - 1ull); + } + size_t numdiff = 0; + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + size_t i = (y * xsize + x) * 4; + bool ok = true; + if (gray_a || gray_b) { + if (!Near(a_full[i + 0], b_full[i + 0], precision)) ok = false; + // If the input was grayscale and the output not, then the output must + // have all channels equal. + if (gray_a && b_full[i + 0] != b_full[i + 1] && + b_full[i + 2] != b_full[i + 2]) { + ok = false; + } + } else { + if (!Near(a_full[i + 0], b_full[i + 0], precision) || + !Near(a_full[i + 1], b_full[i + 1], precision) || + !Near(a_full[i + 2], b_full[i + 2], precision)) { + ok = false; + } + } + if (alpha_a && alpha_b) { + if (!Near(a_full[i + 3], b_full[i + 3], precision)) ok = false; + } else { + // If the input had no alpha channel, the output should be opaque + // after roundtrip. + if (alpha_b && !Near(1.0, b_full[i + 3], precision)) ok = false; + } + if (!ok) numdiff++; + } + } + return numdiff; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +TEST(DecodeTest, JxlSignatureCheckTest) { + std::vector>> tests = { + // No JPEGXL header starts with 'a'. + {JXL_SIG_INVALID, {'a'}}, + {JXL_SIG_INVALID, {'a', 'b', 'c', 'd', 'e', 'f'}}, + + // Empty file is not enough bytes. + {JXL_SIG_NOT_ENOUGH_BYTES, {}}, + + // JPEGXL headers. + {JXL_SIG_NOT_ENOUGH_BYTES, {0xff}}, // Part of a signature. + {JXL_SIG_INVALID, {0xff, 0xD8}}, // JPEG-1 + {JXL_SIG_CODESTREAM, {0xff, 0x0a}}, + + // JPEGXL container file. + {JXL_SIG_CONTAINER, + {0, 0, 0, 0xc, 'J', 'X', 'L', ' ', 0xD, 0xA, 0x87, 0xA}}, + // Ending with invalid byte. + {JXL_SIG_INVALID, {0, 0, 0, 0xc, 'J', 'X', 'L', ' ', 0xD, 0xA, 0x87, 0}}, + // Part of signature. + {JXL_SIG_NOT_ENOUGH_BYTES, + {0, 0, 0, 0xc, 'J', 'X', 'L', ' ', 0xD, 0xA, 0x87}}, + {JXL_SIG_NOT_ENOUGH_BYTES, {0}}, + }; + for (const auto& test : tests) { + EXPECT_EQ(test.first, + JxlSignatureCheck(test.second.data(), test.second.size())) + << "Where test data is " << ::testing::PrintToString(test.second); + } +} + +TEST(DecodeTest, DefaultAllocTest) { + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_NE(nullptr, dec); + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, CustomAllocTest) { + struct CalledCounters { + int allocs = 0; + int frees = 0; + } counters; + + JxlMemoryManager mm; + mm.opaque = &counters; + mm.alloc = [](void* opaque, size_t size) { + reinterpret_cast(opaque)->allocs++; + return malloc(size); + }; + mm.free = [](void* opaque, void* address) { + reinterpret_cast(opaque)->frees++; + free(address); + }; + + JxlDecoder* dec = JxlDecoderCreate(&mm); + EXPECT_NE(nullptr, dec); + EXPECT_LE(1, counters.allocs); + EXPECT_EQ(0, counters.frees); + JxlDecoderDestroy(dec); + EXPECT_LE(1, counters.frees); +} + +// TODO(lode): add multi-threaded test when multithreaded pixel decoding from +// API is implemented. +TEST(DecodeTest, DefaultParallelRunnerTest) { + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_NE(nullptr, dec); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, nullptr, nullptr)); + JxlDecoderDestroy(dec); +} + +// Creates the header of a JPEG XL file with various custom parameters for +// testing. +// xsize, ysize: image dimensions to store in the SizeHeader, max 512. +// bits_per_sample, orientation: a selection of header parameters to test with. +// orientation: image orientation to set in the metadata +// alpha_bits: if non-0, alpha extra channel bits to set in the metadata. Also +// gives the alpha channel the name "alpha_test" +// have_container: add box container format around the codestream. +// metadata_default: if true, ImageMetadata is set to default and +// bits_per_sample, orientation and alpha_bits are ignored. +// insert_box: insert an extra box before the codestream box, making the header +// farther away from the front than is ideal. Only used if have_container. +std::vector GetTestHeader(size_t xsize, size_t ysize, + size_t bits_per_sample, size_t orientation, + size_t alpha_bits, bool xyb_encoded, + bool have_container, bool metadata_default, + bool insert_extra_box, + const jxl::PaddedBytes& icc_profile) { + jxl::BitWriter writer; + jxl::BitWriter::Allotment allotment(&writer, 65536); // Large enough + + if (have_container) { + const std::vector signature_box = {0, 0, 0, 0xc, 'J', 'X', + 'L', ' ', 0xd, 0xa, 0x87, 0xa}; + const std::vector filetype_box = { + 0, 0, 0, 0x14, 'f', 't', 'y', 'p', 'j', 'x', + 'l', ' ', 0, 0, 0, 0, 'j', 'x', 'l', ' '}; + const std::vector extra_box_header = {0, 0, 0, 0xff, + 't', 'e', 's', 't'}; + // Beginning of codestream box, with an arbitrary size certainly large + // enough to contain the header + const std::vector codestream_box_header = {0, 0, 0, 0xff, + 'j', 'x', 'l', 'c'}; + + for (size_t i = 0; i < signature_box.size(); i++) { + writer.Write(8, signature_box[i]); + } + for (size_t i = 0; i < filetype_box.size(); i++) { + writer.Write(8, filetype_box[i]); + } + if (insert_extra_box) { + for (size_t i = 0; i < extra_box_header.size(); i++) { + writer.Write(8, extra_box_header[i]); + } + for (size_t i = 0; i < 255 - 8; i++) { + writer.Write(8, 0); + } + } + for (size_t i = 0; i < codestream_box_header.size(); i++) { + writer.Write(8, codestream_box_header[i]); + } + } + + // JXL signature + writer.Write(8, 0xff); + writer.Write(8, 0x0a); + + // SizeHeader + jxl::CodecMetadata metadata; + EXPECT_TRUE(metadata.size.Set(xsize, ysize)); + EXPECT_TRUE(WriteSizeHeader(metadata.size, &writer, 0, nullptr)); + + if (!metadata_default) { + metadata.m.SetUintSamples(bits_per_sample); + metadata.m.orientation = orientation; + metadata.m.SetAlphaBits(alpha_bits); + metadata.m.xyb_encoded = xyb_encoded; + if (alpha_bits != 0) { + metadata.m.extra_channel_info[0].name = "alpha_test"; + } + } + + if (!icc_profile.empty()) { + jxl::PaddedBytes copy = icc_profile; + EXPECT_TRUE(metadata.m.color_encoding.SetICC(std::move(copy))); + } + + EXPECT_TRUE(jxl::Bundle::Write(metadata.m, &writer, 0, nullptr)); + metadata.transform_data.nonserialized_xyb_encoded = metadata.m.xyb_encoded; + EXPECT_TRUE(jxl::Bundle::Write(metadata.transform_data, &writer, 0, nullptr)); + + if (!icc_profile.empty()) { + EXPECT_TRUE(metadata.m.color_encoding.WantICC()); + EXPECT_TRUE(jxl::WriteICC(icc_profile, &writer, 0, nullptr)); + } + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + return std::vector( + writer.GetSpan().data(), + writer.GetSpan().data() + writer.GetSpan().size()); +} + +TEST(DecodeTest, BasicInfoTest) { + size_t xsize[2] = {50, 33}; + size_t ysize[2] = {50, 77}; + size_t bits_per_sample[2] = {8, 23}; + size_t orientation[2] = {3, 5}; + size_t alpha_bits[2] = {0, 8}; + JXL_BOOL have_container[2] = {0, 1}; + bool xyb_encoded = false; + + std::vector> test_samples; + // Test with direct codestream + test_samples.push_back(GetTestHeader( + xsize[0], ysize[0], bits_per_sample[0], orientation[0], alpha_bits[0], + xyb_encoded, have_container[0], /*metadata_default=*/false, + /*insert_extra_box=*/false, {})); + // Test with container and different parameters + test_samples.push_back(GetTestHeader( + xsize[1], ysize[1], bits_per_sample[1], orientation[1], alpha_bits[1], + xyb_encoded, have_container[1], /*metadata_default=*/false, + /*insert_extra_box=*/false, {})); + + for (size_t i = 0; i < test_samples.size(); ++i) { + const std::vector& data = test_samples[i]; + // Test decoding too small header first, until we reach the final byte. + for (size_t size = 0; size <= data.size(); ++size) { + // Test with a new decoder for each tested byte size. + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO)); + const uint8_t* next_in = data.data(); + size_t avail_in = size; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + + JxlBasicInfo info; + bool have_basic_info = !JxlDecoderGetBasicInfo(dec, &info); + + if (size == data.size()) { + EXPECT_EQ(JXL_DEC_BASIC_INFO, status); + + // All header bytes given so the decoder must have the basic info. + EXPECT_EQ(true, have_basic_info); + EXPECT_EQ(have_container[i], info.have_container); + EXPECT_EQ(alpha_bits[i], info.alpha_bits); + // Orientations 5..8 swap the dimensions + if (orientation[i] >= 5) { + EXPECT_EQ(xsize[i], info.ysize); + EXPECT_EQ(ysize[i], info.xsize); + } else { + EXPECT_EQ(xsize[i], info.xsize); + EXPECT_EQ(ysize[i], info.ysize); + } + // The API should set the orientation to identity by default since it + // already applies the transformation internally by default. + EXPECT_EQ(1u, info.orientation); + + EXPECT_EQ(3u, info.num_color_channels); + + if (alpha_bits[i] != 0) { + // Expect an extra channel + EXPECT_EQ(1u, info.num_extra_channels); + JxlExtraChannelInfo extra; + EXPECT_EQ(0, JxlDecoderGetExtraChannelInfo(dec, 0, &extra)); + EXPECT_EQ(alpha_bits[i], extra.bits_per_sample); + EXPECT_EQ(JXL_CHANNEL_ALPHA, extra.type); + EXPECT_EQ(0, extra.alpha_premultiplied); + // Verify the name "alpha_test" given to the alpha channel + EXPECT_EQ(10u, extra.name_length); + char name[11]; + EXPECT_EQ(0, + JxlDecoderGetExtraChannelName(dec, 0, name, sizeof(name))); + EXPECT_EQ(std::string("alpha_test"), std::string(name)); + } else { + EXPECT_EQ(0u, info.num_extra_channels); + } + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + } else { + // If we did not give the full header, the basic info should not be + // available. Allow a few bytes of slack due to some bits for default + // opsinmatrix/extension bits. + if (size + 2 < data.size()) { + EXPECT_EQ(false, have_basic_info); + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, status); + } + } + + // Test that decoder doesn't allow setting a setting required at beginning + // unless it's reset + EXPECT_EQ(JXL_DEC_ERROR, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO)); + JxlDecoderReset(dec); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO)); + + JxlDecoderDestroy(dec); + } + } +} + +TEST(DecodeTest, BufferSizeTest) { + size_t xsize = 33; + size_t ysize = 77; + size_t bits_per_sample = 8; + size_t orientation = 1; + size_t alpha_bits = 8; + bool have_container = false; + bool xyb_encoded = false; + + std::vector header = + GetTestHeader(xsize, ysize, bits_per_sample, orientation, alpha_bits, + xyb_encoded, have_container, /*metadata_default=*/false, + /*insert_extra_box=*/false, {}); + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO)); + const uint8_t* next_in = header.data(); + size_t avail_in = header.size(); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + EXPECT_EQ(JXL_DEC_BASIC_INFO, status); + + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + + JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + size_t image_out_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &image_out_size)); + EXPECT_EQ(xsize * ysize * 4, image_out_size); + + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, BasicInfoSizeHintTest) { + // Test on a file where the size hint is too small initially due to inserting + // a box before the codestream (something that is normally not recommended) + size_t xsize = 50; + size_t ysize = 50; + size_t bits_per_sample = 16; + size_t orientation = 1; + size_t alpha_bits = 0; + bool xyb_encoded = false; + std::vector data = GetTestHeader( + xsize, ysize, bits_per_sample, orientation, alpha_bits, xyb_encoded, + /*have_container=*/true, /*metadata_default=*/false, + /*insert_extra_box=*/true, {}); + + JxlDecoderStatus status; + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO)); + + size_t hint0 = JxlDecoderSizeHintBasicInfo(dec); + // Test that the test works as intended: we construct a file on purpose to + // be larger than the first hint by having that extra box. + EXPECT_LT(hint0, data.size()); + const uint8_t* next_in = data.data(); + // Do as if we have only as many bytes as indicated by the hint available + size_t avail_in = std::min(hint0, data.size()); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + status = JxlDecoderProcessInput(dec); + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, status); + // Basic info cannot be available yet due to the extra inserted box. + EXPECT_EQ(false, !JxlDecoderGetBasicInfo(dec, nullptr)); + + size_t num_read = avail_in - JxlDecoderReleaseInput(dec); + EXPECT_LT(num_read, data.size()); + + size_t hint1 = JxlDecoderSizeHintBasicInfo(dec); + // The hint must be larger than the previous hint (taking already processed + // bytes into account, the hint is a hint for the next avail_in) since the + // decoder now knows there is a box in between. + EXPECT_GT(hint1 + num_read, hint0); + avail_in = std::min(hint1, data.size() - num_read); + next_in += num_read; + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + status = JxlDecoderProcessInput(dec); + EXPECT_EQ(JXL_DEC_BASIC_INFO, status); + JxlBasicInfo info; + // We should have the basic info now, since we only added one box in-between, + // and the decoder should have known its size, its implementation can return + // a correct hint. + EXPECT_EQ(true, !JxlDecoderGetBasicInfo(dec, &info)); + + // Also test if the basic info is correct. + EXPECT_EQ(1, info.have_container); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + EXPECT_EQ(orientation, info.orientation); + EXPECT_EQ(bits_per_sample, info.bits_per_sample); + + JxlDecoderDestroy(dec); +} + +std::vector GetIccTestHeader(const jxl::PaddedBytes& icc_profile, + bool xyb_encoded) { + size_t xsize = 50; + size_t ysize = 50; + size_t bits_per_sample = 16; + size_t orientation = 1; + size_t alpha_bits = 0; + return GetTestHeader(xsize, ysize, bits_per_sample, orientation, alpha_bits, + xyb_encoded, + /*have_container=*/false, /*metadata_default=*/false, + /*insert_extra_box=*/false, icc_profile); +} + +// Tests the case where pixels and metadata ICC profile are the same +TEST(DecodeTest, IccProfileTestOriginal) { + jxl::PaddedBytes icc_profile = GetIccTestProfile(); + bool xyb_encoded = false; + std::vector data = GetIccTestHeader(icc_profile, xyb_encoded); + JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data(), data.size())); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + + // Expect the opposite of xyb_encoded for uses_original_profile + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(JXL_TRUE, info.uses_original_profile); + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + + // the encoded color profile expected to be not available, since the image + // has an ICC profile instead + EXPECT_EQ(JXL_DEC_ERROR, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr)); + + size_t dec_profile_size; + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &dec_profile_size)); + + // Check that can get return status with NULL size + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr)); + + // The profiles must be equal. This requires they have equal size, and if + // they do, we can get the profile and compare the contents. + EXPECT_EQ(icc_profile.size(), dec_profile_size); + if (icc_profile.size() == dec_profile_size) { + jxl::PaddedBytes icc_profile2(icc_profile.size()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + icc_profile2.data(), icc_profile2.size())); + EXPECT_EQ(icc_profile, icc_profile2); + } + + // the data is not xyb_encoded, so same result expected for the pixel data + // color profile + EXPECT_EQ(JXL_DEC_ERROR, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, nullptr)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, + &dec_profile_size)); + EXPECT_EQ(icc_profile.size(), dec_profile_size); + + JxlDecoderDestroy(dec); +} + +// Tests the case where pixels and metadata ICC profile are different +TEST(DecodeTest, IccProfileTestXybEncoded) { + jxl::PaddedBytes icc_profile = GetIccTestProfile(); + bool xyb_encoded = true; + std::vector data = GetIccTestHeader(icc_profile, xyb_encoded); + JxlPixelFormat format = {4, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + JxlPixelFormat format_int = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data(), data.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + + // Expect the opposite of xyb_encoded for uses_original_profile + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(JXL_FALSE, info.uses_original_profile); + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + + // the encoded color profile expected to be not available, since the image + // has an ICC profile instead + EXPECT_EQ(JXL_DEC_ERROR, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr)); + + // Check that can get return status with NULL size + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, nullptr)); + + size_t dec_profile_size; + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &dec_profile_size)); + + // The profiles must be equal. This requires they have equal size, and if + // they do, we can get the profile and compare the contents. + EXPECT_EQ(icc_profile.size(), dec_profile_size); + if (icc_profile.size() == dec_profile_size) { + jxl::PaddedBytes icc_profile2(icc_profile.size()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, + icc_profile2.data(), icc_profile2.size())); + EXPECT_EQ(icc_profile, icc_profile2); + } + + // Data is xyb_encoded, so the data profile is a different profile, encoded + // as structured profile. + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, nullptr)); + JxlColorEncoding pixel_encoding; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, &pixel_encoding)); + EXPECT_EQ(JXL_PRIMARIES_SRGB, pixel_encoding.primaries); + // The API returns LINEAR by default when the colorspace cannot be represented + // by enum values. + EXPECT_EQ(JXL_TRANSFER_FUNCTION_LINEAR, pixel_encoding.transfer_function); + + // Test the same but with integer format. + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile( + dec, &format_int, JXL_COLOR_PROFILE_TARGET_DATA, &pixel_encoding)); + EXPECT_EQ(JXL_PRIMARIES_SRGB, pixel_encoding.primaries); + EXPECT_EQ(JXL_TRANSFER_FUNCTION_LINEAR, pixel_encoding.transfer_function); + + // Test after setting the preferred color profile to non-linear sRGB: + // for XYB images with ICC profile, this setting is expected to take effect. + jxl::ColorEncoding temp_jxl_srgb = jxl::ColorEncoding::SRGB(false); + JxlColorEncoding pixel_encoding_srgb; + ConvertInternalToExternalColorEncoding(temp_jxl_srgb, &pixel_encoding_srgb); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetPreferredColorProfile(dec, &pixel_encoding_srgb)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsEncodedProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, &pixel_encoding)); + EXPECT_EQ(JXL_TRANSFER_FUNCTION_SRGB, pixel_encoding.transfer_function); + + // The decoder can also output this as a generated ICC profile anyway, and + // we're certain that it will differ from the above defined profile since + // the sRGB data should not have swapped R/G/B primaries. + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, + &dec_profile_size)); + // We don't need to dictate exactly what size the generated ICC profile + // must be (since there are many ways to represent the same color space), + // but it should not be zero. + EXPECT_NE(0u, dec_profile_size); + if (0 != dec_profile_size) { + jxl::PaddedBytes icc_profile2(dec_profile_size); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetColorAsICCProfile( + dec, &format, JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile2.data(), icc_profile2.size())); + // expected not equal + EXPECT_NE(icc_profile, icc_profile2); + } + + JxlDecoderDestroy(dec); +} + +// Test decoding ICC from partial files byte for byte. +// This test must pass also if JXL_CRASH_ON_ERROR is enabled, that is, the +// decoding of the ANS histogram and stream of the encoded ICC profile must also +// handle the case of not enough input bytes with StatusCode::kNotEnoughBytes +// rather than fatal error status codes. +TEST(DecodeTest, ICCPartialTest) { + jxl::PaddedBytes icc_profile = GetIccTestProfile(); + std::vector data = GetIccTestHeader(icc_profile, false); + JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + const uint8_t* next_in = data.data(); + size_t avail_in = 0; + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING)); + + bool seen_basic_info = false; + bool seen_color_encoding = false; + size_t total_size = 0; + + for (;;) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + size_t remaining = JxlDecoderReleaseInput(dec); + EXPECT_LE(remaining, avail_in); + next_in += avail_in - remaining; + avail_in = remaining; + if (status == JXL_DEC_NEED_MORE_INPUT) { + if (total_size >= data.size()) { + // End of partial codestream with codestrema headers and ICC profile + // reached, it should not require more input since full image is not + // requested + FAIL(); + break; + } + size_t increment = 1; + if (total_size + increment > data.size()) { + increment = data.size() - total_size; + } + total_size += increment; + avail_in += increment; + } else if (status == JXL_DEC_BASIC_INFO) { + EXPECT_FALSE(seen_basic_info); + seen_basic_info = true; + } else if (status == JXL_DEC_COLOR_ENCODING) { + EXPECT_TRUE(seen_basic_info); + EXPECT_FALSE(seen_color_encoding); + seen_color_encoding = true; + + // Sanity check that the ICC profile was decoded correctly + size_t dec_profile_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize(dec, &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + &dec_profile_size)); + EXPECT_EQ(icc_profile.size(), dec_profile_size); + + } else if (status == JXL_DEC_SUCCESS) { + EXPECT_TRUE(seen_color_encoding); + break; + } else { + // We do not expect any other events or errors + FAIL(); + break; + } + } + + EXPECT_TRUE(seen_basic_info); + EXPECT_TRUE(seen_color_encoding); + + JxlDecoderDestroy(dec); +} + +struct PixelTestConfig { + // Input image definition. + bool grayscale; + bool include_alpha; + size_t xsize; + size_t ysize; + bool add_preview; + // Output format. + JxlEndianness endianness; + JxlDataType data_type; + uint32_t output_channels; + // Container options. + CodeStreamBoxFormat add_container; + // Decoding mode. + bool use_callback; + bool set_buffer_early; + bool use_resizable_runner; + // Exif orientation, 1-8 + JxlOrientation orientation; + bool keep_orientation; +}; + +class DecodeTestParam : public ::testing::TestWithParam {}; + +TEST_P(DecodeTestParam, PixelTest) { + PixelTestConfig config = GetParam(); + JxlDecoder* dec = JxlDecoderCreate(NULL); + + if (config.keep_orientation) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetKeepOrientation(dec, JXL_TRUE)); + } + + size_t num_pixels = config.xsize * config.ysize; + uint32_t orig_channels = + (config.grayscale ? 1 : 3) + (config.include_alpha ? 1 : 0); + std::vector pixels = + jxl::test::GetSomeTestImage(config.xsize, config.ysize, orig_channels, 0); + JxlPixelFormat format_orig = {orig_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, + 0}; + jxl::CompressParams cparams; + // Lossless to verify pixels exactly after roundtrip. + cparams.SetLossless(); + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), config.xsize, + config.ysize, orig_channels, cparams, config.add_container, + config.orientation, config.add_preview); + + JxlPixelFormat format = {config.output_channels, config.data_type, + config.endianness, 0}; + + bool swap_xy = !config.keep_orientation && (config.orientation > 4); + size_t xsize = swap_xy ? config.ysize : config.xsize; + size_t ysize = swap_xy ? config.xsize : config.ysize; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, config.use_callback, config.set_buffer_early, + config.use_resizable_runner); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * config.output_channels * + GetDataBits(config.data_type) / jxl::kBitsPerByte, + pixels2.size()); + + // If an orientation transformation is expected, to compare the pixels, also + // apply this transformation to the original pixels. ConvertToExternal is + // used to achieve this, with a temporary conversion to CodecInOut and back. + if (config.orientation > 1 && !config.keep_orientation) { + jxl::Span bytes(pixels.data(), pixels.size()); + jxl::ColorEncoding color_encoding = + jxl::ColorEncoding::SRGB(config.grayscale); + + jxl::CodecInOut io; + if (config.include_alpha) io.metadata.m.SetAlphaBits(16); + io.SetSize(config.xsize, config.ysize); + + EXPECT_TRUE(ConvertFromExternal( + bytes, config.xsize, config.ysize, color_encoding, config.include_alpha, + /*alpha_is_premultiplied=*/false, 16, JXL_BIG_ENDIAN, + /*flipped_y=*/false, nullptr, &io.Main(), /*float_in=*/false)); + + for (size_t i = 0; i < pixels.size(); i++) pixels[i] = 0; + EXPECT_TRUE(ConvertToExternal( + io.Main(), 16, + /*float_out=*/false, orig_channels, JXL_BIG_ENDIAN, + xsize * 2 * orig_channels, nullptr, pixels.data(), pixels.size(), + nullptr, nullptr, static_cast(config.orientation))); + } + + EXPECT_EQ(0u, ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, + format_orig, format)); + + JxlDecoderDestroy(dec); +} + +std::vector GeneratePixelTests() { + std::vector all_tests; + struct ChannelInfo { + bool grayscale; + bool include_alpha; + size_t output_channels; + }; + ChannelInfo ch_info[] = { + {false, true, 4}, // RGBA -> RGBA + {true, false, 1}, // G -> G + {true, true, 1}, // GA -> G + {true, true, 2}, // GA -> GA + {false, false, 3}, // RGB -> RGB + {false, true, 3}, // RGBA -> RGB + {false, false, 4}, // RGB -> RGBA + }; + + struct OutputFormat { + JxlEndianness endianness; + JxlDataType data_type; + }; + OutputFormat out_formats[] = { + {JXL_NATIVE_ENDIAN, JXL_TYPE_UINT8}, + {JXL_LITTLE_ENDIAN, JXL_TYPE_UINT16}, + {JXL_BIG_ENDIAN, JXL_TYPE_UINT16}, + {JXL_NATIVE_ENDIAN, JXL_TYPE_FLOAT16}, + {JXL_LITTLE_ENDIAN, JXL_TYPE_FLOAT}, + {JXL_BIG_ENDIAN, JXL_TYPE_FLOAT}, + }; + + auto make_test = [&](ChannelInfo ch, size_t xsize, size_t ysize, bool preview, + CodeStreamBoxFormat box, JxlOrientation orientation, + bool keep_orientation, OutputFormat format, + bool use_callback, bool set_buffer_early, + bool resizable_runner) { + PixelTestConfig c; + c.grayscale = ch.grayscale; + c.include_alpha = ch.include_alpha; + c.add_preview = preview; + c.xsize = xsize; + c.ysize = ysize; + c.add_container = (CodeStreamBoxFormat)box; + c.output_channels = ch.output_channels; + c.data_type = format.data_type; + c.endianness = format.endianness; + c.use_callback = use_callback; + c.set_buffer_early = set_buffer_early; + c.use_resizable_runner = resizable_runner; + c.orientation = orientation; + c.keep_orientation = keep_orientation; + all_tests.push_back(c); + }; + + // Test output formats and methods. + for (ChannelInfo ch : ch_info) { + for (int use_callback = 0; use_callback <= 1; use_callback++) { + for (OutputFormat fmt : out_formats) { + make_test(ch, 301, 33, /*add_preview=*/false, + CodeStreamBoxFormat::kCSBF_None, JXL_ORIENT_IDENTITY, + /*keep_orientation=*/false, fmt, use_callback, + /*set_buffer_early=*/false, /*resizable_runner=*/false); + } + } + } + // Test codestream formats. + for (size_t box = 1; box < kCSBF_NUM_ENTRIES; ++box) { + make_test(ch_info[0], 77, 33, /*add_preview=*/false, + (CodeStreamBoxFormat)box, JXL_ORIENT_IDENTITY, + /*keep_orientation=*/false, out_formats[0], + /*use_callback=*/false, + /*set_buffer_early=*/false, /*resizable_runner=*/false); + } + // Test previews. + for (int add_preview = 0; add_preview <= 1; add_preview++) { + make_test(ch_info[0], 77, 33, add_preview, CodeStreamBoxFormat::kCSBF_None, + JXL_ORIENT_IDENTITY, /*keep_orientation=*/false, out_formats[0], + /*use_callback=*/false, /*set_buffer_early=*/false, + /*resizable_runner=*/false); + } + // Test setting buffers early. + make_test(ch_info[0], 300, 33, /*add_preview=*/false, + CodeStreamBoxFormat::kCSBF_None, JXL_ORIENT_IDENTITY, + /*keep_orientation=*/false, out_formats[0], + /*use_callback=*/false, /*set_buffer_early=*/true, + /*resizable_runner=*/false); + + // Test using the resizable runner + for (size_t i = 0; i < 4; i++) { + make_test(ch_info[0], 300 << i, 33 << i, /*add_preview=*/false, + CodeStreamBoxFormat::kCSBF_None, JXL_ORIENT_IDENTITY, + /*keep_orientation=*/false, out_formats[0], + /*use_callback=*/false, /*set_buffer_early=*/false, + /*resizable_runner=*/true); + } + + // Test orientations. + for (int orientation = 1; orientation <= 8; ++orientation) { + make_test(ch_info[0], 280, 12, /*add_preview=*/false, + CodeStreamBoxFormat::kCSBF_None, + static_cast(orientation), + /*keep_orientation=*/false, out_formats[0], + /*use_callback=*/false, /*set_buffer_early=*/true, + /*resizable_runner=*/false); + make_test(ch_info[0], 280, 12, /*add_preview=*/false, + CodeStreamBoxFormat::kCSBF_None, + static_cast(orientation), + /*keep_orientation=*/true, out_formats[0], + /*use_callback=*/false, /*set_buffer_early=*/true, + /*resizable_runner=*/false); + } + + return all_tests; +} + +std::ostream& operator<<(std::ostream& os, const PixelTestConfig& c) { + os << c.xsize << "x" << c.ysize; + const char* colors[] = {"", "G", "GA", "RGB", "RGBA"}; + os << colors[(c.grayscale ? 1 : 3) + (c.include_alpha ? 1 : 0)]; + os << "to"; + os << colors[c.output_channels]; + switch (c.data_type) { + case JXL_TYPE_UINT8: + os << "u8"; + break; + case JXL_TYPE_UINT16: + os << "u16"; + break; + case JXL_TYPE_FLOAT: + os << "f32"; + break; + case JXL_TYPE_FLOAT16: + os << "f16"; + break; + case JXL_TYPE_UINT32: + os << "u32"; + break; + case JXL_TYPE_BOOLEAN: + os << "b"; + break; + }; + if (GetDataBits(c.data_type) > jxl::kBitsPerByte) { + if (c.endianness == JXL_NATIVE_ENDIAN) { + // add nothing + } else if (c.endianness == JXL_BIG_ENDIAN) { + os << "BE"; + } else if (c.endianness == JXL_LITTLE_ENDIAN) { + os << "LE"; + } + } + if (c.add_container != CodeStreamBoxFormat::kCSBF_None) { + os << "Box"; + os << (size_t)c.add_container; + } + if (c.add_preview) os << "Preview"; + if (c.use_callback) os << "Callback"; + if (c.set_buffer_early) os << "EarlyBuffer"; + if (c.use_resizable_runner) os << "ResizableRunner"; + if (c.orientation != 1) os << "O" << c.orientation; + if (c.keep_orientation) os << "Keep"; + return os; +} + +std::string PixelTestDescription( + const testing::TestParamInfo& info) { + std::stringstream name; + name << info.param; + return name.str(); +} + +JXL_GTEST_INSTANTIATE_TEST_SUITE_P(DecodeTest, DecodeTestParam, + testing::ValuesIn(GeneratePixelTests()), + PixelTestDescription); + +TEST(DecodeTest, PixelTestWithICCProfileLossless) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + jxl::CompressParams cparams; + // Lossless to verify pixels exactly after roundtrip. + cparams.SetLossless(); + // For variation: some have container and no preview, others have preview + // and no container. + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 4, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false, true); + + for (uint32_t channels = 3; channels <= 4; ++channels) { + { + JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/false, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels, pixels2.size()); + EXPECT_EQ(0u, ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, + format_orig, format)); + } + { + JxlPixelFormat format = {channels, JXL_TYPE_UINT16, JXL_LITTLE_ENDIAN, 0}; + + // Test with the container for one of the pixel formats. + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/true, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels * 2, pixels2.size()); + EXPECT_EQ(0u, ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, + format_orig, format)); + } + + { + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/false, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); + EXPECT_EQ(0u, ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, + format_orig, format)); + } + } + + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, PixelTestWithICCProfileLossy) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + jxl::CompressParams cparams; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 3, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, + /*add_icc_profile=*/true); + uint32_t channels = 3; + + JxlPixelFormat format = {channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, 0}; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels * 4, pixels2.size()); + + // The input pixels use the profile matching GetIccTestProfile, since we set + // add_icc_profile for CreateTestJXLCodestream to true. + jxl::ColorEncoding color_encoding0; + EXPECT_TRUE(color_encoding0.SetICC(GetIccTestProfile())); + jxl::Span span0(pixels.data(), pixels.size()); + jxl::CodecInOut io0; + io0.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal( + span0, xsize, ysize, color_encoding0, + /*has_alpha=*/false, false, 16, format_orig.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, &io0.Main(), /*float_in=*/false)); + + // The output pixels are expected to be in the same colorspace as the input + // profile, as the profile can be represented by enum values. + jxl::ColorEncoding color_encoding1 = color_encoding0; + jxl::Span span1(pixels2.data(), pixels2.size()); + jxl::CodecInOut io1; + io1.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal( + span1, xsize, ysize, color_encoding1, + /*has_alpha=*/false, false, 32, format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, &io1.Main(), /*float_in=*/true)); + + jxl::ButteraugliParams ba; + EXPECT_LE(ButteraugliDistance(io0, io1, ba, /*distmap=*/nullptr, nullptr), + 2.4f); + + JxlDecoderDestroy(dec); +} + +// Tests the case of lossy sRGB image without alpha channel, decoded to RGB8 +// and to RGBA8 +TEST(DecodeTest, PixelTestOpaqueSrgbLossy) { + for (unsigned channels = 3; channels <= 4; channels++) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + + size_t xsize = 123, ysize = 77; + size_t num_pixels = xsize * ysize; + std::vector pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + jxl::CompressParams cparams; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 3, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, + /*add_icc_profile=*/false); + + JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/true, /*set_buffer_early=*/false, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels, pixels2.size()); + + // The input pixels use the profile matching GetIccTestProfile, since we set + // add_icc_profile for CreateTestJXLCodestream to true. + jxl::ColorEncoding color_encoding0 = jxl::ColorEncoding::SRGB(false); + jxl::Span span0(pixels.data(), pixels.size()); + jxl::CodecInOut io0; + io0.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal(span0, xsize, ysize, color_encoding0, + /*has_alpha=*/false, false, 16, + format_orig.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io0.Main(), /*float_in=*/false)); + + jxl::ColorEncoding color_encoding1 = jxl::ColorEncoding::SRGB(false); + jxl::Span span1(pixels2.data(), pixels2.size()); + jxl::CodecInOut io1; + if (channels == 4) { + io1.metadata.m.SetAlphaBits(8); + io1.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, + /*has_alpha=*/true, false, 8, + format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io1.Main(), /*float_in=*/false)); + io1.metadata.m.SetAlphaBits(0); + io1.Main().ClearExtraChannels(); + } else { + EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, + /*has_alpha=*/false, false, 8, + format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io1.Main(), /*float_in=*/false)); + } + + jxl::ButteraugliParams ba; + EXPECT_LE(ButteraugliDistance(io0, io1, ba, /*distmap=*/nullptr, nullptr), + 2.4f); + + JxlDecoderDestroy(dec); + } +} + +// Opaque image with noise enabled, decoded to RGB8 and RGBA8. +TEST(DecodeTest, PixelTestOpaqueSrgbLossyNoise) { + for (unsigned channels = 3; channels <= 4; channels++) { + JxlDecoder* dec = JxlDecoderCreate(NULL); + + size_t xsize = 512, ysize = 300; + size_t num_pixels = xsize * ysize; + std::vector pixels = + jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + JxlPixelFormat format_orig = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + jxl::CompressParams cparams; + cparams.noise = jxl::Override::kOn; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 3, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, /*add_preview=*/false, + /*add_icc_profile=*/false); + + JxlPixelFormat format = {channels, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + std::vector pixels2 = jxl::DecodeWithAPI( + dec, jxl::Span(compressed.data(), compressed.size()), + format, /*use_callback=*/false, /*set_buffer_early=*/true, + /*use_resizable_runner=*/false); + JxlDecoderReset(dec); + EXPECT_EQ(num_pixels * channels, pixels2.size()); + + // The input pixels use the profile matching GetIccTestProfile, since we set + // add_icc_profile for CreateTestJXLCodestream to true. + jxl::ColorEncoding color_encoding0 = jxl::ColorEncoding::SRGB(false); + jxl::Span span0(pixels.data(), pixels.size()); + jxl::CodecInOut io0; + io0.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal(span0, xsize, ysize, color_encoding0, + /*has_alpha=*/false, false, 16, + format_orig.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io0.Main(), /*float_in=*/false)); + + jxl::ColorEncoding color_encoding1 = jxl::ColorEncoding::SRGB(false); + jxl::Span span1(pixels2.data(), pixels2.size()); + jxl::CodecInOut io1; + if (channels == 4) { + io1.metadata.m.SetAlphaBits(8); + io1.SetSize(xsize, ysize); + EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, + /*has_alpha=*/true, false, 8, + format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io1.Main(), /*float_in=*/false)); + io1.metadata.m.SetAlphaBits(0); + io1.Main().ClearExtraChannels(); + } else { + EXPECT_TRUE(ConvertFromExternal(span1, xsize, ysize, color_encoding1, + /*has_alpha=*/false, false, 8, + format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, + &io1.Main(), /*float_in=*/false)); + } + + jxl::ButteraugliParams ba; + EXPECT_LE(ButteraugliDistance(io0, io1, ba, /*distmap=*/nullptr, nullptr), + 2.6f); + + JxlDecoderDestroy(dec); + } +} + +void TestPartialStream(bool reconstructible_jpeg) { + size_t xsize = 123, ysize = 77; + uint32_t channels = 4; + if (reconstructible_jpeg) { + channels = 3; + } + std::vector pixels = + jxl::test::GetSomeTestImage(xsize, ysize, channels, 0); + JxlPixelFormat format_orig = {channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + jxl::CompressParams cparams; + if (reconstructible_jpeg) { + cparams.color_transform = jxl::ColorTransform::kNone; + } else { + cparams + .SetLossless(); // Lossless to verify pixels exactly after roundtrip. + } + + std::vector pixels2; + pixels2.resize(pixels.size()); + + jxl::PaddedBytes jpeg_output(64); + size_t used_jpeg_output = 0; + + std::vector codestreams(kCSBF_NUM_ENTRIES); + std::vector jpeg_codestreams(kCSBF_NUM_ENTRIES); + for (size_t i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + CodeStreamBoxFormat add_container = (CodeStreamBoxFormat)i; + + codestreams[i] = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, + channels, cparams, add_container, JXL_ORIENT_IDENTITY, + /*add_preview=*/true, + /*add_icc_profile=*/false, + reconstructible_jpeg ? &jpeg_codestreams[i] : nullptr); + } + + // Test multiple step sizes, to test different combinations of the streaming + // box parsing. + std::vector increments = {1, 3, 17, 23, 120, 700, 1050}; + + for (size_t index = 0; index < increments.size(); index++) { + for (size_t i = 0; i < kCSBF_NUM_ENTRIES; ++i) { + if (reconstructible_jpeg && + (CodeStreamBoxFormat)i == CodeStreamBoxFormat::kCSBF_None) { + continue; + } + const jxl::PaddedBytes& data = codestreams[i]; + const uint8_t* next_in = data.data(); + size_t avail_in = 0; + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE | + JXL_DEC_JPEG_RECONSTRUCTION)); + + bool seen_basic_info = false; + bool seen_full_image = false; + bool seen_jpeg_recon = false; + + size_t total_size = 0; + + for (;;) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + JxlDecoderStatus status = JxlDecoderProcessInput(dec); + size_t remaining = JxlDecoderReleaseInput(dec); + EXPECT_LE(remaining, avail_in); + next_in += avail_in - remaining; + avail_in = remaining; + if (status == JXL_DEC_NEED_MORE_INPUT) { + if (total_size >= data.size()) { + // End of test data reached, it should have successfully decoded the + // image now. + FAIL(); + break; + } + + size_t increment = increments[index]; + // End of the file reached, should be the final test. + if (total_size + increment > data.size()) { + increment = data.size() - total_size; + } + total_size += increment; + avail_in += increment; + } else if (status == JXL_DEC_BASIC_INFO) { + // This event should happen exactly once + EXPECT_FALSE(seen_basic_info); + if (seen_basic_info) break; + seen_basic_info = true; + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); + } else if (status == JXL_DEC_JPEG_RECONSTRUCTION) { + EXPECT_FALSE(seen_basic_info); + EXPECT_FALSE(seen_full_image); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer(dec, jpeg_output.data(), + jpeg_output.size())); + seen_jpeg_recon = true; + } else if (status == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + EXPECT_TRUE(seen_jpeg_recon); + used_jpeg_output = + jpeg_output.size() - JxlDecoderReleaseJPEGBuffer(dec); + jpeg_output.resize(jpeg_output.size() * 2); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer( + dec, jpeg_output.data() + used_jpeg_output, + jpeg_output.size() - used_jpeg_output)); + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer( + dec, &format_orig, pixels2.data(), pixels2.size())); + } else if (status == JXL_DEC_FULL_IMAGE) { + // This event should happen exactly once + EXPECT_FALSE(seen_full_image); + if (seen_full_image) break; + // This event should happen after basic info + EXPECT_TRUE(seen_basic_info); + seen_full_image = true; + if (reconstructible_jpeg) { + used_jpeg_output = + jpeg_output.size() - JxlDecoderReleaseJPEGBuffer(dec); + EXPECT_EQ(used_jpeg_output, jpeg_codestreams[i].size()); + EXPECT_EQ(0, memcmp(jpeg_output.data(), jpeg_codestreams[i].data(), + used_jpeg_output)); + } else { + EXPECT_EQ(pixels, pixels2); + } + } else if (status == JXL_DEC_SUCCESS) { + EXPECT_TRUE(seen_full_image); + break; + } else { + // We do not expect any other events or errors + FAIL(); + break; + } + } + + // Ensure the decoder emitted the basic info and full image events + EXPECT_TRUE(seen_basic_info); + EXPECT_TRUE(seen_full_image); + + JxlDecoderDestroy(dec); + } + } +} + +// Tests the return status when trying to decode pixels on incomplete file: it +// should return JXL_DEC_NEED_MORE_INPUT, not error. +TEST(DecodeTest, PixelPartialTest) { TestPartialStream(false); } + +#if JPEGXL_ENABLE_JPEG +// Tests the return status when trying to decode JPEG bytes on incomplete file. +TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGPartialTest)) { + TestPartialStream(true); +} +#endif // JPEGXL_ENABLE_JPEG + +// The DC event still exists, but is no longer implemented, it is deprecated. +TEST(DecodeTest, DCNotGettableTest) { + // 1x1 pixel JXL image + std::string compressed( + "\377\n\0\20\260\23\0H\200(" + "\0\334\0U\17\0\0\250P\31e\334\340\345\\\317\227\37:," + "\246m\\gh\253m\vK\22E\306\261I\252C&pH\22\353 " + "\363\6\22\bp\0\200\237\34\231W2d\255$\1", + 68); + + JxlDecoder* dec = JxlDecoderCreate(NULL); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_DC_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput( + dec, reinterpret_cast(compressed.data()), + compressed.size())); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + + // Since the image is only 1x1 pixel, there is only 1 group, the decoder is + // unable to get DC size from this, and will not return the DC at all. Since + // no full image is requested either, it is expected to return success. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, PreviewTest) { + size_t xsize = 77, ysize = 120; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + + jxl::CompressParams cparams; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 3, + cparams, kCSBF_Multi, JXL_ORIENT_IDENTITY, /*add_preview=*/true); + + JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0}; + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_PREVIEW_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderPreviewOutBufferSize(dec, &format, &buffer_size)); + + // GetSomeTestImage is hardcoded to use a top-left cropped preview with + // floor of 1/7th of the size + size_t xsize_preview = (xsize / 7); + size_t ysize_preview = (ysize / 7); + EXPECT_EQ(xsize_preview, info.preview.xsize); + EXPECT_EQ(ysize_preview, info.preview.ysize); + EXPECT_EQ(xsize_preview * ysize_preview * 3, buffer_size); + + EXPECT_EQ(JXL_DEC_NEED_PREVIEW_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + std::vector preview(xsize_preview * ysize_preview * 3); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetPreviewOutBuffer( + dec, &format, preview.data(), preview.size())); + + EXPECT_EQ(JXL_DEC_PREVIEW_IMAGE, JxlDecoderProcessInput(dec)); + + jxl::Image3F preview0(xsize_preview, ysize_preview); + jxl::Image3F preview1(xsize_preview, ysize_preview); + + // For preview0, the original: top-left crop the preview image the way + // GetSomeTestImage does. + for (size_t y = 0; y < ysize_preview; y++) { + for (size_t x = 0; x < xsize_preview; x++) { + preview0.PlaneRow(0, y)[x] = + (1.f / 255) * (pixels[(y * xsize + x) * 6 + 0]); + preview0.PlaneRow(1, y)[x] = + (1.f / 255) * (pixels[(y * xsize + x) * 6 + 2]); + preview0.PlaneRow(2, y)[x] = + (1.f / 255) * (pixels[(y * xsize + x) * 6 + 4]); + preview1.PlaneRow(0, y)[x] = + (1.f / 255) * (preview[(y * xsize_preview + x) * 3 + 0]); + preview1.PlaneRow(1, y)[x] = + (1.f / 255) * (preview[(y * xsize_preview + x) * 3 + 1]); + preview1.PlaneRow(2, y)[x] = + (1.f / 255) * (preview[(y * xsize_preview + x) * 3 + 2]); + } + } + + jxl::CodecInOut io0; + io0.SetFromImage(std::move(preview0), jxl::ColorEncoding::SRGB(false)); + jxl::CodecInOut io1; + io1.SetFromImage(std::move(preview1), jxl::ColorEncoding::SRGB(false)); + + jxl::ButteraugliParams ba; + // TODO(lode): this ButteraugliDistance silently returns 0 (dangerous for + // tests) if xsize or ysize is < 8, no matter how different the images, a tiny + // size that could happen for a preview. ButteraugliDiffmap does support + // smaller than 8x8, but jxl's ButteraugliDistance does not. Perhaps move + // butteraugli's <8x8 handling from ButteraugliDiffmap to + // ButteraugliComparator::Diffmap in butteraugli.cc. + EXPECT_LE(ButteraugliDistance(io0, io1, ba, + /*distmap=*/nullptr, nullptr), + 1.4f); + + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, AlignTest) { + size_t xsize = 123, ysize = 77; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 4, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false); + + size_t align = 17; + JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, align}; + // On purpose not using jxl::RoundUpTo to test it independently. + size_t expected_line_bytes = (1 * 3 * xsize + align - 1) / align * align; + + for (int use_callback = 0; use_callback <= 1; ++use_callback) { + std::vector pixels2 = jxl::DecodeWithAPI( + jxl::Span(compressed.data(), compressed.size()), format, + use_callback, /*set_buffer_early=*/false, + /*use_resizable_runner=*/false); + EXPECT_EQ(expected_line_bytes * ysize, pixels2.size()); + EXPECT_EQ(0u, ComparePixels(pixels.data(), pixels2.data(), xsize, ysize, + format_orig, format)); + } +} + +TEST(DecodeTest, AnimationTest) { + size_t xsize = 123, ysize = 77; + static const size_t num_frames = 2; + std::vector frames[2]; + frames[0] = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + frames[1] = jxl::test::GetSomeTestImage(xsize, ysize, 3, 1); + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.have_animation = true; + io.frames.clear(); + io.frames.reserve(num_frames); + io.SetSize(xsize, ysize); + + std::vector frame_durations(num_frames); + for (size_t i = 0; i < num_frames; ++i) { + frame_durations[i] = 5 + i; + } + + for (size_t i = 0; i < num_frames; ++i) { + jxl::ImageBundle bundle(&io.metadata.m); + + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(frames[i].data(), frames[i].size()), xsize, + ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/false, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + /*float_in=*/false)); + bundle.duration = frame_durations[i]; + io.frames.push_back(std::move(bundle)); + } + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::AuxOut aux_out; + jxl::PaddedBytes compressed; + jxl::PassesEncoderState enc_state; + EXPECT_TRUE(jxl::EncodeFile(cparams, &io, &enc_state, &compressed, &aux_out, + nullptr)); + + // Decode and test the animation frames + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + void* runner = JxlThreadParallelRunnerCreate( + NULL, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + + for (size_t i = 0; i < num_frames; ++i) { + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + EXPECT_EQ(0u, frame_header.name_length); + // For now, test with empty name, there's currently no easy way to encode + // a jxl file with a frame name because ImageBundle doesn't have a + // jxl::FrameHeader to set the name in. We can test the null termination + // character though. + char name; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameName(dec, &name, 1)); + EXPECT_EQ(0, name); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + } + + // After all frames were decoded, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, AnimationTestStreaming) { + size_t xsize = 123, ysize = 77; + static const size_t num_frames = 2; + std::vector frames[2]; + frames[0] = jxl::test::GetSomeTestImage(xsize, ysize, 3, 0); + frames[1] = jxl::test::GetSomeTestImage(xsize, ysize, 3, 1); + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.have_animation = true; + io.frames.clear(); + io.frames.reserve(num_frames); + io.SetSize(xsize, ysize); + + std::vector frame_durations(num_frames); + for (size_t i = 0; i < num_frames; ++i) { + frame_durations[i] = 5 + i; + } + + for (size_t i = 0; i < num_frames; ++i) { + jxl::ImageBundle bundle(&io.metadata.m); + + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(frames[i].data(), frames[i].size()), xsize, + ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/false, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + /*float_in=*/false)); + bundle.duration = frame_durations[i]; + io.frames.push_back(std::move(bundle)); + } + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::AuxOut aux_out; + jxl::PaddedBytes compressed; + jxl::PassesEncoderState enc_state; + EXPECT_TRUE(jxl::EncodeFile(cparams, &io, &enc_state, &compressed, &aux_out, + nullptr)); + + // Decode and test the animation frames + + const size_t step_size = 16; + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = 0; + size_t frame_headers_seen = 0; + size_t frames_seen = 0; + bool seen_basic_info = false; + + void* runner = JxlThreadParallelRunnerCreate( + NULL, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + + std::vector frames2[2]; + for (size_t i = 0; i < num_frames; ++i) { + frames2[i].resize(frames[i].size()); + } + + size_t total_in = 0; + size_t loop_count = 0; + + for (;;) { + if (loop_count++ > compressed.size()) { + fprintf(stderr, "Too many loops\n"); + FAIL(); + break; + } + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + auto status = JxlDecoderProcessInput(dec); + size_t remaining = JxlDecoderReleaseInput(dec); + EXPECT_LE(remaining, avail_in); + next_in += avail_in - remaining; + avail_in = remaining; + + if (status == JXL_DEC_SUCCESS) { + break; + } else if (status == JXL_DEC_ERROR) { + FAIL(); + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + if (total_in >= compressed.size()) { + fprintf(stderr, "Already gave all input data\n"); + FAIL(); + break; + } + size_t amount = step_size; + if (total_in + amount > compressed.size()) { + amount = compressed.size() - total_in; + } + avail_in += amount; + total_in += amount; + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, frames2[frames_seen].data(), + frames2[frames_seen].size())); + } else if (status == JXL_DEC_BASIC_INFO) { + EXPECT_EQ(false, seen_basic_info); + seen_basic_info = true; + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + } else if (status == JXL_DEC_FRAME) { + EXPECT_EQ(true, seen_basic_info); + frame_headers_seen++; + } else if (status == JXL_DEC_FULL_IMAGE) { + frames_seen++; + EXPECT_EQ(frame_headers_seen, frames_seen); + } else { + fprintf(stderr, "Unexpected status: %d\n", (int)status); + FAIL(); + } + } + + EXPECT_EQ(true, seen_basic_info); + EXPECT_EQ(num_frames, frames_seen); + EXPECT_EQ(num_frames, frame_headers_seen); + for (size_t i = 0; i < num_frames; ++i) { + EXPECT_EQ(frames[i], frames2[i]); + } + + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, ExtraChannelTest) { + size_t xsize = 55, ysize = 257; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + JxlPixelFormat format_orig = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, 4, + cparams, kCSBF_None, JXL_ORIENT_IDENTITY, false); + + size_t align = 17; + JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, align}; + + JxlDecoder* dec = JxlDecoderCreate(NULL); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetInput(dec, compressed.data(), compressed.size())); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(1u, info.num_extra_channels); + EXPECT_EQ(JXL_FALSE, info.alpha_premultiplied); + + JxlExtraChannelInfo extra_info; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetExtraChannelInfo(dec, 0, &extra_info)); + EXPECT_EQ(0, extra_info.type); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + size_t extra_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderExtraChannelBufferSize(dec, &format, &extra_size, 0)); + + std::vector image(buffer_size); + std::vector extra(extra_size); + size_t bytes_per_pixel = + format.num_channels * GetDataBits(format.data_type) / jxl::kBitsPerByte; + size_t stride = bytes_per_pixel * info.xsize; + if (format.align > 1) { + stride = jxl::DivCeil(stride, format.align) * format.align; + } + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, image.data(), image.size())); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetExtraChannelBuffer( + dec, &format, extra.data(), extra.size(), 0)); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + // After the full image was output, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + JxlDecoderDestroy(dec); + + EXPECT_EQ(0u, ComparePixels(pixels.data(), image.data(), xsize, ysize, + format_orig, format)); + + // Compare the extracted extra channel with the original alpha channel + + std::vector alpha(pixels.size() / 4); + for (size_t i = 0; i < pixels.size(); i += 8) { + size_t index_alpha = i / 4; + alpha[index_alpha + 0] = pixels[i + 6]; + alpha[index_alpha + 1] = pixels[i + 7]; + } + JxlPixelFormat format_alpha = format; + format_alpha.num_channels = 1; + JxlPixelFormat format_orig_alpha = format_orig; + format_orig_alpha.num_channels = 1; + + EXPECT_EQ(0u, ComparePixels(alpha.data(), extra.data(), xsize, ysize, + format_orig_alpha, format_alpha)); +} + +TEST(DecodeTest, SkipFrameTest) { + size_t xsize = 90, ysize = 120; + constexpr size_t num_frames = 16; + std::vector frames[num_frames]; + for (size_t i = 0; i < num_frames; i++) { + frames[i] = jxl::test::GetSomeTestImage(xsize, ysize, 3, i); + } + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.have_animation = true; + io.frames.clear(); + io.frames.reserve(num_frames); + io.SetSize(xsize, ysize); + + std::vector frame_durations(num_frames); + for (size_t i = 0; i < num_frames; ++i) { + frame_durations[i] = 5 + i; + } + + for (size_t i = 0; i < num_frames; ++i) { + jxl::ImageBundle bundle(&io.metadata.m); + if (i & 1) { + // Mark some frames as referenceable, others not. + bundle.use_for_next_frame = true; + } + + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(frames[i].data(), frames[i].size()), xsize, + ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/false, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + /*float_in=*/false)); + bundle.duration = frame_durations[i]; + io.frames.push_back(std::move(bundle)); + } + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::AuxOut aux_out; + jxl::PaddedBytes compressed; + jxl::PassesEncoderState enc_state; + EXPECT_TRUE(jxl::EncodeFile(cparams, &io, &enc_state, &compressed, &aux_out, + nullptr)); + + // Decode and test the animation frames + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + void* runner = JxlThreadParallelRunnerCreate( + NULL, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + + for (size_t i = 0; i < num_frames; ++i) { + if (i == 3) { + JxlDecoderSkipFrames(dec, 5); + i += 5; + } + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + } + + // After all frames were decoded, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + // Test rewinding the decoder and skipping different frames + + JxlDecoderRewind(dec); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + for (size_t i = 0; i < num_frames; ++i) { + int test_skipping = (i == 9) ? 3 : 0; + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + // Since this is after JXL_DEC_FRAME but before JXL_DEC_FULL_IMAGE, this + // should only skip the next frame, not the currently processed one. + if (test_skipping) JxlDecoderSkipFrames(dec, test_skipping); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + + if (test_skipping) i += test_skipping; + } + + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, SkipFrameWithBlendingTest) { + size_t xsize = 90, ysize = 120; + constexpr size_t num_frames = 16; + std::vector frames[num_frames]; + JxlPixelFormat format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetUintSamples(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false); + io.metadata.m.have_animation = true; + io.frames.clear(); + io.frames.reserve(num_frames); + io.SetSize(xsize, ysize); + + std::vector frame_durations(num_frames); + + for (size_t i = 0; i < num_frames; ++i) { + if (i < 5) { + std::vector frame_internal = + jxl::test::GetSomeTestImage(xsize, ysize, 3, i * 2 + 1); + // An internal frame with 0 duration, and use_for_next_frame, this is a + // frame that is not rendered and not output by the API, but on which the + // rendered frames depend + jxl::ImageBundle bundle_internal(&io.metadata.m); + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(frame_internal.data(), + frame_internal.size()), + xsize, ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false), + /*has_alpha=*/false, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, + &bundle_internal, /*float_in=*/false)); + bundle_internal.duration = 0; + bundle_internal.use_for_next_frame = true; + io.frames.push_back(std::move(bundle_internal)); + } + + std::vector frame = + jxl::test::GetSomeTestImage(xsize, ysize, 3, i * 2); + // Actual rendered frame + frame_durations[i] = 5 + i; + jxl::ImageBundle bundle(&io.metadata.m); + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(frame.data(), frame.size()), xsize, ysize, + jxl::ColorEncoding::SRGB(/*is_gray=*/false), /*has_alpha=*/false, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, + JXL_BIG_ENDIAN, /*flipped_y=*/false, /*pool=*/nullptr, &bundle, + /*float_in=*/false)); + bundle.duration = frame_durations[i]; + // Create some variation in which frames depend on which. + if (i != 3 && i != 9 && i != 10) { + bundle.use_for_next_frame = true; + } + if (i != 12) { + bundle.blend = true; + // Choose a blend mode that depends on the pixels of the saved frame and + // doesn't use alpha + bundle.blendmode = jxl::BlendMode::kMul; + } + io.frames.push_back(std::move(bundle)); + } + + jxl::CompressParams cparams; + cparams.SetLossless(); // Lossless to verify pixels exactly after roundtrip. + jxl::AuxOut aux_out; + jxl::PaddedBytes compressed; + jxl::PassesEncoderState enc_state; + EXPECT_TRUE(jxl::EncodeFile(cparams, &io, &enc_state, &compressed, &aux_out, + nullptr)); + + // Independently decode all frames without any skipping, to create the + // expected blended frames, for the actual tests below to compare with. + { + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + void* runner = JxlThreadParallelRunnerCreate( + NULL, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetParallelRunner( + dec, JxlThreadParallelRunner, runner)); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + for (size_t i = 0; i < num_frames; ++i) { + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + frames[i].resize(xsize * ysize * 6); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, frames[i].data(), + frames[i].size())); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + } + + // After all frames were decoded, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); + } + + JxlDecoder* dec = JxlDecoderCreate(NULL); + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + void* runner = JxlThreadParallelRunnerCreate( + NULL, JxlThreadParallelRunnerDefaultNumWorkerThreads()); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetParallelRunner(dec, JxlThreadParallelRunner, runner)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + + for (size_t i = 0; i < num_frames; ++i) { + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + + // Test rewinding mid-way, not decoding all frames. + if (i == 8) { + break; + } + } + + JxlDecoderRewind(dec); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + for (size_t i = 0; i < num_frames; ++i) { + if (i == 3) { + JxlDecoderSkipFrames(dec, 5); + i += 5; + } + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + } + + // After all frames were decoded, JxlDecoderProcessInput should return + // success to indicate all is done. + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec)); + + // Test rewinding the decoder and skipping different frames + + JxlDecoderRewind(dec); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, next_in, avail_in)); + + for (size_t i = 0; i < num_frames; ++i) { + int test_skipping = (i == 9) ? 3 : 0; + std::vector pixels(buffer_size); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + // Since this is after JXL_DEC_FRAME but before JXL_DEC_FULL_IMAGE, this + // should only skip the next frame, not the currently processed one. + if (test_skipping) JxlDecoderSkipFrames(dec, test_skipping); + + JxlFrameHeader frame_header; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec, &frame_header)); + EXPECT_EQ(frame_durations[i], frame_header.duration); + + EXPECT_EQ(i + 1 == num_frames, frame_header.is_last); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels.data(), pixels.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + EXPECT_EQ(0u, ComparePixels(frames[i].data(), pixels.data(), xsize, ysize, + format, format)); + + if (test_skipping) i += test_skipping; + } + + JxlThreadParallelRunnerDestroy(runner); + JxlDecoderDestroy(dec); +} + +TEST(DecodeTest, FlushTest) { + // Size large enough for multiple groups, required to have progressive + // stages + size_t xsize = 333, ysize = 300; + uint32_t num_channels = 3; + std::vector pixels = + jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0); + jxl::CompressParams cparams; + jxl::PaddedBytes data = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, + num_channels, cparams, kCSBF_None, JXL_ORIENT_IDENTITY, true); + JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + + std::vector pixels2; + pixels2.resize(pixels.size()); + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec, JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); + + // Ensure that the first part contains at least the full DC of the image, + // otherwise flush does not work. The DC takes up more than 50% of the + // image generated here. + size_t first_part = data.size() * 3 / 4; + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data(), first_part)); + + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(info.xsize, xsize); + EXPECT_EQ(info.ysize, ysize); + + EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec)); + + // Output buffer not yet set + EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderFlushImage(dec)); + + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(pixels2.size(), buffer_size); + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &format, pixels2.data(), pixels2.size())); + + // Must process input further until we get JXL_DEC_NEED_MORE_INPUT, even if + // data was already input before, since the processing of the frame only + // happens at the JxlDecoderProcessInput call after JXL_DEC_FRAME. + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderFlushImage(dec)); + + // Note: actual pixel data not tested here, it should look similar to the + // input image, but with less fine detail. Instead the expected events are + // tested here. + + EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec)); + + size_t consumed = first_part - JxlDecoderReleaseInput(dec); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec, data.data() + consumed, + data.size() - consumed)); + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + JxlDecoderDestroy(dec); +} + +void VerifyJPEGReconstruction(const jxl::PaddedBytes& container, + const jxl::PaddedBytes& jpeg_bytes) { + JxlDecoderPtr dec = JxlDecoderMake(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE)); + JxlDecoderSetInput(dec.get(), container.data(), container.size()); + EXPECT_EQ(JXL_DEC_JPEG_RECONSTRUCTION, JxlDecoderProcessInput(dec.get())); + std::vector reconstructed_buffer(128); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer(dec.get(), reconstructed_buffer.data(), + reconstructed_buffer.size())); + size_t used = 0; + JxlDecoderStatus process_result = JXL_DEC_JPEG_NEED_MORE_OUTPUT; + while (process_result == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + used = reconstructed_buffer.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + reconstructed_buffer.resize(reconstructed_buffer.size() * 2); + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer(dec.get(), reconstructed_buffer.data() + used, + reconstructed_buffer.size() - used)); + process_result = JxlDecoderProcessInput(dec.get()); + } + ASSERT_EQ(JXL_DEC_FULL_IMAGE, process_result); + used = reconstructed_buffer.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + ASSERT_EQ(used, jpeg_bytes.size()); + EXPECT_EQ(0, memcmp(reconstructed_buffer.data(), jpeg_bytes.data(), used)); +} + +#if JPEGXL_ENABLE_JPEG +TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructTestCodestream)) { + size_t xsize = 123; + size_t ysize = 77; + size_t channels = 3; + std::vector pixels = + jxl::test::GetSomeTestImage(xsize, ysize, channels, /*seed=*/0); + jxl::CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kNone; + jxl::PaddedBytes jpeg_codestream; + jxl::PaddedBytes compressed = jxl::CreateTestJXLCodestream( + jxl::Span(pixels.data(), pixels.size()), xsize, ysize, + channels, cparams, kCSBF_Single, JXL_ORIENT_IDENTITY, + /*add_preview=*/true, + /*add_icc_profile=*/false, &jpeg_codestream); + VerifyJPEGReconstruction(compressed, jpeg_codestream); +} +#endif // JPEGXL_ENABLE_JPEG + +TEST(DecodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { + const std::string jpeg_path = + "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); + jxl::CodecInOut orig_io; + ASSERT_TRUE( + jxl::jpeg::DecodeImageJPG(jxl::Span(orig), &orig_io)); + orig_io.metadata.m.xyb_encoded = false; + jxl::BitWriter writer; + ASSERT_TRUE(WriteHeaders(&orig_io.metadata, &writer, nullptr)); + writer.ZeroPadToByte(); + jxl::PassesEncoderState enc_state; + jxl::CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kNone; + ASSERT_TRUE(jxl::EncodeFrame(cparams, jxl::FrameInfo{}, &orig_io.metadata, + orig_io.Main(), &enc_state, + /*pool=*/nullptr, &writer, + /*aux_out=*/nullptr)); + + jxl::PaddedBytes jpeg_data; + ASSERT_TRUE(EncodeJPEGData(*orig_io.Main().jpeg_data.get(), &jpeg_data)); + jxl::PaddedBytes container; + container.append(jxl::kContainerHeader, + jxl::kContainerHeader + sizeof(jxl::kContainerHeader)); + jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_data.size(), false, + &container); + container.append(jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); + jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), 0, true, &container); + jxl::PaddedBytes codestream = std::move(writer).TakeBytes(); + container.append(codestream.data(), codestream.data() + codestream.size()); + VerifyJPEGReconstruction(container, orig); +} diff --git a/lib/jxl/decode_to_jpeg.cc b/lib/jxl/decode_to_jpeg.cc new file mode 100644 index 0000000..4bab82a --- /dev/null +++ b/lib/jxl/decode_to_jpeg.cc @@ -0,0 +1,77 @@ +// 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/decode_to_jpeg.h" + +namespace jxl { + +#if JPEGXL_ENABLE_TRANSCODE_JPEG + +JxlDecoderStatus JxlToJpegDecoder::Process(const uint8_t** next_in, + size_t* avail_in) { + if (!inside_box_) { + JXL_ABORT( + "processing of JPEG reconstruction data outside JPEG reconstruction " + "box"); + } + Span to_decode; + if (box_until_eof_) { + // Until EOF means consume all data. + to_decode = Span(*next_in, *avail_in); + *next_in += *avail_in; + *avail_in = 0; + } else { + // Defined size means consume min(available, needed). + size_t avail_recon_in = + std::min(*avail_in, box_size_ - buffer_.size()); + to_decode = Span(*next_in, avail_recon_in); + *next_in += avail_recon_in; + *avail_in -= avail_recon_in; + } + bool old_data_exists = !buffer_.empty(); + if (old_data_exists) { + // Append incoming data to buffer if we already had data in the buffer. + buffer_.insert(buffer_.end(), to_decode.data(), + to_decode.data() + to_decode.size()); + to_decode = Span(buffer_.data(), buffer_.size()); + } + if (!box_until_eof_ && to_decode.size() > box_size_) { + JXL_ABORT("JPEG reconstruction data to decode larger than expected"); + } + if (box_until_eof_ || to_decode.size() == box_size_) { + // If undefined size, or the right size, try to decode. + jpeg_data_ = make_unique(); + const auto status = jpeg::DecodeJPEGData(to_decode, jpeg_data_.get()); + if (status.IsFatalError()) return JXL_DEC_ERROR; + if (status) { + // Successful decoding, emit event after updating state to track that we + // are no longer parsing JPEG reconstruction data. + inside_box_ = false; + return JXL_DEC_JPEG_RECONSTRUCTION; + } + if (box_until_eof_) { + // Unsuccessful decoding and undefined size, assume incomplete data. Copy + // the data if we haven't already. + if (!old_data_exists) { + buffer_.insert(buffer_.end(), to_decode.data(), + to_decode.data() + to_decode.size()); + } + } else { + // Unsuccessful decoding of correct amount of data, assume error. + return JXL_DEC_ERROR; + } + } else { + // Not enough data, copy the data if we haven't already. + if (!old_data_exists) { + buffer_.insert(buffer_.end(), to_decode.data(), + to_decode.data() + to_decode.size()); + } + } + return JXL_DEC_NEED_MORE_INPUT; +} + +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +} // namespace jxl diff --git a/lib/jxl/decode_to_jpeg.h b/lib/jxl/decode_to_jpeg.h new file mode 100644 index 0000000..86f0a66 --- /dev/null +++ b/lib/jxl/decode_to_jpeg.h @@ -0,0 +1,173 @@ +// 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_DECODE_TO_JPEG_H_ +#define LIB_JXL_DECODE_TO_JPEG_H_ + +// JPEG XL to JPEG bytes decoder logic. The JxlToJpegDecoder class keeps track +// of the decoder state needed to parse the JPEG reconstruction box and provide +// the reconstructed JPEG to the output buffer. + +#include +#include + +#include +#include + +#include "jxl/decode.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" // JPEGXL_ENABLE_TRANSCODE_JPEG +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/jpeg/dec_jpeg_data.h" +#if JPEGXL_ENABLE_TRANSCODE_JPEG +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +namespace jxl { + +#if JPEGXL_ENABLE_TRANSCODE_JPEG + +class JxlToJpegDecoder { + public: + // Returns whether an output buffer is set. + bool IsOutputSet() const { return next_out_ != nullptr; } + + // Returns whether the decoder is parsing a boxa JPEG box was parsed. + bool IsParsingBox() const { return inside_box_; } + + const jpeg::JPEGData* JpegData() const { return jpeg_data_.get(); } + + // Return the parsed jpeg::JPEGData object and removes it from the + // JxlToJpegDecoder. + jpeg::JPEGData* ReleaseJpegData() { return jpeg_data_.release(); } + + // Sets the output buffer used when producing JPEG output. + JxlDecoderStatus SetOutputBuffer(uint8_t* data, size_t size) { + if (next_out_) return JXL_DEC_ERROR; + next_out_ = data; + avail_size_ = size; + return JXL_DEC_SUCCESS; + } + + // Releases the buffer set with SetOutputBuffer(). + size_t ReleaseOutputBuffer() { + size_t result = avail_size_; + next_out_ = nullptr; + avail_size_ = 0; + return result; + } + + void StartBox(uint64_t box_size, size_t contents_size) { + // A new box implies that we clear the buffer. + buffer_.clear(); + inside_box_ = true; + if (box_size == 0) { + box_until_eof_ = true; + } else { + box_size_ = contents_size; + } + } + + // Consumes data from next_in/avail_in to reconstruct JPEG data. + // Uses box_size_, inside_box_ and box_until_eof_ to calculate how much to + // consume. Potentially stores unparsed data in buffer_. + // Potentially populates jpeg_data_. Potentially updates inside_box_. + JxlDecoderStatus Process(const uint8_t** next_in, size_t* avail_in); + + // Sets the JpegData of the ImageBundle passed if there is anything to set. + // Releases the JpegData from this decoder if set. + Status SetImageBundleJpegData(ImageBundle* ib) { + if (IsOutputSet() && jpeg_data_ != nullptr) { + if (!jpeg::SetJPEGDataFromICC(ib->metadata()->color_encoding.ICC(), + jpeg_data_.get())) { + return false; + } + ib->jpeg_data.reset(jpeg_data_.release()); + } + return true; + } + + JxlDecoderStatus WriteOutput(const jpeg::JPEGData& jpeg_data) { + // Copy JPEG bytestream if desired. + uint8_t* tmp_next_out = next_out_; + size_t tmp_avail_size = avail_size_; + auto write = [&tmp_next_out, &tmp_avail_size](const uint8_t* buf, + size_t len) { + size_t to_write = std::min(tmp_avail_size, len); + memcpy(tmp_next_out, buf, to_write); + tmp_next_out += to_write; + tmp_avail_size -= to_write; + return to_write; + }; + Status write_result = jpeg::WriteJpeg(jpeg_data, write); + if (!write_result) { + if (tmp_avail_size == 0) { + return JXL_DEC_JPEG_NEED_MORE_OUTPUT; + } + return JXL_DEC_ERROR; + } + next_out_ = tmp_next_out; + avail_size_ = tmp_avail_size; + return JXL_DEC_SUCCESS; + } + + private: + // Content of the most recently parsed JPEG reconstruction box if any. + std::vector buffer_; + + // Decoded content of the most recently parsed JPEG reconstruction box is + // stored here. + std::unique_ptr jpeg_data_; + + // True if the decoder is currently reading bytes inside a JPEG reconstruction + // box. + bool inside_box_ = false; + + // True if the JPEG reconstruction box had undefined size (all remaining + // bytes). + bool box_until_eof_ = false; + // Size of most recently parsed JPEG reconstruction box contents. + size_t box_size_ = 0; + + // Next bytes to write JPEG reconstruction to. + uint8_t* next_out_ = nullptr; + // Available bytes to write JPEG reconstruction to. + size_t avail_size_ = 0; +}; + +#else + +// Fake class that disables support for decoding JPEG XL to JPEG. +class JxlToJpegDecoder { + public: + bool IsOutputSet() const { return false; } + bool IsParsingBox() const { return false; } + + const jpeg::JPEGData* JpegData() const { return nullptr; } + jpeg::JPEGData* ReleaseJpegData() { return nullptr; } + + JxlDecoderStatus SetOutputBuffer(uint8_t* /* data */, size_t /* size */) { + return JXL_DEC_ERROR; + } + size_t ReleaseOutputBuffer() { return 0; } + + void StartBox(uint64_t /* box_size */, size_t /* contents_size */) {} + + JxlDecoderStatus Process(const uint8_t** next_in, size_t* avail_in) { + return JXL_DEC_ERROR; + } + + Status SetImageBundleJpegData(ImageBundle* /* ib */) { return true; } + + JxlDecoderStatus WriteOutput(const jpeg::JPEGData& /* jpeg_data */) { + return JXL_DEC_SUCCESS; + } +}; + +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +} // namespace jxl + +#endif // LIB_JXL_DECODE_TO_JPEG_H_ diff --git a/lib/jxl/descriptive_statistics_test.cc b/lib/jxl/descriptive_statistics_test.cc new file mode 100644 index 0000000..7891c72 --- /dev/null +++ b/lib/jxl/descriptive_statistics_test.cc @@ -0,0 +1,152 @@ +// 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/descriptive_statistics.h" + +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/noise_distributions.h" + +namespace jxl { +namespace { + +// Assigns x to one of two streams so we can later test Assimilate. +template +void NotifyEither(float x, Random* rng, Stats* JXL_RESTRICT stats1, + Stats* JXL_RESTRICT stats2) { + if ((*rng)() & 128) { + stats1->Notify(x); + } else { + stats2->Notify(x); + } +} + +TEST(StatsTest, TestGaussian) { + Stats stats; + Stats stats1, stats2; + const float mean = 5.0f; + const float stddev = 4.0f; + NoiseGaussian noise(stddev); + std::mt19937 rng(129); + for (size_t i = 0; i < 1000 * 1000; ++i) { + const float x = noise(mean, &rng); + stats.Notify(x); + NotifyEither(x, &rng, &stats1, &stats2); + } + EXPECT_NEAR(mean, stats.Mean(), 0.01); + EXPECT_NEAR(stddev, stats.StandardDeviation(), 0.02); + EXPECT_NEAR(0.0, stats.Skewness(), 0.02); + EXPECT_NEAR(0.0, stats.Kurtosis() - 3, 0.02); + printf("%s\n", stats.ToString().c_str()); + + // Same results after merging both accumulators. + stats1.Assimilate(stats2); + EXPECT_NEAR(mean, stats1.Mean(), 0.01); + EXPECT_NEAR(stddev, stats1.StandardDeviation(), 0.02); + EXPECT_NEAR(0.0, stats1.Skewness(), 0.02); + EXPECT_NEAR(0.0, stats1.Kurtosis() - 3, 0.02); +} + +TEST(StatsTest, TestUniform) { + Stats stats; + Stats stats1, stats2; + NoiseUniform noise(0, 256); + std::mt19937 rng(129), rng_split(65537); + for (size_t i = 0; i < 1000 * 1000; ++i) { + const float x = noise(0.0f, &rng); + stats.Notify(x); + NotifyEither(x, &rng_split, &stats1, &stats2); + } + EXPECT_NEAR(128.0, stats.Mean(), 0.05); + EXPECT_NEAR(0.0, stats.Min(), 0.01); + EXPECT_NEAR(256.0, stats.Max(), 0.01); + EXPECT_NEAR(70, stats.StandardDeviation(), 10); + // No outliers. + EXPECT_NEAR(-1.2, stats.Kurtosis() - 3, 0.1); + printf("%s\n", stats.ToString().c_str()); + + // Same results after merging both accumulators. + stats1.Assimilate(stats2); + EXPECT_NEAR(128.0, stats1.Mean(), 0.05); + EXPECT_NEAR(0.0, stats1.Min(), 0.01); + EXPECT_NEAR(256.0, stats1.Max(), 0.01); + EXPECT_NEAR(70, stats1.StandardDeviation(), 10); +} + +TEST(StatsTest, CompareCentralMomentsAgainstTwoPass) { + // Vary seed so the thresholds are not specific to one distribution. + for (int rep = 0; rep < 200; ++rep) { + // Uniform avoids outliers. + NoiseUniform noise(0, 256); + std::mt19937 rng(129 + 13 * rep), rng_split(65537); + + // Small count so bias (population vs sample) is visible. + const size_t kSamples = 20; + + // First pass: compute mean + std::vector samples; + samples.reserve(kSamples); + double sum = 0.0; + for (size_t i = 0; i < kSamples; ++i) { + const float x = noise(0.0f, &rng); + samples.push_back(x); + sum += x; + } + const double mean = sum / kSamples; + + // Second pass: compute stats and moments + Stats stats; + Stats stats1, stats2; + double sum2 = 0.0; + double sum3 = 0.0; + double sum4 = 0.0; + for (const double x : samples) { + const double d = x - mean; + sum2 += d * d; + sum3 += d * d * d; + sum4 += d * d * d * d; + + stats.Notify(x); + NotifyEither(x, &rng_split, &stats1, &stats2); + } + const double mu1 = mean; + const double mu2 = sum2 / kSamples; + const double mu3 = sum3 / kSamples; + const double mu4 = sum4 / kSamples; + + // Raw central moments (note: Mu1 is zero by definition) + EXPECT_NEAR(mu1, stats.Mu1(), 1E-13); + EXPECT_NEAR(mu2, stats.Mu2(), 1E-11); + EXPECT_NEAR(mu3, stats.Mu3(), 1E-9); + EXPECT_NEAR(mu4, stats.Mu4(), 1E-6); + + // Same results after merging both accumulators. + stats1.Assimilate(stats2); + EXPECT_NEAR(mu1, stats1.Mu1(), 1E-13); + EXPECT_NEAR(mu2, stats1.Mu2(), 1E-11); + EXPECT_NEAR(mu3, stats1.Mu3(), 1E-9); + EXPECT_NEAR(mu4, stats1.Mu4(), 1E-6); + + const double sample_variance = mu2; + // Scaling factor for sampling bias + const double r = (kSamples - 1.0) / kSamples; + const double skewness = mu3 * pow(r / mu2, 1.5); + const double kurtosis = mu4 * pow(r / mu2, 2.0); + + EXPECT_NEAR(sample_variance, stats.SampleVariance(), + sample_variance * 1E-12); + EXPECT_NEAR(skewness, stats.Skewness(), std::abs(skewness * 1E-11)); + EXPECT_NEAR(kurtosis, stats.Kurtosis(), kurtosis * 1E-12); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/docs/color_management.md b/lib/jxl/docs/color_management.md new file mode 100644 index 0000000..56f4a28 --- /dev/null +++ b/lib/jxl/docs/color_management.md @@ -0,0 +1,68 @@ +# Color Management + +[TOC] + + + +## Why + +The vast majority of web images are still sRGB. However, wide-gamut material is +increasingly being produced (photography, cinema, 4K). Screens covering most of +the Adobe RGB gamut are readily available and some also cover most of DCI P3 +(iPhone, Pixel2) or even BT.2020. + +Currently, after a camera records a very saturated red pixel, most raw +processors would clip it to the rather small sRGB gamut before saving as JPEG. +In keeping with our high-quality goal, we prevent such loss by allowing wider +input color spaces. + +## Which color space + +Even wide gamuts could be expressed relative to the sRGB primaries, but the +resulting coordinates may be outside the valid 0..1 range. Surprisingly, such +'unbounded' coordinates can be passed through color transforms provided the +transfer functions are expressed as parametric functions (not lookup tables). +However, most image file formats (including PNG and PNM) lack min/max metadata +and thus do not support unbounded coordinates. + +Instead, we need a larger working gamut to ensure most pixel coordinates are +within bounds and thus not clipped. However, larger gamuts result in lower +precision/resolution when using <= 16 bit encodings (as opposed to 32-bit float +in PFM). BT.2100 or P3 DCI appear to be good compromises. + +## CMS library + +Transforms with unbounded pixels are desirable because they reduce round-trip +error in tests. This requires parametric curves, which are only supported for +the common sRGB case in ICC v4 profiles. ArgyllCMS does not support v4. The +other popular open-source CMS is LittleCMS. It is also used by color-managed +editors (Krita/darktable), which increases the chances of interoperability. +However, LCMS has race conditions and overflow issues that prevent fuzzing. We +will later switch to the newer skcms. Note that this library does not intend to +support multiProcessElements, so HDR transfer functions cannot be represented +accurately. Thus in the long term, we will probably migrate away from ICC +profiles entirely. + +## Which viewer + +On Linux, Krita and darktable support loading our PNG output images and their +ICC profile. + +## How to compress/decompress + +### Embedded ICC profile + +- Create an 8-bit or 16-bit PNG with an iCCP chunk, e.g. using darktable. +- Pass it to `cjxl`, then `djxl` with no special arguments. The decoded output + will have the same bit depth (can override with `--output_bit_depth`) and + color space. + +### Images without metadata (e.g. HDR) + +- Create a PGM/PPM/PFM file in a known color space. +- Invoke `cjxl` with `-x color_space=RGB_D65_202_Rel_Lin` (linear 2020). For + details/possible values, see color_encoding.cc `Description`. +- Invoke `djxl` as above with no special arguments. diff --git a/lib/jxl/docs/dc_predictor.md b/lib/jxl/docs/dc_predictor.md new file mode 100644 index 0000000..478b048 --- /dev/null +++ b/lib/jxl/docs/dc_predictor.md @@ -0,0 +1,129 @@ +# DC prediction + +[TOC] + + + +## Background + +After the DCT integral transform, we wish to reduce the encoded size of DC +coefficients (or original pixels if the transform is omitted). +pik/dc_predictor.h provides `Shrink` and `Expand` functions to reduce the +magnitude of pixels by subtracting predicted values computed from their +neighbors. In the ideal case of flat or slowly varying regions, the resulting +"residuals" are zero and thus very efficiently encodable. + +## Design goals + +1. Benefit from SIMD. +1. Use prior horizontal neighbor for prediction because it has the highest + correlation. +1. Use previously decoded planes (i.e. luminance) for improving prediction of + chrominance. +1. Lossless (no overflow/rounding errors) for signed integers in [-32768, + 32768). + +## Prior approaches + +CALIC GAP and LOCO-I/JPEG-LS MED are somewhat adaptive: they choose from one of +several simple predictors based on properties of the pixel neighborhood (let N +denote the pixel above the current one, W = left, and NW above-left). History +Based Blending introduced more freedom by adding adaptive weights and several +more predictors including averaging, gradients and neighbors. We use a similar +approach, but with selecting rather than blending predictors to avoid +multiplications, and a more efficient cost estimator. + +## Basic algorithm + +``` +Compute all predictors for the already decoded N and W pixel. +See which single predictor performs best (lowest total absolute residual). +Residual := current pixel - best predictor's prediction for current pixel. +To expand again, add this same prediction to the stored residual. +``` + +## Details + +### Dependencies between SIMD lanes + +The SIMD and prior horizontal neighbor requirements seem to be contradictory. If +all steps are computed in units of SIMD vectors, then the prior neighbor may be +part of the same vector and thus not computed beforehand. Instead, we compute +multiple _predictors_ at a time using SIMD for the same pixel. This is feasible +because several predictors use the same computation (e.g. Average) on +independent inputs in the SIMD vector lanes. + +### Predictors + +We use several kinds of predictors. + +1. **Average**: Noisy areas have outliers; computing the average is a simple + way to reduce their impact without expensive non-linear computations. We use + a (saturated) add and shift rather than the average+round SIMD instruction. +1. **Gradient**: Smooth areas benefit from a gradient; we find it helpful to + clamp the JPEG-LS-style N+W-NW to the min/max of all neighbor pixels. +1. **Edge**: Finally, unchanged N/W neighbor values are also helpful for + regions with edges because they violate the assumptions underlying the + average and gradient predictors (namely a shared central tendency or + smoothly varying neighborhood). + +We chose predictors subject to the constraint that 128-bit SIMD instruction sets +can compute eight of them at a time, with a simple optimization algorithm that +tried various permutations of these basic predictors. Note that order matters: +the first predictor with min cost is chosen. The test data set was small, so it +is possible that the choice of predictors could be improved, but that involves +rewriting some intricate SIMD code. + +### Finding best predictor + +Given a SIMD vector with all prediction results, we compute each of their +residuals by subtracting from the (broadcasted) current pixel. X86 SSE4 provides +special support for finding the minimum element in u16x8 vectors. This lane can +efficiently moved to the first lane via shuffling. On other architectures, we +scan through the 8 lanes manually. + +### Threading + +No separate parallelization is needed because this is part of the JXL decoder, +which ensures all pixel decoding steps are independent (in 512x512 groups) and +computed in parallel. + +### Cross-channel correlations + +The adaptive predictor exploits correlations between neighboring pixels. To also +reduce correlation between channels, we require the Y channel to be decoded +first (green is in the middle of the frequency band and thus has higher +correlation with red/blue). Then, rather than computing the cost (prediction +residual) at N and W neighbors, we compute the cost at the same position in the +previously decoded luminance image. +Note: this assumes there is some remaining correlation (despite chroma from +luma already being carried out), which is plausible because the current CfL +model for DC is global, i.e. unable to remove all correlations. Once that +changes, it may be better to no longer use Y to influence the chroma channels. + +### Bounds checking + +Predictors only use immediately adjacent pixels, and the cost estimator also +applies predictors at immediately adjacent neighbors, so we need a border of two +pixels on each side. For the first pixel, we have no predictor; subsequent +pixels in the same row use their left neighbor. In the second row, we can apply +the adaptive predictor while supplying an imaginary extra top row that is +identical to the first row. + +### Side information + +Only causal (previously decoded in left to right, then top to bottom line scan +order) neighbors are used, so the decoder-side `Expand` can run without any side +information. + +### Related work + +lossless16 by Alex Rhatushnyak implements a more powerful predictor which uses +the error history as feedback for the prediction weights. lossless16 is indeed +better than dc_predictor for lossless photo compression. However, using +lossless16 for encoding DCs has not yet proven to be advantageous - even after +entropy-coding of the channel compact transforms, and encoding the entire DC +image in a single call. diff --git a/lib/jxl/docs/entropy_coding_basic.md b/lib/jxl/docs/entropy_coding_basic.md new file mode 100644 index 0000000..973e62f --- /dev/null +++ b/lib/jxl/docs/entropy_coding_basic.md @@ -0,0 +1,111 @@ +# Basic entropy encoders + +[TOC] + + + +This document describes low level encodings used in JXL. + +## Uint32 field + +Field is a variable length encoding for values with predefined distribution. + +Distribution is described with \\(\text{distribution}\\) value. + +If \\(\text{distribution} > \texttt{0xFFFFFFDF} = 2^{32} - 32\\) then value is +always encoded as \\(\text{distribution} - \texttt{0xFFFFFFDF}\\) bits. +This mode is called "raw" encoding. Raw encoding is typically used for +edge cases, e.g. when exactly 1 or 32 bit value needs to be encoded. + +In "regular" (non-raw) mode \\(\text{distribution}\\) is interpreted as array +\\(\text{L}\\) containing 4 uint8 values. + +To decode regular field, first 2 bits are read; +those bits represent "selector" value \\(\text{S}\\). + +Depending on mode \\(\text{M} = \text{L}[\text{S}]\\): + +- if \\(\text{M} \ge \texttt{0x80}\\), + then \\(\text{M} - \texttt{0x80}\\) is the "direct" encoded value +- else if \\(\text{M} \ge \texttt{0x40}\\), + let \\(\text{V}\\) be the \\((\text{M} \& \texttt{0x7}) + 1\\) + following bits, "shifted" encoded value is + \\(\text{V} + ((\text{M} \gg 3) \& \texttt{0x7}) + 1\\) +- otherwise \\(\text{M}\\) following bits represent the encoded value + +Source code: cs/jxl/fields.h + +## Uint64 field + +This field supports bigger values than [Uint32](#uint32-field), but has single +fixed distribution. + +Value is decoded as following: + +- "selector" \\(\text{S}\\) 2 bits +- if \\(\text{S} = 0\\) then value is 0 +- if \\(\text{S} = 1\\) then next 4 bits represent \\(\text{value} - 1\\) +- if \\(\text{S} = 2\\) then next 8 bits represent \\(\text{value} - 17\\) +- if \\(\text{S} = 3\\) then: + - 12 bits represent the lowest 12 bits of value + - while next bit is 1: + - if less than 60 value bits are already read, + then 8 bits represent higher 8 bits of value + - otherwise 4 bits represent the highest 4 bits of value and 'while' + loop is finished + +Source code: cs/jxl/fields.h + +## Byte array field + +Byte array field holds (optionally compressed) array of bytes. Byte array is +encoded as: + +- \\(\text{type}\\) [field](#uint32-field) / + L = [direct 0, direct 1, direct 2, 3 bits + 3] +- if \\(\text{type}\\) is \\(\text{None} = 0\\), then byte array is empty +- if \\(\text{type}\\) is \\(\text{Raw} = 1\\) or \\(\text{Brotli} = 2\\), + then: + - \\(\text{size}\\) [field](#uint32-field) / + L = [8 bits, 16 bits, 24 bits, 32 bits] + - uint8 repeated \\(\text{size}\\) times; + - payload is compressed if \\(\text{type}\\) is \\(\text{Brotli}\\) + +Source code: cs/jxl/fields.h + +## VarSignedMantissa + +VarMantissa is a mapping from \\(\text{L}\\) bits to signed values. +The principle is that \\(\text{L} + 1\\) bits may only represent values whose +absolute values are bigger than absolute values that could be represented with +\\(\text{L}\\) bits. + +- 0-bit values represent \\(\{0\}\\) +- 1-bit values represent \\(\{-1, 1\}\\) +- 2-bit values represent \\(\{-3, -2, 2, 3\}\\) +- L-bit values represent \\(\{\pm 2^{L - 1} .. \pm(2^L - 1)\}\\) + +## VarUnsignedMantissa + +Analogous to [VarSignedMantissa](#varsignedmantissa), but for unsigned values. + +TODO: provide examples + +## VarLenUint8 + +- \\(\text{zero}\\) 1 bit +- if \\(\text{zero}\\) is \\(0\\), then value is \\(0\\), otherwise: + - \\(\text{L}\\) 3 bits + - \\(\text{value} - 1\\) encoded as + \\(\text{L}\\)-bit [VarUnsignedMantissa](#vaunrsignedmantissa) + +## VarLenUint16 + +- \\(\text{zero}\\) 1 bit +- if \\(\text{zero}\\) is \\(0\\), then value is \\(0\\), otherwise: + - \\(\text{L}\\) 4 bits + - \\(\text{value} - 1\\) encoded as + \\(\text{L}\\)-bit [VarUnsignedMantissa](#varunsignedmantissa) diff --git a/lib/jxl/docs/file_format.md b/lib/jxl/docs/file_format.md new file mode 100644 index 0000000..429ee66 --- /dev/null +++ b/lib/jxl/docs/file_format.md @@ -0,0 +1,129 @@ +# File format overview + +[TOC] + + + +This document describes high level PIK format and metadata. + +## FileHeader + +Topmost structure that contains most general image related metadata and a file +signature used to distinguish PIK files. + +Structure: + +- "signature" 32 bits; must be equal to little-endian representation of + \\(\texttt{0x0A4CD74A}\\) value; \\(\texttt{0x0A}\\) causes files opened in + text mode to be rejected, and \\(\texttt{0xD7}\\) detects 7-bit transfers +- "xsize_minus_1" [field](entropy_coding_basic.md#uint32-field) / + L = [9 bits, 11 bits, 13 bits, 32 bits]; 13-bit values support most existing + cameras (up to 8K x 8K images), 32-bit values cover provide support for up + to 4G x 4G images; "xsize" is derived from this field by adding \\(1\\), + thus zero-width images are not supported +- "ysize_minus_1" [field](entropy_coding_basic.md#uint32-field) / + L = [9 bits, 11 bits, 13 bits, 32 bits]; similar to "xsize_minus_1" +- orientation indicates 8 possible orientations, as defined in EXIF. +- nested ["metadata"](#metadata) structure +- nested ["preview"](#preview) structure +- nested ["animation"](#animation) structure +- ["extensions"](#extensions) stub that allows future format extension + +## Metadata + +[Optional structure](#optional-structures) that contains meta-information about +image and other supportive information. + +Structure: +- "all_default" 1 bit; see ["optional structures"](#optional-structures) +- nested ["transcoded"](#transcoded) structure +- "target_nits_div50" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 2, direct 5, direct 80, 8 bits]; + most common values (100, 250, 4000) are encoded directly; maximal + expressible intensity is 12750 +- "exif" compressed [byte array](entropy_coding_basic.md#byte-array-field); + original image metadata +- "iptc" compressed [byte array](entropy_coding_basic.md#byte-array-field); + original image metadata +- "xmp" compressed [byte array](entropy_coding_basic.md#byte-array-field); + original image metadata + +## Transcoded + +[Optional structure](#optional-structures) that contains information original +image. + +Structure: + +- "all_default" 1 bit; see ["optional structures"](#optional-structures) +- "original_bit_depth" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 8, direct 16, direct 32, 5 bits] +- "original_color_encoding" nested [ColorEncoding](#colorencoding) structure +- "original_bytes_per_alpha" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 0, direct 1, direct 2, direct 4] + + +## Preview + +[Optional structure](#optional-structures) that contains information about +"discardable preview" image. Unlike early "progressive" passes, this image is +completely independent and optimized for better "preview" experience, i.e. +appropriately preprocessed and non-power-of-two scaled. + +Preview images have different size constraints than main image and currently +limited to 8K x 8K; size limit is set to be 128MiB. + +Structure: + +- "all_default" 1 bit; see ["optional structures"](#optional-structures) +- "size_bits" [field](entropy_coding_basic.md#uint32-field) / + L = [12 bits, 16 bits, 20 bits, 28 bits] +- "xsize" [field](entropy_coding_basic.md#uint32-field) / + L = [7 bits, 9 bits, 11 bits, 13 bits] +- "ysize" [field](entropy_coding_basic.md#uint32-field) / + L = [7 bits, 9 bits, 11 bits, 13 bits] + +## Animation + +[Optional structure](#optional-structures) that contains meta-information about +animation (image sequence). + +Structure: + +- "all_default" 1 bit; see ["optional structures"](#optional-structures) +- "num_loops" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 0, 3 bits, 16 bits, 32 bits]; \\(0\\) means to repeat infinitely +- "ticks_numerator" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 1, 9 bits, 20 bits, 32 bits] +- "ticks_denominator" [field](entropy_coding_basic.md#uint32-field) / + L = [direct 1, 9 bits, 20 bits, 32 bits] + +## Extensions + +This "structure" is usually put at the end of other structures which would be +extended in future. It allows earlier versions of decoders to skip the newer +fields. + +Structure: + +- "extensions" [field](entropy_coding_basic.md#uint64-field) +- if "extensions" is \\(0\\), then no extra information follows +- otherwise: + - "extension_bits" [field](entropy_coding_basic.md#uint64-field) - number + of bits to be skipped by decoder that does not expect extensions here + +## Optional structures + +Some structures are "optional". In case all the field values are equal to their +defaults, encoder is eligible to represent the structure with a single bit. + +Structures that contain non-empty [extension](#extensions) tail are ineligible +for 1-bit encoding. + +Technically, this bit is represented as "all_default" field that comes first; if +the value of this field is \\(1\\), then the rest of structure is not decoded. diff --git a/lib/jxl/docs/upsample.md b/lib/jxl/docs/upsample.md new file mode 100644 index 0000000..bcbc54c --- /dev/null +++ b/lib/jxl/docs/upsample.md @@ -0,0 +1,151 @@ +# Upsampling + +[TOC] + + + +jxl/resample.h provides `Upsampler8` for fast and high-quality 8x8 upsampling by +4x4 (separable/non-separable) or 6x6 (non-separable) floating-point kernels. +This was previously used for the "smooth predictor", which has been removed. It +would still be useful for a progressive mode that upsamples DC for a preview, +though this code is not yet used for that purpose. + +See 'Separability' section below for the surprising result that non-separable +can be faster than separable and possibly better. + +## Performance evaluation + +### 4x4 separable/non-separable + +__Single-core__: 5.1 GB/s (single-channel, floating-point, 160x96 input) + +* 40x speedup vs. an unoptimized Nehab/Hoppe bicubic upsampler +* 25x or 6x speedup vs. [Pillow 4.3](http://python-pillow.org/pillow-perf/) + bicubic (AVX2: 66M RGB 8-bit per second) in terms of bytes or samples +* 2.6x AVX2 speedup vs. our SSE4 version (similar algorithm) + +__12 independent instances on 12 cores__: same speed for each instance (linear +scalability: not limited by memory bandwidth). + +__Multicore__: 15-18 GB/s (single-channel, floating-point, 320x192 input) + +* 9-11x or 2.3-2.8x speedup vs. a parallel Halide bicubic (1.6G single-channel + 8-bit per second, 320x192 input) in terms of bytes or samples. + +### 6x6 non-separable + +Note that a separable (outer-product) kernel only requires 6+6 (12) +multiplications per output pixel/vector. However, we do not assume anything +about the kernel rank (separability), and thus require 6x6 (36) multiplications. + +__Single-core__: 2.8 GB/s (single-channel, floating-point, 320x192 input) + +* 5x speedup vs. optimized (outer-product of two 1D) Lanczos without SIMD + +__Multicore__: 9-10 GB/s (single-channel, floating-point, 320x192 input) + +* 7-8x or 2x speedup vs. a parallel Halide 6-tap tensor-product Lanczos (1.3G + single-channel 8-bit per second, 320x192 input) in terms of bytes or + samples. + +## Implementation details + +### Data type + +Our input pixels are 16-bit, so 8-bit integer multiplications are insufficient. +16-bit fixed-point arithmetic is fast but risks overflow or loss of precision +unless the value intervals are carefully managed. 32-bit integers reduce this +concern, but 32-bit multiplies are slow on Haswell (one mullo32 per cycle with +10+1 cycle latency). We instead use single-precision float, which enables 2 FMA +per cycle with 5 cycle latency. The extra bandwidth vs. 16-bit types types may +be a concern, but we can run 12 instances (on a 12-core socket) with zero +slowdown, indicating that memory bandwidth is not the bottleneck at the moment. + +### Pixel layout + +We require planar inputs, e.g. all red channel samples in a 2D matrix. This +allows better utilization of 8-lane SIMD compared to dedicating four SIMD lanes +to R,G,B,A samples. If upsampling is the only operation, this requires an extra +deinterleaving step, but our application involves a larger processing pipeline. + +### No prefilter + +Nehab and Hoppe (http://hhoppe.com/filtering.pdf) advocate generalized sampling +with an additional digital filter step. They claim "significantly higher +quality" for upsampling operations, which is contradicted by minimal MSSIM +differences between cardinal and ordinary cubic interpolators in their +experiment on page 65. We also see only minor Butteraugli differences between +Catmull-Rom and B-spline3 or OMOMS3 when upsampling 8x. As they note on page 29, +a prefilter is often omitted when upsampling because the reconstruction filter's +frequency cutoff is lower than that of the prefilter. The prefilter is claimed +to be efficient (relative to normal separable convolutions), but still involves +separate horizontal and vertical passes. To avoid cache thrashing would require +a separate ring buffer of rows, which may be less efficient than our single-pass +algorithm which only writes final outputs. + +### 8x upsampling + +Our code is currently specific to 8x upsampling because that is what is required +for our (DCT prediction) application. This happens to be a particularly good fit +for both 4-lane and 8-lane (AVX2) SIMD AVX2. It would be relatively easy to +adapt the code to 4x upsampling. + +### Kernel support + +Even (asymmetric) kernels are often used to reduce computation. Many upsampling +applications use 4-tap cubic interpolation kernels but at such extreme +magnifications (8x) we find 6x6 to be better. + +### Separability + +Separable kernels can be expressed as the (outer) product of two 1D kernels. For +n x n kernels, this requires n + n multiplications per pixel rather than n x n. +However, the Fourier transform of such kernels (except the Gaussian) has a +square rather than disc shape. + +We instead allow arbitrary (possibly non-separable) kernels. This can avoid +structural dependencies between output pixels within the same 8x8 block. + +Surprisingly, 4x4 non-separable is actually faster than the separable version +due to better utilization of FMA units. For 6x6, we only implement the +non-separable version because our application benefits from such kernels. + +### Dual grid + +A primal grid with `n` grid points at coordinates `k/n` would be convenient for +index computations, but is asymmetric at the borders (0 and `(n-1)/n` != 1) and +does not compensate for the asymmetric (even) kernel support. We instead use a +dual grid with coordinates offset by half the sample spacing, which only adds a +integer shift term to the index computations. Note that the offset still leads +to four identical input pixels in 8x upsampling, which is convenient for 4 and +even 8-lane SIMD. + +### Kernel + +We use Catmull-Rom splines out of convenience. Computational cost does not +matter because the weights are precomputed. Also, Catmull-Rom splines pass +through the control points, but that does not matter in this application. +B-splines or other kernels (arbitrary coefficients, even non-separable) can also +be used. + +### Single pixel loads + +For SIMD convolution, we have the choice whether to broadcast inputs or weights. +To ensure we write a unit-stride vector of output pixels, we need to broadcast +the input pixels and vary the weights. This has the additional benefit of +avoiding complexity at the borders, where it is not safe to load an entire +vector. Instead, we only load and broadcast single pixels, with bounds checking +at the borders but not in the interior. + +### Single pass + +Separable 2D convolutions are often implemented as two separate 1D passes. +However, this leads to cache thrashing in the vertical pass, assuming the image +does not fit into L2 caches. We instead use a single-pass algorithm that +generates four (see "Kernel support" above) horizontal convolution results and +immediately convolves them in the vertical direction to produce a final output. +This is simpler than sliding windows and has a smaller working set, thus +enabling the major speedups reported above. diff --git a/lib/jxl/enc_ac_strategy.cc b/lib/jxl/enc_ac_strategy.cc new file mode 100644 index 0000000..bc50465 --- /dev/null +++ b/lib/jxl/enc_ac_strategy.cc @@ -0,0 +1,1104 @@ +// 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/enc_ac_strategy.h" + +#include +#include + +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_ac_strategy.cc" +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_transforms-inl.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/fast_math-inl.h" + +// Some of the floating point constants in this file and in other +// files in the libjxl project have been obtained using the +// tools/optimizer/simplex_fork.py tool. It is a variation of +// Nelder-Mead optimization, and we generally try to minimize +// BPP * pnorm aggregate as reported by the benchmark_xl tool, +// but occasionally the values are optimized by using additional +// constraints such as maintaining a certain density, or ratio of +// popularity of integral transforms. Jyrki visually reviews all +// such changes and often makes manual changes to maintain good +// visual quality to changes where butteraugli was not sufficiently +// sensitive to some kind of degradation. Unfortunately image quality +// is still more of an art than science. + +// This must come before the begin/end_target, but HWY_ONCE is only true +// after that, so use an "include guard". +#ifndef LIB_JXL_ENC_AC_STRATEGY_ +#define LIB_JXL_ENC_AC_STRATEGY_ +// Parameters of the heuristic are marked with a OPTIMIZE comment. +namespace jxl { + +// Debugging utilities. + +// Returns a linear sRGB color (as bytes) for each AC strategy. +const uint8_t* TypeColor(const uint8_t& raw_strategy) { + JXL_ASSERT(AcStrategy::IsRawStrategyValid(raw_strategy)); + static_assert(AcStrategy::kNumValidStrategies == 27, "Change colors"); + static constexpr uint8_t kColors[][3] = { + {0xFF, 0xFF, 0x00}, // DCT8 + {0xFF, 0x80, 0x80}, // HORNUSS + {0xFF, 0x80, 0x80}, // DCT2x2 + {0xFF, 0x80, 0x80}, // DCT4x4 + {0x80, 0xFF, 0x00}, // DCT16x16 + {0x00, 0xC0, 0x00}, // DCT32x32 + {0xC0, 0xFF, 0x00}, // DCT16x8 + {0xC0, 0xFF, 0x00}, // DCT8x16 + {0x00, 0xFF, 0x00}, // DCT32x8 + {0x00, 0xFF, 0x00}, // DCT8x32 + {0x00, 0xFF, 0x00}, // DCT32x16 + {0x00, 0xFF, 0x00}, // DCT16x32 + {0xFF, 0x80, 0x00}, // DCT4x8 + {0xFF, 0x80, 0x00}, // DCT8x4 + {0xFF, 0xFF, 0x80}, // AFV0 + {0xFF, 0xFF, 0x80}, // AFV1 + {0xFF, 0xFF, 0x80}, // AFV2 + {0xFF, 0xFF, 0x80}, // AFV3 + {0x00, 0xC0, 0xFF}, // DCT64x64 + {0x00, 0xFF, 0xFF}, // DCT64x32 + {0x00, 0xFF, 0xFF}, // DCT32x64 + {0x00, 0x40, 0xFF}, // DCT128x128 + {0x00, 0x80, 0xFF}, // DCT128x64 + {0x00, 0x80, 0xFF}, // DCT64x128 + {0x00, 0x00, 0xC0}, // DCT256x256 + {0x00, 0x00, 0xFF}, // DCT256x128 + {0x00, 0x00, 0xFF}, // DCT128x256 + }; + return kColors[raw_strategy]; +} + +const uint8_t* TypeMask(const uint8_t& raw_strategy) { + JXL_ASSERT(AcStrategy::IsRawStrategyValid(raw_strategy)); + static_assert(AcStrategy::kNumValidStrategies == 27, "Add masks"); + // implicitly, first row and column is made dark + static constexpr uint8_t kMask[][64] = { + { + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + }, // DCT8 + { + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 1, 1, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 1, 0, 0, 1, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + }, // HORNUSS + { + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 0, 1, 0, 1, 0, 1, 0, // + }, // 2x2 + { + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + }, // 4x4 + {}, // DCT16x16 (unused) + {}, // DCT32x32 (unused) + {}, // DCT16x8 (unused) + {}, // DCT8x16 (unused) + {}, // DCT32x8 (unused) + {}, // DCT8x32 (unused) + {}, // DCT32x16 (unused) + {}, // DCT16x32 (unused) + { + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + }, // DCT4x8 + { + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + 0, 0, 0, 0, 1, 0, 0, 0, // + }, // DCT8x4 + { + 1, 1, 1, 1, 1, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + }, // AFV0 + { + 0, 0, 0, 0, 1, 1, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + }, // AFV1 + { + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 0, 0, 0, 0, // + }, // AFV2 + { + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, 0, 0, 1, // + 0, 0, 0, 0, 0, 0, 1, 1, // + 0, 0, 0, 0, 0, 1, 1, 1, // + }, // AFV3 + }; + return kMask[raw_strategy]; +} + +void DumpAcStrategy(const AcStrategyImage& ac_strategy, size_t xsize, + size_t ysize, const char* tag, AuxOut* aux_out) { + Image3F color_acs(xsize, ysize); + for (size_t y = 0; y < ysize; y++) { + float* JXL_RESTRICT rows[3] = { + color_acs.PlaneRow(0, y), + color_acs.PlaneRow(1, y), + color_acs.PlaneRow(2, y), + }; + const AcStrategyRow acs_row = ac_strategy.ConstRow(y / kBlockDim); + for (size_t x = 0; x < xsize; x++) { + AcStrategy acs = acs_row[x / kBlockDim]; + const uint8_t* JXL_RESTRICT color = TypeColor(acs.RawStrategy()); + for (size_t c = 0; c < 3; c++) { + rows[c][x] = color[c] / 255.f; + } + } + } + size_t stride = color_acs.PixelsPerRow(); + for (size_t c = 0; c < 3; c++) { + for (size_t by = 0; by < DivCeil(ysize, kBlockDim); by++) { + float* JXL_RESTRICT row = color_acs.PlaneRow(c, by * kBlockDim); + const AcStrategyRow acs_row = ac_strategy.ConstRow(by); + for (size_t bx = 0; bx < DivCeil(xsize, kBlockDim); bx++) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + const uint8_t* JXL_RESTRICT color = TypeColor(acs.RawStrategy()); + const uint8_t* JXL_RESTRICT mask = TypeMask(acs.RawStrategy()); + if (acs.covered_blocks_x() == 1 && acs.covered_blocks_y() == 1) { + for (size_t iy = 0; iy < kBlockDim && by * kBlockDim + iy < ysize; + iy++) { + for (size_t ix = 0; ix < kBlockDim && bx * kBlockDim + ix < xsize; + ix++) { + if (mask[iy * kBlockDim + ix]) { + row[iy * stride + bx * kBlockDim + ix] = color[c] / 800.f; + } + } + } + } + // draw block edges + for (size_t ix = 0; ix < kBlockDim * acs.covered_blocks_x() && + bx * kBlockDim + ix < xsize; + ix++) { + row[0 * stride + bx * kBlockDim + ix] = color[c] / 350.f; + } + for (size_t iy = 0; iy < kBlockDim * acs.covered_blocks_y() && + by * kBlockDim + iy < ysize; + iy++) { + row[iy * stride + bx * kBlockDim + 0] = color[c] / 350.f; + } + } + } + } + aux_out->DumpImage(tag, color_acs); +} + +} // namespace jxl +#endif // LIB_JXL_ENC_AC_STRATEGY_ + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +bool MultiBlockTransformCrossesHorizontalBoundary( + const AcStrategyImage& ac_strategy, size_t start_x, size_t y, + size_t end_x) { + if (start_x >= ac_strategy.xsize() || y >= ac_strategy.ysize()) { + return false; + } + if (y % 8 == 0) { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + end_x = std::min(end_x, ac_strategy.xsize()); + // The first multiblock might be before the start_x, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + AcStrategyRow row = ac_strategy.ConstRow(y); + const size_t start_x_limit = start_x & ~7; + while (start_x != start_x_limit && !row[start_x].IsFirstBlock()) { + --start_x; + } + for (size_t x = start_x; x < end_x;) { + if (row[x].IsFirstBlock()) { + x += row[x].covered_blocks_x(); + } else { + return true; + } + } + return false; +} + +bool MultiBlockTransformCrossesVerticalBoundary( + const AcStrategyImage& ac_strategy, size_t x, size_t start_y, + size_t end_y) { + if (x >= ac_strategy.xsize() || start_y >= ac_strategy.ysize()) { + return false; + } + if (x % 8 == 0) { + // Nothing crosses 64x64 boundaries, and the memory on the other side + // of the 64x64 block may still uninitialized. + return false; + } + end_y = std::min(end_y, ac_strategy.ysize()); + // The first multiblock might be before the start_y, let's adjust it + // to point to the first IsFirstBlock() == true block we find by backward + // tracing. + const size_t start_y_limit = start_y & ~7; + while (start_y != start_y_limit && + !ac_strategy.ConstRow(start_y)[x].IsFirstBlock()) { + --start_y; + } + + for (size_t y = start_y; y < end_y;) { + AcStrategyRow row = ac_strategy.ConstRow(y); + if (row[x].IsFirstBlock()) { + y += row[x].covered_blocks_y(); + } else { + return true; + } + } + return false; +} + +float EstimateEntropy(const AcStrategy& acs, size_t x, size_t y, + const ACSConfig& config, + const float* JXL_RESTRICT cmap_factors, float* block, + float* scratch_space, uint32_t* quantized) { + const size_t size = (1 << acs.log2_covered_blocks()) * kDCTBlockSize; + + // Apply transform. + for (size_t c = 0; c < 3; c++) { + float* JXL_RESTRICT block_c = block + size * c; + TransformFromPixels(acs.Strategy(), &config.Pixel(c, x, y), + config.src_stride, block_c, scratch_space); + } + + HWY_FULL(float) df; + + const size_t num_blocks = acs.covered_blocks_x() * acs.covered_blocks_y(); + float quant_norm8 = 0; + float masking = 0; + if (num_blocks == 1) { + // When it is only one 8x8, we don't need aggregation of values. + quant_norm8 = config.Quant(x / 8, y / 8); + masking = 2.0f * config.Masking(x / 8, y / 8); + } else if (num_blocks == 2) { + // Taking max instead of 8th norm seems to work + // better for smallest blocks up to 16x8. Jyrki couldn't get + // improvements in trying the same for 16x16 blocks. + if (acs.covered_blocks_y() == 2) { + quant_norm8 = + std::max(config.Quant(x / 8, y / 8), config.Quant(x / 8, y / 8 + 1)); + masking = 2.0f * std::max(config.Masking(x / 8, y / 8), + config.Masking(x / 8, y / 8 + 1)); + } else { + quant_norm8 = + std::max(config.Quant(x / 8, y / 8), config.Quant(x / 8 + 1, y / 8)); + masking = 2.0f * std::max(config.Masking(x / 8, y / 8), + config.Masking(x / 8 + 1, y / 8)); + } + } else { + float masking_norm2 = 0; + float masking_max = 0; + // Load QF value, calculate empirical heuristic on masking field + // for weighting the information loss. Information loss manifests + // itself as ringing, and masking could hide it. + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + float qval = config.Quant(x / 8 + ix, y / 8 + iy); + qval *= qval; + qval *= qval; + quant_norm8 += qval * qval; + float maskval = config.Masking(x / 8 + ix, y / 8 + iy); + masking_max = std::max(masking_max, maskval); + masking_norm2 += maskval * maskval; + } + } + quant_norm8 /= num_blocks; + quant_norm8 = FastPowf(quant_norm8, 1.0f / 8.0f); + masking_norm2 = sqrt(masking_norm2 / num_blocks); + // This is a highly empirical formula. + masking = (masking_norm2 + masking_max); + } + const auto q = Set(df, quant_norm8); + + // Compute entropy. + float entropy = config.base_entropy; + auto info_loss = Zero(df); + auto info_loss2 = Zero(df); + + for (size_t c = 0; c < 3; c++) { + const float* inv_matrix = config.dequant->InvMatrix(acs.RawStrategy(), c); + const auto cmap_factor = Set(df, cmap_factors[c]); + + auto entropy_v = Zero(df); + auto nzeros_v = Zero(df); + auto cost1 = Set(df, config.cost1); + auto cost2 = Set(df, config.cost2); + auto cost_delta = Set(df, config.cost_delta); + for (size_t i = 0; i < num_blocks * kDCTBlockSize; i += Lanes(df)) { + const auto in = Load(df, block + c * size + i); + const auto in_y = Load(df, block + size + i) * cmap_factor; + const auto im = Load(df, inv_matrix + i); + const auto val = (in - in_y) * im * q; + const auto rval = Round(val); + const auto diff = AbsDiff(val, rval); + info_loss += diff; + info_loss2 += diff * diff; + const auto q = Abs(rval); + const auto q_is_zero = q == Zero(df); + entropy_v += IfThenElseZero(q >= Set(df, 1.5f), cost2); + // We used to have q * C here, but that cost model seems to + // be punishing large values more than necessary. Sqrt tries + // to avoid large values less aggressively. Having high accuracy + // around zero is most important at low qualities, and there + // we have directly specified costs for 0, 1, and 2. + entropy_v += Sqrt(q) * cost_delta; + nzeros_v += IfThenZeroElse(q_is_zero, Set(df, 1.0f)); + } + entropy_v += nzeros_v * cost1; + + entropy += GetLane(SumOfLanes(entropy_v)); + size_t num_nzeros = GetLane(SumOfLanes(nzeros_v)); + // Add #bit of num_nonzeros, as an estimate of the cost for encoding the + // number of non-zeros of the block. + size_t nbits = CeilLog2Nonzero(num_nzeros + 1) + 1; + // Also add #bit of #bit of num_nonzeros, to estimate the ANS cost, with a + // bias. + entropy += config.zeros_mul * (CeilLog2Nonzero(nbits + 17) + nbits); + } + float ret = + entropy + + masking * + ((config.info_loss_multiplier * GetLane(SumOfLanes(info_loss))) + + (config.info_loss_multiplier2 * + sqrt(num_blocks * GetLane(SumOfLanes(info_loss2))))); + return ret; +} + +uint8_t FindBest8x8Transform(size_t x, size_t y, int encoding_speed_tier, + const ACSConfig& config, + const float* JXL_RESTRICT cmap_factors, + AcStrategyImage* JXL_RESTRICT ac_strategy, + float* block, float* scratch_space, + uint32_t* quantized, float* entropy_out) { + struct TransformTry8x8 { + AcStrategy::Type type; + int encoding_speed_tier_max_limit; + float entropy_add; + float entropy_mul; + }; + static const TransformTry8x8 kTransforms8x8[] = { + { + AcStrategy::Type::DCT, + 9, + 3.0f, + 0.745f, + }, + { + AcStrategy::Type::DCT4X4, + 5, + 4.0f, + 1.0179946967008329f, + }, + { + AcStrategy::Type::DCT2X2, + 4, + 4.0f, + 0.76721119707580943f, + }, + { + AcStrategy::Type::DCT4X8, + 5, + 0.0f, + 0.700754622182473063f, + }, + { + AcStrategy::Type::DCT8X4, + 5, + 0.0f, + 0.700754622182473063f, + }, + { + AcStrategy::Type::IDENTITY, + 5, + 8.0f, + 0.81217614513585534f, + }, + { + AcStrategy::Type::AFV0, + 4, + 3.0f, + 0.70086131125719425f, + }, + { + AcStrategy::Type::AFV1, + 4, + 3.0f, + 0.70086131125719425f, + }, + { + AcStrategy::Type::AFV2, + 4, + 3.0f, + 0.70086131125719425f, + }, + { + AcStrategy::Type::AFV3, + 4, + 3.0f, + 0.70086131125719425f, + }, + }; + double best = 1e30; + uint8_t best_tx = kTransforms8x8[0].type; + for (auto tx : kTransforms8x8) { + if (tx.encoding_speed_tier_max_limit < encoding_speed_tier) { + continue; + } + AcStrategy acs = AcStrategy::FromRawStrategy(tx.type); + float entropy = EstimateEntropy(acs, x, y, config, cmap_factors, block, + scratch_space, quantized); + entropy = tx.entropy_add + tx.entropy_mul * entropy; + if (entropy < best) { + best_tx = tx.type; + best = entropy; + } + } + *entropy_out = best; + return best_tx; +} + +// bx, by addresses the 64x64 block at 8x8 subresolution +// cx, cy addresses the left, upper 8x8 block position of the candidate +// transform. +void TryMergeAcs(AcStrategy::Type acs_raw, size_t bx, size_t by, size_t cx, + size_t cy, const ACSConfig& config, + const float* JXL_RESTRICT cmap_factors, + AcStrategyImage* JXL_RESTRICT ac_strategy, + const float entropy_mul, const uint8_t candidate_priority, + uint8_t* priority, float* JXL_RESTRICT entropy_estimate, + float* block, float* scratch_space, uint32_t* quantized) { + AcStrategy acs = AcStrategy::FromRawStrategy(acs_raw); + float entropy_current = 0; + for (size_t iy = 0; iy < acs.covered_blocks_y(); ++iy) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ++ix) { + if (priority[(cy + iy) * 8 + (cx + ix)] >= candidate_priority) { + // Transform would reuse already allocated blocks and + // lead to invalid overlaps, for example DCT64X32 vs. + // DCT32X64. + return; + } + entropy_current += entropy_estimate[(cy + iy) * 8 + (cx + ix)]; + } + } + float entropy_candidate = + entropy_mul * EstimateEntropy(acs, (bx + cx) * 8, (by + cy) * 8, config, + cmap_factors, block, scratch_space, + quantized); + if (entropy_candidate >= entropy_current) return; + // Accept the candidate. + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + entropy_estimate[(cy + iy) * 8 + cx + ix] = 0; + priority[(cy + iy) * 8 + cx + ix] = candidate_priority; + } + } + ac_strategy->Set(bx + cx, by + cy, acs_raw); + entropy_estimate[cy * 8 + cx] = entropy_candidate; +} + +static void SetEntropyForTransform(size_t cx, size_t cy, + const AcStrategy::Type acs_raw, + float entropy, + float* JXL_RESTRICT entropy_estimate) { + const AcStrategy acs = AcStrategy::FromRawStrategy(acs_raw); + for (size_t dy = 0; dy < acs.covered_blocks_y(); ++dy) { + for (size_t dx = 0; dx < acs.covered_blocks_x(); ++dx) { + entropy_estimate[(cy + dy) * 8 + cx + dx] = 0.0; + } + } + entropy_estimate[cy * 8 + cx] = entropy; +} + +AcStrategy::Type AcsSquare(size_t blocks) { + if (blocks == 2) { + return AcStrategy::Type::DCT16X16; + } else if (blocks == 4) { + return AcStrategy::Type::DCT32X32; + } else { + return AcStrategy::Type::DCT64X64; + } +} + +AcStrategy::Type AcsVerticalSplit(size_t blocks) { + if (blocks == 2) { + return AcStrategy::Type::DCT16X8; + } else if (blocks == 4) { + return AcStrategy::Type::DCT32X16; + } else { + return AcStrategy::Type::DCT64X32; + } +} + +AcStrategy::Type AcsHorizontalSplit(size_t blocks) { + if (blocks == 2) { + return AcStrategy::Type::DCT8X16; + } else if (blocks == 4) { + return AcStrategy::Type::DCT16X32; + } else { + return AcStrategy::Type::DCT32X64; + } +} + +// The following function tries to merge smaller transforms into +// squares and the rectangles originating from a single middle division +// (horizontal or vertical) fairly. +// +// This is now generalized to concern about squares +// of blocks X blocks size, where a block is 8x8 pixels. +void FindBestFirstLevelDivisionForSquare( + size_t blocks, bool allow_square_transform, size_t bx, size_t by, size_t cx, + size_t cy, const ACSConfig& config, const float* JXL_RESTRICT cmap_factors, + AcStrategyImage* JXL_RESTRICT ac_strategy, const float entropy_mul_JXK, + const float entropy_mul_JXJ, float* JXL_RESTRICT entropy_estimate, + float* block, float* scratch_space, uint32_t* quantized) { + // We denote J for the larger dimension here, and K for the smaller. + // For example, for 32x32 block splitting, J would be 32, K 16. + const size_t blocks_half = blocks / 2; + const AcStrategy::Type acs_rawJXK = AcsVerticalSplit(blocks); + const AcStrategy::Type acs_rawKXJ = AcsHorizontalSplit(blocks); + const AcStrategy::Type acs_rawJXJ = AcsSquare(blocks); + const AcStrategy acsJXK = AcStrategy::FromRawStrategy(acs_rawJXK); + const AcStrategy acsKXJ = AcStrategy::FromRawStrategy(acs_rawKXJ); + const AcStrategy acsJXJ = AcStrategy::FromRawStrategy(acs_rawJXJ); + AcStrategyRow row0 = ac_strategy->ConstRow(by + cy + 0); + AcStrategyRow row1 = ac_strategy->ConstRow(by + cy + blocks_half); + // Let's check if we can consider a JXJ block here at all. + // This is not necessary in the basic use of hierarchically merging + // blocks in the simplest possible way, but is needed when we try other + // 'floating' options of merging, possibly after a simple hierarchical + // merge has been explored. + if (MultiBlockTransformCrossesHorizontalBoundary(*ac_strategy, bx + cx, + by + cy, bx + cx + blocks) || + MultiBlockTransformCrossesHorizontalBoundary( + *ac_strategy, bx + cx, by + cy + blocks, bx + cx + blocks) || + MultiBlockTransformCrossesVerticalBoundary(*ac_strategy, bx + cx, by + cy, + by + cy + blocks) || + MultiBlockTransformCrossesVerticalBoundary(*ac_strategy, bx + cx + blocks, + by + cy, by + cy + blocks)) { + return; // not suitable for JxJ analysis, some transforms leak out. + } + // For floating transforms there may be + // already blocks selected that make either or both JXK and + // KXJ not feasible for this location. + const bool allow_JXK = !MultiBlockTransformCrossesVerticalBoundary( + *ac_strategy, bx + cx + blocks_half, by + cy, by + cy + blocks); + const bool allow_KXJ = !MultiBlockTransformCrossesHorizontalBoundary( + *ac_strategy, bx + cx, by + cy + blocks_half, bx + cx + blocks); + // Current entropies aggregated on NxN resolution. + float entropy[2][2] = {}; + for (size_t dy = 0; dy < blocks; ++dy) { + for (size_t dx = 0; dx < blocks; ++dx) { + entropy[dy / blocks_half][dx / blocks_half] += + entropy_estimate[(cy + dy) * 8 + (cx + dx)]; + } + } + float entropy_JXK_left = std::numeric_limits::max(); + float entropy_JXK_right = std::numeric_limits::max(); + float entropy_KXJ_top = std::numeric_limits::max(); + float entropy_KXJ_bottom = std::numeric_limits::max(); + float entropy_JXJ = std::numeric_limits::max(); + if (allow_JXK) { + if (row0[bx + cx + 0].RawStrategy() != acs_rawJXK) { + entropy_JXK_left = + entropy_mul_JXK * + EstimateEntropy(acsJXK, (bx + cx + 0) * 8, (by + cy + 0) * 8, config, + cmap_factors, block, scratch_space, quantized); + } + if (row0[bx + cx + blocks_half].RawStrategy() != acs_rawJXK) { + entropy_JXK_right = + entropy_mul_JXK * EstimateEntropy(acsJXK, (bx + cx + blocks_half) * 8, + (by + cy + 0) * 8, config, + cmap_factors, block, scratch_space, + quantized); + } + } + if (allow_KXJ) { + if (row0[bx + cx].RawStrategy() != acs_rawKXJ) { + entropy_KXJ_top = + entropy_mul_JXK * + EstimateEntropy(acsKXJ, (bx + cx + 0) * 8, (by + cy + 0) * 8, config, + cmap_factors, block, scratch_space, quantized); + } + if (row1[bx + cx].RawStrategy() != acs_rawKXJ) { + entropy_KXJ_bottom = + entropy_mul_JXK * EstimateEntropy(acsKXJ, (bx + cx + 0) * 8, + (by + cy + blocks_half) * 8, config, + cmap_factors, block, scratch_space, + quantized); + } + } + if (allow_square_transform) { + // We control the exploration of the square transform separately so that + // we can turn it off at high decoding speeds for 32x32, but still allow + // exploring 16x32 and 32x16. + entropy_JXJ = entropy_mul_JXJ * EstimateEntropy(acsJXJ, (bx + cx + 0) * 8, + (by + cy + 0) * 8, config, + cmap_factors, block, + scratch_space, quantized); + } + + // Test if this block should have JXK or KXJ transforms, + // because it can have only one or the other. + float costJxN = std::min(entropy_JXK_left, entropy[0][0] + entropy[1][0]) + + std::min(entropy_JXK_right, entropy[0][1] + entropy[1][1]); + float costNxJ = std::min(entropy_KXJ_top, entropy[0][0] + entropy[0][1]) + + std::min(entropy_KXJ_bottom, entropy[1][0] + entropy[1][1]); + if (entropy_JXJ < costJxN && entropy_JXJ < costNxJ) { + ac_strategy->Set(bx + cx, by + cy, acs_rawJXJ); + SetEntropyForTransform(cx, cy, acs_rawJXJ, entropy_JXJ, entropy_estimate); + } else if (costJxN < costNxJ) { + if (entropy_JXK_left < entropy[0][0] + entropy[1][0]) { + ac_strategy->Set(bx + cx, by + cy, acs_rawJXK); + SetEntropyForTransform(cx, cy, acs_rawJXK, entropy_JXK_left, + entropy_estimate); + } + if (entropy_JXK_right < entropy[0][1] + entropy[1][1]) { + ac_strategy->Set(bx + cx + blocks_half, by + cy, acs_rawJXK); + SetEntropyForTransform(cx + blocks_half, cy, acs_rawJXK, + entropy_JXK_right, entropy_estimate); + } + } else { + if (entropy_KXJ_top < entropy[0][0] + entropy[0][1]) { + ac_strategy->Set(bx + cx, by + cy, acs_rawKXJ); + SetEntropyForTransform(cx, cy, acs_rawKXJ, entropy_KXJ_top, + entropy_estimate); + } + if (entropy_KXJ_bottom < entropy[1][0] + entropy[1][1]) { + ac_strategy->Set(bx + cx, by + cy + blocks_half, acs_rawKXJ); + SetEntropyForTransform(cx, cy + blocks_half, acs_rawKXJ, + entropy_KXJ_bottom, entropy_estimate); + } + } +} + +void ProcessRectACS(PassesEncoderState* JXL_RESTRICT enc_state, + const ACSConfig& config, const Rect& rect) { + // Main philosophy here: + // 1. First find best 8x8 transform for each area. + // 2. Merging them into larger transforms where possibly, but + // starting from the smallest transforms (16x8 and 8x16). + // Additional complication: 16x8 and 8x16 are considered + // simultanouesly and fairly against each other. + // We are looking at 64x64 squares since the YtoX and YtoB + // maps happen to be at that resolution, and having + // integral transforms cross these boundaries leads to + // additional complications. + const CompressParams& cparams = enc_state->cparams; + const float butteraugli_target = cparams.butteraugli_distance; + AcStrategyImage* ac_strategy = &enc_state->shared.ac_strategy; + // TODO(veluca): reuse allocations + auto mem = hwy::AllocateAligned(5 * AcStrategy::kMaxCoeffArea); + auto qmem = hwy::AllocateAligned(AcStrategy::kMaxCoeffArea); + uint32_t* JXL_RESTRICT quantized = qmem.get(); + float* JXL_RESTRICT block = mem.get(); + float* JXL_RESTRICT scratch_space = mem.get() + 3 * AcStrategy::kMaxCoeffArea; + size_t bx = rect.x0(); + size_t by = rect.y0(); + JXL_ASSERT(rect.xsize() <= 8); + JXL_ASSERT(rect.ysize() <= 8); + size_t tx = bx / kColorTileDimInBlocks; + size_t ty = by / kColorTileDimInBlocks; + const float cmap_factors[3] = { + enc_state->shared.cmap.YtoXRatio( + enc_state->shared.cmap.ytox_map.ConstRow(ty)[tx]), + 0.0f, + enc_state->shared.cmap.YtoBRatio( + enc_state->shared.cmap.ytob_map.ConstRow(ty)[tx]), + }; + if (cparams.speed_tier > SpeedTier::kHare) return; + // First compute the best 8x8 transform for each square. Later, we do not + // experiment with different combinations, but only use the best of the 8x8s + // when DCT8X8 is specified in the tree search. + // 8x8 transforms have 10 variants, but every larger transform is just a DCT. + float entropy_estimate[64] = {}; + // Favor all 8x8 transforms (against 16x8 and larger transforms)) at + // low butteraugli_target distances. + static const float k8x8mul1 = -0.55; + static const float k8x8mul2 = 1.0735757687292623f; + static const float k8x8base = 1.4; + const float mul8x8 = k8x8mul2 + k8x8mul1 / (butteraugli_target + k8x8base); + for (size_t iy = 0; iy < rect.ysize(); iy++) { + for (size_t ix = 0; ix < rect.xsize(); ix++) { + float entropy = 0.0; + const uint8_t best_of_8x8s = FindBest8x8Transform( + 8 * (bx + ix), 8 * (by + iy), static_cast(cparams.speed_tier), + config, cmap_factors, ac_strategy, block, scratch_space, quantized, + &entropy); + ac_strategy->Set(bx + ix, by + iy, + static_cast(best_of_8x8s)); + entropy_estimate[iy * 8 + ix] = entropy * mul8x8; + } + } + // Merge when a larger transform is better than the previously + // searched best combination of 8x8 transforms. + struct MergeTry { + AcStrategy::Type type; + uint8_t priority; + uint8_t decoding_speed_tier_max_limit; + uint8_t encoding_speed_tier_max_limit; + float entropy_mul; + }; + static const float k8X16mul1 = -0.55; + static const float k8X16mul2 = 0.9019587899705066; + static const float k8X16base = 1.6; + const float entropy_mul16X8 = + k8X16mul2 + k8X16mul1 / (butteraugli_target + k8X16base); + // const float entropy_mul16X8 = mul8X16 * 0.91195782912371126f; + + static const float k16X16mul1 = -0.35; + static const float k16X16mul2 = 0.82; + static const float k16X16base = 2.0; + const float entropy_mul16X16 = + k16X16mul2 + k16X16mul1 / (butteraugli_target + k16X16base); + // const float entropy_mul16X16 = mul16X16 * 0.83183417727960129f; + + static const float k32X16mul1 = -0.1; + static const float k32X16mul2 = 0.84; + static const float k32X16base = 2.5; + const float entropy_mul16X32 = + k32X16mul2 + k32X16mul1 / (butteraugli_target + k32X16base); + + const float entropy_mul32X32 = 0.9; + const float entropy_mul64X64 = 1.43f; + // TODO(jyrki): Consider this feedback in further changes: + // Also effectively when the multipliers for smaller blocks are + // below 1, this raises the bar for the bigger blocks even higher + // in that sense these constants are not independent (e.g. changing + // the constant for DCT16x32 by -5% (making it more likely) also + // means that DCT32x32 becomes harder to do when starting from + // two DCT16x32s). It might be better to make them more independent, + // e.g. by not applying the multiplier when storing the new entropy + // estimates in TryMergeToACSCandidate(). + const MergeTry kTransformsForMerge[9] = { + {AcStrategy::Type::DCT16X8, 2, 4, 5, entropy_mul16X8}, + {AcStrategy::Type::DCT8X16, 2, 4, 5, entropy_mul16X8}, + // FindBestFirstLevelDivisionForSquare looks for DCT16X16 and its + // subdivisions. {AcStrategy::Type::DCT16X16, 3, entropy_mul16X16}, + {AcStrategy::Type::DCT16X32, 4, 4, 4, entropy_mul16X32}, + {AcStrategy::Type::DCT32X16, 4, 4, 4, entropy_mul16X32}, + // FindBestFirstLevelDivisionForSquare looks for DCT32X32 and its + // subdivisions. {AcStrategy::Type::DCT32X32, 5, 1, 5, + // 0.9822994906548809f}, + // TODO(jyrki): re-enable 64x32 and 64x64 if/when possible. + {AcStrategy::Type::DCT64X32, 6, 1, 3, 1.26f}, + {AcStrategy::Type::DCT32X64, 6, 1, 3, 1.26f}, + // {AcStrategy::Type::DCT64X64, 8, 1, 3, 2.0846542128012948f}, + }; + /* + These sizes not yet included in merge heuristic: + set(AcStrategy::Type::DCT32X8, 0.0f, 2.261390410971102f); + set(AcStrategy::Type::DCT8X32, 0.0f, 2.261390410971102f); + set(AcStrategy::Type::DCT128X128, 0.0f, 1.0f); + set(AcStrategy::Type::DCT128X64, 0.0f, 0.73f); + set(AcStrategy::Type::DCT64X128, 0.0f, 0.73f); + set(AcStrategy::Type::DCT256X256, 0.0f, 1.0f); + set(AcStrategy::Type::DCT256X128, 0.0f, 0.73f); + set(AcStrategy::Type::DCT128X256, 0.0f, 0.73f); + */ + + // Priority is a tricky kludge to avoid collisions so that transforms + // don't overlap. + uint8_t priority[64] = {}; + for (auto tx : kTransformsForMerge) { + if (tx.decoding_speed_tier_max_limit < cparams.decoding_speed_tier) { + continue; + } + AcStrategy acs = AcStrategy::FromRawStrategy(tx.type); + + for (size_t cy = 0; cy + acs.covered_blocks_y() - 1 < rect.ysize(); + cy += acs.covered_blocks_y()) { + for (size_t cx = 0; cx + acs.covered_blocks_x() - 1 < rect.xsize(); + cx += acs.covered_blocks_x()) { + if (cy + 7 < rect.ysize() && cx + 7 < rect.xsize()) { + if (cparams.decoding_speed_tier < 4 && + tx.type == AcStrategy::Type::DCT32X64) { + // We handle both DCT8X16 and DCT16X8 at the same time. + if ((cy | cx) % 8 == 0) { + FindBestFirstLevelDivisionForSquare( + 8, true, bx, by, cx, cy, config, cmap_factors, ac_strategy, + tx.entropy_mul, entropy_mul64X64, entropy_estimate, block, + scratch_space, quantized); + } + continue; + } else if (tx.type == AcStrategy::Type::DCT32X16) { + // We handled both DCT8X16 and DCT16X8 at the same time, + // and that is above. The last column and last row, + // when the last column or last row is odd numbered, + // are still handled by TryMergeAcs. + continue; + } + } + if ((tx.type == AcStrategy::Type::DCT16X32 && cy % 4 != 0) || + (tx.type == AcStrategy::Type::DCT32X16 && cx % 4 != 0)) { + // already covered by FindBest32X32 + continue; + } + + if (cy + 3 < rect.ysize() && cx + 3 < rect.xsize()) { + if (tx.type == AcStrategy::Type::DCT16X32) { + // We handle both DCT8X16 and DCT16X8 at the same time. + bool enable_32x32 = cparams.decoding_speed_tier < 4; + if ((cy | cx) % 4 == 0) { + FindBestFirstLevelDivisionForSquare( + 4, enable_32x32, bx, by, cx, cy, config, cmap_factors, + ac_strategy, tx.entropy_mul, entropy_mul32X32, + entropy_estimate, block, scratch_space, quantized); + } + continue; + } else if (tx.type == AcStrategy::Type::DCT32X16) { + // We handled both DCT8X16 and DCT16X8 at the same time, + // and that is above. The last column and last row, + // when the last column or last row is odd numbered, + // are still handled by TryMergeAcs. + continue; + } + } + if ((tx.type == AcStrategy::Type::DCT16X32 && cy % 4 != 0) || + (tx.type == AcStrategy::Type::DCT32X16 && cx % 4 != 0)) { + // already covered by FindBest32X32 + continue; + } + if (cy + 1 < rect.ysize() && cx + 1 < rect.xsize()) { + if (tx.type == AcStrategy::Type::DCT8X16) { + // We handle both DCT8X16 and DCT16X8 at the same time. + if ((cy | cx) % 2 == 0) { + FindBestFirstLevelDivisionForSquare( + 2, true, bx, by, cx, cy, config, cmap_factors, ac_strategy, + tx.entropy_mul, entropy_mul16X16, entropy_estimate, block, + scratch_space, quantized); + } + continue; + } else if (tx.type == AcStrategy::Type::DCT16X8) { + // We handled both DCT8X16 and DCT16X8 at the same time, + // and that is above. The last column and last row, + // when the last column or last row is odd numbered, + // are still handled by TryMergeAcs. + continue; + } + } + if ((tx.type == AcStrategy::Type::DCT8X16 && cy % 2 == 1) || + (tx.type == AcStrategy::Type::DCT16X8 && cx % 2 == 1)) { + // already covered by FindBestFirstLevelDivisionForSquare + continue; + } + // All other merge sizes are handled here. + // Some of the DCT16X8s and DCT8X16s will still leak through here + // when there is an odd number of 8x8 blocks, then the last row + // and column will get their DCT16X8s and DCT8X16s through the + // normal integral transform merging process. + TryMergeAcs(tx.type, bx, by, cx, cy, config, cmap_factors, ac_strategy, + tx.entropy_mul, tx.priority, &priority[0], entropy_estimate, + block, scratch_space, quantized); + } + } + } + // Here we still try to do some non-aligned matching, find a few more + // 16X8, 8X16 and 16X16s between the non-2-aligned blocks. + if (cparams.speed_tier >= SpeedTier::kHare) { + return; + } + for (int ii = 0; ii < 3; ++ii) { + for (size_t cy = 1 - (ii == 1); cy + 1 < rect.ysize(); cy += 2) { + for (size_t cx = 1 - (ii == 2); cx + 1 < rect.xsize(); cx += 2) { + FindBestFirstLevelDivisionForSquare( + 2, true, bx, by, cx, cy, config, cmap_factors, ac_strategy, + entropy_mul16X8, entropy_mul16X16, entropy_estimate, block, + scratch_space, quantized); + } + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ProcessRectACS); + +void AcStrategyHeuristics::Init(const Image3F& src, + PassesEncoderState* enc_state) { + this->enc_state = enc_state; + config.dequant = &enc_state->shared.matrices; + const CompressParams& cparams = enc_state->cparams; + const float butteraugli_target = cparams.butteraugli_distance; + + // Image row pointers and strides. + config.quant_field_row = enc_state->initial_quant_field.Row(0); + config.quant_field_stride = enc_state->initial_quant_field.PixelsPerRow(); + auto& mask = enc_state->initial_quant_masking; + if (mask.xsize() > 0 && mask.ysize() > 0) { + config.masking_field_row = mask.Row(0); + config.masking_field_stride = mask.PixelsPerRow(); + } + + config.src_rows[0] = src.ConstPlaneRow(0, 0); + config.src_rows[1] = src.ConstPlaneRow(1, 0); + config.src_rows[2] = src.ConstPlaneRow(2, 0); + config.src_stride = src.PixelsPerRow(); + + // Entropy estimate is composed of two factors: + // - estimate of the number of bits that will be used by the block + // - information loss due to quantization + // The following constant controls the relative weights of these components. + config.info_loss_multiplier = 138.0f; + config.info_loss_multiplier2 = 50.46839691767866; + // TODO(jyrki): explore base_entropy setting more. + // A small value (0?) works better at high distance, while a larger value + // may be more effective at low distance/high bpp. + config.base_entropy = 0.0; + config.zeros_mul = 7.565053364251793f; + // Lots of +1 and -1 coefficients at high quality, it is + // beneficial to favor them. At low qualities zeros matter more + // and +1 / -1 coefficients are already quite harmful. + float slope = std::min(1.0f, butteraugli_target * (1.0f / 3)); + config.cost1 = 1 + slope * 8.8703248061477744f; + config.cost2 = 4.4628149885273363f; + config.cost_delta = 5.3359184934516337f; + JXL_ASSERT(enc_state->shared.ac_strategy.xsize() == + enc_state->shared.frame_dim.xsize_blocks); + JXL_ASSERT(enc_state->shared.ac_strategy.ysize() == + enc_state->shared.frame_dim.ysize_blocks); +} + +void AcStrategyHeuristics::ProcessRect(const Rect& rect) { + PROFILER_FUNC; + const CompressParams& cparams = enc_state->cparams; + // In Falcon mode, use DCT8 everywhere and uniform quantization. + if (cparams.speed_tier >= SpeedTier::kCheetah) { + enc_state->shared.ac_strategy.FillDCT8(rect); + return; + } + HWY_DYNAMIC_DISPATCH(ProcessRectACS) + (enc_state, config, rect); +} + +void AcStrategyHeuristics::Finalize(AuxOut* aux_out) { + const auto& ac_strategy = enc_state->shared.ac_strategy; + // Accounting and debug output. + if (aux_out != nullptr) { + aux_out->num_small_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::IDENTITY) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT2X2) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT4X4); + aux_out->num_dct4x8_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT4X8) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT8X4); + aux_out->num_afv_blocks = ac_strategy.CountBlocks(AcStrategy::Type::AFV0) + + ac_strategy.CountBlocks(AcStrategy::Type::AFV1) + + ac_strategy.CountBlocks(AcStrategy::Type::AFV2) + + ac_strategy.CountBlocks(AcStrategy::Type::AFV3); + aux_out->num_dct8_blocks = ac_strategy.CountBlocks(AcStrategy::Type::DCT); + aux_out->num_dct8x16_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT8X16) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT16X8); + aux_out->num_dct8x32_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT8X32) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT32X8); + aux_out->num_dct16_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT16X16); + aux_out->num_dct16x32_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT16X32) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT32X16); + aux_out->num_dct32_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT32X32); + aux_out->num_dct32x64_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT32X64) + + ac_strategy.CountBlocks(AcStrategy::Type::DCT64X32); + aux_out->num_dct64_blocks = + ac_strategy.CountBlocks(AcStrategy::Type::DCT64X64); + } + + if (WantDebugOutput(aux_out)) { + DumpAcStrategy(ac_strategy, enc_state->shared.frame_dim.xsize, + enc_state->shared.frame_dim.ysize, "ac_strategy", aux_out); + } +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_ac_strategy.h b/lib/jxl/enc_ac_strategy.h new file mode 100644 index 0000000..6cf82d5 --- /dev/null +++ b/lib/jxl/enc_ac_strategy.h @@ -0,0 +1,79 @@ +// 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_ENC_AC_STRATEGY_H_ +#define LIB_JXL_ENC_AC_STRATEGY_H_ + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/quant_weights.h" + +// `FindBestAcStrategy` uses heuristics to choose which AC strategy should be +// used in each block, as well as the initial quantization field. + +namespace jxl { + +// AC strategy selection: utility struct. + +struct ACSConfig { + const DequantMatrices* JXL_RESTRICT dequant; + float info_loss_multiplier; + float info_loss_multiplier2; + float* JXL_RESTRICT quant_field_row; + size_t quant_field_stride; + float* JXL_RESTRICT masking_field_row; + size_t masking_field_stride; + const float* JXL_RESTRICT src_rows[3]; + size_t src_stride; + // Cost for 1 (-1), 2 (-2) explicitly, cost for others computed with cost1 + + // cost2 + sqrt(q) * cost_delta. + float cost1; + float cost2; + float cost_delta; + float base_entropy; + float zeros_mul; + const float& Pixel(size_t c, size_t x, size_t y) const { + return src_rows[c][y * src_stride + x]; + } + float Masking(size_t bx, size_t by) const { + JXL_DASSERT(masking_field_row[by * masking_field_stride + bx] > 0); + return masking_field_row[by * masking_field_stride + bx]; + } + float Quant(size_t bx, size_t by) const { + JXL_DASSERT(quant_field_row[by * quant_field_stride + bx] > 0); + return quant_field_row[by * quant_field_stride + bx]; + } + void SetQuant(size_t bx, size_t by, float value) const { + JXL_DASSERT(value > 0); + quant_field_row[by * quant_field_stride + bx] = value; + } +}; + +struct AcStrategyHeuristics { + void Init(const Image3F& src, PassesEncoderState* enc_state); + void ProcessRect(const Rect& rect); + void Finalize(AuxOut* aux_out); + ACSConfig config; + PassesEncoderState* enc_state; +}; + +// Debug. +void DumpAcStrategy(const AcStrategyImage& ac_strategy, size_t xsize, + size_t ysize, const char* tag, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_AC_STRATEGY_H_ diff --git a/lib/jxl/enc_adaptive_quantization.cc b/lib/jxl/enc_adaptive_quantization.cc new file mode 100644 index 0000000..f53393f --- /dev/null +++ b/lib/jxl/enc_adaptive_quantization.cc @@ -0,0 +1,1118 @@ +// 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/enc_adaptive_quantization.h" + +#include +#include +#include + +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_adaptive_quantization.cc" +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_group.h" +#include "lib/jxl/dec_reconstruct.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_group.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_transforms-inl.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/gauss_blur.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quant_weights.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; + +// The following functions modulate an exponent (out_val) and return the updated +// value. Their descriptor is limited to 8 lanes for 8x8 blocks. + +// Hack for mask estimation. Eventually replace this code with butteraugli's +// masking. +float ComputeMaskForAcStrategyUse(const float out_val) { + const float kMul = 1.0f; + const float kOffset = 0.001f; + return kMul / (out_val + kOffset); +} + +template +V ComputeMask(const D d, const V out_val) { + const auto kBase = Set(d, -0.74174993f); + const auto kMul4 = Set(d, 3.2353257320940401f); + const auto kMul2 = Set(d, 12.906028311180409f); + const auto kOffset2 = Set(d, 305.04035728311436f); + const auto kMul3 = Set(d, 5.0220313103171232f); + const auto kOffset3 = Set(d, 2.1925739705298404f); + const auto kOffset4 = Set(d, 0.25f) * kOffset3; + const auto kMul0 = Set(d, 0.74760422233706747f); + const auto k1 = Set(d, 1.0f); + + // Avoid division by zero. + const auto v1 = Max(out_val * kMul0, Set(d, 1e-3f)); + const auto v2 = k1 / (v1 + kOffset2); + const auto v3 = k1 / MulAdd(v1, v1, kOffset3); + const auto v4 = k1 / MulAdd(v1, v1, kOffset4); + // TODO(jyrki): + // A log or two here could make sense. In butteraugli we have effectively + // log(log(x + C)) for this kind of use, as a single log is used in + // saturating visual masking and here the modulation values are exponential, + // another log would counter that. + return kBase + MulAdd(kMul4, v4, MulAdd(kMul2, v2, kMul3 * v3)); +} + +// For converting full vectors to a subset. Assumes `vfull` lanes are identical. +template +Vec CapTo(const D d, VFull vfull) { + using T = typename D::T; + const HWY_FULL(T) dfull; + HWY_ALIGN T lanes[MaxLanes(dfull)]; + Store(vfull, dfull, lanes); + return Load(d, lanes); +} + +// mul and mul2 represent a scaling difference between jxl and butteraugli. +static const float kSGmul = 226.0480446705883f; +static const float kSGmul2 = 1.0f / 73.377132366608819f; +static const float kLog2 = 0.693147181f; +// Includes correction factor for std::log -> log2. +static const float kSGRetMul = kSGmul2 * 18.6580932135f * kLog2; +static const float kSGVOffset = 7.14672470003f; + +template +V RatioOfDerivativesOfCubicRootToSimpleGamma(const D d, V v) { + // The opsin space in jxl is the cubic root of photons, i.e., v * v * v + // is related to the number of photons. + // + // SimpleGamma(v * v * v) is the psychovisual space in butteraugli. + // This ratio allows quantization to move from jxl's opsin space to + // butteraugli's log-gamma space. + float kEpsilon = 1e-2; + v = ZeroIfNegative(v); + const auto kNumMul = Set(d, kSGRetMul * 3 * kSGmul); + const auto kVOffset = Set(d, kSGVOffset * kLog2 + kEpsilon); + const auto kDenMul = Set(d, kLog2 * kSGmul); + + const auto v2 = v * v; + + const auto num = MulAdd(kNumMul, v2, Set(d, kEpsilon)); + const auto den = MulAdd(kDenMul * v, v2, kVOffset); + return invert ? num / den : den / num; +} + +template +static float RatioOfDerivativesOfCubicRootToSimpleGamma(float v) { + using DScalar = HWY_CAPPED(float, 1); + auto vscalar = Load(DScalar(), &v); + return GetLane( + RatioOfDerivativesOfCubicRootToSimpleGamma(DScalar(), vscalar)); +} + +// TODO(veluca): this function computes an approximation of the derivative of +// SimpleGamma with (f(x+eps)-f(x))/eps. Consider two-sided approximation or +// exact derivatives. For reference, SimpleGamma was: +/* +template +V SimpleGamma(const D d, V v) { + // A simple HDR compatible gamma function. + const auto mul = Set(d, kSGmul); + const auto kRetMul = Set(d, kSGRetMul); + const auto kRetAdd = Set(d, kSGmul2 * -20.2789020414f); + const auto kVOffset = Set(d, kSGVOffset); + + v *= mul; + + // This should happen rarely, but may lead to a NaN, which is rather + // undesirable. Since negative photons don't exist we solve the NaNs by + // clamping here. + // TODO(veluca): with FastLog2f, this no longer leads to NaNs. + v = ZeroIfNegative(v); + return kRetMul * FastLog2f(d, v + kVOffset) + kRetAdd; +} +*/ + +template +V GammaModulation(const D d, const size_t x, const size_t y, + const ImageF& xyb_x, const ImageF& xyb_y, const V out_val) { + const float kBias = 0.16f; + JXL_DASSERT(kBias > kOpsinAbsorbanceBias[0]); + JXL_DASSERT(kBias > kOpsinAbsorbanceBias[1]); + JXL_DASSERT(kBias > kOpsinAbsorbanceBias[2]); + auto overall_ratio = Zero(d); + auto bias = Set(d, kBias); + auto half = Set(d, 0.5f); + for (size_t dy = 0; dy < 8; ++dy) { + const float* const JXL_RESTRICT row_in_x = xyb_x.Row(y + dy); + const float* const JXL_RESTRICT row_in_y = xyb_y.Row(y + dy); + for (size_t dx = 0; dx < 8; dx += Lanes(d)) { + const auto iny = Load(d, row_in_y + x + dx) + bias; + const auto inx = Load(d, row_in_x + x + dx); + const auto r = iny - inx; + const auto g = iny + inx; + const auto ratio_r = + RatioOfDerivativesOfCubicRootToSimpleGamma(d, r); + const auto ratio_g = + RatioOfDerivativesOfCubicRootToSimpleGamma(d, g); + const auto avg_ratio = half * (ratio_r + ratio_g); + + overall_ratio += avg_ratio; + } + } + overall_ratio = SumOfLanes(overall_ratio); + overall_ratio *= Set(d, 1.0f / 64); + // ideally -1.0, but likely optimal correction adds some entropy, so slightly + // less than that. + // ln(2) constant folded in because we want std::log but have FastLog2f. + const auto kGam = Set(d, -0.15526878023684174f * 0.693147180559945f); + return MulAdd(kGam, FastLog2f(d, overall_ratio), out_val); +} + +template +V ColorModulation(const D d, const size_t x, const size_t y, + const ImageF& xyb_x, const ImageF& xyb_y, const ImageF& xyb_b, + const double butteraugli_target, V out_val) { + static const float kStrengthMul = 2.177823400325309; + static const float kRedRampStart = 0.0073200141118951231; + static const float kRedRampLength = 0.019421555948474039; + static const float kBlueRampLength = 0.086890611400405895; + static const float kBlueRampStart = 0.26973418507870539; + const float strength = kStrengthMul * (1.0f - 0.25f * butteraugli_target); + if (strength < 0) { + return out_val; + } + // x values are smaller than y and b values, need to take the difference into + // account. + const float red_strength = strength * 5.992297772961519f; + const float blue_strength = strength; + { + // Reduce some bits from areas not blue or red. + const float offset = strength * -0.009174542291185913f; + out_val += Set(d, offset); + } + // Calculate how much of the 8x8 block is covered with blue or red. + auto blue_coverage = Zero(d); + auto red_coverage = Zero(d); + for (size_t dy = 0; dy < 8; ++dy) { + const float* const JXL_RESTRICT row_in_x = xyb_x.Row(y + dy); + const float* const JXL_RESTRICT row_in_y = xyb_y.Row(y + dy); + const float* const JXL_RESTRICT row_in_b = xyb_b.Row(y + dy); + for (size_t dx = 0; dx < 8; dx += Lanes(d)) { + const auto pixel_x = + Max(Set(d, 0.0f), Load(d, row_in_x + x + dx) - Set(d, kRedRampStart)); + const auto pixel_y = Load(d, row_in_y + x + dx); + const auto pixel_b = + Max(Set(d, 0.0f), + Load(d, row_in_b + x + dx) - pixel_y - Set(d, kBlueRampStart)); + const auto blue_slope = Min(pixel_b, Set(d, kBlueRampLength)); + const auto red_slope = Min(pixel_x, Set(d, kRedRampLength)); + red_coverage += red_slope; + blue_coverage += blue_slope; + } + } + + // Saturate when the high red or high blue coverage is above a level. + // The idea here is that if a certain fraction of the block is red or + // blue we consider as if it was fully red or blue. + static const float ratio = 30.610615782142737f; // out of 64 pixels. + + auto overall_red_coverage = SumOfLanes(red_coverage); + overall_red_coverage = + Min(overall_red_coverage, Set(d, ratio * kRedRampLength)); + overall_red_coverage *= Set(d, red_strength / ratio); + + auto overall_blue_coverage = SumOfLanes(blue_coverage); + overall_blue_coverage = + Min(overall_blue_coverage, Set(d, ratio * kBlueRampLength)); + overall_blue_coverage *= Set(d, blue_strength / ratio); + + return overall_red_coverage + overall_blue_coverage + out_val; +} + +// Change precision in 8x8 blocks that have high frequency content. +template +V HfModulation(const D d, const size_t x, const size_t y, const ImageF& xyb, + const V out_val) { + // Zero out the invalid differences for the rightmost value per row. + const Rebind du; + HWY_ALIGN constexpr uint32_t kMaskRight[kBlockDim] = {~0u, ~0u, ~0u, ~0u, + ~0u, ~0u, ~0u, 0}; + + auto sum = Zero(d); // sum of absolute differences with right and below + + for (size_t dy = 0; dy < 8; ++dy) { + const float* JXL_RESTRICT row_in = xyb.Row(y + dy) + x; + const float* JXL_RESTRICT row_in_next = + dy == 7 ? row_in : xyb.Row(y + dy + 1) + x; + + // In SCALAR, there is no guarantee of having extra row padding. + // Hence, we need to ensure we don't access pixels outside the row itself. + // In SIMD modes, however, rows are padded, so it's safe to access one + // garbage value after the row. The vector then gets masked with kMaskRight + // to remove the influence of that value. +#if HWY_TARGET != HWY_SCALAR + for (size_t dx = 0; dx < 8; dx += Lanes(d)) { +#else + for (size_t dx = 0; dx < 7; dx += Lanes(d)) { +#endif + const auto p = Load(d, row_in + dx); + const auto pr = LoadU(d, row_in + dx + 1); + const auto mask = BitCast(d, Load(du, kMaskRight + dx)); + sum += And(mask, AbsDiff(p, pr)); + + const auto pd = Load(d, row_in_next + dx); + sum += AbsDiff(p, pd); + } + } + + sum = SumOfLanes(sum); + return MulAdd(sum, Set(d, -2.0052193233688884f / 112), out_val); +} + +void PerBlockModulations(const float butteraugli_target, const ImageF& xyb_x, + const ImageF& xyb_y, const ImageF& xyb_b, + const float scale, const Rect& rect, ImageF* out) { + JXL_ASSERT(SameSize(xyb_x, xyb_y)); + JXL_ASSERT(DivCeil(xyb_x.xsize(), kBlockDim) == out->xsize()); + JXL_ASSERT(DivCeil(xyb_x.ysize(), kBlockDim) == out->ysize()); + + float base_level = 0.5f * scale; + float kDampenRampStart = 7.0f; + float kDampenRampEnd = 14.0f; + float dampen = 1.0f; + if (butteraugli_target >= kDampenRampStart) { + dampen = 1.0f - ((butteraugli_target - kDampenRampStart) / + (kDampenRampEnd - kDampenRampStart)); + if (dampen < 0) { + dampen = 0; + } + } + const float mul = scale * dampen; + const float add = (1.0f - dampen) * base_level; + for (size_t iy = rect.y0(); iy < rect.y0() + rect.ysize(); iy++) { + const size_t y = iy * 8; + float* const JXL_RESTRICT row_out = out->Row(iy); + const HWY_CAPPED(float, kBlockDim) df; + for (size_t ix = rect.x0(); ix < rect.x0() + rect.xsize(); ix++) { + size_t x = ix * 8; + auto out_val = Set(df, row_out[ix]); + out_val = ComputeMask(df, out_val); + out_val = HfModulation(df, x, y, xyb_y, out_val); + out_val = ColorModulation(df, x, y, xyb_x, xyb_y, xyb_b, + butteraugli_target, out_val); + out_val = GammaModulation(df, x, y, xyb_x, xyb_y, out_val); + // We want multiplicative quantization field, so everything + // until this point has been modulating the exponent. + row_out[ix] = FastPow2f(GetLane(out_val) * 1.442695041f) * mul + add; + } + } +} + +template +V MaskingSqrt(const D d, V v) { + static const float kLogOffset = 26.481471032459346f; + static const float kMul = 211.50759899638012f; + const auto mul_v = Set(d, kMul * 1e8); + const auto offset_v = Set(d, kLogOffset); + return Set(d, 0.25f) * Sqrt(MulAdd(v, Sqrt(mul_v), offset_v)); +} + +float MaskingSqrt(const float v) { + using DScalar = HWY_CAPPED(float, 1); + auto vscalar = Load(DScalar(), &v); + return GetLane(MaskingSqrt(DScalar(), vscalar)); +} + +void StoreMin4(const float v, float& min0, float& min1, float& min2, + float& min3) { + if (v < min3) { + if (v < min0) { + min3 = min2; + min2 = min1; + min1 = min0; + min0 = v; + } else if (v < min1) { + min3 = min2; + min2 = min1; + min1 = v; + } else if (v < min2) { + min3 = min2; + min2 = v; + } else { + min3 = v; + } + } +} + +// Look for smooth areas near the area of degradation. +// If the areas are generally smooth, don't do masking. +// Output is downsampled 2x. +void FuzzyErosion(const Rect& from_rect, const ImageF& from, + const Rect& to_rect, ImageF* to) { + const size_t xsize = from.xsize(); + const size_t ysize = from.ysize(); + constexpr int kStep = 1; + static_assert(kStep == 1, "Step must be 1"); + JXL_ASSERT(to_rect.xsize() * 2 == from_rect.xsize()); + JXL_ASSERT(to_rect.ysize() * 2 == from_rect.ysize()); + for (size_t fy = 0; fy < from_rect.ysize(); ++fy) { + size_t y = fy + from_rect.y0(); + size_t ym1 = y >= kStep ? y - kStep : y; + size_t yp1 = y + kStep < ysize ? y + kStep : y; + const float* rowt = from.Row(ym1); + const float* row = from.Row(y); + const float* rowb = from.Row(yp1); + float* row_out = to_rect.Row(to, fy / 2); + for (size_t fx = 0; fx < from_rect.xsize(); ++fx) { + size_t x = fx + from_rect.x0(); + size_t xm1 = x >= kStep ? x - kStep : x; + size_t xp1 = x + kStep < xsize ? x + kStep : x; + float min0 = row[x]; + float min1 = row[xm1]; + float min2 = row[xp1]; + float min3 = rowt[xm1]; + // Sort the first four values. + if (min0 > min1) std::swap(min0, min1); + if (min0 > min2) std::swap(min0, min2); + if (min0 > min3) std::swap(min0, min3); + if (min1 > min2) std::swap(min1, min2); + if (min1 > min3) std::swap(min1, min3); + if (min2 > min3) std::swap(min2, min3); + // The remaining five values of a 3x3 neighbourhood. + StoreMin4(rowt[x], min0, min1, min2, min3); + StoreMin4(rowt[xp1], min0, min1, min2, min3); + StoreMin4(rowb[xm1], min0, min1, min2, min3); + StoreMin4(rowb[x], min0, min1, min2, min3); + StoreMin4(rowb[xp1], min0, min1, min2, min3); + static const float kMulC = 0.05f; + static const float kMul0 = 0.05f; + static const float kMul1 = 0.05f; + static const float kMul2 = 0.05f; + static const float kMul3 = 0.05f; + float v = kMulC * row[x] + kMul0 * min0 + kMul1 * min1 + kMul2 * min2 + + kMul3 * min3; + if (fx % 2 == 0 && fy % 2 == 0) { + row_out[fx / 2] = v; + } else { + row_out[fx / 2] += v; + } + } + } +} + +struct AdaptiveQuantizationImpl { + void Init(const Image3F& xyb) { + JXL_DASSERT(xyb.xsize() % kBlockDim == 0); + JXL_DASSERT(xyb.ysize() % kBlockDim == 0); + const size_t xsize = xyb.xsize(); + const size_t ysize = xyb.ysize(); + aq_map = ImageF(xsize / kBlockDim, ysize / kBlockDim); + } + void PrepareBuffers(size_t num_threads) { + diff_buffer = ImageF(kEncTileDim + 8, num_threads); + for (size_t i = pre_erosion.size(); i < num_threads; i++) { + pre_erosion.emplace_back(kEncTileDimInBlocks * 2 + 2, + kEncTileDimInBlocks * 2 + 2); + } + } + + void ComputeTile(float butteraugli_target, float scale, const Image3F& xyb, + const Rect& rect, const int thread, ImageF* mask) { + PROFILER_ZONE("aq DiffPrecompute"); + const size_t xsize = xyb.xsize(); + const size_t ysize = xyb.ysize(); + + // The XYB gamma is 3.0 to be able to decode faster with two muls. + // Butteraugli's gamma is matching the gamma of human eye, around 2.6. + // We approximate the gamma difference by adding one cubic root into + // the adaptive quantization. This gives us a total gamma of 2.6666 + // for quantization uses. + const float match_gamma_offset = 0.019; + + const HWY_FULL(float) df; + const float kXMul = 23.426802998210313f; + const auto kXMulv = Set(df, kXMul); + + size_t y_start = rect.y0() * 8; + size_t y_end = y_start + rect.ysize() * 8; + + size_t x0 = rect.x0() * 8; + size_t x1 = x0 + rect.xsize() * 8; + if (x0 != 0) x0 -= 4; + if (x1 != xyb.xsize()) x1 += 4; + if (y_start != 0) y_start -= 4; + if (y_end != xyb.ysize()) y_end += 4; + pre_erosion[thread].ShrinkTo((x1 - x0) / 4, (y_end - y_start) / 4); + + // Computes image (padded to multiple of 8x8) of local pixel differences. + // Subsample both directions by 4. + for (size_t y = y_start; y < y_end; ++y) { + size_t y2 = y + 1 < ysize ? y + 1 : y; + size_t y1 = y > 0 ? y - 1 : y; + + const float* row_in = xyb.PlaneRow(1, y); + const float* row_in1 = xyb.PlaneRow(1, y1); + const float* row_in2 = xyb.PlaneRow(1, y2); + const float* row_x_in = xyb.PlaneRow(0, y); + const float* row_x_in1 = xyb.PlaneRow(0, y1); + const float* row_x_in2 = xyb.PlaneRow(0, y2); + float* JXL_RESTRICT row_out = diff_buffer.Row(thread); + + auto scalar_pixel = [&](size_t x) { + const size_t x2 = x + 1 < xsize ? x + 1 : x; + const size_t x1 = x > 0 ? x - 1 : x; + const float base = + 0.25f * (row_in2[x] + row_in1[x] + row_in[x1] + row_in[x2]); + const float gammac = RatioOfDerivativesOfCubicRootToSimpleGamma( + row_in[x] + match_gamma_offset); + float diff = gammac * (row_in[x] - base); + diff *= diff; + const float base_x = + 0.25f * (row_x_in2[x] + row_x_in1[x] + row_x_in[x1] + row_x_in[x2]); + float diff_x = gammac * (row_x_in[x] - base_x); + diff_x *= diff_x; + diff += kXMul * diff_x; + diff = MaskingSqrt(diff); + if ((y % 4) != 0) { + row_out[x - x0] += diff; + } else { + row_out[x - x0] = diff; + } + }; + + size_t x = x0; + // First pixel of the row. + if (x0 == 0) { + scalar_pixel(x0); + ++x; + } + // SIMD + const auto match_gamma_offset_v = Set(df, match_gamma_offset); + const auto quarter = Set(df, 0.25f); + for (; x + 1 + Lanes(df) < x1; x += Lanes(df)) { + const auto in = LoadU(df, row_in + x); + const auto in_r = LoadU(df, row_in + x + 1); + const auto in_l = LoadU(df, row_in + x - 1); + const auto in_t = LoadU(df, row_in2 + x); + const auto in_b = LoadU(df, row_in1 + x); + auto base = quarter * (in_r + in_l + in_t + in_b); + auto gammacv = + RatioOfDerivativesOfCubicRootToSimpleGamma( + df, in + match_gamma_offset_v); + auto diff = gammacv * (in - base); + diff *= diff; + + const auto in_x = LoadU(df, row_x_in + x); + const auto in_x_r = LoadU(df, row_x_in + x + 1); + const auto in_x_l = LoadU(df, row_x_in + x - 1); + const auto in_x_t = LoadU(df, row_x_in2 + x); + const auto in_x_b = LoadU(df, row_x_in1 + x); + auto base_x = quarter * (in_x_r + in_x_l + in_x_t + in_x_b); + auto diff_x = gammacv * (in_x - base_x); + diff_x *= diff_x; + diff += kXMulv * diff_x; + diff = MaskingSqrt(df, diff); + if ((y & 3) != 0) { + diff += LoadU(df, row_out + x - x0); + } + StoreU(diff, df, row_out + x - x0); + } + // Scalar + for (; x < x1; ++x) { + scalar_pixel(x); + } + if (y % 4 == 3) { + float* row_dout = pre_erosion[thread].Row((y - y_start) / 4); + for (size_t x = 0; x < (x1 - x0) / 4; x++) { + row_dout[x] = (row_out[x * 4] + row_out[x * 4 + 1] + + row_out[x * 4 + 2] + row_out[x * 4 + 3]) * + 0.25f; + } + } + } + Rect from_rect(x0 % 8 == 0 ? 0 : 1, y_start % 8 == 0 ? 0 : 1, + rect.xsize() * 2, rect.ysize() * 2); + FuzzyErosion(from_rect, pre_erosion[thread], rect, &aq_map); + for (size_t y = 0; y < rect.ysize(); ++y) { + const float* aq_map_row = rect.ConstRow(aq_map, y); + float* mask_row = rect.Row(mask, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + mask_row[x] = ComputeMaskForAcStrategyUse(aq_map_row[x]); + } + } + PerBlockModulations(butteraugli_target, xyb.Plane(0), xyb.Plane(1), + xyb.Plane(2), scale, rect, &aq_map); + } + std::vector pre_erosion; + ImageF aq_map; + ImageF diff_buffer; +}; + +ImageF AdaptiveQuantizationMap(const float butteraugli_target, + const Image3F& xyb, + const FrameDimensions& frame_dim, float scale, + ThreadPool* pool, ImageF* mask) { + PROFILER_ZONE("aq AdaptiveQuantMap"); + + AdaptiveQuantizationImpl impl; + impl.Init(xyb); + *mask = ImageF(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + RunOnPool( + pool, 0, + DivCeil(frame_dim.xsize_blocks, kEncTileDimInBlocks) * + DivCeil(frame_dim.ysize_blocks, kEncTileDimInBlocks), + [&](size_t num_threads) { + impl.PrepareBuffers(num_threads); + return true; + }, + [&](const int tid, int thread) { + size_t n_enc_tiles = + DivCeil(frame_dim.xsize_blocks, kEncTileDimInBlocks); + size_t tx = tid % n_enc_tiles; + size_t ty = tid / n_enc_tiles; + size_t by0 = ty * kEncTileDimInBlocks; + size_t by1 = + std::min((ty + 1) * kEncTileDimInBlocks, frame_dim.ysize_blocks); + size_t bx0 = tx * kEncTileDimInBlocks; + size_t bx1 = + std::min((tx + 1) * kEncTileDimInBlocks, frame_dim.xsize_blocks); + Rect r(bx0, by0, bx1 - bx0, by1 - by0); + impl.ComputeTile(butteraugli_target, scale, xyb, r, thread, mask); + }, + "AQ DiffPrecompute"); + + return std::move(impl).aq_map; +} + +} // namespace + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(AdaptiveQuantizationMap); + +namespace { +bool FLAGS_log_search_state = false; +// If true, prints the quantization maps at each iteration. +bool FLAGS_dump_quant_state = false; + +void DumpHeatmap(const AuxOut* aux_out, const std::string& label, + const ImageF& image, float good_threshold, + float bad_threshold) { + Image3F heatmap = CreateHeatMapImage(image, good_threshold, bad_threshold); + char filename[200]; + snprintf(filename, sizeof(filename), "%s%05d", label.c_str(), + aux_out->num_butteraugli_iters); + aux_out->DumpImage(filename, heatmap); +} + +void DumpHeatmaps(const AuxOut* aux_out, float ba_target, + const ImageF& quant_field, const ImageF& tile_heatmap, + const ImageF& bt_diffmap) { + if (!WantDebugOutput(aux_out)) return; + ImageF inv_qmap(quant_field.xsize(), quant_field.ysize()); + for (size_t y = 0; y < quant_field.ysize(); ++y) { + const float* JXL_RESTRICT row_q = quant_field.ConstRow(y); + float* JXL_RESTRICT row_inv_q = inv_qmap.Row(y); + for (size_t x = 0; x < quant_field.xsize(); ++x) { + row_inv_q[x] = 1.0f / row_q[x]; // never zero + } + } + DumpHeatmap(aux_out, "quant_heatmap", inv_qmap, 4.0f * ba_target, + 6.0f * ba_target); + DumpHeatmap(aux_out, "tile_heatmap", tile_heatmap, ba_target, + 1.5f * ba_target); + // matches heat maps produced by the command line tool. + DumpHeatmap(aux_out, "bt_diffmap", bt_diffmap, ButteraugliFuzzyInverse(1.5), + ButteraugliFuzzyInverse(0.5)); +} + +ImageF TileDistMap(const ImageF& distmap, int tile_size, int margin, + const AcStrategyImage& ac_strategy) { + PROFILER_FUNC; + const int tile_xsize = (distmap.xsize() + tile_size - 1) / tile_size; + const int tile_ysize = (distmap.ysize() + tile_size - 1) / tile_size; + ImageF tile_distmap(tile_xsize, tile_ysize); + size_t distmap_stride = tile_distmap.PixelsPerRow(); + for (int tile_y = 0; tile_y < tile_ysize; ++tile_y) { + AcStrategyRow ac_strategy_row = ac_strategy.ConstRow(tile_y); + float* JXL_RESTRICT dist_row = tile_distmap.Row(tile_y); + for (int tile_x = 0; tile_x < tile_xsize; ++tile_x) { + AcStrategy acs = ac_strategy_row[tile_x]; + if (!acs.IsFirstBlock()) continue; + int this_tile_xsize = acs.covered_blocks_x() * tile_size; + int this_tile_ysize = acs.covered_blocks_y() * tile_size; + int y_begin = std::max(0, tile_size * tile_y - margin); + int y_end = std::min(distmap.ysize(), + tile_size * tile_y + this_tile_ysize + margin); + int x_begin = std::max(0, tile_size * tile_x - margin); + int x_end = std::min(distmap.xsize(), + tile_size * tile_x + this_tile_xsize + margin); + float dist_norm = 0.0; + double pixels = 0; + for (int y = y_begin; y < y_end; ++y) { + float ymul = 1.0; + constexpr float kBorderMul = 0.98f; + constexpr float kCornerMul = 0.7f; + if (margin != 0 && (y == y_begin || y == y_end - 1)) { + ymul = kBorderMul; + } + const float* const JXL_RESTRICT row = distmap.Row(y); + for (int x = x_begin; x < x_end; ++x) { + float xmul = ymul; + if (margin != 0 && (x == x_begin || x == x_end - 1)) { + if (xmul == 1.0) { + xmul = kBorderMul; + } else { + xmul = kCornerMul; + } + } + float v = row[x]; + v *= v; + v *= v; + v *= v; + v *= v; + dist_norm += xmul * v; + pixels += xmul; + } + } + if (pixels == 0) pixels = 1; + // 16th norm is less than the max norm, we reduce the difference + // with this normalization factor. + constexpr float kTileNorm = 1.2f; + const float tile_dist = + kTileNorm * std::pow(dist_norm / pixels, 1.0f / 16.0f); + dist_row[tile_x] = tile_dist; + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + dist_row[tile_x + distmap_stride * iy + ix] = tile_dist; + } + } + } + } + return tile_distmap; +} + +constexpr float kDcQuantPow = 0.57f; +static const float kDcQuant = 1.12f; +static const float kAcQuant = 0.7886f; + +void FindBestQuantization(const ImageBundle& linear, const Image3F& opsin, + PassesEncoderState* enc_state, ThreadPool* pool, + AuxOut* aux_out) { + const CompressParams& cparams = enc_state->cparams; + Quantizer& quantizer = enc_state->shared.quantizer; + ImageI& raw_quant_field = enc_state->shared.raw_quant_field; + ImageF& quant_field = enc_state->initial_quant_field; + + const float butteraugli_target = cparams.butteraugli_distance; + ButteraugliParams params = cparams.ba_params; + params.intensity_target = linear.metadata()->IntensityTarget(); + // Hack the default intensity target value to be 80.0, the intensity + // target of sRGB images and a more reasonable viewing default than + // JPEG XL file format's default. + if (fabs(params.intensity_target - 255.0f) < 1e-3) { + params.intensity_target = 80.0f; + } + JxlButteraugliComparator comparator(params); + JXL_CHECK(comparator.SetReferenceImage(linear)); + bool lower_is_better = + (comparator.GoodQualityScore() < comparator.BadQualityScore()); + const float initial_quant_dc = InitialQuantDC(butteraugli_target); + AdjustQuantField(enc_state->shared.ac_strategy, Rect(quant_field), + &quant_field); + ImageF tile_distmap; + ImageF initial_quant_field = CopyImage(quant_field); + + float initial_qf_min, initial_qf_max; + ImageMinMax(initial_quant_field, &initial_qf_min, &initial_qf_max); + float initial_qf_ratio = initial_qf_max / initial_qf_min; + float qf_max_deviation_low = std::sqrt(250 / initial_qf_ratio); + float asymmetry = 2; + if (qf_max_deviation_low < asymmetry) asymmetry = qf_max_deviation_low; + float qf_lower = initial_qf_min / (asymmetry * qf_max_deviation_low); + float qf_higher = initial_qf_max * (qf_max_deviation_low / asymmetry); + + JXL_ASSERT(qf_higher / qf_lower < 253); + + constexpr int kOriginalComparisonRound = 1; + int iters = cparams.max_butteraugli_iters; + if (iters > 7) { + iters = 7; + } + if (cparams.speed_tier != SpeedTier::kTortoise) { + iters = 2; + } + for (int i = 0; i < iters + 1; ++i) { + if (FLAGS_dump_quant_state) { + printf("\nQuantization field:\n"); + for (size_t y = 0; y < quant_field.ysize(); ++y) { + for (size_t x = 0; x < quant_field.xsize(); ++x) { + printf(" %.5f", quant_field.Row(y)[x]); + } + printf("\n"); + } + } + quantizer.SetQuantField(initial_quant_dc, quant_field, &raw_quant_field); + ImageBundle linear = RoundtripImage(opsin, enc_state, pool); + PROFILER_ZONE("enc Butteraugli"); + float score; + ImageF diffmap; + JXL_CHECK(comparator.CompareWith(linear, &diffmap, &score)); + if (!lower_is_better) { + score = -score; + diffmap = ScaleImage(-1.0f, diffmap); + } + tile_distmap = TileDistMap(diffmap, 8, 0, enc_state->shared.ac_strategy); + if (WantDebugOutput(aux_out)) { + aux_out->DumpImage(("dec" + ToString(i)).c_str(), *linear.color()); + DumpHeatmaps(aux_out, butteraugli_target, quant_field, tile_distmap, + diffmap); + } + if (aux_out != nullptr) ++aux_out->num_butteraugli_iters; + if (FLAGS_log_search_state) { + float minval, maxval; + ImageMinMax(quant_field, &minval, &maxval); + printf("\nButteraugli iter: %d/%d\n", i, cparams.max_butteraugli_iters); + printf("Butteraugli distance: %f\n", score); + printf("quant range: %f ... %f DC quant: %f\n", minval, maxval, + initial_quant_dc); + if (FLAGS_dump_quant_state) { + quantizer.DumpQuantizationMap(raw_quant_field); + } + } + + if (i == iters) break; + + double kPow[8] = { + 0.2, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + }; + double kPowMod[8] = { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + }; + if (i == kOriginalComparisonRound) { + // Don't allow optimization to make the quant field a lot worse than + // what the initial guess was. This allows the AC field to have enough + // precision to reduce the oscillations due to the dc reconstruction. + double kInitMul = 0.6; + const double kOneMinusInitMul = 1.0 - kInitMul; + for (size_t y = 0; y < quant_field.ysize(); ++y) { + float* const JXL_RESTRICT row_q = quant_field.Row(y); + const float* const JXL_RESTRICT row_init = initial_quant_field.Row(y); + for (size_t x = 0; x < quant_field.xsize(); ++x) { + double clamp = kOneMinusInitMul * row_q[x] + kInitMul * row_init[x]; + if (row_q[x] < clamp) { + row_q[x] = clamp; + if (row_q[x] > qf_higher) row_q[x] = qf_higher; + if (row_q[x] < qf_lower) row_q[x] = qf_lower; + } + } + } + } + + double cur_pow = 0.0; + if (i < 7) { + cur_pow = kPow[i] + (butteraugli_target - 1.0) * kPowMod[i]; + if (cur_pow < 0) { + cur_pow = 0; + } + } + if (cur_pow == 0.0) { + for (size_t y = 0; y < quant_field.ysize(); ++y) { + const float* const JXL_RESTRICT row_dist = tile_distmap.Row(y); + float* const JXL_RESTRICT row_q = quant_field.Row(y); + for (size_t x = 0; x < quant_field.xsize(); ++x) { + const float diff = row_dist[x] / butteraugli_target; + if (diff > 1.0f) { + float old = row_q[x]; + row_q[x] *= diff; + int qf_old = old * quantizer.InvGlobalScale() + 0.5; + int qf_new = row_q[x] * quantizer.InvGlobalScale() + 0.5; + if (qf_old == qf_new) { + row_q[x] = old + quantizer.Scale(); + } + } + if (row_q[x] > qf_higher) row_q[x] = qf_higher; + if (row_q[x] < qf_lower) row_q[x] = qf_lower; + } + } + } else { + for (size_t y = 0; y < quant_field.ysize(); ++y) { + const float* const JXL_RESTRICT row_dist = tile_distmap.Row(y); + float* const JXL_RESTRICT row_q = quant_field.Row(y); + for (size_t x = 0; x < quant_field.xsize(); ++x) { + const float diff = row_dist[x] / butteraugli_target; + if (diff <= 1.0f) { + row_q[x] *= std::pow(diff, cur_pow); + } else { + float old = row_q[x]; + row_q[x] *= diff; + int qf_old = old * quantizer.InvGlobalScale() + 0.5; + int qf_new = row_q[x] * quantizer.InvGlobalScale() + 0.5; + if (qf_old == qf_new) { + row_q[x] = old + quantizer.Scale(); + } + } + if (row_q[x] > qf_higher) row_q[x] = qf_higher; + if (row_q[x] < qf_lower) row_q[x] = qf_lower; + } + } + } + } + quantizer.SetQuantField(initial_quant_dc, quant_field, &raw_quant_field); +} + +void FindBestQuantizationMaxError(const Image3F& opsin, + PassesEncoderState* enc_state, + ThreadPool* pool, AuxOut* aux_out) { + // TODO(veluca): this only works if opsin is in XYB. The current encoder does + // not have code paths that produce non-XYB opsin here. + JXL_CHECK(enc_state->shared.frame_header.color_transform == + ColorTransform::kXYB); + const CompressParams& cparams = enc_state->cparams; + Quantizer& quantizer = enc_state->shared.quantizer; + ImageI& raw_quant_field = enc_state->shared.raw_quant_field; + ImageF& quant_field = enc_state->initial_quant_field; + + // TODO(veluca): better choice of this value. + const float initial_quant_dc = + 16 * std::sqrt(0.1f / cparams.butteraugli_distance); + AdjustQuantField(enc_state->shared.ac_strategy, Rect(quant_field), + &quant_field); + + const float inv_max_err[3] = {1.0f / enc_state->cparams.max_error[0], + 1.0f / enc_state->cparams.max_error[1], + 1.0f / enc_state->cparams.max_error[2]}; + + for (int i = 0; i < cparams.max_butteraugli_iters + 1; ++i) { + quantizer.SetQuantField(initial_quant_dc, quant_field, &raw_quant_field); + if (aux_out) { + aux_out->DumpXybImage(("ops" + ToString(i)).c_str(), opsin); + } + ImageBundle decoded = RoundtripImage(opsin, enc_state, pool); + if (aux_out) { + aux_out->DumpXybImage(("dec" + ToString(i)).c_str(), *decoded.color()); + } + + for (size_t by = 0; by < enc_state->shared.frame_dim.ysize_blocks; by++) { + AcStrategyRow ac_strategy_row = + enc_state->shared.ac_strategy.ConstRow(by); + for (size_t bx = 0; bx < enc_state->shared.frame_dim.xsize_blocks; bx++) { + AcStrategy acs = ac_strategy_row[bx]; + if (!acs.IsFirstBlock()) continue; + float max_error = 0; + for (size_t c = 0; c < 3; c++) { + for (size_t y = by * kBlockDim; + y < (by + acs.covered_blocks_y()) * kBlockDim; y++) { + if (y >= decoded.ysize()) continue; + const float* JXL_RESTRICT in_row = opsin.ConstPlaneRow(c, y); + const float* JXL_RESTRICT dec_row = + decoded.color()->ConstPlaneRow(c, y); + for (size_t x = bx * kBlockDim; + x < (bx + acs.covered_blocks_x()) * kBlockDim; x++) { + if (x >= decoded.xsize()) continue; + max_error = std::max( + std::abs(in_row[x] - dec_row[x]) * inv_max_err[c], max_error); + } + } + } + // Target an error between max_error/2 and max_error. + // If the error in the varblock is above the target, increase the qf to + // compensate. If the error is below the target, decrease the qf. + // However, to avoid an excessive increase of the qf, only do so if the + // error is less than half the maximum allowed error. + const float qf_mul = (max_error < 0.5f) ? max_error * 2.0f + : (max_error > 1.0f) ? max_error + : 1.0f; + for (size_t qy = by; qy < by + acs.covered_blocks_y(); qy++) { + float* JXL_RESTRICT quant_field_row = quant_field.Row(qy); + for (size_t qx = bx; qx < bx + acs.covered_blocks_x(); qx++) { + quant_field_row[qx] *= qf_mul; + } + } + } + } + } + quantizer.SetQuantField(initial_quant_dc, quant_field, &raw_quant_field); +} + +} // namespace + +void AdjustQuantField(const AcStrategyImage& ac_strategy, const Rect& rect, + ImageF* quant_field) { + // Replace the whole quant_field in non-8x8 blocks with the maximum of each + // 8x8 block. + size_t stride = quant_field->PixelsPerRow(); + for (size_t y = 0; y < rect.ysize(); ++y) { + AcStrategyRow ac_strategy_row = ac_strategy.ConstRow(rect, y); + float* JXL_RESTRICT quant_row = rect.Row(quant_field, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + AcStrategy acs = ac_strategy_row[x]; + if (!acs.IsFirstBlock()) continue; + JXL_ASSERT(x + acs.covered_blocks_x() <= quant_field->xsize()); + JXL_ASSERT(y + acs.covered_blocks_y() <= quant_field->ysize()); + float max = quant_row[x]; + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + max = std::max(quant_row[x + ix + iy * stride], max); + } + } + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + quant_row[x + ix + iy * stride] = max; + } + } + } + } +} + +float InitialQuantDC(float butteraugli_target) { + const float kDcMul = 2.9; // Butteraugli target where non-linearity kicks in. + const float butteraugli_target_dc = std::max( + 0.5f * butteraugli_target, + std::min(butteraugli_target, + kDcMul * std::pow((1.0f / kDcMul) * butteraugli_target, + kDcQuantPow))); + // We want the maximum DC value to be at most 2**15 * kInvDCQuant / quant_dc. + // The maximum DC value might not be in the kXybRange because of inverse + // gaborish, so we add some slack to the maximum theoretical quant obtained + // this way (64). + return std::min(kDcQuant / butteraugli_target_dc, 50.f); +} + +ImageF InitialQuantField(const float butteraugli_target, const Image3F& opsin, + const FrameDimensions& frame_dim, ThreadPool* pool, + float rescale, ImageF* mask) { + PROFILER_FUNC; + const float quant_ac = kAcQuant / butteraugli_target; + return HWY_DYNAMIC_DISPATCH(AdaptiveQuantizationMap)( + butteraugli_target, opsin, frame_dim, quant_ac * rescale, pool, mask); +} + +void FindBestQuantizer(const ImageBundle* linear, const Image3F& opsin, + PassesEncoderState* enc_state, ThreadPool* pool, + AuxOut* aux_out, double rescale) { + const CompressParams& cparams = enc_state->cparams; + if (cparams.max_error_mode) { + PROFILER_ZONE("enc find best maxerr"); + FindBestQuantizationMaxError(opsin, enc_state, pool, aux_out); + } else if (cparams.speed_tier <= SpeedTier::kKitten) { + // Normal encoding to a butteraugli score. + PROFILER_ZONE("enc find best2"); + FindBestQuantization(*linear, opsin, enc_state, pool, aux_out); + } +} + +ImageBundle RoundtripImage(const Image3F& opsin, PassesEncoderState* enc_state, + ThreadPool* pool) { + PROFILER_ZONE("enc roundtrip"); + std::unique_ptr dec_state = + jxl::make_unique(); + JXL_CHECK(dec_state->output_encoding_info.Set( + *enc_state->shared.metadata, + ColorEncoding::LinearSRGB( + enc_state->shared.metadata->m.color_encoding.IsGray()))); + dec_state->shared = &enc_state->shared; + JXL_ASSERT(opsin.ysize() % kBlockDim == 0); + + const size_t xsize_groups = DivCeil(opsin.xsize(), kGroupDim); + const size_t ysize_groups = DivCeil(opsin.ysize(), kGroupDim); + const size_t num_groups = xsize_groups * ysize_groups; + + size_t num_special_frames = enc_state->special_frames.size(); + + std::unique_ptr modular_frame_encoder = + jxl::make_unique(enc_state->shared.frame_header, + enc_state->cparams); + InitializePassesEncoder(opsin, pool, enc_state, modular_frame_encoder.get(), + nullptr); + JXL_CHECK(dec_state->Init()); + dec_state->InitForAC(pool); + + ImageBundle decoded(&enc_state->shared.metadata->m); + decoded.origin = enc_state->shared.frame_header.frame_origin; + decoded.SetFromImage(Image3F(opsin.xsize(), opsin.ysize()), + dec_state->output_encoding_info.color_encoding); + + // Same as dec_state->shared->frame_header.nonserialized_metadata->m + const ImageMetadata& metadata = *decoded.metadata(); + if (!metadata.extra_channel_info.empty()) { + // Add dummy extra channels to the dec_state: FinalizeFrameDecoding moves + // these extra channels to the ImageBundle, and is required that the amount + // of extra channels matches its metadata()->extra_channel_info.size(). + // Normally we'd place these extra channels in the ImageBundle, but in this + // case FinalizeFrameDecoding is the one that does this. + std::vector extra_channels; + extra_channels.reserve(metadata.extra_channel_info.size()); + for (size_t i = 0; i < metadata.extra_channel_info.size(); i++) { + extra_channels.emplace_back(decoded.xsize(), decoded.ysize()); + // Must initialize the image with data to not affect blending with + // uninitialized memory. + ZeroFillImage(&extra_channels.back()); + } + dec_state->extra_channels = std::move(extra_channels); + } + + hwy::AlignedUniquePtr group_dec_caches; + const auto allocate_storage = [&](size_t num_threads) { + dec_state->EnsureStorage(num_threads); + group_dec_caches = hwy::MakeUniqueAlignedArray(num_threads); + return true; + }; + const auto process_group = [&](const int group_index, const int thread) { + if (dec_state->shared->frame_header.loop_filter.epf_iters > 0) { + ComputeSigma(dec_state->shared->BlockGroupRect(group_index), + dec_state.get()); + } + JXL_CHECK(DecodeGroupForRoundtrip( + enc_state->coeffs, group_index, dec_state.get(), + &group_dec_caches[thread], thread, &decoded, nullptr)); + }; + RunOnPool(pool, 0, num_groups, allocate_storage, process_group, "AQ loop"); + + // Fine to do a JXL_ASSERT instead of error handling, since this only happens + // on the encoder side where we can't be fed with invalid data. + JXL_CHECK(FinalizeFrameDecoding(&decoded, dec_state.get(), pool, + /*force_fir=*/false, /*skip_blending=*/true)); + // Ensure we don't create any new special frames. + enc_state->special_frames.resize(num_special_frames); + + return decoded; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_adaptive_quantization.h b/lib/jxl/enc_adaptive_quantization.h new file mode 100644 index 0000000..d9666f4 --- /dev/null +++ b/lib/jxl/enc_adaptive_quantization.h @@ -0,0 +1,65 @@ +// 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_ENC_ADAPTIVE_QUANTIZATION_H_ +#define LIB_JXL_ENC_ADAPTIVE_QUANTIZATION_H_ + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/splines.h" + +// Heuristics to find a good quantizer for a given image. InitialQuantField +// produces a quantization field (i.e. relative quantization amounts for each +// block) out of an opsin-space image. `InitialQuantField` uses heuristics, +// `FindBestQuantizer` (in non-fast mode) will run multiple encoding-decoding +// steps and try to improve the given quant field. + +namespace jxl { + +// Computes the decoded image for a given set of compression parameters. Mainly +// used in the FindBestQuantization loops and in some tests. +// TODO(veluca): this doesn't seem the best possible file for this function. +ImageBundle RoundtripImage(const Image3F& opsin, PassesEncoderState* enc_state, + ThreadPool* pool); + +// Returns an image subsampled by kBlockDim in each direction. If the value +// at pixel (x,y) in the returned image is greater than 1.0, it means that +// more fine-grained quantization should be used in the corresponding block +// of the input image, while a value less than 1.0 indicates that less +// fine-grained quantization should be enough. Returns a mask, too, which +// can later be used to make better decisions about ac strategy. +ImageF InitialQuantField(float butteraugli_target, const Image3F& opsin, + const FrameDimensions& frame_dim, ThreadPool* pool, + float rescale, ImageF* initial_quant_mask); + +float InitialQuantDC(float butteraugli_target); + +void AdjustQuantField(const AcStrategyImage& ac_strategy, const Rect& rect, + ImageF* quant_field); + +// Returns a quantizer that uses an adjusted version of the provided +// quant_field. Also computes the dequant_map corresponding to the given +// dequant_float_map and chosen quantization levels. +// `linear` is only used in Kitten mode or slower. +void FindBestQuantizer(const ImageBundle* linear, const Image3F& opsin, + PassesEncoderState* enc_state, ThreadPool* pool, + AuxOut* aux_out, double rescale = 1.0); + +} // namespace jxl + +#endif // LIB_JXL_ENC_ADAPTIVE_QUANTIZATION_H_ diff --git a/lib/jxl/enc_ans.cc b/lib/jxl/enc_ans.cc new file mode 100644 index 0000000..48bc745 --- /dev/null +++ b/lib/jxl/enc_ans.cc @@ -0,0 +1,1622 @@ +// 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/enc_ans.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/ans_common.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/enc_cluster.h" +#include "lib/jxl/enc_context_map.h" +#include "lib/jxl/enc_huffman.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/fields.h" + +namespace jxl { + +namespace { + +bool ans_fuzzer_friendly_ = false; + +static const int kMaxNumSymbolsForSmallCode = 4; + +void ANSBuildInfoTable(const ANSHistBin* counts, const AliasTable::Entry* table, + size_t alphabet_size, size_t log_alpha_size, + ANSEncSymbolInfo* info) { + size_t log_entry_size = ANS_LOG_TAB_SIZE - log_alpha_size; + size_t entry_size_minus_1 = (1 << log_entry_size) - 1; + // create valid alias table for empty streams. + for (size_t s = 0; s < std::max(1, alphabet_size); ++s) { + const ANSHistBin freq = s == alphabet_size ? ANS_TAB_SIZE : counts[s]; + info[s].freq_ = static_cast(freq); +#ifdef USE_MULT_BY_RECIPROCAL + if (freq != 0) { + info[s].ifreq_ = + ((1ull << RECIPROCAL_PRECISION) + info[s].freq_ - 1) / info[s].freq_; + } else { + info[s].ifreq_ = 1; // shouldn't matter (symbol shouldn't occur), but... + } +#endif + info[s].reverse_map_.resize(freq); + } + for (int i = 0; i < ANS_TAB_SIZE; i++) { + AliasTable::Symbol s = + AliasTable::Lookup(table, i, log_entry_size, entry_size_minus_1); + info[s.value].reverse_map_[s.offset] = i; + } +} + +float EstimateDataBits(const ANSHistBin* histogram, const ANSHistBin* counts, + size_t len) { + float sum = 0.0f; + int total_histogram = 0; + int total_counts = 0; + for (size_t i = 0; i < len; ++i) { + total_histogram += histogram[i]; + total_counts += counts[i]; + if (histogram[i] > 0) { + JXL_ASSERT(counts[i] > 0); + // += histogram[i] * -log(counts[i]/total_counts) + sum += histogram[i] * + std::max(0.0f, ANS_LOG_TAB_SIZE - FastLog2f(counts[i])); + } + } + if (total_histogram > 0) { + JXL_ASSERT(total_counts == ANS_TAB_SIZE); + } + return sum; +} + +float EstimateDataBitsFlat(const ANSHistBin* histogram, size_t len) { + const float flat_bits = std::max(FastLog2f(len), 0.0f); + int total_histogram = 0; + for (size_t i = 0; i < len; ++i) { + total_histogram += histogram[i]; + } + return total_histogram * flat_bits; +} + +// Static Huffman code for encoding logcounts. The last symbol is used as RLE +// sequence. +static const uint8_t kLogCountBitLengths[ANS_LOG_TAB_SIZE + 2] = { + 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 6, 7, 7, +}; +static const uint8_t kLogCountSymbols[ANS_LOG_TAB_SIZE + 2] = { + 17, 11, 15, 3, 9, 7, 4, 2, 5, 6, 0, 33, 1, 65, +}; + +// Returns the difference between largest count that can be represented and is +// smaller than "count" and smallest representable count larger than "count". +static int SmallestIncrement(uint32_t count, uint32_t shift) { + int bits = count == 0 ? -1 : FloorLog2Nonzero(count); + int drop_bits = bits - GetPopulationCountPrecision(bits, shift); + return drop_bits < 0 ? 1 : (1 << drop_bits); +} + +template +bool RebalanceHistogram(const float* targets, int max_symbol, int table_size, + uint32_t shift, int* omit_pos, ANSHistBin* counts) { + int sum = 0; + float sum_nonrounded = 0.0; + int remainder_pos = 0; // if all of them are handled in first loop + int remainder_log = -1; + for (int n = 0; n < max_symbol; ++n) { + if (targets[n] > 0 && targets[n] < 1.0f) { + counts[n] = 1; + sum_nonrounded += targets[n]; + sum += counts[n]; + } + } + const float discount_ratio = + (table_size - sum) / (table_size - sum_nonrounded); + JXL_ASSERT(discount_ratio > 0); + JXL_ASSERT(discount_ratio <= 1.0f); + // Invariant for minimize_error_of_sum == true: + // abs(sum - sum_nonrounded) + // <= SmallestIncrement(max(targets[])) + max_symbol + for (int n = 0; n < max_symbol; ++n) { + if (targets[n] >= 1.0f) { + sum_nonrounded += targets[n]; + counts[n] = + static_cast(targets[n] * discount_ratio); // truncate + if (counts[n] == 0) counts[n] = 1; + if (counts[n] == table_size) counts[n] = table_size - 1; + // Round the count to the closest nonzero multiple of SmallestIncrement + // (when minimize_error_of_sum is false) or one of two closest so as to + // keep the sum as close as possible to sum_nonrounded. + int inc = SmallestIncrement(counts[n], shift); + counts[n] -= counts[n] & (inc - 1); + // TODO(robryk): Should we rescale targets[n]? + const float target = + minimize_error_of_sum ? (sum_nonrounded - sum) : targets[n]; + if (counts[n] == 0 || + (target > counts[n] + inc / 2 && counts[n] + inc < table_size)) { + counts[n] += inc; + } + sum += counts[n]; + const int count_log = FloorLog2Nonzero(static_cast(counts[n])); + if (count_log > remainder_log) { + remainder_pos = n; + remainder_log = count_log; + } + } + } + JXL_ASSERT(remainder_pos != -1); + // NOTE: This is the only place where counts could go negative. We could + // detect that, return false and make ANSHistBin uint32_t. + counts[remainder_pos] -= sum - table_size; + *omit_pos = remainder_pos; + return counts[remainder_pos] > 0; +} + +Status NormalizeCounts(ANSHistBin* counts, int* omit_pos, const int length, + const int precision_bits, uint32_t shift, + int* num_symbols, int* symbols) { + const int32_t table_size = 1 << precision_bits; // target sum / table size + uint64_t total = 0; + int max_symbol = 0; + int symbol_count = 0; + for (int n = 0; n < length; ++n) { + total += counts[n]; + if (counts[n] > 0) { + if (symbol_count < kMaxNumSymbolsForSmallCode) { + symbols[symbol_count] = n; + } + ++symbol_count; + max_symbol = n + 1; + } + } + *num_symbols = symbol_count; + if (symbol_count == 0) { + return true; + } + if (symbol_count == 1) { + counts[symbols[0]] = table_size; + return true; + } + if (symbol_count > table_size) + return JXL_FAILURE("Too many entries in an ANS histogram"); + + const float norm = 1.f * table_size / total; + std::vector targets(max_symbol); + for (size_t n = 0; n < targets.size(); ++n) { + targets[n] = norm * counts[n]; + } + if (!RebalanceHistogram(&targets[0], max_symbol, table_size, shift, + omit_pos, counts)) { + // Use an alternative rebalancing mechanism if the one above failed + // to create a histogram that is positive wherever the original one was. + if (!RebalanceHistogram(&targets[0], max_symbol, table_size, shift, + omit_pos, counts)) { + return JXL_FAILURE("Logic error: couldn't rebalance a histogram"); + } + } + return true; +} + +struct SizeWriter { + size_t size = 0; + void Write(size_t num, size_t bits) { size += num; } +}; + +template +void StoreVarLenUint8(size_t n, Writer* writer) { + JXL_DASSERT(n <= 255); + if (n == 0) { + writer->Write(1, 0); + } else { + writer->Write(1, 1); + size_t nbits = FloorLog2Nonzero(n); + writer->Write(3, nbits); + writer->Write(nbits, n - (1ULL << nbits)); + } +} + +template +void StoreVarLenUint16(size_t n, Writer* writer) { + JXL_DASSERT(n <= 65535); + if (n == 0) { + writer->Write(1, 0); + } else { + writer->Write(1, 1); + size_t nbits = FloorLog2Nonzero(n); + writer->Write(4, nbits); + writer->Write(nbits, n - (1ULL << nbits)); + } +} + +template +bool EncodeCounts(const ANSHistBin* counts, const int alphabet_size, + const int omit_pos, const int num_symbols, uint32_t shift, + const int* symbols, Writer* writer) { + bool ok = true; + if (num_symbols <= 2) { + // Small tree marker to encode 1-2 symbols. + writer->Write(1, 1); + if (num_symbols == 0) { + writer->Write(1, 0); + StoreVarLenUint8(0, writer); + } else { + writer->Write(1, num_symbols - 1); + for (int i = 0; i < num_symbols; ++i) { + StoreVarLenUint8(symbols[i], writer); + } + } + if (num_symbols == 2) { + writer->Write(ANS_LOG_TAB_SIZE, counts[symbols[0]]); + } + } else { + // Mark non-small tree. + writer->Write(1, 0); + // Mark non-flat histogram. + writer->Write(1, 0); + + // Precompute sequences for RLE encoding. Contains the number of identical + // values starting at a given index. Only contains the value at the first + // element of the series. + std::vector same(alphabet_size, 0); + int last = 0; + for (int i = 1; i < alphabet_size; i++) { + // Store the sequence length once different symbol reached, or we're at + // the end, or the length is longer than we can encode, or we are at + // the omit_pos. We don't support including the omit_pos in an RLE + // sequence because this value may use a different amount of log2 bits + // than standard, it is too complex to handle in the decoder. + if (counts[i] != counts[last] || i + 1 == alphabet_size || + (i - last) >= 255 || i == omit_pos || i == omit_pos + 1) { + same[last] = (i - last); + last = i + 1; + } + } + + int length = 0; + std::vector logcounts(alphabet_size); + int omit_log = 0; + for (int i = 0; i < alphabet_size; ++i) { + JXL_ASSERT(counts[i] <= ANS_TAB_SIZE); + JXL_ASSERT(counts[i] >= 0); + if (i == omit_pos) { + length = i + 1; + } else if (counts[i] > 0) { + logcounts[i] = FloorLog2Nonzero(static_cast(counts[i])) + 1; + length = i + 1; + if (i < omit_pos) { + omit_log = std::max(omit_log, logcounts[i] + 1); + } else { + omit_log = std::max(omit_log, logcounts[i]); + } + } + } + logcounts[omit_pos] = omit_log; + + // Elias gamma-like code for shift. Only difference is that if the number + // of bits to be encoded is equal to FloorLog2(ANS_LOG_TAB_SIZE+1), we skip + // the terminating 0 in unary coding. + int upper_bound_log = FloorLog2Nonzero(ANS_LOG_TAB_SIZE + 1); + int log = FloorLog2Nonzero(shift + 1); + writer->Write(log, (1 << log) - 1); + if (log != upper_bound_log) writer->Write(1, 0); + writer->Write(log, ((1 << log) - 1) & (shift + 1)); + + // Since num_symbols >= 3, we know that length >= 3, therefore we encode + // length - 3. + if (length - 3 > 255) { + // Pretend that everything is OK, but complain about correctness later. + StoreVarLenUint8(255, writer); + ok = false; + } else { + StoreVarLenUint8(length - 3, writer); + } + + // The logcount values are encoded with a static Huffman code. + static const size_t kMinReps = 4; + size_t rep = ANS_LOG_TAB_SIZE + 1; + for (int i = 0; i < length; ++i) { + if (i > 0 && same[i - 1] > kMinReps) { + // Encode the RLE symbol and skip the repeated ones. + writer->Write(kLogCountBitLengths[rep], kLogCountSymbols[rep]); + StoreVarLenUint8(same[i - 1] - kMinReps - 1, writer); + i += same[i - 1] - 2; + continue; + } + writer->Write(kLogCountBitLengths[logcounts[i]], + kLogCountSymbols[logcounts[i]]); + } + for (int i = 0; i < length; ++i) { + if (i > 0 && same[i - 1] > kMinReps) { + // Skip symbols encoded by RLE. + i += same[i - 1] - 2; + continue; + } + if (logcounts[i] > 1 && i != omit_pos) { + int bitcount = GetPopulationCountPrecision(logcounts[i] - 1, shift); + int drop_bits = logcounts[i] - 1 - bitcount; + JXL_CHECK((counts[i] & ((1 << drop_bits) - 1)) == 0); + writer->Write(bitcount, (counts[i] >> drop_bits) - (1 << bitcount)); + } + } + } + return ok; +} + +void EncodeFlatHistogram(const int alphabet_size, BitWriter* writer) { + // Mark non-small tree. + writer->Write(1, 0); + // Mark uniform histogram. + writer->Write(1, 1); + JXL_ASSERT(alphabet_size > 0); + // Encode alphabet size. + StoreVarLenUint8(alphabet_size - 1, writer); +} + +float ComputeHistoAndDataCost(const ANSHistBin* histogram, size_t alphabet_size, + uint32_t method) { + if (method == 0) { // Flat code + return ANS_LOG_TAB_SIZE + 2 + + EstimateDataBitsFlat(histogram, alphabet_size); + } + // Non-flat: shift = method-1. + uint32_t shift = method - 1; + std::vector counts(histogram, histogram + alphabet_size); + int omit_pos = 0; + int num_symbols; + int symbols[kMaxNumSymbolsForSmallCode] = {}; + JXL_CHECK(NormalizeCounts(counts.data(), &omit_pos, alphabet_size, + ANS_LOG_TAB_SIZE, shift, &num_symbols, symbols)); + SizeWriter writer; + // Ignore the correctness, no real encoding happens at this stage. + (void)EncodeCounts(counts.data(), alphabet_size, omit_pos, num_symbols, shift, + symbols, &writer); + return writer.size + + EstimateDataBits(histogram, counts.data(), alphabet_size); +} + +uint32_t ComputeBestMethod( + const ANSHistBin* histogram, size_t alphabet_size, float* cost, + HistogramParams::ANSHistogramStrategy ans_histogram_strategy) { + size_t method = 0; + float fcost = ComputeHistoAndDataCost(histogram, alphabet_size, 0); + for (uint32_t shift = 0; shift <= ANS_LOG_TAB_SIZE; + ans_histogram_strategy != HistogramParams::ANSHistogramStrategy::kPrecise + ? shift += 2 + : shift++) { + float c = ComputeHistoAndDataCost(histogram, alphabet_size, shift + 1); + if (c < fcost) { + method = shift + 1; + fcost = c; + } else if (ans_histogram_strategy == + HistogramParams::ANSHistogramStrategy::kFast) { + // do not be as precise if estimating cost. + break; + } + } + *cost = fcost; + return method; +} + +} // namespace + +// Returns an estimate of the cost of encoding this histogram and the +// corresponding data. +size_t BuildAndStoreANSEncodingData( + HistogramParams::ANSHistogramStrategy ans_histogram_strategy, + const ANSHistBin* histogram, size_t alphabet_size, size_t log_alpha_size, + bool use_prefix_code, ANSEncSymbolInfo* info, BitWriter* writer) { + if (use_prefix_code) { + if (alphabet_size <= 1) return 0; + std::vector histo(alphabet_size); + for (size_t i = 0; i < alphabet_size; i++) { + histo[i] = histogram[i]; + JXL_CHECK(histogram[i] >= 0); + } + size_t cost = 0; + { + std::vector depths(alphabet_size); + std::vector bits(alphabet_size); + BitWriter tmp_writer; + BitWriter* w = writer ? writer : &tmp_writer; + size_t start = w->BitsWritten(); + BitWriter::Allotment allotment( + w, 8 * alphabet_size + 8); // safe upper bound + BuildAndStoreHuffmanTree(histo.data(), alphabet_size, depths.data(), + bits.data(), w); + ReclaimAndCharge(w, &allotment, 0, /*aux_out=*/nullptr); + + for (size_t i = 0; i < alphabet_size; i++) { + info[i].bits = depths[i] == 0 ? 0 : bits[i]; + info[i].depth = depths[i]; + } + cost = w->BitsWritten() - start; + } + // Estimate data cost. + for (size_t i = 0; i < alphabet_size; i++) { + cost += histogram[i] * info[i].depth; + } + return cost; + } + JXL_ASSERT(alphabet_size <= ANS_TAB_SIZE); + // Ensure we ignore trailing zeros in the histogram. + if (alphabet_size != 0) { + size_t largest_symbol = 0; + for (size_t i = 0; i < alphabet_size; i++) { + if (histogram[i] != 0) largest_symbol = i; + } + alphabet_size = largest_symbol + 1; + } + float cost; + uint32_t method = ComputeBestMethod(histogram, alphabet_size, &cost, + ans_histogram_strategy); + JXL_ASSERT(cost >= 0); + int num_symbols; + int symbols[kMaxNumSymbolsForSmallCode] = {}; + std::vector counts(histogram, histogram + alphabet_size); + if (!counts.empty()) { + size_t sum = 0; + for (size_t i = 0; i < counts.size(); i++) { + sum += counts[i]; + } + if (sum == 0) { + counts[0] = ANS_TAB_SIZE; + } + } + if (method == 0) { + counts = CreateFlatHistogram(alphabet_size, ANS_TAB_SIZE); + AliasTable::Entry a[ANS_MAX_ALPHABET_SIZE]; + InitAliasTable(counts, ANS_TAB_SIZE, log_alpha_size, a); + ANSBuildInfoTable(counts.data(), a, alphabet_size, log_alpha_size, info); + if (writer != nullptr) { + EncodeFlatHistogram(alphabet_size, writer); + } + return cost; + } + int omit_pos = 0; + uint32_t shift = method - 1; + JXL_CHECK(NormalizeCounts(counts.data(), &omit_pos, alphabet_size, + ANS_LOG_TAB_SIZE, shift, &num_symbols, symbols)); + AliasTable::Entry a[ANS_MAX_ALPHABET_SIZE]; + InitAliasTable(counts, ANS_TAB_SIZE, log_alpha_size, a); + ANSBuildInfoTable(counts.data(), a, alphabet_size, log_alpha_size, info); + if (writer != nullptr) { + bool ok = EncodeCounts(counts.data(), alphabet_size, omit_pos, num_symbols, + shift, symbols, writer); + (void)ok; + JXL_DASSERT(ok); + } + return cost; +} + +float ANSPopulationCost(const ANSHistBin* data, size_t alphabet_size) { + float c; + ComputeBestMethod(data, alphabet_size, &c, + HistogramParams::ANSHistogramStrategy::kFast); + return c; +} + +template +void EncodeUintConfig(const HybridUintConfig uint_config, Writer* writer, + size_t log_alpha_size) { + writer->Write(CeilLog2Nonzero(log_alpha_size + 1), + uint_config.split_exponent); + if (uint_config.split_exponent == log_alpha_size) { + return; // msb/lsb don't matter. + } + size_t nbits = CeilLog2Nonzero(uint_config.split_exponent + 1); + writer->Write(nbits, uint_config.msb_in_token); + nbits = CeilLog2Nonzero(uint_config.split_exponent - + uint_config.msb_in_token + 1); + writer->Write(nbits, uint_config.lsb_in_token); +} +template +void EncodeUintConfigs(const std::vector& uint_config, + Writer* writer, size_t log_alpha_size) { + // TODO(veluca): RLE? + for (size_t i = 0; i < uint_config.size(); i++) { + EncodeUintConfig(uint_config[i], writer, log_alpha_size); + } +} +template void EncodeUintConfigs(const std::vector&, + BitWriter*, size_t); + +namespace { + +void ChooseUintConfigs(const HistogramParams& params, + const std::vector>& tokens, + const std::vector& context_map, + std::vector* clustered_histograms, + EntropyEncodingData* codes, size_t* log_alpha_size) { + codes->uint_config.resize(clustered_histograms->size()); + if (params.uint_method == HistogramParams::HybridUintMethod::kNone) return; + if (params.uint_method == HistogramParams::HybridUintMethod::kContextMap) { + codes->uint_config.clear(); + codes->uint_config.resize(clustered_histograms->size(), + HybridUintConfig(2, 0, 1)); + return; + } + + // Brute-force method that tries a few options. + std::vector configs; + if (params.uint_method == HistogramParams::HybridUintMethod::kBest) { + configs = { + HybridUintConfig(4, 2, 0), // default + HybridUintConfig(4, 1, 0), // less precise + HybridUintConfig(4, 2, 1), // add sign + HybridUintConfig(4, 2, 2), // add sign+parity + HybridUintConfig(4, 1, 2), // add parity but less msb + // Same as above, but more direct coding. + HybridUintConfig(5, 2, 0), HybridUintConfig(5, 1, 0), + HybridUintConfig(5, 2, 1), HybridUintConfig(5, 2, 2), + HybridUintConfig(5, 1, 2), + // Same as above, but less direct coding. + HybridUintConfig(3, 2, 0), HybridUintConfig(3, 1, 0), + HybridUintConfig(3, 2, 1), HybridUintConfig(3, 1, 2), + // For near-lossless. + HybridUintConfig(4, 1, 3), HybridUintConfig(5, 1, 4), + HybridUintConfig(5, 2, 3), HybridUintConfig(6, 1, 5), + HybridUintConfig(6, 2, 4), HybridUintConfig(6, 0, 0), + // Other + HybridUintConfig(0, 0, 0), // varlenuint + HybridUintConfig(2, 0, 1), // works well for ctx map + HybridUintConfig(7, 0, 0), // direct coding + HybridUintConfig(8, 0, 0), // direct coding + HybridUintConfig(9, 0, 0), // direct coding + HybridUintConfig(10, 0, 0), // direct coding + HybridUintConfig(11, 0, 0), // direct coding + HybridUintConfig(12, 0, 0), // direct coding + }; + } else if (params.uint_method == HistogramParams::HybridUintMethod::kFast) { + configs = { + HybridUintConfig(4, 2, 0), // default + HybridUintConfig(4, 1, 2), // add parity but less msb + HybridUintConfig(0, 0, 0), // smallest histograms + HybridUintConfig(2, 0, 1), // works well for ctx map + }; + } + + std::vector costs(clustered_histograms->size(), + std::numeric_limits::max()); + std::vector extra_bits(clustered_histograms->size()); + std::vector is_valid(clustered_histograms->size()); + size_t max_alpha = + codes->use_prefix_code ? PREFIX_MAX_ALPHABET_SIZE : ANS_MAX_ALPHABET_SIZE; + for (HybridUintConfig cfg : configs) { + std::fill(is_valid.begin(), is_valid.end(), true); + std::fill(extra_bits.begin(), extra_bits.end(), 0); + + for (size_t i = 0; i < clustered_histograms->size(); i++) { + (*clustered_histograms)[i].Clear(); + } + for (size_t i = 0; i < tokens.size(); ++i) { + for (size_t j = 0; j < tokens[i].size(); ++j) { + const Token token = tokens[i][j]; + // TODO(veluca): do not ignore lz77 commands. + if (token.is_lz77_length) continue; + size_t histo = context_map[token.context]; + uint32_t tok, nbits, bits; + cfg.Encode(token.value, &tok, &nbits, &bits); + if (tok >= max_alpha || + (codes->lz77.enabled && tok >= codes->lz77.min_symbol)) { + is_valid[histo] = false; + continue; + } + extra_bits[histo] += nbits; + (*clustered_histograms)[histo].Add(tok); + } + } + + for (size_t i = 0; i < clustered_histograms->size(); i++) { + if (!is_valid[i]) continue; + float cost = (*clustered_histograms)[i].PopulationCost() + extra_bits[i]; + if (cost < costs[i]) { + codes->uint_config[i] = cfg; + costs[i] = cost; + } + } + } + + // Rebuild histograms. + for (size_t i = 0; i < clustered_histograms->size(); i++) { + (*clustered_histograms)[i].Clear(); + } + *log_alpha_size = 4; + for (size_t i = 0; i < tokens.size(); ++i) { + for (size_t j = 0; j < tokens[i].size(); ++j) { + const Token token = tokens[i][j]; + uint32_t tok, nbits, bits; + size_t histo = context_map[token.context]; + (token.is_lz77_length ? codes->lz77.length_uint_config + : codes->uint_config[histo]) + .Encode(token.value, &tok, &nbits, &bits); + tok += token.is_lz77_length ? codes->lz77.min_symbol : 0; + (*clustered_histograms)[histo].Add(tok); + while (tok >= (1u << *log_alpha_size)) (*log_alpha_size)++; + } + } +#if JXL_ENABLE_ASSERT + size_t max_log_alpha_size = codes->use_prefix_code ? PREFIX_MAX_BITS : 8; + JXL_ASSERT(*log_alpha_size <= max_log_alpha_size); +#endif +} + +class HistogramBuilder { + public: + explicit HistogramBuilder(const size_t num_contexts) + : histograms_(num_contexts) {} + + void VisitSymbol(int symbol, size_t histo_idx) { + JXL_DASSERT(histo_idx < histograms_.size()); + histograms_[histo_idx].Add(symbol); + } + + // NOTE: `layer` is only for clustered_entropy; caller does ReclaimAndCharge. + size_t BuildAndStoreEntropyCodes( + const HistogramParams& params, + const std::vector>& tokens, EntropyEncodingData* codes, + std::vector* context_map, bool use_prefix_code, + BitWriter* writer, size_t layer, AuxOut* aux_out) const { + size_t cost = 0; + codes->encoding_info.clear(); + std::vector clustered_histograms(histograms_); + context_map->resize(histograms_.size()); + if (histograms_.size() > 1) { + if (!ans_fuzzer_friendly_) { + std::vector histogram_symbols; + ClusterHistograms(params, histograms_, histograms_.size(), + kClustersLimit, &clustered_histograms, + &histogram_symbols); + for (size_t c = 0; c < histograms_.size(); ++c) { + (*context_map)[c] = static_cast(histogram_symbols[c]); + } + } else { + fill(context_map->begin(), context_map->end(), 0); + size_t max_symbol = 0; + for (const Histogram& h : histograms_) { + max_symbol = std::max(h.data_.size(), max_symbol); + } + size_t num_symbols = 1 << CeilLog2Nonzero(max_symbol + 1); + clustered_histograms.resize(1); + clustered_histograms[0].Clear(); + for (size_t i = 0; i < num_symbols; i++) { + clustered_histograms[0].Add(i); + } + } + if (writer != nullptr) { + EncodeContextMap(*context_map, clustered_histograms.size(), writer); + } + } + if (aux_out != nullptr) { + for (size_t i = 0; i < clustered_histograms.size(); ++i) { + aux_out->layers[layer].clustered_entropy += + clustered_histograms[i].ShannonEntropy(); + } + } + codes->use_prefix_code = use_prefix_code; + size_t log_alpha_size = codes->lz77.enabled ? 8 : 7; // Sane default. + if (ans_fuzzer_friendly_) { + codes->uint_config.clear(); + codes->uint_config.resize(1, HybridUintConfig(7, 0, 0)); + } else { + ChooseUintConfigs(params, tokens, *context_map, &clustered_histograms, + codes, &log_alpha_size); + } + if (log_alpha_size < 5) log_alpha_size = 5; + SizeWriter size_writer; // Used if writer == nullptr to estimate costs. + cost += 1; + if (writer) writer->Write(1, use_prefix_code); + + if (use_prefix_code) { + log_alpha_size = PREFIX_MAX_BITS; + } else { + cost += 2; + } + if (writer == nullptr) { + EncodeUintConfigs(codes->uint_config, &size_writer, log_alpha_size); + } else { + if (!use_prefix_code) writer->Write(2, log_alpha_size - 5); + EncodeUintConfigs(codes->uint_config, writer, log_alpha_size); + } + if (use_prefix_code) { + for (size_t c = 0; c < clustered_histograms.size(); ++c) { + size_t num_symbol = 1; + for (size_t i = 0; i < clustered_histograms[c].data_.size(); i++) { + if (clustered_histograms[c].data_[i]) num_symbol = i + 1; + } + if (writer) { + StoreVarLenUint16(num_symbol - 1, writer); + } else { + StoreVarLenUint16(num_symbol - 1, &size_writer); + } + } + } + cost += size_writer.size; + for (size_t c = 0; c < clustered_histograms.size(); ++c) { + size_t num_symbol = 1; + for (size_t i = 0; i < clustered_histograms[c].data_.size(); i++) { + if (clustered_histograms[c].data_[i]) num_symbol = i + 1; + } + codes->encoding_info.emplace_back(); + codes->encoding_info.back().resize(std::max(1, num_symbol)); + + BitWriter::Allotment allotment(writer, 256 + num_symbol * 24); + cost += BuildAndStoreANSEncodingData( + params.ans_histogram_strategy, clustered_histograms[c].data_.data(), + num_symbol, log_alpha_size, use_prefix_code, + codes->encoding_info.back().data(), writer); + allotment.FinishedHistogram(writer); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + } + return cost; + } + + const Histogram& Histo(size_t i) const { return histograms_[i]; } + + private: + std::vector histograms_; +}; + +class SymbolCostEstimator { + public: + SymbolCostEstimator(size_t num_contexts, bool force_huffman, + const std::vector>& tokens, + const LZ77Params& lz77) { + HistogramBuilder builder(num_contexts); + // Build histograms for estimating lz77 savings. + HybridUintConfig uint_config; + for (size_t i = 0; i < tokens.size(); ++i) { + for (size_t j = 0; j < tokens[i].size(); ++j) { + const Token token = tokens[i][j]; + uint32_t tok, nbits, bits; + (token.is_lz77_length ? lz77.length_uint_config : uint_config) + .Encode(token.value, &tok, &nbits, &bits); + tok += token.is_lz77_length ? lz77.min_symbol : 0; + builder.VisitSymbol(tok, token.context); + } + } + max_alphabet_size_ = 0; + for (size_t i = 0; i < num_contexts; i++) { + max_alphabet_size_ = + std::max(max_alphabet_size_, builder.Histo(i).data_.size()); + } + bits_.resize(num_contexts * max_alphabet_size_); + // TODO(veluca): SIMD? + add_symbol_cost_.resize(num_contexts); + for (size_t i = 0; i < num_contexts; i++) { + float inv_total = 1.0f / (builder.Histo(i).total_count_ + 1e-8f); + float total_cost = 0; + for (size_t j = 0; j < builder.Histo(i).data_.size(); j++) { + size_t cnt = builder.Histo(i).data_[j]; + float cost = 0; + if (cnt != 0 && cnt != builder.Histo(i).total_count_) { + cost = -FastLog2f(cnt * inv_total); + if (force_huffman) cost = std::ceil(cost); + } else if (cnt == 0) { + cost = ANS_LOG_TAB_SIZE; // Highest possible cost. + } + bits_[i * max_alphabet_size_ + j] = cost; + total_cost += cost * builder.Histo(i).data_[j]; + } + // Penalty for adding a lz77 symbol to this contest (only used for static + // cost model). Higher penalty for contexts that have a very low + // per-symbol entropy. + add_symbol_cost_[i] = std::max(0.0f, 6.0f - total_cost * inv_total); + } + } + float Bits(size_t ctx, size_t sym) const { + return bits_[ctx * max_alphabet_size_ + sym]; + } + float LenCost(size_t ctx, size_t len, const LZ77Params& lz77) const { + uint32_t nbits, bits, tok; + lz77.length_uint_config.Encode(len, &tok, &nbits, &bits); + tok += lz77.min_symbol; + return nbits + Bits(ctx, tok); + } + float DistCost(size_t len, const LZ77Params& lz77) const { + uint32_t nbits, bits, tok; + HybridUintConfig().Encode(len, &tok, &nbits, &bits); + return nbits + Bits(lz77.nonserialized_distance_context, tok); + } + float AddSymbolCost(size_t idx) const { return add_symbol_cost_[idx]; } + + private: + size_t max_alphabet_size_; + std::vector bits_; + std::vector add_symbol_cost_; +}; + +void ApplyLZ77_RLE(const HistogramParams& params, size_t num_contexts, + const std::vector>& tokens, + LZ77Params& lz77, + std::vector>& tokens_lz77) { + // TODO(veluca): tune heuristics here. + SymbolCostEstimator sce(num_contexts, params.force_huffman, tokens, lz77); + float bit_decrease = 0; + size_t total_symbols = 0; + tokens_lz77.resize(tokens.size()); + std::vector sym_cost; + HybridUintConfig uint_config; + for (size_t stream = 0; stream < tokens.size(); stream++) { + size_t distance_multiplier = + params.image_widths.size() > stream ? params.image_widths[stream] : 0; + const auto& in = tokens[stream]; + auto& out = tokens_lz77[stream]; + total_symbols += in.size(); + // Cumulative sum of bit costs. + sym_cost.resize(in.size() + 1); + for (size_t i = 0; i < in.size(); i++) { + uint32_t tok, nbits, unused_bits; + uint_config.Encode(in[i].value, &tok, &nbits, &unused_bits); + sym_cost[i + 1] = sce.Bits(in[i].context, tok) + nbits + sym_cost[i]; + } + out.reserve(in.size()); + for (size_t i = 0; i < in.size(); i++) { + size_t num_to_copy = 0; + size_t distance_symbol = 0; // 1 for RLE. + if (distance_multiplier != 0) { + distance_symbol = 1; // Special distance 1 if enabled. + JXL_DASSERT(kSpecialDistances[1][0] == 1); + JXL_DASSERT(kSpecialDistances[1][1] == 0); + } + if (i > 0) { + for (; i + num_to_copy < in.size(); num_to_copy++) { + if (in[i + num_to_copy].value != in[i - 1].value) { + break; + } + } + } + if (num_to_copy == 0) { + out.push_back(in[i]); + continue; + } + float cost = sym_cost[i + num_to_copy] - sym_cost[i]; + // This subtraction might overflow, but that's OK. + size_t lz77_len = num_to_copy - lz77.min_length; + float lz77_cost = num_to_copy >= lz77.min_length + ? CeilLog2Nonzero(lz77_len + 1) + 1 + : 0; + if (num_to_copy < lz77.min_length || cost <= lz77_cost) { + for (size_t j = 0; j < num_to_copy; j++) { + out.push_back(in[i + j]); + } + i += num_to_copy - 1; + continue; + } + // Output the LZ77 length + out.emplace_back(in[i].context, lz77_len); + out.back().is_lz77_length = true; + i += num_to_copy - 1; + bit_decrease += cost - lz77_cost; + // Output the LZ77 copy distance. + out.emplace_back(lz77.nonserialized_distance_context, distance_symbol); + } + } + + if (bit_decrease > total_symbols * 0.2 + 16) { + lz77.enabled = true; + } +} + +// Hash chain for LZ77 matching +struct HashChain { + size_t size_; + std::vector data_; + + unsigned hash_num_values_ = 32768; + unsigned hash_mask_ = hash_num_values_ - 1; + unsigned hash_shift_ = 5; + + std::vector head; + std::vector chain; + std::vector val; + + // Speed up repetitions of zero + std::vector headz; + std::vector chainz; + std::vector zeros; + uint32_t numzeros = 0; + + size_t window_size_; + size_t window_mask_; + size_t min_length_; + size_t max_length_; + + // Map of special distance codes. + std::unordered_map special_dist_table_; + size_t num_special_distances_ = 0; + + uint32_t maxchainlength = 256; // window_size_ to allow all + + HashChain(const Token* data, size_t size, size_t window_size, + size_t min_length, size_t max_length, size_t distance_multiplier) + : size_(size), + window_size_(window_size), + window_mask_(window_size - 1), + min_length_(min_length), + max_length_(max_length) { + data_.resize(size); + for (size_t i = 0; i < size; i++) { + data_[i] = data[i].value; + } + + head.resize(hash_num_values_, -1); + val.resize(window_size_, -1); + chain.resize(window_size_); + for (uint32_t i = 0; i < window_size_; ++i) { + chain[i] = i; // same value as index indicates uninitialized + } + + zeros.resize(window_size_); + headz.resize(window_size_ + 1, -1); + chainz.resize(window_size_); + for (uint32_t i = 0; i < window_size_; ++i) { + chainz[i] = i; + } + // Translate distance to special distance code. + if (distance_multiplier) { + // Count down, so if due to small distance multiplier multiple distances + // map to the same code, the smallest code will be used in the end. + for (int i = kNumSpecialDistances - 1; i >= 0; --i) { + int xi = kSpecialDistances[i][0]; + int yi = kSpecialDistances[i][1]; + int distance = yi * distance_multiplier + xi; + // Ensure that we map distance 1 to the lowest symbols. + if (distance < 1) distance = 1; + special_dist_table_[distance] = i; + } + num_special_distances_ = kNumSpecialDistances; + } + } + + uint32_t GetHash(size_t pos) const { + uint32_t result = 0; + if (pos + 2 < size_) { + // TODO(lode): take the MSB's of the uint32_t values into account as well, + // given that the hash code itself is less than 32 bits. + result ^= (uint32_t)(data_[pos + 0] << 0u); + result ^= (uint32_t)(data_[pos + 1] << hash_shift_); + result ^= (uint32_t)(data_[pos + 2] << (hash_shift_ * 2)); + } else { + // No need to compute hash of last 2 bytes, the length 2 is too short. + return 0; + } + return result & hash_mask_; + } + + uint32_t CountZeros(size_t pos, uint32_t prevzeros) const { + size_t end = pos + window_size_; + if (end > size_) end = size_; + if (prevzeros > 0) { + if (prevzeros >= window_mask_ && data_[end - 1] == 0 && + end == pos + window_size_) { + return prevzeros; + } else { + return prevzeros - 1; + } + } + uint32_t num = 0; + while (pos + num < end && data_[pos + num] == 0) num++; + return num; + } + + void Update(size_t pos) { + uint32_t hashval = GetHash(pos); + uint32_t wpos = pos & window_mask_; + + val[wpos] = (int)hashval; + if (head[hashval] != -1) chain[wpos] = head[hashval]; + head[hashval] = wpos; + + if (pos > 0 && data_[pos] != data_[pos - 1]) numzeros = 0; + numzeros = CountZeros(pos, numzeros); + + zeros[wpos] = numzeros; + if (headz[numzeros] != -1) chainz[wpos] = headz[numzeros]; + headz[numzeros] = wpos; + } + + void Update(size_t pos, size_t len) { + for (size_t i = 0; i < len; i++) { + Update(pos + i); + } + } + + template + void FindMatches(size_t pos, int max_dist, const CB& found_match) const { + uint32_t wpos = pos & window_mask_; + uint32_t hashval = GetHash(pos); + uint32_t hashpos = chain[wpos]; + + int prev_dist = 0; + int end = std::min(pos + max_length_, size_); + uint32_t chainlength = 0; + uint32_t best_len = 0; + for (;;) { + int dist = (hashpos <= wpos) ? (wpos - hashpos) + : (wpos - hashpos + window_mask_ + 1); + if (dist < prev_dist) break; + prev_dist = dist; + uint32_t len = 0; + if (dist > 0) { + int i = pos; + int j = pos - dist; + if (numzeros > 3) { + int r = std::min(numzeros - 1, zeros[hashpos]); + if (i + r >= end) r = end - i - 1; + i += r; + j += r; + } + while (i < end && data_[i] == data_[j]) { + i++; + j++; + } + len = i - pos; + // This can trigger even if the new length is slightly smaller than the + // best length, because it is possible for a slightly cheaper distance + // symbol to occur. + if (len >= min_length_ && len + 2 >= best_len) { + auto it = special_dist_table_.find(dist); + int dist_symbol = (it == special_dist_table_.end()) + ? (num_special_distances_ + dist - 1) + : it->second; + found_match(len, dist_symbol); + if (len > best_len) best_len = len; + } + } + + chainlength++; + if (chainlength >= maxchainlength) break; + + if (numzeros >= 3 && len > numzeros) { + if (hashpos == chainz[hashpos]) break; + hashpos = chainz[hashpos]; + if (zeros[hashpos] != numzeros) break; + } else { + if (hashpos == chain[hashpos]) break; + hashpos = chain[hashpos]; + if (val[hashpos] != (int)hashval) break; // outdated hash value + } + } + } + void FindMatch(size_t pos, int max_dist, size_t* result_dist_symbol, + size_t* result_len) const { + *result_dist_symbol = 0; + *result_len = 1; + FindMatches(pos, max_dist, [&](size_t len, size_t dist_symbol) { + if (len > *result_len || + (len == *result_len && *result_dist_symbol > dist_symbol)) { + *result_len = len; + *result_dist_symbol = dist_symbol; + } + }); + } +}; + +float LenCost(size_t len) { + uint32_t nbits, bits, tok; + HybridUintConfig(1, 0, 0).Encode(len, &tok, &nbits, &bits); + constexpr float kCostTable[] = { + 2.797667318563126, 3.213177690381199, 2.5706009246743737, + 2.408392498667534, 2.829649191872326, 3.3923087753324577, + 4.029267451554331, 4.415576699706408, 4.509357574741465, + 9.21481543803004, 10.020590190114898, 11.858671627804766, + 12.45853300490526, 11.713105831990857, 12.561996324849314, + 13.775477692278367, 13.174027068768641, + }; + size_t table_size = sizeof kCostTable / sizeof *kCostTable; + if (tok >= table_size) tok = table_size - 1; + return kCostTable[tok] + nbits; +} + +// TODO(veluca): this does not take into account usage or non-usage of distance +// multipliers. +float DistCost(size_t dist) { + uint32_t nbits, bits, tok; + HybridUintConfig(7, 0, 0).Encode(dist, &tok, &nbits, &bits); + constexpr float kCostTable[] = { + 6.368282626312716, 5.680793277090298, 8.347404197105247, + 7.641619201599141, 6.914328374119438, 7.959808291537444, + 8.70023120759855, 8.71378518934703, 9.379132523982769, + 9.110472749092708, 9.159029569270908, 9.430936766731973, + 7.278284055315169, 7.8278514904267755, 10.026641158289236, + 9.976049229827066, 9.64351607048908, 9.563403863480442, + 10.171474111762747, 10.45950155077234, 9.994813912104219, + 10.322524683741156, 8.465808729388186, 8.756254166066853, + 10.160930174662234, 10.247329273413435, 10.04090403724809, + 10.129398517544082, 9.342311691539546, 9.07608009102374, + 10.104799540677513, 10.378079384990906, 10.165828974075072, + 10.337595322341553, 7.940557464567944, 10.575665823319431, + 11.023344321751955, 10.736144698831827, 11.118277044595054, + 7.468468230648442, 10.738305230932939, 10.906980780216568, + 10.163468216353817, 10.17805759656433, 11.167283670483565, + 11.147050200274544, 10.517921919244333, 10.651764778156886, + 10.17074446448919, 11.217636876224745, 11.261630721139484, + 11.403140815247259, 10.892472096873417, 11.1859607804481, + 8.017346947551262, 7.895143720278828, 11.036577113822025, + 11.170562110315794, 10.326988722591086, 10.40872184751056, + 11.213498225466386, 11.30580635516863, 10.672272515665442, + 10.768069466228063, 11.145257364153565, 11.64668307145549, + 10.593156194627339, 11.207499484844943, 10.767517766396908, + 10.826629811407042, 10.737764794499988, 10.6200448518045, + 10.191315385198092, 8.468384171390085, 11.731295299170432, + 11.824619886654398, 10.41518844301179, 10.16310536548649, + 10.539423685097576, 10.495136599328031, 10.469112847728267, + 11.72057686174922, 10.910326337834674, 11.378921834673758, + 11.847759036098536, 11.92071647623854, 10.810628276345282, + 11.008601085273893, 11.910326337834674, 11.949212023423133, + 11.298614839104337, 11.611603659010392, 10.472930394619985, + 11.835564720850282, 11.523267392285337, 12.01055816679611, + 8.413029688994023, 11.895784139536406, 11.984679534970505, + 11.220654278717394, 11.716311684833672, 10.61036646226114, + 10.89849965960364, 10.203762898863669, 10.997560826267238, + 11.484217379438984, 11.792836176993665, 12.24310468755171, + 11.464858097919262, 12.212747017409377, 11.425595666074955, + 11.572048533398757, 12.742093965163013, 11.381874288645637, + 12.191870445817015, 11.683156920035426, 11.152442115262197, + 11.90303691580457, 11.653292787169159, 11.938615382266098, + 16.970641701570223, 16.853602280380002, 17.26240782594733, + 16.644655390108507, 17.14310889757499, 16.910935455445955, + 17.505678976959697, 17.213498225466388, 2.4162310293553024, + 3.494587244462329, 3.5258600986408344, 3.4959806589517095, + 3.098390886949687, 3.343454654302911, 3.588847442290287, + 4.14614790111827, 5.152948641990529, 7.433696808092598, + 9.716311684833672, + }; + size_t table_size = sizeof kCostTable / sizeof *kCostTable; + if (tok >= table_size) tok = table_size - 1; + return kCostTable[tok] + nbits; +} + +void ApplyLZ77_LZ77(const HistogramParams& params, size_t num_contexts, + const std::vector>& tokens, + LZ77Params& lz77, + std::vector>& tokens_lz77) { + // TODO(veluca): tune heuristics here. + SymbolCostEstimator sce(num_contexts, params.force_huffman, tokens, lz77); + float bit_decrease = 0; + size_t total_symbols = 0; + tokens_lz77.resize(tokens.size()); + HybridUintConfig uint_config; + std::vector sym_cost; + for (size_t stream = 0; stream < tokens.size(); stream++) { + size_t distance_multiplier = + params.image_widths.size() > stream ? params.image_widths[stream] : 0; + const auto& in = tokens[stream]; + auto& out = tokens_lz77[stream]; + total_symbols += in.size(); + // Cumulative sum of bit costs. + sym_cost.resize(in.size() + 1); + for (size_t i = 0; i < in.size(); i++) { + uint32_t tok, nbits, unused_bits; + uint_config.Encode(in[i].value, &tok, &nbits, &unused_bits); + sym_cost[i + 1] = sce.Bits(in[i].context, tok) + nbits + sym_cost[i]; + } + + out.reserve(in.size()); + size_t max_distance = in.size(); + size_t min_length = lz77.min_length; + JXL_ASSERT(min_length >= 3); + size_t max_length = in.size(); + + // Use next power of two as window size. + size_t window_size = 1; + while (window_size < max_distance && window_size < kWindowSize) { + window_size <<= 1; + } + + HashChain chain(in.data(), in.size(), window_size, min_length, max_length, + distance_multiplier); + size_t len, dist_symbol; + + const size_t max_lazy_match_len = 256; // 0 to disable lazy matching + + // Whether the next symbol was already updated (to test lazy matching) + bool already_updated = false; + for (size_t i = 0; i < in.size(); i++) { + out.push_back(in[i]); + if (!already_updated) chain.Update(i); + already_updated = false; + chain.FindMatch(i, max_distance, &dist_symbol, &len); + if (len >= min_length) { + if (len < max_lazy_match_len && i + 1 < in.size()) { + // Try length at next symbol lazy matching + chain.Update(i + 1); + already_updated = true; + size_t len2, dist_symbol2; + chain.FindMatch(i + 1, max_distance, &dist_symbol2, &len2); + if (len2 > len) { + // Use the lazy match. Add literal, and use the next length starting + // from the next byte. + ++i; + already_updated = false; + len = len2; + dist_symbol = dist_symbol2; + out.push_back(in[i]); + } + } + + float cost = sym_cost[i + len] - sym_cost[i]; + size_t lz77_len = len - lz77.min_length; + float lz77_cost = LenCost(lz77_len) + DistCost(dist_symbol) + + sce.AddSymbolCost(out.back().context); + + if (lz77_cost <= cost) { + out.back().value = len - min_length; + out.back().is_lz77_length = true; + out.emplace_back(lz77.nonserialized_distance_context, dist_symbol); + bit_decrease += cost - lz77_cost; + } else { + // LZ77 match ignored, and symbol already pushed. Push all other + // symbols and skip. + for (size_t j = 1; j < len; j++) { + out.push_back(in[i + j]); + } + } + + if (already_updated) { + chain.Update(i + 2, len - 2); + already_updated = false; + } else { + chain.Update(i + 1, len - 1); + } + i += len - 1; + } else { + // Literal, already pushed + } + } + } + + if (bit_decrease > total_symbols * 0.2 + 16) { + lz77.enabled = true; + } +} + +void ApplyLZ77_Optimal(const HistogramParams& params, size_t num_contexts, + const std::vector>& tokens, + LZ77Params& lz77, + std::vector>& tokens_lz77) { + std::vector> tokens_for_cost_estimate; + ApplyLZ77_LZ77(params, num_contexts, tokens, lz77, tokens_for_cost_estimate); + // If greedy-LZ77 does not give better compression than no-lz77, no reason to + // run the optimal matching. + if (!lz77.enabled) return; + SymbolCostEstimator sce(num_contexts + 1, params.force_huffman, + tokens_for_cost_estimate, lz77); + tokens_lz77.resize(tokens.size()); + HybridUintConfig uint_config; + std::vector sym_cost; + std::vector dist_symbols; + for (size_t stream = 0; stream < tokens.size(); stream++) { + size_t distance_multiplier = + params.image_widths.size() > stream ? params.image_widths[stream] : 0; + const auto& in = tokens[stream]; + auto& out = tokens_lz77[stream]; + // Cumulative sum of bit costs. + sym_cost.resize(in.size() + 1); + for (size_t i = 0; i < in.size(); i++) { + uint32_t tok, nbits, unused_bits; + uint_config.Encode(in[i].value, &tok, &nbits, &unused_bits); + sym_cost[i + 1] = sce.Bits(in[i].context, tok) + nbits + sym_cost[i]; + } + + out.reserve(in.size()); + size_t max_distance = in.size(); + size_t min_length = lz77.min_length; + JXL_ASSERT(min_length >= 3); + size_t max_length = in.size(); + + // Use next power of two as window size. + size_t window_size = 1; + while (window_size < max_distance && window_size < kWindowSize) { + window_size <<= 1; + } + + HashChain chain(in.data(), in.size(), window_size, min_length, max_length, + distance_multiplier); + + struct MatchInfo { + uint32_t len; + uint32_t dist_symbol; + uint32_t ctx; + float total_cost = std::numeric_limits::max(); + }; + // Total cost to encode the first N symbols. + std::vector prefix_costs(in.size() + 1); + prefix_costs[0].total_cost = 0; + + size_t rle_length = 0; + size_t skip_lz77 = 0; + for (size_t i = 0; i < in.size(); i++) { + chain.Update(i); + float lit_cost = + prefix_costs[i].total_cost + sym_cost[i + 1] - sym_cost[i]; + if (prefix_costs[i + 1].total_cost > lit_cost) { + prefix_costs[i + 1].dist_symbol = 0; + prefix_costs[i + 1].len = 1; + prefix_costs[i + 1].ctx = in[i].context; + prefix_costs[i + 1].total_cost = lit_cost; + } + if (skip_lz77 > 0) { + skip_lz77--; + continue; + } + dist_symbols.clear(); + chain.FindMatches(i, max_distance, + [&dist_symbols](size_t len, size_t dist_symbol) { + if (dist_symbols.size() <= len) { + dist_symbols.resize(len + 1, dist_symbol); + } + if (dist_symbol < dist_symbols[len]) { + dist_symbols[len] = dist_symbol; + } + }); + if (dist_symbols.size() <= min_length) continue; + { + size_t best_cost = dist_symbols.back(); + for (size_t j = dist_symbols.size() - 1; j >= min_length; j--) { + if (dist_symbols[j] < best_cost) { + best_cost = dist_symbols[j]; + } + dist_symbols[j] = best_cost; + } + } + for (size_t j = min_length; j < dist_symbols.size(); j++) { + // Cost model that uses results from lazy LZ77. + float lz77_cost = sce.LenCost(in[i].context, j - min_length, lz77) + + sce.DistCost(dist_symbols[j], lz77); + float cost = prefix_costs[i].total_cost + lz77_cost; + if (prefix_costs[i + j].total_cost > cost) { + prefix_costs[i + j].len = j; + prefix_costs[i + j].dist_symbol = dist_symbols[j] + 1; + prefix_costs[i + j].ctx = in[i].context; + prefix_costs[i + j].total_cost = cost; + } + } + // We are in a RLE sequence: skip all the symbols except the first 8 and + // the last 8. This avoid quadratic costs for sequences with long runs of + // the same symbol. + if ((dist_symbols.back() == 0 && distance_multiplier == 0) || + (dist_symbols.back() == 1 && distance_multiplier != 0)) { + rle_length++; + } else { + rle_length = 0; + } + if (rle_length >= 8 && dist_symbols.size() > 9) { + skip_lz77 = dist_symbols.size() - 10; + rle_length = 0; + } + } + size_t pos = in.size(); + while (pos > 0) { + bool is_lz77_length = prefix_costs[pos].dist_symbol != 0; + if (is_lz77_length) { + size_t dist_symbol = prefix_costs[pos].dist_symbol - 1; + out.emplace_back(lz77.nonserialized_distance_context, dist_symbol); + } + size_t val = is_lz77_length ? prefix_costs[pos].len - min_length + : in[pos - 1].value; + out.emplace_back(prefix_costs[pos].ctx, val); + out.back().is_lz77_length = is_lz77_length; + pos -= prefix_costs[pos].len; + } + std::reverse(out.begin(), out.end()); + } +} + +void ApplyLZ77(const HistogramParams& params, size_t num_contexts, + const std::vector>& tokens, LZ77Params& lz77, + std::vector>& tokens_lz77) { + lz77.enabled = false; + if (params.force_huffman) { + lz77.min_symbol = std::min(PREFIX_MAX_ALPHABET_SIZE - 32, 512); + } else { + lz77.min_symbol = 224; + } + if (params.lz77_method == HistogramParams::LZ77Method::kNone) { + return; + } else if (params.lz77_method == HistogramParams::LZ77Method::kRLE) { + ApplyLZ77_RLE(params, num_contexts, tokens, lz77, tokens_lz77); + } else if (params.lz77_method == HistogramParams::LZ77Method::kLZ77) { + ApplyLZ77_LZ77(params, num_contexts, tokens, lz77, tokens_lz77); + } else if (params.lz77_method == HistogramParams::LZ77Method::kOptimal) { + ApplyLZ77_Optimal(params, num_contexts, tokens, lz77, tokens_lz77); + } else { + JXL_ABORT("Not implemented"); + } +} +} // namespace + +size_t BuildAndEncodeHistograms(const HistogramParams& params, + size_t num_contexts, + std::vector>& tokens, + EntropyEncodingData* codes, + std::vector* context_map, + BitWriter* writer, size_t layer, + AuxOut* aux_out) { + size_t total_bits = 0; + codes->lz77.nonserialized_distance_context = num_contexts; + std::vector> tokens_lz77; + ApplyLZ77(params, num_contexts, tokens, codes->lz77, tokens_lz77); + if (ans_fuzzer_friendly_) { + codes->lz77.length_uint_config = HybridUintConfig(10, 0, 0); + codes->lz77.min_symbol = 2048; + } + + const size_t max_contexts = std::min(num_contexts, kClustersLimit); + BitWriter::Allotment allotment(writer, + 128 + num_contexts * 40 + max_contexts * 96); + if (writer) { + JXL_CHECK(Bundle::Write(codes->lz77, writer, layer, aux_out)); + } else { + size_t ebits, bits; + JXL_CHECK(Bundle::CanEncode(codes->lz77, &ebits, &bits)); + total_bits += bits; + } + if (codes->lz77.enabled) { + if (writer) { + size_t b = writer->BitsWritten(); + EncodeUintConfig(codes->lz77.length_uint_config, writer, + /*log_alpha_size=*/8); + total_bits += writer->BitsWritten() - b; + } else { + SizeWriter size_writer; + EncodeUintConfig(codes->lz77.length_uint_config, &size_writer, + /*log_alpha_size=*/8); + total_bits += size_writer.size; + } + num_contexts += 1; + tokens = std::move(tokens_lz77); + } + size_t total_tokens = 0; + // Build histograms. + HistogramBuilder builder(num_contexts); + HybridUintConfig uint_config; // Default config for clustering. + // Unless we are using the kContextMap histogram option. + if (params.uint_method == HistogramParams::HybridUintMethod::kContextMap) { + uint_config = HybridUintConfig(2, 0, 1); + } + if (ans_fuzzer_friendly_) { + uint_config = HybridUintConfig(10, 0, 0); + } + for (size_t i = 0; i < tokens.size(); ++i) { + for (size_t j = 0; j < tokens[i].size(); ++j) { + const Token token = tokens[i][j]; + total_tokens++; + uint32_t tok, nbits, bits; + (token.is_lz77_length ? codes->lz77.length_uint_config : uint_config) + .Encode(token.value, &tok, &nbits, &bits); + tok += token.is_lz77_length ? codes->lz77.min_symbol : 0; + builder.VisitSymbol(tok, token.context); + } + } + + bool use_prefix_code = + params.force_huffman || total_tokens < 100 || + params.clustering == HistogramParams::ClusteringType::kFastest || + ans_fuzzer_friendly_; + if (!use_prefix_code) { + bool all_singleton = true; + for (size_t i = 0; i < num_contexts; i++) { + if (builder.Histo(i).ShannonEntropy() >= 1e-5) { + all_singleton = false; + } + } + if (all_singleton) { + use_prefix_code = true; + } + } + + // Encode histograms. + total_bits += builder.BuildAndStoreEntropyCodes(params, tokens, codes, + context_map, use_prefix_code, + writer, layer, aux_out); + allotment.FinishedHistogram(writer); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + + if (aux_out != nullptr) { + aux_out->layers[layer].num_clustered_histograms += + codes->encoding_info.size(); + } + return total_bits; +} + +size_t WriteTokens(const std::vector& tokens, + const EntropyEncodingData& codes, + const std::vector& context_map, BitWriter* writer) { + size_t num_extra_bits = 0; + if (codes.use_prefix_code) { + for (size_t i = 0; i < tokens.size(); i++) { + uint32_t tok, nbits, bits; + const Token& token = tokens[i]; + size_t histo = context_map[token.context]; + (token.is_lz77_length ? codes.lz77.length_uint_config + : codes.uint_config[histo]) + .Encode(token.value, &tok, &nbits, &bits); + tok += token.is_lz77_length ? codes.lz77.min_symbol : 0; + // Combine two calls to the BitWriter. Equivalent to: + // writer->Write(codes.encoding_info[histo][tok].depth, + // codes.encoding_info[histo][tok].bits); + // writer->Write(nbits, bits); + uint64_t data = codes.encoding_info[histo][tok].bits; + data |= bits << codes.encoding_info[histo][tok].depth; + writer->Write(codes.encoding_info[histo][tok].depth + nbits, data); + num_extra_bits += nbits; + } + return num_extra_bits; + } + std::vector out; + std::vector out_nbits; + out.reserve(tokens.size()); + out_nbits.reserve(tokens.size()); + uint64_t allbits = 0; + size_t numallbits = 0; + // Writes in *reversed* order. + auto addbits = [&](size_t bits, size_t nbits) { + JXL_DASSERT(bits >> nbits == 0); + if (JXL_UNLIKELY(numallbits + nbits > BitWriter::kMaxBitsPerCall)) { + out.push_back(allbits); + out_nbits.push_back(numallbits); + numallbits = allbits = 0; + } + allbits <<= nbits; + allbits |= bits; + numallbits += nbits; + }; + const int end = tokens.size(); + ANSCoder ans; + for (int i = end - 1; i >= 0; --i) { + const Token token = tokens[i]; + const uint8_t histo = context_map[token.context]; + uint32_t tok, nbits, bits; + (token.is_lz77_length ? codes.lz77.length_uint_config + : codes.uint_config[histo]) + .Encode(tokens[i].value, &tok, &nbits, &bits); + tok += token.is_lz77_length ? codes.lz77.min_symbol : 0; + const ANSEncSymbolInfo& info = codes.encoding_info[histo][tok]; + // Extra bits first as this is reversed. + addbits(bits, nbits); + num_extra_bits += nbits; + uint8_t ans_nbits = 0; + uint32_t ans_bits = ans.PutSymbol(info, &ans_nbits); + addbits(ans_bits, ans_nbits); + } + const uint32_t state = ans.GetState(); + writer->Write(32, state); + writer->Write(numallbits, allbits); + for (int i = out.size(); i > 0; --i) { + writer->Write(out_nbits[i - 1], out[i - 1]); + } + return num_extra_bits; +} + +void WriteTokens(const std::vector& tokens, + const EntropyEncodingData& codes, + const std::vector& context_map, BitWriter* writer, + size_t layer, AuxOut* aux_out) { + BitWriter::Allotment allotment(writer, 32 * tokens.size() + 32 * 1024 * 4); + size_t num_extra_bits = WriteTokens(tokens, codes, context_map, writer); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + if (aux_out != nullptr) { + aux_out->layers[layer].extra_bits += num_extra_bits; + } +} + +void SetANSFuzzerFriendly(bool ans_fuzzer_friendly) { +#if JXL_IS_DEBUG_BUILD // Guard against accidental / malicious changes. + ans_fuzzer_friendly_ = ans_fuzzer_friendly; +#endif +} +} // namespace jxl diff --git a/lib/jxl/enc_ans.h b/lib/jxl/enc_ans.h new file mode 100644 index 0000000..9614ede --- /dev/null +++ b/lib/jxl/enc_ans.h @@ -0,0 +1,142 @@ +// 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_ENC_ANS_H_ +#define LIB_JXL_ENC_ANS_H_ + +// Library to encode the ANS population counts to the bit-stream and encode +// symbols based on the respective distributions. + +#include +#include +#include +#include +#include + +#include +#include + +#include "lib/jxl/ans_common.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/enc_ans_params.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/huffman_table.h" + +namespace jxl { + +#define USE_MULT_BY_RECIPROCAL + +// precision must be equal to: #bits(state_) + #bits(freq) +#define RECIPROCAL_PRECISION (32 + ANS_LOG_TAB_SIZE) + +// Data structure representing one element of the encoding table built +// from a distribution. +// TODO(veluca): split this up, or use an union. +struct ANSEncSymbolInfo { + // ANS + uint16_t freq_; + std::vector reverse_map_; +#ifdef USE_MULT_BY_RECIPROCAL + uint64_t ifreq_; +#endif + // Prefix coding. + uint8_t depth; + uint16_t bits; +}; + +class ANSCoder { + public: + ANSCoder() : state_(ANS_SIGNATURE << 16) {} + + uint32_t PutSymbol(const ANSEncSymbolInfo& t, uint8_t* nbits) { + uint32_t bits = 0; + *nbits = 0; + if ((state_ >> (32 - ANS_LOG_TAB_SIZE)) >= t.freq_) { + bits = state_ & 0xffff; + state_ >>= 16; + *nbits = 16; + } +#ifdef USE_MULT_BY_RECIPROCAL + // We use mult-by-reciprocal trick, but that requires 64b calc. + const uint32_t v = (state_ * t.ifreq_) >> RECIPROCAL_PRECISION; + const uint32_t offset = t.reverse_map_[state_ - v * t.freq_]; + state_ = (v << ANS_LOG_TAB_SIZE) + offset; +#else + state_ = ((state_ / t.freq_) << ANS_LOG_TAB_SIZE) + + t.reverse_map_[state_ % t.freq_]; +#endif + return bits; + } + + uint32_t GetState() const { return state_; } + + private: + uint32_t state_; +}; + +// RebalanceHistogram requires a signed type. +using ANSHistBin = int32_t; + +struct EntropyEncodingData { + std::vector> encoding_info; + bool use_prefix_code; + std::vector uint_config; + LZ77Params lz77; +}; + +// Integer to be encoded by an entropy coder, either ANS or Huffman. +struct Token { + Token(uint32_t c, uint32_t value) + : is_lz77_length(false), context(c), value(value) {} + uint32_t is_lz77_length : 1; + uint32_t context : 31; + uint32_t value; +}; + +// Returns an estimate of the number of bits required to encode the given +// histogram (header bits plus data bits). +float ANSPopulationCost(const ANSHistBin* data, size_t alphabet_size); + +// Apply context clustering, compute histograms and encode them. Returns an +// estimate of the total bits used for encoding the stream. If `writer` == +// nullptr, the bit estimate will not take into account the context map (which +// does not get written if `num_contexts` == 1). +size_t BuildAndEncodeHistograms(const HistogramParams& params, + size_t num_contexts, + std::vector>& tokens, + EntropyEncodingData* codes, + std::vector* context_map, + BitWriter* writer, size_t layer, + AuxOut* aux_out); + +// Write the tokens to a string. +void WriteTokens(const std::vector& tokens, + const EntropyEncodingData& codes, + const std::vector& context_map, BitWriter* writer, + size_t layer, AuxOut* aux_out); + +// Same as above, but assumes allotment created by caller. +size_t WriteTokens(const std::vector& tokens, + const EntropyEncodingData& codes, + const std::vector& context_map, BitWriter* writer); + +// Exposed for tests; to be used with Writer=BitWriter only. +template +void EncodeUintConfigs(const std::vector& uint_config, + Writer* writer, size_t log_alpha_size); +extern template void EncodeUintConfigs(const std::vector&, + BitWriter*, size_t); + +// Globally set the option to create fuzzer-friendly ANS streams. Negatively +// impacts compression. Not thread-safe. +void SetANSFuzzerFriendly(bool ans_fuzzer_friendly); +} // namespace jxl + +#endif // LIB_JXL_ENC_ANS_H_ diff --git a/lib/jxl/enc_ans_params.h b/lib/jxl/enc_ans_params.h new file mode 100644 index 0000000..6f7cd89 --- /dev/null +++ b/lib/jxl/enc_ans_params.h @@ -0,0 +1,75 @@ +// 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_ENC_ANS_PARAMS_H_ +#define LIB_JXL_ENC_ANS_PARAMS_H_ + +// Encoder-only parameter needed for ANS entropy encoding methods. + +#include +#include + +#include "lib/jxl/enc_params.h" + +namespace jxl { + +struct HistogramParams { + enum class ClusteringType { + kFastest, // Only 4 clusters. + kFast, + kBest, + }; + + enum class HybridUintMethod { + kNone, // just use kHybridUint420Config. + kFast, // just try a couple of options. + kContextMap, // fast choice for ctx map. + kBest, + }; + + enum class LZ77Method { + kNone, // do not try lz77. + kRLE, // only try doing RLE. + kLZ77, // try lz77 with backward references. + kOptimal, // optimal-matching LZ77 parsing. + }; + + enum class ANSHistogramStrategy { + kFast, // Only try some methods, early exit. + kApproximate, // Only try some methods. + kPrecise, // Try all methods. + }; + + HistogramParams() = default; + + HistogramParams(SpeedTier tier, size_t num_ctx) { + if (tier > SpeedTier::kFalcon) { + clustering = ClusteringType::kFastest; + lz77_method = LZ77Method::kNone; + } else if (tier > SpeedTier::kTortoise) { + clustering = ClusteringType::kFast; + } else { + clustering = ClusteringType::kBest; + } + if (tier > SpeedTier::kTortoise) { + uint_method = HybridUintMethod::kNone; + } + if (tier >= SpeedTier::kSquirrel) { + ans_histogram_strategy = ANSHistogramStrategy::kApproximate; + } + } + + ClusteringType clustering = ClusteringType::kBest; + HybridUintMethod uint_method = HybridUintMethod::kBest; + LZ77Method lz77_method = LZ77Method::kRLE; + ANSHistogramStrategy ans_histogram_strategy = ANSHistogramStrategy::kPrecise; + std::vector image_widths; + size_t max_histograms = ~0; + bool force_huffman = false; +}; + +} // namespace jxl + +#endif // LIB_JXL_ENC_ANS_PARAMS_H_ diff --git a/lib/jxl/enc_ar_control_field.cc b/lib/jxl/enc_ar_control_field.cc new file mode 100644 index 0000000..f43340e --- /dev/null +++ b/lib/jxl/enc_ar_control_field.cc @@ -0,0 +1,318 @@ +// 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/enc_ar_control_field.h" + +#include +#include + +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_ar_control_field.cc" +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +void ProcessTile(const Image3F& opsin, PassesEncoderState* enc_state, + const Rect& rect, + ArControlFieldHeuristics::TempImages* temp_image) { + constexpr size_t N = kBlockDim; + ImageB* JXL_RESTRICT epf_sharpness = &enc_state->shared.epf_sharpness; + ImageF* JXL_RESTRICT quant = &enc_state->initial_quant_field; + JXL_ASSERT( + epf_sharpness->xsize() == enc_state->shared.frame_dim.xsize_blocks && + epf_sharpness->ysize() == enc_state->shared.frame_dim.ysize_blocks); + + if (enc_state->cparams.butteraugli_distance < kMinButteraugliForDynamicAR || + enc_state->cparams.speed_tier > SpeedTier::kWombat || + enc_state->shared.frame_header.loop_filter.epf_iters == 0) { + FillPlane(static_cast(4), epf_sharpness, rect); + return; + } + + // Likely better to have a higher X weight, like: + // const float kChannelWeights[3] = {47.0f, 4.35f, 0.287f}; + const float kChannelWeights[3] = {4.35f, 4.35f, 0.287f}; + const float kChannelWeightsLapNeg[3] = {-0.125f * kChannelWeights[0], + -0.125f * kChannelWeights[1], + -0.125f * kChannelWeights[2]}; + const size_t sharpness_stride = + static_cast(epf_sharpness->PixelsPerRow()); + + size_t by0 = rect.y0(); + size_t by1 = rect.y0() + rect.ysize(); + size_t bx0 = rect.x0(); + size_t bx1 = rect.x0() + rect.xsize(); + temp_image->InitOnce(); + ImageF& laplacian_sqrsum = temp_image->laplacian_sqrsum; + // Calculate the L2 of the 3x3 Laplacian in an integral transform + // (for example 32x32 dct). This relates to transforms ability + // to propagate artefacts. + size_t y0 = by0 == 0 ? 2 : 0; + size_t y1 = by1 * N + 4 <= opsin.ysize() + 2 ? (by1 - by0) * N + 4 + : opsin.ysize() + 2 - by0 * N; + size_t x0 = bx0 == 0 ? 2 : 0; + size_t x1 = bx1 * N + 4 <= opsin.xsize() + 2 ? (bx1 - bx0) * N + 4 + : opsin.xsize() + 2 - bx0 * N; + HWY_FULL(float) df; + for (size_t y = y0; y < y1; y++) { + float* JXL_RESTRICT laplacian_sqrsum_row = laplacian_sqrsum.Row(y); + size_t cy = y + by0 * N - 2; + const float* JXL_RESTRICT in_row_t[3]; + const float* JXL_RESTRICT in_row[3]; + const float* JXL_RESTRICT in_row_b[3]; + for (size_t c = 0; c < 3; c++) { + in_row_t[c] = opsin.PlaneRow(c, cy > 0 ? cy - 1 : cy); + in_row[c] = opsin.PlaneRow(c, cy); + in_row_b[c] = opsin.PlaneRow(c, cy + 1 < opsin.ysize() ? cy + 1 : cy); + } + auto compute_laplacian_scalar = [&](size_t x) { + size_t cx = x + bx0 * N - 2; + const size_t prevX = cx >= 1 ? cx - 1 : cx; + const size_t nextX = cx + 1 < opsin.xsize() ? cx + 1 : cx; + float sumsqr = 0; + for (size_t c = 0; c < 3; c++) { + float laplacian = + kChannelWeights[c] * in_row[c][cx] + + kChannelWeightsLapNeg[c] * + (in_row[c][prevX] + in_row[c][nextX] + in_row_b[c][prevX] + + in_row_b[c][cx] + in_row_b[c][nextX] + in_row_t[c][prevX] + + in_row_t[c][cx] + in_row_t[c][nextX]); + sumsqr += laplacian * laplacian; + } + laplacian_sqrsum_row[x] = sumsqr; + }; + size_t x = x0; + for (; x + bx0 * N < 3; x++) { + compute_laplacian_scalar(x); + } + // Interior. One extra pixel of border as the last pixel is special. + for (; x + Lanes(df) <= x1 && x + Lanes(df) + bx0 * N - 1 <= opsin.xsize(); + x += Lanes(df)) { + size_t cx = x + bx0 * N - 2; + auto sumsqr = Zero(df); + for (size_t c = 0; c < 3; c++) { + auto laplacian = + LoadU(df, in_row[c] + cx) * Set(df, kChannelWeights[c]); + auto sum_oth0 = LoadU(df, in_row[c] + cx - 1); + auto sum_oth1 = LoadU(df, in_row[c] + cx + 1); + auto sum_oth2 = LoadU(df, in_row_t[c] + cx - 1); + auto sum_oth3 = LoadU(df, in_row_t[c] + cx); + sum_oth0 += LoadU(df, in_row_t[c] + cx + 1); + sum_oth1 += LoadU(df, in_row_b[c] + cx - 1); + sum_oth2 += LoadU(df, in_row_b[c] + cx); + sum_oth3 += LoadU(df, in_row_b[c] + cx + 1); + sum_oth0 += sum_oth1; + sum_oth2 += sum_oth3; + sum_oth0 += sum_oth2; + laplacian = + MulAdd(Set(df, kChannelWeightsLapNeg[c]), sum_oth0, laplacian); + sumsqr = MulAdd(laplacian, laplacian, sumsqr); + } + StoreU(sumsqr, df, laplacian_sqrsum_row + x); + } + for (; x < x1; x++) { + compute_laplacian_scalar(x); + } + } + HWY_CAPPED(float, 4) df4; + // Calculate the L2 of the 3x3 Laplacian in 4x4 blocks within the area + // of the integral transform. Sample them within the integral transform + // with two offsets (0,0) and (-2, -2) pixels (sqrsum_00 and sqrsum_22, + // respectively). + ImageF& sqrsum_00 = temp_image->sqrsum_00; + size_t sqrsum_00_stride = sqrsum_00.PixelsPerRow(); + float* JXL_RESTRICT sqrsum_00_row = sqrsum_00.Row(0); + for (size_t y = 0; y < (by1 - by0) * 2; y++) { + const float* JXL_RESTRICT rows_in[4]; + for (size_t iy = 0; iy < 4; iy++) { + rows_in[iy] = laplacian_sqrsum.ConstRow(y * 4 + iy + 2); + } + float* JXL_RESTRICT row_out = sqrsum_00_row + y * sqrsum_00_stride; + for (size_t x = 0; x < (bx1 - bx0) * 2; x++) { + auto sum = Zero(df4); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { + sum += LoadU(df4, rows_in[iy] + x * 4 + ix + 2); + } + } + row_out[x] = GetLane(Sqrt(SumOfLanes(sum))) * (1.0f / 4.0f); + } + } + // Indexing iy and ix is a bit tricky as we include a 2 pixel border + // around the block for evenness calculations. This is similar to what + // we did in guetzli for the observability of artefacts, except there + // the element is a sliding 5x5, not sparsely sampled 4x4 box like here. + ImageF& sqrsum_22 = temp_image->sqrsum_22; + size_t sqrsum_22_stride = sqrsum_22.PixelsPerRow(); + float* JXL_RESTRICT sqrsum_22_row = sqrsum_22.Row(0); + for (size_t y = 0; y < (by1 - by0) * 2 + 1; y++) { + const float* JXL_RESTRICT rows_in[4]; + for (size_t iy = 0; iy < 4; iy++) { + rows_in[iy] = laplacian_sqrsum.ConstRow(y * 4 + iy); + } + float* JXL_RESTRICT row_out = sqrsum_22_row + y * sqrsum_22_stride; + // ignore pixels outside the image. + // Y coordinates are relative to by0*8+y*4. + size_t sy = y * 4 + by0 * 8 > 0 ? 0 : 2; + size_t ey = y * 4 + by0 * 8 + 4 <= opsin.ysize() + 2 + ? 4 + : opsin.ysize() - y * 4 - by0 * 8 + 2; + for (size_t x = 0; x < (bx1 - bx0) * 2 + 1; x++) { + // ignore pixels outside the image. + // X coordinates are relative to bx0*8. + size_t sx = x * 4 + bx0 * 8 > 0 ? x * 4 : x * 4 + 2; + size_t ex = x * 4 + bx0 * 8 + 4 <= opsin.xsize() + 2 + ? x * 4 + 4 + : opsin.xsize() - bx0 * 8 + 2; + if (ex - sx == 4 && ey - sy == 4) { + auto sum = Zero(df4); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { + sum += Load(df4, rows_in[iy] + sx + ix); + } + } + row_out[x] = GetLane(Sqrt(SumOfLanes(sum))) * (1.0f / 4.0f); + } else { + float sum = 0; + for (size_t iy = sy; iy < ey; iy++) { + for (size_t ix = sx; ix < ex; ix++) { + sum += rows_in[iy][ix]; + } + } + row_out[x] = std::sqrt(sum / ((ex - sx) * (ey - sy))); + } + } + } + for (size_t by = by0; by < by1; by++) { + AcStrategyRow acs_row = enc_state->shared.ac_strategy.ConstRow(by); + uint8_t* JXL_RESTRICT out_row = epf_sharpness->Row(by); + float* JXL_RESTRICT quant_row = quant->Row(by); + for (size_t bx = bx0; bx < bx1; bx++) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + // The errors are going to be linear to the quantization value in this + // locality. We only have access to the initial quant field here. + float quant_val = 1.0f / quant_row[bx]; + + const auto sq00 = [&](size_t y, size_t x) { + return sqrsum_00_row[((by - by0) * 2 + y) * sqrsum_00_stride + + (bx - bx0) * 2 + x]; + }; + const auto sq22 = [&](size_t y, size_t x) { + return sqrsum_22_row[((by - by0) * 2 + y) * sqrsum_22_stride + + (bx - bx0) * 2 + x]; + }; + float sqrsum_integral_transform = 0; + for (size_t iy = 0; iy < acs.covered_blocks_y() * 2; iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x() * 2; ix++) { + sqrsum_integral_transform += sq00(iy, ix) * sq00(iy, ix); + } + } + sqrsum_integral_transform /= + 4 * acs.covered_blocks_x() * acs.covered_blocks_y(); + sqrsum_integral_transform = std::sqrt(sqrsum_integral_transform); + // If masking is high or amplitude of the artefacts is low, then no + // smoothing is needed. + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + // Five 4x4 blocks for masking estimation, all within the + // 8x8 area. + float minval_1 = std::min(sq00(2 * iy + 0, 2 * ix + 0), + sq00(2 * iy + 0, 2 * ix + 1)); + float minval_2 = std::min(sq00(2 * iy + 1, 2 * ix + 0), + sq00(2 * iy + 1, 2 * ix + 1)); + float minval = std::min(minval_1, minval_2); + minval = std::min(minval, sq22(2 * iy + 1, 2 * ix + 1)); + // Nine more 4x4 blocks for masking estimation, includes + // the 2 pixel area around the 8x8 block being controlled. + float minval2_1 = std::min(sq22(2 * iy + 0, 2 * ix + 0), + sq22(2 * iy + 0, 2 * ix + 1)); + float minval2_2 = std::min(sq22(2 * iy + 0, 2 * ix + 2), + sq22(2 * iy + 1, 2 * ix + 0)); + float minval2_3 = std::min(sq22(2 * iy + 1, 2 * ix + 1), + sq22(2 * iy + 1, 2 * ix + 2)); + float minval2_4 = std::min(sq22(2 * iy + 2, 2 * ix + 0), + sq22(2 * iy + 2, 2 * ix + 1)); + float minval2_5 = std::min(minval2_1, minval2_2); + float minval2_6 = std::min(minval2_3, minval2_4); + float minval2 = std::min(minval2_5, minval2_6); + minval2 = std::min(minval2, sq22(2 * iy + 2, 2 * ix + 2)); + float minval3 = std::min(minval, minval2); + minval *= 0.125f; + minval += 0.625f * minval3; + minval += + 0.125f * std::min(1.5f * minval3, sq22(2 * iy + 1, 2 * ix + 1)); + minval += 0.125f * minval2; + // Larger kBias, less smoothing for low intensity changes. + float kDeltaLimit = 3.2; + float bias = 0.0625f * quant_val; + float delta = + (sqrsum_integral_transform + (kDeltaLimit + 0.05) * bias) / + (minval + bias); + int out = 4; + if (delta > kDeltaLimit) { + out = 4; // smooth + } else { + out = 0; + } + // 'threshold' is separate from 'bias' for easier tuning of these + // heuristics. + float threshold = 0.0625f * quant_val; + const float kSmoothLimit = 0.085f; + float smooth = 0.20f * (sq00(2 * iy + 0, 2 * ix + 0) + + sq00(2 * iy + 0, 2 * ix + 1) + + sq00(2 * iy + 1, 2 * ix + 0) + + sq00(2 * iy + 1, 2 * ix + 1) + minval); + if (smooth < kSmoothLimit * threshold) { + out = 4; + } + out_row[bx + sharpness_stride * iy + ix] = out; + } + } + } + } +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ProcessTile); + +void ArControlFieldHeuristics::RunRect(const Rect& block_rect, + const Image3F& opsin, + PassesEncoderState* enc_state, + size_t thread) { + HWY_DYNAMIC_DISPATCH(ProcessTile) + (opsin, enc_state, block_rect, &temp_images[thread]); +} + +} // namespace jxl + +#endif diff --git a/lib/jxl/enc_ar_control_field.h b/lib/jxl/enc_ar_control_field.h new file mode 100644 index 0000000..ae9d399 --- /dev/null +++ b/lib/jxl/enc_ar_control_field.h @@ -0,0 +1,49 @@ +// 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_ENC_AR_CONTROL_FIELD_H_ +#define LIB_JXL_ENC_AR_CONTROL_FIELD_H_ + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/quant_weights.h" + +namespace jxl { + +struct ArControlFieldHeuristics { + struct TempImages { + void InitOnce() { + if (laplacian_sqrsum.xsize() != 0) return; + laplacian_sqrsum = ImageF(kEncTileDim + 4, kEncTileDim + 4); + sqrsum_00 = ImageF(kEncTileDim / 4, kEncTileDim / 4); + sqrsum_22 = ImageF(kEncTileDim / 4 + 1, kEncTileDim / 4 + 1); + } + + ImageF laplacian_sqrsum; + ImageF sqrsum_00; + ImageF sqrsum_22; + }; + + void PrepareForThreads(size_t num_threads) { + temp_images.resize(num_threads); + } + + void RunRect(const Rect& block_rect, const Image3F& opsin, + PassesEncoderState* enc_state, size_t thread); + + std::vector temp_images; + ImageB* epf_sharpness; + ImageF* quant; + bool all_default; +}; + +} // namespace jxl + +#endif // LIB_JXL_AR_ENC_CONTROL_FIELD_H_ diff --git a/lib/jxl/enc_bit_writer.cc b/lib/jxl/enc_bit_writer.cc new file mode 100644 index 0000000..bc686f8 --- /dev/null +++ b/lib/jxl/enc_bit_writer.cc @@ -0,0 +1,250 @@ +// 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/enc_bit_writer.h" + +#include // memcpy + +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/dec_bit_reader.h" + +namespace jxl { + +BitWriter::Allotment::Allotment(BitWriter* JXL_RESTRICT writer, size_t max_bits) + : max_bits_(max_bits) { + if (writer == nullptr) return; + prev_bits_written_ = writer->BitsWritten(); + const size_t prev_bytes = writer->storage_.size(); + const size_t next_bytes = DivCeil(max_bits, kBitsPerByte); + writer->storage_.resize(prev_bytes + next_bytes); + parent_ = writer->current_allotment_; + writer->current_allotment_ = this; +} + +BitWriter::Allotment::~Allotment() { + if (!called_) { + // Not calling is a bug - unused storage will not be reclaimed. + JXL_ABORT("Did not call Allotment::ReclaimUnused"); + } +} + +void BitWriter::Allotment::FinishedHistogram(BitWriter* JXL_RESTRICT writer) { + if (writer == nullptr) return; + JXL_ASSERT(!called_); // Call before ReclaimUnused + JXL_ASSERT(histogram_bits_ == 0); // Do not call twice + JXL_ASSERT(writer->BitsWritten() >= prev_bits_written_); + histogram_bits_ = writer->BitsWritten() - prev_bits_written_; +} + +void BitWriter::Allotment::PrivateReclaim(BitWriter* JXL_RESTRICT writer, + size_t* JXL_RESTRICT used_bits, + size_t* JXL_RESTRICT unused_bits) { + JXL_ASSERT(!called_); // Do not call twice + called_ = true; + if (writer == nullptr) return; + + JXL_ASSERT(writer->BitsWritten() >= prev_bits_written_); + *used_bits = writer->BitsWritten() - prev_bits_written_; + JXL_ASSERT(*used_bits <= max_bits_); + *unused_bits = max_bits_ - *used_bits; + + // Reclaim unused bytes whole bytes from writer's allotment. + const size_t unused_bytes = *unused_bits / kBitsPerByte; // truncate + JXL_ASSERT(writer->storage_.size() >= unused_bytes); + writer->storage_.resize(writer->storage_.size() - unused_bytes); + writer->current_allotment_ = parent_; + // Ensure we don't also charge the parent for these bits. + auto parent = parent_; + while (parent != nullptr) { + parent->prev_bits_written_ += *used_bits; + parent = parent->parent_; + } +} + +void BitWriter::AppendByteAligned(const Span& span) { + if (!span.size()) return; + storage_.resize(storage_.size() + span.size() + 1); // extra zero padding + + // Concatenate by copying bytes because both source and destination are bytes. + JXL_ASSERT(BitsWritten() % kBitsPerByte == 0); + size_t pos = BitsWritten() / kBitsPerByte; + memcpy(storage_.data() + pos, span.data(), span.size()); + pos += span.size(); + storage_[pos++] = 0; // for next Write + JXL_ASSERT(pos <= storage_.size()); + bits_written_ += span.size() * kBitsPerByte; +} + +void BitWriter::AppendByteAligned(const BitWriter& other) { + JXL_ASSERT(other.BitsWritten() % kBitsPerByte == 0); + JXL_ASSERT(other.BitsWritten() / kBitsPerByte != 0); + + AppendByteAligned(other.GetSpan()); +} + +void BitWriter::AppendByteAligned(const std::vector& others) { + // Total size to add so we can preallocate + size_t other_bytes = 0; + for (const BitWriter& writer : others) { + JXL_ASSERT(writer.BitsWritten() % kBitsPerByte == 0); + other_bytes += writer.BitsWritten() / kBitsPerByte; + } + if (other_bytes == 0) { + // No bytes to append: this happens for example when creating per-group + // storage for groups, but not writing anything in them for e.g. lossless + // images with no alpha. Do nothing. + return; + } + storage_.resize(storage_.size() + other_bytes + 1); // extra zero padding + + // Concatenate by copying bytes because both source and destination are bytes. + JXL_ASSERT(BitsWritten() % kBitsPerByte == 0); + size_t pos = BitsWritten() / kBitsPerByte; + for (const BitWriter& writer : others) { + const Span span = writer.GetSpan(); + memcpy(storage_.data() + pos, span.data(), span.size()); + pos += span.size(); + } + storage_[pos++] = 0; // for next Write + JXL_ASSERT(pos <= storage_.size()); + bits_written_ += other_bytes * kBitsPerByte; +} + +// TODO(lode): avoid code duplication +void BitWriter::AppendByteAligned( + const std::vector>& others) { + // Total size to add so we can preallocate + size_t other_bytes = 0; + for (const auto& writer : others) { + JXL_ASSERT(writer->BitsWritten() % kBitsPerByte == 0); + other_bytes += writer->BitsWritten() / kBitsPerByte; + } + if (other_bytes == 0) { + // No bytes to append: this happens for example when creating per-group + // storage for groups, but not writing anything in them for e.g. lossless + // images with no alpha. Do nothing. + return; + } + storage_.resize(storage_.size() + other_bytes + 1); // extra zero padding + + // Concatenate by copying bytes because both source and destination are bytes. + JXL_ASSERT(BitsWritten() % kBitsPerByte == 0); + size_t pos = BitsWritten() / kBitsPerByte; + for (const auto& writer : others) { + const Span span = writer->GetSpan(); + memcpy(storage_.data() + pos, span.data(), span.size()); + pos += span.size(); + } + storage_[pos++] = 0; // for next Write + JXL_ASSERT(pos <= storage_.size()); + bits_written_ += other_bytes * kBitsPerByte; +} + +BitWriter& BitWriter::operator+=(const BitWriter& other) { + // Required for correctness, otherwise owned[bits_written_] is out of bounds. + if (other.bits_written_ == 0) return *this; + const size_t other_bytes = DivCeil(other.bits_written_, kBitsPerByte); + const size_t prev_bytes = storage_.size(); + storage_.resize(prev_bytes + other_bytes + 1); // extra zero padding + + if (bits_written_ % kBitsPerByte == 0) { + // Only copy fully-initialized bytes. + const size_t full_bytes = other.bits_written_ / kBitsPerByte; // truncated + memcpy(&storage_[bits_written_ / kBitsPerByte], other.storage_.data(), + full_bytes); + storage_[bits_written_ / kBitsPerByte + full_bytes] = 0; // for next Write + bits_written_ += full_bytes * kBitsPerByte; + + const size_t leftovers = other.bits_written_ % kBitsPerByte; + if (leftovers != 0) { + BitReader reader(Span(other.storage_.data() + full_bytes, + other_bytes - full_bytes)); + Write(leftovers, reader.ReadBits(leftovers)); + JXL_CHECK(reader.Close()); + } + return *this; + } + + constexpr size_t N = kMaxBitsPerCall < BitReader::kMaxBitsPerCall + ? kMaxBitsPerCall + : BitReader::kMaxBitsPerCall; + + // Do not use GetSpan because other may not be byte-aligned. + BitReader reader(other.storage_); + size_t i = 0; + for (; i + N <= other.bits_written_; i += N) { + Write(N, reader.ReadFixedBits()); + } + const size_t leftovers = other.bits_written_ - i; + if (leftovers != 0) { + Write(leftovers, reader.ReadBits(leftovers)); + } + JXL_CHECK(reader.Close()); + return *this; +} + +// Example: let's assume that 3 bits (Rs below) have been written already: +// BYTE+0 BYTE+1 BYTE+2 +// 0000 0RRR ???? ???? ???? ???? +// +// Now, we could write up to 5 bits by just shifting them left by 3 bits and +// OR'ing to BYTE-0. +// +// For n > 5 bits, we write the lowest 5 bits as above, then write the next +// lowest bits into BYTE+1 starting from its lower bits and so on. +void BitWriter::Write(size_t n_bits, uint64_t bits) { + JXL_DASSERT((bits >> n_bits) == 0); + JXL_DASSERT(n_bits <= kMaxBitsPerCall); + uint8_t* p = &storage_[bits_written_ / kBitsPerByte]; + const size_t bits_in_first_byte = bits_written_ % kBitsPerByte; + bits <<= bits_in_first_byte; +#if JXL_BYTE_ORDER_LITTLE + uint64_t v = *p; + // Last (partial) or next byte to write must be zero-initialized! + // PaddedBytes initializes the first, and Write/Append maintain this. + JXL_DASSERT(v >> bits_in_first_byte == 0); + v |= bits; + memcpy(p, &v, sizeof(v)); // Write bytes: possibly more than n_bits/8 +#else + *p++ |= static_cast(bits & 0xFF); + for (size_t bits_left_to_write = n_bits + bits_in_first_byte; + bits_left_to_write >= 9; bits_left_to_write -= 8) { + bits >>= 8; + *p++ = static_cast(bits & 0xFF); + } + *p = 0; +#endif + bits_written_ += n_bits; +} + +BitWriter& BitWriter::operator+=(const PaddedBytes& other) { + const size_t other_bytes = other.size(); + // Required for correctness, otherwise owned[bits_written_] is out of bounds. + if (other_bytes == 0) return *this; + const size_t other_bits = other_bytes * kBitsPerByte; + + storage_.resize(storage_.size() + other_bytes + 1); + if (bits_written_ % kBitsPerByte == 0) { + memcpy(&storage_[bits_written_ / kBitsPerByte], other.data(), other_bytes); + storage_[bits_written_ / kBitsPerByte + other_bytes] = 0; // for next Write + bits_written_ += other_bits; + return *this; + } + constexpr size_t N = kMaxBitsPerCall < BitReader::kMaxBitsPerCall + ? kMaxBitsPerCall + : BitReader::kMaxBitsPerCall; + + BitReader reader(other); + size_t i = 0; + for (; i + N <= other_bits; i += N) { + Write(N, reader.ReadFixedBits()); + } + const size_t leftovers = other_bits - i; + Write(leftovers, reader.ReadBits(leftovers)); + JXL_CHECK(reader.Close()); + return *this; +} + +} // namespace jxl diff --git a/lib/jxl/enc_bit_writer.h b/lib/jxl/enc_bit_writer.h new file mode 100644 index 0000000..bae51fd --- /dev/null +++ b/lib/jxl/enc_bit_writer.h @@ -0,0 +1,144 @@ +// 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_ENC_BIT_WRITER_H_ +#define LIB_JXL_ENC_BIT_WRITER_H_ + +// BitWriter class: unbuffered writes using unaligned 64-bit stores. + +#include +#include + +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" + +namespace jxl { + +struct BitWriter { + // Upper bound on `n_bits` in each call to Write. We shift a 64-bit word by + // 7 bits (max already valid bits in the last byte) and at least 1 bit is + // needed to zero-initialize the bit-stream ahead (i.e. if 7 bits are valid + // and we write 57 bits, then the next write will access a byte that was not + // yet zero-initialized). + static constexpr size_t kMaxBitsPerCall = 56; + + BitWriter() : bits_written_(0) {} + + // Disallow copying - may lead to bugs. + BitWriter(const BitWriter&) = delete; + BitWriter& operator=(const BitWriter&) = delete; + BitWriter(BitWriter&&) = default; + BitWriter& operator=(BitWriter&&) = default; + + explicit BitWriter(PaddedBytes&& donor) + : bits_written_(donor.size() * kBitsPerByte), + storage_(std::move(donor)) {} + + size_t BitsWritten() const { return bits_written_; } + + Span GetSpan() const { + // Callers must ensure byte alignment to avoid uninitialized bits. + JXL_ASSERT(bits_written_ % kBitsPerByte == 0); + return Span(storage_.data(), bits_written_ / kBitsPerByte); + } + + // Example usage: bytes = std::move(writer).TakeBytes(); Useful for the + // top-level encoder which returns PaddedBytes, not a BitWriter. + // *this must be an rvalue reference and is invalid afterwards. + PaddedBytes&& TakeBytes() && { + // Callers must ensure byte alignment to avoid uninitialized bits. + JXL_ASSERT(bits_written_ % kBitsPerByte == 0); + storage_.resize(bits_written_ / kBitsPerByte); + return std::move(storage_); + } + + // Must be byte-aligned before calling. + void AppendByteAligned(const Span& span); + // NOTE: no allotment needed, the other BitWriters have already been charged. + void AppendByteAligned(const BitWriter& other); + void AppendByteAligned(const std::vector>& others); + void AppendByteAligned(const std::vector& others); + + class Allotment { + public: + // Expands a BitWriter's storage. Must happen before calling Write or + // ZeroPadToByte. Must call ReclaimUnused after writing to reclaim the + // unused storage so that BitWriter memory use remains tightly bounded. + Allotment(BitWriter* JXL_RESTRICT writer, size_t max_bits); + ~Allotment(); + + size_t MaxBits() const { return max_bits_; } + + // Call after writing a histogram, but before ReclaimUnused. + void FinishedHistogram(BitWriter* JXL_RESTRICT writer); + + size_t HistogramBits() const { + JXL_ASSERT(called_); + return histogram_bits_; + } + + // Do not call directly - use ::ReclaimAndCharge instead, which ensures + // the bits are charged to a layer. + void PrivateReclaim(BitWriter* JXL_RESTRICT writer, + size_t* JXL_RESTRICT used_bits, + size_t* JXL_RESTRICT unused_bits); + + private: + size_t prev_bits_written_; + const size_t max_bits_; + size_t histogram_bits_ = 0; + bool called_ = false; + Allotment* parent_; + }; + + // WARNING: think twice before using this. Concatenating two BitWriters that + // pad to bytes is NOT the same as one contiguous BitWriter. + BitWriter& operator+=(const BitWriter& other); + + // TODO(janwas): remove once all callers use BitWriter + BitWriter& operator+=(const PaddedBytes& other); + + // Writes bits into bytes in increasing addresses, and within a byte + // least-significant-bit first. + // + // The function can write up to 56 bits in one go. + void Write(size_t n_bits, uint64_t bits); + + // This should only rarely be used - e.g. when the current location will be + // referenced via byte offset (TOCs point to groups), or byte-aligned reading + // is required for speed. WARNING: this interacts badly with operator+=, + // see above. + void ZeroPadToByte() { + const size_t remainder_bits = + RoundUpBitsToByteMultiple(bits_written_) - bits_written_; + if (remainder_bits == 0) return; + Write(remainder_bits, 0); + JXL_ASSERT(bits_written_ % kBitsPerByte == 0); + } + + // TODO(janwas): remove? only called from ANS + void RewindStorage(const size_t pos0) { + JXL_ASSERT(pos0 <= bits_written_); + bits_written_ = pos0; + static const uint8_t kRewindMasks[8] = {0x0, 0x1, 0x3, 0x7, + 0xf, 0x1f, 0x3f, 0x7f}; + storage_[pos0 >> 3] &= kRewindMasks[pos0 & 7]; + } + + private: + size_t bits_written_; + PaddedBytes storage_; + Allotment* current_allotment_ = nullptr; +}; + +} // namespace jxl + +#endif // LIB_JXL_ENC_BIT_WRITER_H_ diff --git a/lib/jxl/enc_butteraugli_comparator.cc b/lib/jxl/enc_butteraugli_comparator.cc new file mode 100644 index 0000000..e253509 --- /dev/null +++ b/lib/jxl/enc_butteraugli_comparator.cc @@ -0,0 +1,93 @@ +// 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/enc_butteraugli_comparator.h" + +#include +#include + +#include "lib/jxl/color_management.h" + +namespace jxl { + +JxlButteraugliComparator::JxlButteraugliComparator( + const ButteraugliParams& params) + : params_(params) {} + +Status JxlButteraugliComparator::SetReferenceImage(const ImageBundle& ref) { + const ImageBundle* ref_linear_srgb; + ImageMetadata metadata = *ref.metadata(); + ImageBundle store(&metadata); + if (!TransformIfNeeded(ref, ColorEncoding::LinearSRGB(ref.IsGray()), + /*pool=*/nullptr, &store, &ref_linear_srgb)) { + return false; + } + + comparator_.reset( + new ButteraugliComparator(ref_linear_srgb->color(), params_)); + xsize_ = ref.xsize(); + ysize_ = ref.ysize(); + return true; +} + +Status JxlButteraugliComparator::CompareWith(const ImageBundle& actual, + ImageF* diffmap, float* score) { + if (!comparator_) { + return JXL_FAILURE("Must set reference image first"); + } + if (xsize_ != actual.xsize() || ysize_ != actual.ysize()) { + return JXL_FAILURE("Images must have same size"); + } + + const ImageBundle* actual_linear_srgb; + ImageMetadata metadata = *actual.metadata(); + ImageBundle store(&metadata); + if (!TransformIfNeeded(actual, ColorEncoding::LinearSRGB(actual.IsGray()), + /*pool=*/nullptr, &store, &actual_linear_srgb)) { + return false; + } + + ImageF temp_diffmap(xsize_, ysize_); + comparator_->Diffmap(actual_linear_srgb->color(), temp_diffmap); + + if (score != nullptr) { + *score = ButteraugliScoreFromDiffmap(temp_diffmap, ¶ms_); + } + if (diffmap != nullptr) { + diffmap->Swap(temp_diffmap); + } + + return true; +} + +float JxlButteraugliComparator::GoodQualityScore() const { + return ButteraugliFuzzyInverse(1.5); +} + +float JxlButteraugliComparator::BadQualityScore() const { + return ButteraugliFuzzyInverse(0.5); +} + +float ButteraugliDistance(const ImageBundle& rgb0, const ImageBundle& rgb1, + const ButteraugliParams& params, ImageF* distmap, + ThreadPool* pool) { + JxlButteraugliComparator comparator(params); + return ComputeScore(rgb0, rgb1, &comparator, distmap, pool); +} + +float ButteraugliDistance(const CodecInOut& rgb0, const CodecInOut& rgb1, + const ButteraugliParams& params, ImageF* distmap, + ThreadPool* pool) { + JxlButteraugliComparator comparator(params); + JXL_ASSERT(rgb0.frames.size() == rgb1.frames.size()); + float max_dist = 0.0f; + for (size_t i = 0; i < rgb0.frames.size(); ++i) { + max_dist = std::max(max_dist, ComputeScore(rgb0.frames[i], rgb1.frames[i], + &comparator, distmap, pool)); + } + return max_dist; +} + +} // namespace jxl diff --git a/lib/jxl/enc_butteraugli_comparator.h b/lib/jxl/enc_butteraugli_comparator.h new file mode 100644 index 0000000..48a1d89 --- /dev/null +++ b/lib/jxl/enc_butteraugli_comparator.h @@ -0,0 +1,56 @@ +// 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_ENC_BUTTERAUGLI_COMPARATOR_H_ +#define LIB_JXL_ENC_BUTTERAUGLI_COMPARATOR_H_ + +#include + +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_comparator.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +class JxlButteraugliComparator : public Comparator { + public: + explicit JxlButteraugliComparator(const ButteraugliParams& params); + + Status SetReferenceImage(const ImageBundle& ref) override; + + Status CompareWith(const ImageBundle& actual, ImageF* diffmap, + float* score) override; + + float GoodQualityScore() const override; + float BadQualityScore() const override; + + private: + ButteraugliParams params_; + std::unique_ptr comparator_; + size_t xsize_ = 0; + size_t ysize_ = 0; +}; + +// Returns the butteraugli distance between rgb0 and rgb1. +// If distmap is not null, it must be the same size as rgb0 and rgb1. +float ButteraugliDistance(const ImageBundle& rgb0, const ImageBundle& rgb1, + const ButteraugliParams& params, + ImageF* distmap = nullptr, + ThreadPool* pool = nullptr); + +float ButteraugliDistance(const CodecInOut& rgb0, const CodecInOut& rgb1, + const ButteraugliParams& params, + ImageF* distmap = nullptr, + ThreadPool* pool = nullptr); + +} // namespace jxl + +#endif // LIB_JXL_ENC_BUTTERAUGLI_COMPARATOR_H_ diff --git a/lib/jxl/enc_butteraugli_pnorm.cc b/lib/jxl/enc_butteraugli_pnorm.cc new file mode 100644 index 0000000..7c3fb9c --- /dev/null +++ b/lib/jxl/enc_butteraugli_pnorm.cc @@ -0,0 +1,212 @@ +// 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/enc_butteraugli_pnorm.h" + +#include +#include + +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_butteraugli_pnorm.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; + +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p) { + PROFILER_FUNC; + // In approximate-border mode, skip pixels on the border likely to be affected + // by FastGauss' zero-valued-boundary behavior. The border is less than half + // the largest-diameter kernel (37x37 pixels), and 0 if the image is tiny. + // NOTE: chosen such that it is vector-aligned. + size_t border = (params.approximate_border) ? 8 : 0; + if (distmap.xsize() <= 2 * border || distmap.ysize() <= 2 * border) { + border = 0; + } + + const double onePerPixels = 1.0 / (distmap.ysize() * distmap.xsize()); + if (std::abs(p - 3.0) < 1E-6) { + double sum1[3] = {0.0}; + +// Prefer double if possible, but otherwise use float rather than scalar. +#if HWY_CAP_FLOAT64 + using T = double; + const Rebind df; +#else + using T = float; +#endif + const HWY_FULL(T) d; + constexpr size_t N = MaxLanes(HWY_FULL(T)()); + // Manually aligned storage to avoid asan crash on clang-7 due to + // unaligned spill. + HWY_ALIGN T sum_totals0[N] = {0}; + HWY_ALIGN T sum_totals1[N] = {0}; + HWY_ALIGN T sum_totals2[N] = {0}; + + for (size_t y = border; y < distmap.ysize() - border; ++y) { + const float* JXL_RESTRICT row = distmap.ConstRow(y); + + auto sums0 = Zero(d); + auto sums1 = Zero(d); + auto sums2 = Zero(d); + + size_t x = border; + for (; x + Lanes(d) <= distmap.xsize() - border; x += Lanes(d)) { +#if HWY_CAP_FLOAT64 + const auto d1 = PromoteTo(d, Load(df, row + x)); +#else + const auto d1 = Load(d, row + x); +#endif + const auto d2 = d1 * d1 * d1; + sums0 += d2; + const auto d3 = d2 * d2; + sums1 += d3; + const auto d4 = d3 * d3; + sums2 += d4; + } + + Store(sums0 + Load(d, sum_totals0), d, sum_totals0); + Store(sums1 + Load(d, sum_totals1), d, sum_totals1); + Store(sums2 + Load(d, sum_totals2), d, sum_totals2); + + for (; x < distmap.xsize() - border; ++x) { + const double d1 = row[x]; + double d2 = d1 * d1 * d1; + sum1[0] += d2; + d2 *= d2; + sum1[1] += d2; + d2 *= d2; + sum1[2] += d2; + } + } + double v = 0; + v += pow( + onePerPixels * (sum1[0] + GetLane(SumOfLanes(Load(d, sum_totals0)))), + 1.0 / (p * 1.0)); + v += pow( + onePerPixels * (sum1[1] + GetLane(SumOfLanes(Load(d, sum_totals1)))), + 1.0 / (p * 2.0)); + v += pow( + onePerPixels * (sum1[2] + GetLane(SumOfLanes(Load(d, sum_totals2)))), + 1.0 / (p * 4.0)); + v /= 3.0; + return v; + } else { + static std::atomic once{0}; + if (once.fetch_add(1, std::memory_order_relaxed) == 0) { + JXL_WARNING("WARNING: using slow ComputeDistanceP"); + } + double sum1[3] = {0.0}; + for (size_t y = border; y < distmap.ysize() - border; ++y) { + const float* JXL_RESTRICT row = distmap.ConstRow(y); + for (size_t x = border; x < distmap.xsize() - border; ++x) { + double d2 = std::pow(row[x], p); + sum1[0] += d2; + d2 *= d2; + sum1[1] += d2; + d2 *= d2; + sum1[2] += d2; + } + } + double v = 0; + for (int i = 0; i < 3; ++i) { + v += pow(onePerPixels * (sum1[i]), 1.0 / (p * (1 << i))); + } + v /= 3.0; + return v; + } +} + +// TODO(lode): take alpha into account when needed +double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2) { + PROFILER_FUNC; + // Convert to sRGB - closer to perception than linear. + const Image3F* srgb1 = &ib1.color(); + Image3F copy1; + if (!ib1.IsSRGB()) { + JXL_CHECK(ib1.CopyTo(Rect(ib1), ColorEncoding::SRGB(ib1.IsGray()), ©1)); + srgb1 = ©1; + } + const Image3F* srgb2 = &ib2.color(); + Image3F copy2; + if (!ib2.IsSRGB()) { + JXL_CHECK(ib2.CopyTo(Rect(ib2), ColorEncoding::SRGB(ib2.IsGray()), ©2)); + srgb2 = ©2; + } + + JXL_CHECK(SameSize(*srgb1, *srgb2)); + + // TODO(veluca): SIMD. + float yuvmatrix[3][3] = {{0.299, 0.587, 0.114}, + {-0.14713, -0.28886, 0.436}, + {0.615, -0.51499, -0.10001}}; + double sum_of_squares[3] = {}; + for (size_t y = 0; y < srgb1->ysize(); ++y) { + const float* JXL_RESTRICT row1[3]; + const float* JXL_RESTRICT row2[3]; + for (size_t j = 0; j < 3; j++) { + row1[j] = srgb1->ConstPlaneRow(j, y); + row2[j] = srgb2->ConstPlaneRow(j, y); + } + for (size_t x = 0; x < srgb1->xsize(); ++x) { + float cdiff[3] = {}; + // YUV conversion is linear, so we can run it on the difference. + for (size_t j = 0; j < 3; j++) { + cdiff[j] = row1[j][x] - row2[j][x]; + } + float yuvdiff[3] = {}; + for (size_t j = 0; j < 3; j++) { + for (size_t k = 0; k < 3; k++) { + yuvdiff[j] += yuvmatrix[j][k] * cdiff[k]; + } + } + for (size_t j = 0; j < 3; j++) { + sum_of_squares[j] += yuvdiff[j] * yuvdiff[j]; + } + } + } + // Weighted PSNR as in JPEG-XL: chroma counts 1/8. + const float weights[3] = {6.0f / 8, 1.0f / 8, 1.0f / 8}; + // Avoid squaring the weight - 1/64 is too extreme. + double norm = 0; + for (size_t i = 0; i < 3; i++) { + norm += std::sqrt(sum_of_squares[i]) * weights[i]; + } + // This function returns distance *squared*. + return norm * norm; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ComputeDistanceP); +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p) { + return HWY_DYNAMIC_DISPATCH(ComputeDistanceP)(distmap, params, p); +} + +HWY_EXPORT(ComputeDistance2); +double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2) { + return HWY_DYNAMIC_DISPATCH(ComputeDistance2)(ib1, ib2); +} + +} // namespace jxl +#endif diff --git a/lib/jxl/enc_butteraugli_pnorm.h b/lib/jxl/enc_butteraugli_pnorm.h new file mode 100644 index 0000000..5579c0a --- /dev/null +++ b/lib/jxl/enc_butteraugli_pnorm.h @@ -0,0 +1,24 @@ +// 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_ENC_BUTTERAUGLI_PNORM_H_ +#define LIB_JXL_ENC_BUTTERAUGLI_PNORM_H_ + +#include + +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Computes p-norm given the butteraugli distmap. +double ComputeDistanceP(const ImageF& distmap, const ButteraugliParams& params, + double p); + +double ComputeDistance2(const ImageBundle& ib1, const ImageBundle& ib2); + +} // namespace jxl + +#endif // LIB_JXL_ENC_BUTTERAUGLI_PNORM_H_ diff --git a/lib/jxl/enc_cache.cc b/lib/jxl/enc_cache.cc new file mode 100644 index 0000000..038a706 --- /dev/null +++ b/lib/jxl/enc_cache.cc @@ -0,0 +1,198 @@ +// 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/enc_cache.h" + +#include +#include + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/compressed_dc.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_frame.h" +#include "lib/jxl/enc_frame.h" +#include "lib/jxl/enc_group.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/quantizer.h" + +namespace jxl { + +void InitializePassesEncoder(const Image3F& opsin, ThreadPool* pool, + PassesEncoderState* enc_state, + ModularFrameEncoder* modular_frame_encoder, + AuxOut* aux_out) { + PROFILER_FUNC; + + PassesSharedState& JXL_RESTRICT shared = enc_state->shared; + + enc_state->histogram_idx.resize(shared.frame_dim.num_groups); + + enc_state->x_qm_multiplier = + std::pow(1.25f, shared.frame_header.x_qm_scale - 2.0f); + enc_state->b_qm_multiplier = + std::pow(1.25f, shared.frame_header.b_qm_scale - 2.0f); + + if (enc_state->coeffs.size() < shared.frame_header.passes.num_passes) { + enc_state->coeffs.reserve(shared.frame_header.passes.num_passes); + for (size_t i = enc_state->coeffs.size(); + i < shared.frame_header.passes.num_passes; i++) { + // Allocate enough coefficients for each group on every row. + enc_state->coeffs.emplace_back(make_unique>( + kGroupDim * kGroupDim, shared.frame_dim.num_groups)); + } + } + while (enc_state->coeffs.size() > shared.frame_header.passes.num_passes) { + enc_state->coeffs.pop_back(); + } + + Image3F dc(shared.frame_dim.xsize_blocks, shared.frame_dim.ysize_blocks); + RunOnPool( + pool, 0, shared.frame_dim.num_groups, ThreadPool::SkipInit(), + [&](size_t group_idx, size_t _) { + ComputeCoefficients(group_idx, enc_state, opsin, &dc); + }, + "Compute coeffs"); + + if (shared.frame_header.flags & FrameHeader::kUseDcFrame) { + CompressParams cparams = enc_state->cparams; + // Guess a distance that produces good initial results. + cparams.butteraugli_distance = + std::max(kMinButteraugliDistance, + enc_state->cparams.butteraugli_distance * 0.1f); + cparams.dots = Override::kOff; + cparams.noise = Override::kOff; + cparams.patches = Override::kOff; + cparams.gaborish = Override::kOff; + cparams.epf = 0; + cparams.max_error_mode = true; + cparams.resampling = 1; + cparams.ec_resampling = 1; + for (size_t c = 0; c < 3; c++) { + cparams.max_error[c] = shared.quantizer.MulDC()[c]; + } + JXL_ASSERT(cparams.progressive_dc > 0); + cparams.progressive_dc--; + // The DC frame will have alpha=0. Don't erase its contents. + cparams.keep_invisible = Override::kOn; + // No EPF or Gaborish in DC frames. + cparams.epf = 0; + cparams.gaborish = Override::kOff; + // Use kVarDCT in max_error_mode for intermediate progressive DC, + // 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; + } + ImageBundle ib(&shared.metadata->m); + // This is a lie - dc is in XYB + // (but EncodeFrame will skip RGB->XYB conversion anyway) + ib.SetFromImage( + std::move(dc), + ColorEncoding::LinearSRGB(shared.metadata->m.color_encoding.IsGray())); + if (!ib.metadata()->extra_channel_info.empty()) { + // Add dummy extra channels to the patch image: dc_level frames do not yet + // support extra channels, but the codec expects that the amount of extra + // channels in frames matches that in the metadata of the codestream. + std::vector extra_channels; + extra_channels.reserve(ib.metadata()->extra_channel_info.size()); + for (size_t i = 0; i < ib.metadata()->extra_channel_info.size(); i++) { + extra_channels.emplace_back(ib.xsize(), ib.ysize()); + // Must initialize the image with data to not affect blending with + // uninitialized memory. + // TODO(lode): dc_level must copy and use the real extra channels + // instead. + ZeroFillImage(&extra_channels.back()); + } + ib.SetExtraChannels(std::move(extra_channels)); + } + std::unique_ptr state = + jxl::make_unique(); + + auto special_frame = std::unique_ptr(new BitWriter()); + FrameInfo dc_frame_info; + dc_frame_info.frame_type = FrameType::kDCFrame; + dc_frame_info.dc_level = shared.frame_header.dc_level + 1; + dc_frame_info.ib_needs_color_transform = false; + dc_frame_info.save_before_color_transform = true; // Implicitly true + // TODO(lode): the EncodeFrame / DecodeFrame pair here is likely broken in + // case of dc_level >= 3, since EncodeFrame may output multiple frames + // to the bitwriter, while DecodeFrame reads only one. + JXL_CHECK(EncodeFrame(cparams, dc_frame_info, shared.metadata, ib, + state.get(), pool, special_frame.get(), nullptr)); + const Span encoded = special_frame->GetSpan(); + enc_state->special_frames.emplace_back(std::move(special_frame)); + + BitReader br(encoded); + ImageBundle decoded(&shared.metadata->m); + std::unique_ptr dec_state = + jxl::make_unique(); + JXL_CHECK(dec_state->output_encoding_info.Set( + *shared.metadata, + ColorEncoding::LinearSRGB(shared.metadata->m.color_encoding.IsGray()))); + JXL_CHECK(DecodeFrame({}, dec_state.get(), pool, &br, &decoded, + *shared.metadata, /*constraints=*/nullptr)); + // TODO(lode): shared.frame_header.dc_level should be equal to + // dec_state.shared->frame_header.dc_level - 1 here, since above we set + // dc_frame_info.dc_level = shared.frame_header.dc_level + 1, and + // dc_frame_info.dc_level is used by EncodeFrame. However, if EncodeFrame + // outputs multiple frames, this assumption could be wrong. + shared.dc_storage = + CopyImage(dec_state->shared->dc_frames[shared.frame_header.dc_level]); + ZeroFillImage(&shared.quant_dc); + shared.dc = &shared.dc_storage; + JXL_CHECK(br.Close()); + } else { + auto compute_dc_coeffs = [&](int group_index, int /* thread */) { + modular_frame_encoder->AddVarDCTDC( + dc, group_index, + enc_state->cparams.butteraugli_distance >= 2.0f && + enc_state->cparams.speed_tier < SpeedTier::kFalcon, + enc_state); + }; + RunOnPool(pool, 0, shared.frame_dim.num_dc_groups, ThreadPool::SkipInit(), + compute_dc_coeffs, "Compute DC coeffs"); + // TODO(veluca): this is only useful in tests and if inspection is enabled. + if (!(shared.frame_header.flags & FrameHeader::kSkipAdaptiveDCSmoothing)) { + AdaptiveDCSmoothing(shared.quantizer.MulDC(), &shared.dc_storage, pool); + } + } + auto compute_ac_meta = [&](int group_index, int /* thread */) { + modular_frame_encoder->AddACMetadata(group_index, /*jpeg_transcode=*/false, + enc_state); + }; + RunOnPool(pool, 0, shared.frame_dim.num_dc_groups, ThreadPool::SkipInit(), + compute_ac_meta, "Compute AC Metadata"); + + if (aux_out != nullptr) { + aux_out->InspectImage3F("compressed_image:InitializeFrameEncCache:dc_dec", + shared.dc_storage); + } +} + +void EncCache::InitOnce() { + PROFILER_FUNC; + + if (num_nzeroes.xsize() == 0) { + num_nzeroes = Image3I(kGroupDimInBlocks, kGroupDimInBlocks); + } +} + +} // namespace jxl diff --git a/lib/jxl/enc_cache.h b/lib/jxl/enc_cache.h new file mode 100644 index 0000000..b8530f1 --- /dev/null +++ b/lib/jxl/enc_cache.h @@ -0,0 +1,92 @@ +// 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_ENC_CACHE_H_ +#define LIB_JXL_ENC_CACHE_H_ + +#include +#include + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_heuristics.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/passes_state.h" +#include "lib/jxl/progressive_split.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" + +namespace jxl { + +// Contains encoder state. +struct PassesEncoderState { + PassesSharedState shared; + + ImageF initial_quant_field; // Invalid in Falcon mode. + ImageF initial_quant_masking; // Invalid in Falcon mode. + + // Per-pass DCT coefficients for the image. One row per group. + std::vector> coeffs; + + // Raw data for special (reference+DC) frames. + std::vector> special_frames; + + // For splitting into passes. + ProgressiveSplitter progressive_splitter; + + CompressParams cparams; + + struct PassData { + std::vector> ac_tokens; + std::vector context_map; + EntropyEncodingData codes; + }; + + std::vector passes; + std::vector histogram_idx; + + // Coefficient orders that are non-default. + std::vector used_orders; + + // Multiplier to be applied to the quant matrices of the x channel. + float x_qm_multiplier = 1.0f; + float b_qm_multiplier = 1.0f; + + // Heuristics to be used by the encoder. + std::unique_ptr heuristics = + make_unique(); +}; + +// Initialize per-frame information. +class ModularFrameEncoder; +void InitializePassesEncoder(const Image3F& opsin, ThreadPool* pool, + PassesEncoderState* passes_enc_state, + ModularFrameEncoder* modular_frame_encoder, + AuxOut* aux_out); + +// Working area for ComputeCoefficients (per-group!) +struct EncCache { + // Allocates memory when first called, shrinks images to current group size. + void InitOnce(); + + // TokenizeCoefficients + Image3I num_nzeroes; +}; + +} // namespace jxl + +#endif // LIB_JXL_ENC_CACHE_H_ diff --git a/lib/jxl/enc_chroma_from_luma.cc b/lib/jxl/enc_chroma_from_luma.cc new file mode 100644 index 0000000..e5c3f38 --- /dev/null +++ b/lib/jxl/enc_chroma_from_luma.cc @@ -0,0 +1,375 @@ +// 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/enc_chroma_from_luma.h" + +#include +#include + +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_chroma_from_luma.cc" +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_transforms-inl.h" +#include "lib/jxl/enc_transforms-inl.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/quantizer.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +static HWY_FULL(float) df; + +struct CFLFunction { + static constexpr float kCoeff = 1.f / 3; + static constexpr float kThres = 100.0f; + static constexpr float kInvColorFactor = 1.0f / kDefaultColorFactor; + CFLFunction(const float* values_m, const float* values_s, size_t num, + float base, float distance_mul) + : values_m(values_m), + values_s(values_s), + num(num), + base(base), + distance_mul(distance_mul) {} + + // Returns f'(x), where f is 1/3 * sum ((|color residual| + 1)^2-1) + + // distance_mul * x^2 * num. + float Compute(float x, float eps, float* fpeps, float* fmeps) const { + float first_derivative = 2 * distance_mul * num * x; + float first_derivative_peps = 2 * distance_mul * num * (x + eps); + float first_derivative_meps = 2 * distance_mul * num * (x - eps); + + const auto inv_color_factor = Set(df, kInvColorFactor); + const auto thres = Set(df, kThres); + const auto coeffx2 = Set(df, kCoeff * 2.0f); + const auto one = Set(df, 1.0f); + const auto zero = Set(df, 0.0f); + const auto base_v = Set(df, base); + const auto x_v = Set(df, x); + const auto xpe_v = Set(df, x + eps); + const auto xme_v = Set(df, x - eps); + auto fd_v = Zero(df); + auto fdpe_v = Zero(df); + auto fdme_v = Zero(df); + JXL_ASSERT(num % Lanes(df) == 0); + + for (size_t i = 0; i < num; i += Lanes(df)) { + // color residual = ax + b + const auto a = inv_color_factor * Load(df, values_m + i); + const auto b = base_v * Load(df, values_m + i) - Load(df, values_s + i); + const auto v = a * x_v + b; + const auto vpe = a * xpe_v + b; + const auto vme = a * xme_v + b; + const auto av = Abs(v); + const auto avpe = Abs(vpe); + const auto avme = Abs(vme); + auto d = coeffx2 * (av + one) * a; + auto dpe = coeffx2 * (avpe + one) * a; + auto dme = coeffx2 * (avme + one) * a; + d = IfThenElse(v < zero, zero - d, d); + dpe = IfThenElse(vpe < zero, zero - dpe, dpe); + dme = IfThenElse(vme < zero, zero - dme, dme); + fd_v += IfThenElse(av >= thres, zero, d); + fdpe_v += IfThenElse(av >= thres, zero, dpe); + fdme_v += IfThenElse(av >= thres, zero, dme); + } + + *fpeps = first_derivative_peps + GetLane(SumOfLanes(fdpe_v)); + *fmeps = first_derivative_meps + GetLane(SumOfLanes(fdme_v)); + return first_derivative + GetLane(SumOfLanes(fd_v)); + } + + const float* JXL_RESTRICT values_m; + const float* JXL_RESTRICT values_s; + size_t num; + float base; + float distance_mul; +}; + +int32_t FindBestMultiplier(const float* values_m, const float* values_s, + size_t num, float base, float distance_mul, + bool fast) { + if (num == 0) { + return 0; + } + float x; + if (fast) { + static constexpr float kInvColorFactor = 1.0f / kDefaultColorFactor; + auto ca = Zero(df); + auto cb = Zero(df); + const auto inv_color_factor = Set(df, kInvColorFactor); + const auto base_v = Set(df, base); + for (size_t i = 0; i < num; i += Lanes(df)) { + // color residual = ax + b + const auto a = inv_color_factor * Load(df, values_m + i); + const auto b = base_v * Load(df, values_m + i) - Load(df, values_s + i); + ca = MulAdd(a, a, ca); + cb = MulAdd(a, b, cb); + } + // + distance_mul * x^2 * num + x = -GetLane(SumOfLanes(cb)) / + (GetLane(SumOfLanes(ca)) + num * distance_mul * 0.5f); + } else { + constexpr float eps = 1; + constexpr float kClamp = 20.0f; + CFLFunction fn(values_m, values_s, num, base, distance_mul); + x = 0; + // Up to 20 Newton iterations, with approximate derivatives. + // Derivatives are approximate due to the high amount of noise in the exact + // derivatives. + for (size_t i = 0; i < 20; i++) { + float dfpeps, dfmeps; + float df = fn.Compute(x, eps, &dfpeps, &dfmeps); + float ddf = (dfpeps - dfmeps) / (2 * eps); + float step = df / ddf; + x -= std::min(kClamp, std::max(-kClamp, step)); + if (std::abs(step) < 3e-3) break; + } + } + return std::max(-128.0f, std::min(127.0f, roundf(x))); +} + +void InitDCStorage(size_t num_blocks, ImageF* dc_values) { + // First row: Y channel + // Second row: X channel + // Third row: Y channel + // Fourth row: B channel + *dc_values = ImageF(RoundUpTo(num_blocks, Lanes(df)), 4); + + JXL_ASSERT(dc_values->xsize() != 0); + // Zero-fill the last lanes + for (size_t y = 0; y < 4; y++) { + for (size_t x = dc_values->xsize() - Lanes(df); x < dc_values->xsize(); + x++) { + dc_values->Row(y)[x] = 0; + } + } +} + +void ComputeDC(const ImageF& dc_values, bool fast, int* dc_x, int* dc_b) { + constexpr float kDistanceMultiplierDC = 1e-5f; + const float* JXL_RESTRICT dc_values_yx = dc_values.Row(0); + const float* JXL_RESTRICT dc_values_x = dc_values.Row(1); + const float* JXL_RESTRICT dc_values_yb = dc_values.Row(2); + const float* JXL_RESTRICT dc_values_b = dc_values.Row(3); + *dc_x = FindBestMultiplier(dc_values_yx, dc_values_x, dc_values.xsize(), 0.0f, + kDistanceMultiplierDC, fast); + *dc_b = FindBestMultiplier(dc_values_yb, dc_values_b, dc_values.xsize(), + kYToBRatio, kDistanceMultiplierDC, fast); +} + +void ComputeTile(const Image3F& opsin, const DequantMatrices& dequant, + const AcStrategyImage* ac_strategy, const Quantizer* quantizer, + const Rect& r, bool fast, bool use_dct8, ImageSB* map_x, + ImageSB* map_b, ImageF* dc_values, float* mem) { + static_assert(kEncTileDimInBlocks == kColorTileDimInBlocks, + "Invalid color tile dim"); + size_t xsize_blocks = opsin.xsize() / kBlockDim; + constexpr float kDistanceMultiplierAC = 1e-3f; + + const size_t y0 = r.y0(); + const size_t x0 = r.x0(); + const size_t x1 = r.x0() + r.xsize(); + const size_t y1 = r.y0() + r.ysize(); + + int ty = y0 / kColorTileDimInBlocks; + int tx = x0 / kColorTileDimInBlocks; + + int8_t* JXL_RESTRICT row_out_x = map_x->Row(ty); + int8_t* JXL_RESTRICT row_out_b = map_b->Row(ty); + + float* JXL_RESTRICT dc_values_yx = dc_values->Row(0); + float* JXL_RESTRICT dc_values_x = dc_values->Row(1); + float* JXL_RESTRICT dc_values_yb = dc_values->Row(2); + float* JXL_RESTRICT dc_values_b = dc_values->Row(3); + + // All are aligned. + float* HWY_RESTRICT block_y = mem; + float* HWY_RESTRICT block_x = block_y + AcStrategy::kMaxCoeffArea; + float* HWY_RESTRICT block_b = block_x + AcStrategy::kMaxCoeffArea; + float* HWY_RESTRICT coeffs_yx = block_b + AcStrategy::kMaxCoeffArea; + float* HWY_RESTRICT coeffs_x = coeffs_yx + kColorTileDim * kColorTileDim; + float* HWY_RESTRICT coeffs_yb = coeffs_x + kColorTileDim * kColorTileDim; + float* HWY_RESTRICT coeffs_b = coeffs_yb + kColorTileDim * kColorTileDim; + float* HWY_RESTRICT scratch_space = coeffs_b + kColorTileDim * kColorTileDim; + JXL_DASSERT(scratch_space + 2 * AcStrategy::kMaxCoeffArea == + block_y + CfLHeuristics::kItemsPerThread); + + // Small (~256 bytes each) + HWY_ALIGN_MAX float + dc_y[AcStrategy::kMaxCoeffBlocks * AcStrategy::kMaxCoeffBlocks] = {}; + HWY_ALIGN_MAX float + dc_x[AcStrategy::kMaxCoeffBlocks * AcStrategy::kMaxCoeffBlocks] = {}; + HWY_ALIGN_MAX float + dc_b[AcStrategy::kMaxCoeffBlocks * AcStrategy::kMaxCoeffBlocks] = {}; + size_t num_ac = 0; + + for (size_t y = y0; y < y1; ++y) { + const float* JXL_RESTRICT row_y = opsin.ConstPlaneRow(1, y * kBlockDim); + const float* JXL_RESTRICT row_x = opsin.ConstPlaneRow(0, y * kBlockDim); + const float* JXL_RESTRICT row_b = opsin.ConstPlaneRow(2, y * kBlockDim); + size_t stride = opsin.PixelsPerRow(); + + for (size_t x = x0; x < x1; x++) { + AcStrategy acs = use_dct8 + ? AcStrategy::FromRawStrategy(AcStrategy::Type::DCT) + : ac_strategy->ConstRow(y)[x]; + if (!acs.IsFirstBlock()) continue; + size_t xs = acs.covered_blocks_x(); + TransformFromPixels(acs.Strategy(), row_y + x * kBlockDim, stride, + block_y, scratch_space); + DCFromLowestFrequencies(acs.Strategy(), block_y, dc_y, xs); + TransformFromPixels(acs.Strategy(), row_x + x * kBlockDim, stride, + block_x, scratch_space); + DCFromLowestFrequencies(acs.Strategy(), block_x, dc_x, xs); + TransformFromPixels(acs.Strategy(), row_b + x * kBlockDim, stride, + block_b, scratch_space); + DCFromLowestFrequencies(acs.Strategy(), block_b, dc_b, xs); + const float* const JXL_RESTRICT qm_x = + dequant.InvMatrix(acs.Strategy(), 0); + const float* const JXL_RESTRICT qm_b = + dequant.InvMatrix(acs.Strategy(), 2); + // Why does a constant seem to work better than + // raw_quant_field->Row(y)[x] ? + float q = use_dct8 ? 1 : quantizer->Scale() * 400.0f; + float q_dc_x = use_dct8 ? 1 : 1.0f / quantizer->GetInvDcStep(0); + float q_dc_b = use_dct8 ? 1 : 1.0f / quantizer->GetInvDcStep(2); + + // Copy DCs in dc_values. + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < xs; ix++) { + dc_values_yx[(iy + y) * xsize_blocks + ix + x] = + dc_y[iy * xs + ix] * q_dc_x; + dc_values_x[(iy + y) * xsize_blocks + ix + x] = + dc_x[iy * xs + ix] * q_dc_x; + dc_values_yb[(iy + y) * xsize_blocks + ix + x] = + dc_y[iy * xs + ix] * q_dc_b; + dc_values_b[(iy + y) * xsize_blocks + ix + x] = + dc_b[iy * xs + ix] * q_dc_b; + } + } + + // Do not use this block for computing AC CfL. + if (acs.covered_blocks_x() + x0 > x1 || + acs.covered_blocks_y() + y0 > y1) { + continue; + } + + // Copy AC coefficients in the local block. The order in which + // coefficients get stored does not matter. + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + CoefficientLayout(&cy, &cx); + // Zero out LFs. This introduces terms in the optimization loop that + // don't affect the result, as they are all 0, but allow for simpler + // SIMDfication. + for (size_t iy = 0; iy < cy; iy++) { + for (size_t ix = 0; ix < cx; ix++) { + block_y[cx * kBlockDim * iy + ix] = 0; + block_x[cx * kBlockDim * iy + ix] = 0; + block_b[cx * kBlockDim * iy + ix] = 0; + } + } + const auto qv = Set(df, q); + for (size_t i = 0; i < cx * cy * 64; i += Lanes(df)) { + const auto b_y = Load(df, block_y + i); + const auto b_x = Load(df, block_x + i); + const auto b_b = Load(df, block_b + i); + const auto qqm_x = qv * Load(df, qm_x + i); + const auto qqm_b = qv * Load(df, qm_b + i); + Store(b_y * qqm_x, df, coeffs_yx + num_ac); + Store(b_x * qqm_x, df, coeffs_x + num_ac); + Store(b_y * qqm_b, df, coeffs_yb + num_ac); + Store(b_b * qqm_b, df, coeffs_b + num_ac); + num_ac += Lanes(df); + } + } + } + JXL_CHECK(num_ac % Lanes(df) == 0); + row_out_x[tx] = FindBestMultiplier(coeffs_yx, coeffs_x, num_ac, 0.0f, + kDistanceMultiplierAC, fast); + row_out_b[tx] = FindBestMultiplier(coeffs_yb, coeffs_b, num_ac, kYToBRatio, + kDistanceMultiplierAC, fast); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(InitDCStorage); +HWY_EXPORT(ComputeDC); +HWY_EXPORT(ComputeTile); + +void CfLHeuristics::Init(const Image3F& opsin) { + size_t xsize_blocks = opsin.xsize() / kBlockDim; + size_t ysize_blocks = opsin.ysize() / kBlockDim; + HWY_DYNAMIC_DISPATCH(InitDCStorage) + (xsize_blocks * ysize_blocks, &dc_values); +} + +void CfLHeuristics::ComputeTile(const Rect& r, const Image3F& opsin, + const DequantMatrices& dequant, + const AcStrategyImage* ac_strategy, + const Quantizer* quantizer, bool fast, + size_t thread, ColorCorrelationMap* cmap) { + bool use_dct8 = ac_strategy == nullptr; + HWY_DYNAMIC_DISPATCH(ComputeTile) + (opsin, dequant, ac_strategy, quantizer, r, fast, use_dct8, &cmap->ytox_map, + &cmap->ytob_map, &dc_values, mem.get() + thread * kItemsPerThread); +} + +void CfLHeuristics::ComputeDC(bool fast, ColorCorrelationMap* cmap) { + int32_t ytob_dc = 0; + int32_t ytox_dc = 0; + HWY_DYNAMIC_DISPATCH(ComputeDC)(dc_values, fast, &ytox_dc, &ytob_dc); + cmap->SetYToBDC(ytob_dc); + cmap->SetYToXDC(ytox_dc); +} + +void ColorCorrelationMapEncodeDC(ColorCorrelationMap* map, BitWriter* writer, + size_t layer, AuxOut* aux_out) { + float color_factor = map->GetColorFactor(); + float base_correlation_x = map->GetBaseCorrelationX(); + float base_correlation_b = map->GetBaseCorrelationB(); + int32_t ytox_dc = map->GetYToXDC(); + int32_t ytob_dc = map->GetYToBDC(); + + BitWriter::Allotment allotment(writer, 1 + 2 * kBitsPerByte + 12 + 32); + if (ytox_dc == 0 && ytob_dc == 0 && color_factor == kDefaultColorFactor && + base_correlation_x == 0.0f && base_correlation_b == kYToBRatio) { + writer->Write(1, 1); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + return; + } + writer->Write(1, 0); + JXL_CHECK(U32Coder::Write(kColorFactorDist, color_factor, writer)); + JXL_CHECK(F16Coder::Write(base_correlation_x, writer)); + JXL_CHECK(F16Coder::Write(base_correlation_b, writer)); + writer->Write(kBitsPerByte, ytox_dc - std::numeric_limits::min()); + writer->Write(kBitsPerByte, ytob_dc - std::numeric_limits::min()); + ReclaimAndCharge(writer, &allotment, layer, aux_out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_chroma_from_luma.h b/lib/jxl/enc_chroma_from_luma.h new file mode 100644 index 0000000..a097774 --- /dev/null +++ b/lib/jxl/enc_chroma_from_luma.h @@ -0,0 +1,67 @@ +// 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_ENC_CHROMA_FROM_LUMA_H_ +#define LIB_JXL_ENC_CHROMA_FROM_LUMA_H_ + +// Chroma-from-luma, computed using heuristics to determine the best linear +// model for the X and B channels from the Y channel. + +#include +#include + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quant_weights.h" + +namespace jxl { + +void ColorCorrelationMapEncodeDC(ColorCorrelationMap* map, BitWriter* writer, + size_t layer, AuxOut* aux_out); + +struct CfLHeuristics { + void Init(const Image3F& opsin); + + void PrepareForThreads(size_t num_threads) { + mem = hwy::AllocateAligned(num_threads * kItemsPerThread); + } + + void ComputeTile(const Rect& r, const Image3F& opsin, + const DequantMatrices& dequant, + const AcStrategyImage* ac_strategy, + const Quantizer* quantizer, bool fast, size_t thread, + ColorCorrelationMap* cmap); + + void ComputeDC(bool fast, ColorCorrelationMap* cmap); + + ImageF dc_values; + hwy::AlignedFreeUniquePtr mem; + + // Working set is too large for stack; allocate dynamically. + constexpr static size_t kItemsPerThread = + AcStrategy::kMaxCoeffArea * 3 // Blocks + + kColorTileDim * kColorTileDim * 4 // AC coeff storage + + AcStrategy::kMaxCoeffArea * 2; // Scratch space +}; + +} // namespace jxl + +#endif // LIB_JXL_ENC_CHROMA_FROM_LUMA_H_ diff --git a/lib/jxl/enc_cluster.cc b/lib/jxl/enc_cluster.cc new file mode 100644 index 0000000..1f12a29 --- /dev/null +++ b/lib/jxl/enc_cluster.cc @@ -0,0 +1,310 @@ +// 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/enc_cluster.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_cluster.cc" +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/fast_math-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +template +V Entropy(V count, V inv_total, V total) { + const HWY_CAPPED(float, Histogram::kRounding) d; + const auto zero = Set(d, 0.0f); + return IfThenZeroElse(count == total, + zero - count * FastLog2f(d, inv_total * count)); +} + +void HistogramEntropy(const Histogram& a) { + a.entropy_ = 0.0f; + if (a.total_count_ == 0) return; + + const HWY_CAPPED(float, Histogram::kRounding) df; + const HWY_CAPPED(int32_t, Histogram::kRounding) di; + + const auto inv_tot = Set(df, 1.0f / a.total_count_); + auto entropy_lanes = Zero(df); + auto total = Set(df, a.total_count_); + + for (size_t i = 0; i < a.data_.size(); i += Lanes(di)) { + const auto counts = LoadU(di, &a.data_[i]); + entropy_lanes += Entropy(ConvertTo(df, counts), inv_tot, total); + } + a.entropy_ += GetLane(SumOfLanes(entropy_lanes)); +} + +float HistogramDistance(const Histogram& a, const Histogram& b) { + if (a.total_count_ == 0 || b.total_count_ == 0) return 0; + + const HWY_CAPPED(float, Histogram::kRounding) df; + const HWY_CAPPED(int32_t, Histogram::kRounding) di; + + const auto inv_tot = Set(df, 1.0f / (a.total_count_ + b.total_count_)); + auto distance_lanes = Zero(df); + auto total = Set(df, a.total_count_ + b.total_count_); + + for (size_t i = 0; i < std::max(a.data_.size(), b.data_.size()); + i += Lanes(di)) { + const auto a_counts = + a.data_.size() > i ? LoadU(di, &a.data_[i]) : Zero(di); + const auto b_counts = + b.data_.size() > i ? LoadU(di, &b.data_[i]) : Zero(di); + const auto counts = ConvertTo(df, a_counts + b_counts); + distance_lanes += Entropy(counts, inv_tot, total); + } + const float total_distance = GetLane(SumOfLanes(distance_lanes)); + return total_distance - a.entropy_ - b.entropy_; +} + +// First step of a k-means clustering with a fancy distance metric. +void FastClusterHistograms(const std::vector& in, + const size_t num_contexts_in, size_t max_histograms, + float min_distance, std::vector* out, + std::vector* histogram_symbols) { + PROFILER_FUNC; + size_t largest_idx = 0; + std::vector nonempty_histograms; + nonempty_histograms.reserve(in.size()); + for (size_t i = 0; i < num_contexts_in; i++) { + if (in[i].total_count_ == 0) continue; + HistogramEntropy(in[i]); + if (in[i].total_count_ > in[largest_idx].total_count_) { + largest_idx = i; + } + nonempty_histograms.push_back(i); + } + // No symbols. + if (nonempty_histograms.empty()) { + out->resize(1); + histogram_symbols->clear(); + histogram_symbols->resize(in.size(), 0); + return; + } + largest_idx = std::find(nonempty_histograms.begin(), + nonempty_histograms.end(), largest_idx) - + nonempty_histograms.begin(); + size_t num_contexts = nonempty_histograms.size(); + out->clear(); + out->reserve(max_histograms); + std::vector dists(num_contexts, std::numeric_limits::max()); + histogram_symbols->clear(); + histogram_symbols->resize(in.size(), max_histograms); + + while (out->size() < max_histograms && out->size() < num_contexts) { + (*histogram_symbols)[nonempty_histograms[largest_idx]] = out->size(); + out->push_back(in[nonempty_histograms[largest_idx]]); + largest_idx = 0; + for (size_t i = 0; i < num_contexts; i++) { + dists[i] = std::min( + HistogramDistance(in[nonempty_histograms[i]], out->back()), dists[i]); + // Avoid repeating histograms + if ((*histogram_symbols)[nonempty_histograms[i]] != max_histograms) { + continue; + } + if (dists[i] > dists[largest_idx]) largest_idx = i; + } + if (dists[largest_idx] < min_distance) break; + } + + for (size_t i = 0; i < num_contexts_in; i++) { + if ((*histogram_symbols)[i] != max_histograms) continue; + if (in[i].total_count_ == 0) { + (*histogram_symbols)[i] = 0; + continue; + } + size_t best = 0; + float best_dist = HistogramDistance(in[i], (*out)[best]); + for (size_t j = 1; j < out->size(); j++) { + float dist = HistogramDistance(in[i], (*out)[j]); + if (dist < best_dist) { + best = j; + best_dist = dist; + } + } + (*out)[best].AddHistogram(in[i]); + HistogramEntropy((*out)[best]); + (*histogram_symbols)[i] = best; + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(FastClusterHistograms); // Local function +HWY_EXPORT(HistogramEntropy); // Local function + +float Histogram::ShannonEntropy() const { + HWY_DYNAMIC_DISPATCH(HistogramEntropy)(*this); + return entropy_; +} + +namespace { +// ----------------------------------------------------------------------------- +// Histogram refinement + +// Reorder histograms in *out so that the new symbols in *symbols come in +// increasing order. +void HistogramReindex(std::vector* out, + std::vector* symbols) { + std::vector tmp(*out); + std::map new_index; + int next_index = 0; + for (uint32_t symbol : *symbols) { + if (new_index.find(symbol) == new_index.end()) { + new_index[symbol] = next_index; + (*out)[next_index] = tmp[symbol]; + ++next_index; + } + } + out->resize(next_index); + for (uint32_t& symbol : *symbols) { + symbol = new_index[symbol]; + } +} + +} // namespace + +// Clusters similar histograms in 'in' together, the selected histograms are +// placed in 'out', and for each index in 'in', *histogram_symbols will +// indicate which of the 'out' histograms is the best approximation. +void ClusterHistograms(const HistogramParams params, + const std::vector& in, + const size_t num_contexts, size_t max_histograms, + std::vector* out, + std::vector* histogram_symbols) { + constexpr float kMinDistanceForDistinctFast = 64.0f; + constexpr float kMinDistanceForDistinctBest = 16.0f; + max_histograms = std::min(max_histograms, params.max_histograms); + if (params.clustering == HistogramParams::ClusteringType::kFastest) { + HWY_DYNAMIC_DISPATCH(FastClusterHistograms) + (in, num_contexts, 4, kMinDistanceForDistinctFast, out, histogram_symbols); + } else if (params.clustering == HistogramParams::ClusteringType::kFast) { + HWY_DYNAMIC_DISPATCH(FastClusterHistograms) + (in, num_contexts, max_histograms, kMinDistanceForDistinctFast, out, + histogram_symbols); + } else { + PROFILER_FUNC; + HWY_DYNAMIC_DISPATCH(FastClusterHistograms) + (in, num_contexts, max_histograms, kMinDistanceForDistinctBest, out, + histogram_symbols); + for (size_t i = 0; i < out->size(); i++) { + (*out)[i].entropy_ = + ANSPopulationCost((*out)[i].data_.data(), (*out)[i].data_.size()); + } + uint32_t next_version = 2; + std::vector version(out->size(), 1); + std::vector renumbering(out->size()); + std::iota(renumbering.begin(), renumbering.end(), 0); + + // Try to pair up clusters if doing so reduces the total cost. + + struct HistogramPair { + // validity of a pair: p.version == max(version[i], version[j]) + float cost; + uint32_t first; + uint32_t second; + uint32_t version; + // We use > because priority queues sort in *decreasing* order, but we + // want lower cost elements to appear first. + bool operator<(const HistogramPair& other) const { + return std::make_tuple(cost, first, second, version) > + std::make_tuple(other.cost, other.first, other.second, + other.version); + } + }; + + // Create list of all pairs by increasing merging cost. + std::priority_queue pairs_to_merge; + for (uint32_t i = 0; i < out->size(); i++) { + for (uint32_t j = i + 1; j < out->size(); j++) { + Histogram histo; + histo.AddHistogram((*out)[i]); + histo.AddHistogram((*out)[j]); + float cost = ANSPopulationCost(histo.data_.data(), histo.data_.size()) - + (*out)[i].entropy_ - (*out)[j].entropy_; + // Avoid enqueueing pairs that are not advantageous to merge. + if (cost >= 0) continue; + pairs_to_merge.push( + HistogramPair{cost, i, j, std::max(version[i], version[j])}); + } + } + + // Merge the best pair to merge, add new pairs that get formed as a + // consequence. + while (!pairs_to_merge.empty()) { + uint32_t first = pairs_to_merge.top().first; + uint32_t second = pairs_to_merge.top().second; + uint32_t ver = pairs_to_merge.top().version; + pairs_to_merge.pop(); + if (ver != std::max(version[first], version[second]) || + version[first] == 0 || version[second] == 0) { + continue; + } + (*out)[first].AddHistogram((*out)[second]); + (*out)[first].entropy_ = ANSPopulationCost((*out)[first].data_.data(), + (*out)[first].data_.size()); + for (size_t i = 0; i < renumbering.size(); i++) { + if (renumbering[i] == second) { + renumbering[i] = first; + } + } + version[second] = 0; + version[first] = next_version++; + for (uint32_t j = 0; j < out->size(); j++) { + if (j == first) continue; + if (version[j] == 0) continue; + Histogram histo; + histo.AddHistogram((*out)[first]); + histo.AddHistogram((*out)[j]); + float cost = ANSPopulationCost(histo.data_.data(), histo.data_.size()) - + (*out)[first].entropy_ - (*out)[j].entropy_; + // Avoid enqueueing pairs that are not advantageous to merge. + if (cost >= 0) continue; + pairs_to_merge.push( + HistogramPair{cost, std::min(first, j), std::max(first, j), + std::max(version[first], version[j])}); + } + } + std::vector reverse_renumbering(out->size(), -1); + size_t num_alive = 0; + for (size_t i = 0; i < out->size(); i++) { + if (version[i] == 0) continue; + (*out)[num_alive++] = (*out)[i]; + reverse_renumbering[i] = num_alive - 1; + } + out->resize(num_alive); + for (size_t i = 0; i < histogram_symbols->size(); i++) { + (*histogram_symbols)[i] = + reverse_renumbering[renumbering[(*histogram_symbols)[i]]]; + } + } + + // Convert the context map to a canonical form. + HistogramReindex(out, histogram_symbols); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_cluster.h b/lib/jxl/enc_cluster.h new file mode 100644 index 0000000..622a567 --- /dev/null +++ b/lib/jxl/enc_cluster.h @@ -0,0 +1,61 @@ +// 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. + +// Functions for clustering similar histograms together. + +#ifndef LIB_JXL_ENC_CLUSTER_H_ +#define LIB_JXL_ENC_CLUSTER_H_ + +#include +#include +#include + +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/enc_ans.h" + +namespace jxl { + +struct Histogram { + Histogram() { total_count_ = 0; } + void Clear() { + data_.clear(); + total_count_ = 0; + } + void Add(size_t symbol) { + if (data_.size() <= symbol) { + data_.resize(DivCeil(symbol + 1, kRounding) * kRounding); + } + ++data_[symbol]; + ++total_count_; + } + void AddHistogram(const Histogram& other) { + if (other.data_.size() > data_.size()) { + data_.resize(other.data_.size()); + } + for (size_t i = 0; i < other.data_.size(); ++i) { + data_[i] += other.data_[i]; + } + total_count_ += other.total_count_; + } + float PopulationCost() const { + return ANSPopulationCost(data_.data(), data_.size()); + } + float ShannonEntropy() const; + + std::vector data_; + size_t total_count_; + mutable float entropy_; // WARNING: not kept up-to-date. + static constexpr size_t kRounding = 8; +}; + +void ClusterHistograms(HistogramParams params, const std::vector& in, + size_t num_contexts, size_t max_histograms, + std::vector* out, + std::vector* histogram_symbols); +} // namespace jxl + +#endif // LIB_JXL_ENC_CLUSTER_H_ diff --git a/lib/jxl/enc_coeff_order.cc b/lib/jxl/enc_coeff_order.cc new file mode 100644 index 0000000..81315a0 --- /dev/null +++ b/lib/jxl/enc_coeff_order.cc @@ -0,0 +1,274 @@ +// 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 + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/lehmer_code.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +uint32_t ComputeUsedOrders(const SpeedTier speed, + const AcStrategyImage& ac_strategy, + const Rect& rect) { + // Use default orders for small images. + if (ac_strategy.xsize() < 5 && ac_strategy.ysize() < 5) return 0; + + // Only uses DCT8 = 0, so bitfield = 1. + if (speed >= SpeedTier::kFalcon) return 1; + + uint32_t ret = 0; + size_t xsize_blocks = rect.xsize(); + size_t ysize_blocks = rect.ysize(); + // TODO(veluca): precompute when doing DCT. + for (size_t by = 0; by < ysize_blocks; ++by) { + AcStrategyRow acs_row = ac_strategy.ConstRow(rect, by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + int ord = kStrategyOrder[acs_row[bx].RawStrategy()]; + // Do not customize coefficient orders for blocks bigger than 32x32. + if (ord > 6) { + continue; + } + ret |= 1u << ord; + } + } + return ret; +} + +void ComputeCoeffOrder(SpeedTier speed, const ACImage& acs, + const AcStrategyImage& ac_strategy, + const FrameDimensions& frame_dim, uint32_t& used_orders, + coeff_order_t* JXL_RESTRICT order) { + std::vector num_zeros(kCoeffOrderMaxSize); + // If compressing at high speed and only using 8x8 DCTs, only consider a + // subset of blocks. + double block_fraction = 1.0f; + // TODO(veluca): figure out why sampling blocks if non-8x8s are used makes + // encoding significantly less dense. + if (speed >= SpeedTier::kSquirrel && used_orders == 1) { + block_fraction = 0.5f; + } + // No need to compute number of zero coefficients if all orders are the + // default. + if (used_orders != 0) { + uint64_t threshold = + (std::numeric_limits::max() >> 32) * block_fraction; + uint64_t s[2] = {0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull}; + // Xorshift128+ adapted from xorshift128+-inl.h + auto use_sample = [&]() { + auto s1 = s[0]; + const auto s0 = s[1]; + const auto bits = s1 + s0; // b, c + s[0] = s0; + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + s[1] = s1; + return (bits >> 32) <= threshold; + }; + + // Count number of zero coefficients, separately for each DCT band. + // TODO(veluca): precompute when doing DCT. + for (size_t group_index = 0; group_index < frame_dim.num_groups; + group_index++) { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + const Rect rect(gx * kGroupDimInBlocks, gy * kGroupDimInBlocks, + kGroupDimInBlocks, kGroupDimInBlocks, + frame_dim.xsize_blocks, frame_dim.ysize_blocks); + ConstACPtr rows[3]; + ACType type = acs.Type(); + for (size_t c = 0; c < 3; c++) { + rows[c] = acs.PlaneRow(c, group_index, 0); + } + size_t ac_offset = 0; + + // TODO(veluca): SIMDfy. + for (size_t by = 0; by < rect.ysize(); ++by) { + AcStrategyRow acs_row = ac_strategy.ConstRow(rect, by); + for (size_t bx = 0; bx < rect.xsize(); ++bx) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + if (!use_sample()) continue; + size_t size = kDCTBlockSize << acs.log2_covered_blocks(); + for (size_t c = 0; c < 3; ++c) { + const size_t order_offset = + CoeffOrderOffset(kStrategyOrder[acs.RawStrategy()], c); + if (type == ACType::k16) { + for (size_t k = 0; k < size; k++) { + bool is_zero = rows[c].ptr16[ac_offset + k] == 0; + num_zeros[order_offset + k] += is_zero ? 1 : 0; + } + } else { + for (size_t k = 0; k < size; k++) { + bool is_zero = rows[c].ptr32[ac_offset + k] == 0; + num_zeros[order_offset + k] += is_zero ? 1 : 0; + } + } + // Ensure LLFs are first in the order. + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + CoefficientLayout(&cy, &cx); + for (size_t iy = 0; iy < cy; iy++) { + for (size_t ix = 0; ix < cx; ix++) { + num_zeros[order_offset + iy * kBlockDim * cx + ix] = -1; + } + } + } + ac_offset += size; + } + } + } + } + struct PosAndCount { + uint32_t pos; + uint32_t count; + }; + auto mem = hwy::AllocateAligned(AcStrategy::kMaxCoeffArea); + + uint16_t computed = 0; + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + AcStrategy acs = AcStrategy::FromRawStrategy(o); + size_t sz = kDCTBlockSize * acs.covered_blocks_x() * acs.covered_blocks_y(); + // Ensure natural coefficient order is not permuted if the order is + // not transmitted. + if ((1 << ord) & ~used_orders) { + for (size_t c = 0; c < 3; c++) { + size_t offset = CoeffOrderOffset(ord, c); + JXL_DASSERT(CoeffOrderOffset(ord, c + 1) - offset == sz); + SetDefaultOrder(AcStrategy::FromRawStrategy(o), &order[offset]); + } + continue; + } + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + + bool is_nondefault = false; + for (uint8_t c = 0; c < 3; c++) { + // Apply zig-zag order. + PosAndCount* pos_and_val = mem.get(); + size_t offset = CoeffOrderOffset(ord, c); + JXL_DASSERT(CoeffOrderOffset(ord, c + 1) - offset == sz); + float inv_sqrt_sz = 1.0f / std::sqrt(sz); + for (size_t i = 0; i < sz; ++i) { + size_t pos = natural_coeff_order[i]; + pos_and_val[i].pos = pos; + // We don't care for the exact number -> quantize number of zeros, + // to get less permuted order. + pos_and_val[i].count = num_zeros[offset + pos] * inv_sqrt_sz + 0.1f; + } + + // Stable-sort -> elements with same number of zeros will preserve their + // order. + auto comparator = [](const PosAndCount& a, const PosAndCount& b) -> bool { + return a.count < b.count; + }; + std::stable_sort(pos_and_val, pos_and_val + sz, comparator); + + // Grab indices. + for (size_t i = 0; i < sz; ++i) { + order[offset + i] = pos_and_val[i].pos; + is_nondefault |= natural_coeff_order[i] != pos_and_val[i].pos; + } + } + if (!is_nondefault) { + used_orders &= ~(1 << ord); + } + } +} + +namespace { + +void TokenizePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, std::vector* tokens) { + std::vector lehmer(size); + std::vector temp(size + 1); + ComputeLehmerCode(order, temp.data(), size, lehmer.data()); + size_t end = size; + while (end > skip && lehmer[end - 1] == 0) { + --end; + } + tokens->emplace_back(CoeffOrderContext(size), end - skip); + uint32_t last = 0; + for (size_t i = skip; i < end; ++i) { + tokens->emplace_back(CoeffOrderContext(last), lehmer[i]); + last = lehmer[i]; + } +} + +} // namespace + +void EncodePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, BitWriter* writer, int layer, + AuxOut* aux_out) { + std::vector> tokens(1); + TokenizePermutation(order, skip, size, &tokens[0]); + std::vector context_map; + EntropyEncodingData codes; + BuildAndEncodeHistograms(HistogramParams(), kPermutationContexts, tokens, + &codes, &context_map, writer, layer, aux_out); + WriteTokens(tokens[0], codes, context_map, writer, layer, aux_out); +} + +namespace { +void EncodeCoeffOrder(const coeff_order_t* JXL_RESTRICT order, AcStrategy acs, + std::vector* tokens, coeff_order_t* order_zigzag) { + const size_t llf = acs.covered_blocks_x() * acs.covered_blocks_y(); + const size_t size = kDCTBlockSize * llf; + const coeff_order_t* natural_coeff_order_lut = acs.NaturalCoeffOrderLut(); + for (size_t i = 0; i < size; ++i) { + order_zigzag[i] = natural_coeff_order_lut[order[i]]; + } + TokenizePermutation(order_zigzag, llf, size, tokens); +} +} // namespace + +void EncodeCoeffOrders(uint16_t used_orders, + const coeff_order_t* JXL_RESTRICT order, + BitWriter* writer, size_t layer, + AuxOut* JXL_RESTRICT aux_out) { + auto mem = hwy::AllocateAligned(AcStrategy::kMaxCoeffArea); + uint16_t computed = 0; + std::vector> tokens(1); + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + if ((used_orders & (1 << ord)) == 0) continue; + AcStrategy acs = AcStrategy::FromRawStrategy(o); + for (size_t c = 0; c < 3; c++) { + EncodeCoeffOrder(&order[CoeffOrderOffset(ord, c)], acs, &tokens[0], + mem.get()); + } + } + // Do not write anything if no order is used. + if (used_orders != 0) { + std::vector context_map; + EntropyEncodingData codes; + BuildAndEncodeHistograms(HistogramParams(), kPermutationContexts, tokens, + &codes, &context_map, writer, layer, aux_out); + WriteTokens(tokens[0], codes, context_map, writer, layer, aux_out); + } +} + +} // namespace jxl diff --git a/lib/jxl/enc_coeff_order.h b/lib/jxl/enc_coeff_order.h new file mode 100644 index 0000000..5eee746 --- /dev/null +++ b/lib/jxl/enc_coeff_order.h @@ -0,0 +1,52 @@ +// 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_ENC_COEFF_ORDER_H_ +#define LIB_JXL_ENC_COEFF_ORDER_H_ + +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_params.h" + +namespace jxl { + +// Orders that are actually used in part of image. `rect` is in block units. +uint32_t ComputeUsedOrders(SpeedTier speed, const AcStrategyImage& ac_strategy, + const Rect& rect); + +// Modify zig-zag order, so that DCT bands with more zeros go later. +// Order of DCT bands with same number of zeros is untouched, so +// permutation will be cheaper to encode. +void ComputeCoeffOrder(SpeedTier speed, const ACImage& acs, + const AcStrategyImage& ac_strategy, + const FrameDimensions& frame_dim, uint32_t& used_orders, + coeff_order_t* JXL_RESTRICT order); + +void EncodeCoeffOrders(uint16_t used_orders, + const coeff_order_t* JXL_RESTRICT order, + BitWriter* writer, size_t layer, + AuxOut* JXL_RESTRICT aux_out); + +// Encoding/decoding of a single permutation. `size`: number of elements in the +// permutation. `skip`: number of elements to skip from the *beginning* of the +// permutation. +void EncodePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, BitWriter* writer, int layer, + AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_COEFF_ORDER_H_ diff --git a/lib/jxl/enc_color_management.cc b/lib/jxl/enc_color_management.cc new file mode 100644 index 0000000..8e50636 --- /dev/null +++ b/lib/jxl/enc_color_management.cc @@ -0,0 +1,891 @@ +// 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. + +// Defined by build system; this avoids IDE warnings. Must come before +// color_management.h (affects header definitions). +#ifndef JPEGXL_ENABLE_SKCMS +#define JPEGXL_ENABLE_SKCMS 0 +#endif + +#include "lib/jxl/enc_color_management.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_color_management.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/linalg.h" +#include "lib/jxl/transfer_functions-inl.h" +#if JPEGXL_ENABLE_SKCMS +#include "lib/jxl/enc_jxl_skcms.h" +#else // JPEGXL_ENABLE_SKCMS +#include "lcms2.h" +#include "lcms2_plugin.h" +#endif // JPEGXL_ENABLE_SKCMS + +#define JXL_CMS_VERBOSE 0 + +// Define these only once. We can't use HWY_ONCE here because it is defined as +// 1 only on the last pass. +#ifndef LIB_JXL_ENC_COLOR_MANAGEMENT_CC_ +#define LIB_JXL_ENC_COLOR_MANAGEMENT_CC_ + +namespace jxl { +#if JPEGXL_ENABLE_SKCMS +struct ColorSpaceTransform::SkcmsICC { + // Parsed skcms_ICCProfiles retain pointers to the original data. + PaddedBytes icc_src_, icc_dst_; + skcms_ICCProfile profile_src_, profile_dst_; +}; +#endif // JPEGXL_ENABLE_SKCMS +} // namespace jxl + +#endif // LIB_JXL_ENC_COLOR_MANAGEMENT_CC_ + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +#if JXL_CMS_VERBOSE >= 2 +const size_t kX = 0; // pixel index, multiplied by 3 for RGB +#endif + +// xform_src = UndoGammaCompression(buf_src). +void BeforeTransform(ColorSpaceTransform* t, const float* buf_src, + float* xform_src) { + switch (t->preprocess_) { + case ExtraTF::kNone: + JXL_DASSERT(false); // unreachable + break; + + case ExtraTF::kPQ: { + // By default, PQ content has an intensity target of 10000, stored + // exactly. + HWY_FULL(float) df; + const auto multiplier = Set(df, t->intensity_target_ == 10000.f + ? 1.0f + : 10000.f / t->intensity_target_); + for (size_t i = 0; i < t->buf_src_.xsize(); i += Lanes(df)) { + const auto val = Load(df, buf_src + i); + const auto result = multiplier * TF_PQ().DisplayFromEncoded(df, val); + Store(result, df, xform_src + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoPQ %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + } + + case ExtraTF::kHLG: + for (size_t i = 0; i < t->buf_src_.xsize(); ++i) { + xform_src[i] = static_cast( + TF_HLG().DisplayFromEncoded(static_cast(buf_src[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoHLG %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + + case ExtraTF::kSRGB: + HWY_FULL(float) df; + for (size_t i = 0; i < t->buf_src_.xsize(); i += Lanes(df)) { + const auto val = Load(df, buf_src + i); + const auto result = TF_SRGB().DisplayFromEncoded(val); + Store(result, df, xform_src + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoSRGB %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + } +} + +// Applies gamma compression in-place. +void AfterTransform(ColorSpaceTransform* t, float* JXL_RESTRICT buf_dst) { + switch (t->postprocess_) { + case ExtraTF::kNone: + JXL_DASSERT(false); // unreachable + break; + case ExtraTF::kPQ: { + HWY_FULL(float) df; + const auto multiplier = Set(df, t->intensity_target_ == 10000.f + ? 1.0f + : t->intensity_target_ * 1e-4f); + for (size_t i = 0; i < t->buf_dst_.xsize(); i += Lanes(df)) { + const auto val = Load(df, buf_dst + i); + const auto result = TF_PQ().EncodedFromDisplay(df, multiplier * val); + Store(result, df, buf_dst + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after PQ enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + } + case ExtraTF::kHLG: + for (size_t i = 0; i < t->buf_dst_.xsize(); ++i) { + buf_dst[i] = static_cast( + TF_HLG().EncodedFromDisplay(static_cast(buf_dst[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after HLG enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + case ExtraTF::kSRGB: + HWY_FULL(float) df; + for (size_t i = 0; i < t->buf_dst_.xsize(); i += Lanes(df)) { + const auto val = Load(df, buf_dst + i); + const auto result = + TF_SRGB().EncodedFromDisplay(HWY_FULL(float)(), val); + Store(result, df, buf_dst + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after SRGB enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + } +} + +void DoColorSpaceTransform(ColorSpaceTransform* t, const size_t thread, + const float* buf_src, float* buf_dst) { + // No lock needed. + + float* xform_src = const_cast(buf_src); // Read-only. + if (t->preprocess_ != ExtraTF::kNone) { + xform_src = t->buf_src_.Row(thread); // Writable buffer. + BeforeTransform(t, buf_src, xform_src); + } + +#if JXL_CMS_VERBOSE >= 2 + // Save inputs for printing before in-place transforms overwrite them. + const float in0 = xform_src[3 * kX + 0]; + const float in1 = xform_src[3 * kX + 1]; + const float in2 = xform_src[3 * kX + 2]; +#endif + + if (t->skip_lcms_) { + if (buf_dst != xform_src) { + memcpy(buf_dst, xform_src, t->buf_dst_.xsize() * sizeof(*buf_dst)); + } // else: in-place, no need to copy + } else { +#if JPEGXL_ENABLE_SKCMS + JXL_CHECK(skcms_Transform( + xform_src, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque, + &t->skcms_icc_->profile_src_, buf_dst, skcms_PixelFormat_RGB_fff, + skcms_AlphaFormat_Opaque, &t->skcms_icc_->profile_dst_, t->xsize_)); +#else // JPEGXL_ENABLE_SKCMS + cmsDoTransform(t->lcms_transform_, xform_src, buf_dst, + static_cast(t->xsize_)); +#endif // JPEGXL_ENABLE_SKCMS + } +#if JXL_CMS_VERBOSE >= 2 + printf("xform skip%d: %.4f %.4f %.4f (%p) -> (%p) %.4f %.4f %.4f\n", + t->skip_lcms_, in0, in1, in2, xform_src, buf_dst, buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + + if (t->postprocess_ != ExtraTF::kNone) { + AfterTransform(t, buf_dst); + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(DoColorSpaceTransform); +void DoColorSpaceTransform(ColorSpaceTransform* t, size_t thread, + const float* buf_src, float* buf_dst) { + return HWY_DYNAMIC_DISPATCH(DoColorSpaceTransform)(t, thread, buf_src, + buf_dst); +} + +namespace { + +// Define to 1 on OS X as a workaround for older LCMS lacking MD5. +#define JXL_CMS_OLD_VERSION 0 + +// cms functions (even *THR) are not thread-safe, except cmsDoTransform. +// To ensure all functions are covered without frequent lock-taking nor risk of +// recursive lock, we lock in the top-level APIs. +static std::mutex& LcmsMutex() { + static std::mutex m; + return m; +} + +#if JPEGXL_ENABLE_SKCMS + +JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const float XYZ[3]) { + const float factor = 1.f / (XYZ[0] + XYZ[1] + XYZ[2]); + CIExy xy; + xy.x = XYZ[0] * factor; + xy.y = XYZ[1] * factor; + return xy; +} + +#else // JPEGXL_ENABLE_SKCMS +// (LCMS interface requires xyY but we omit the Y for white points/primaries.) + +JXL_MUST_USE_RESULT CIExy CIExyFromxyY(const cmsCIExyY& xyY) { + CIExy xy; + xy.x = xyY.x; + xy.y = xyY.y; + return xy; +} + +JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const cmsCIEXYZ& XYZ) { + cmsCIExyY xyY; + cmsXYZ2xyY(/*Dest=*/&xyY, /*Source=*/&XYZ); + return CIExyFromxyY(xyY); +} + +JXL_MUST_USE_RESULT cmsCIEXYZ D50_XYZ() { + // Quantized D50 as stored in ICC profiles. + return {0.96420288, 1.0, 0.82490540}; +} + +JXL_MUST_USE_RESULT cmsCIExyY xyYFromCIExy(const CIExy& xy) { + const cmsCIExyY xyY = {xy.x, xy.y, 1.0}; + return xyY; +} + +// RAII + +struct ProfileDeleter { + void operator()(void* p) { cmsCloseProfile(p); } +}; +using Profile = std::unique_ptr; + +struct TransformDeleter { + void operator()(void* p) { cmsDeleteTransform(p); } +}; +using Transform = std::unique_ptr; + +struct CurveDeleter { + void operator()(cmsToneCurve* p) { cmsFreeToneCurve(p); } +}; +using Curve = std::unique_ptr; + +Status CreateProfileXYZ(const cmsContext context, + Profile* JXL_RESTRICT profile) { + profile->reset(cmsCreateXYZProfileTHR(context)); + if (profile->get() == nullptr) return JXL_FAILURE("Failed to create XYZ"); + return true; +} + +#endif // !JPEGXL_ENABLE_SKCMS + +#if JPEGXL_ENABLE_SKCMS +// IMPORTANT: icc must outlive profile. +Status DecodeProfile(const PaddedBytes& icc, skcms_ICCProfile* const profile) { + if (!skcms_Parse(icc.data(), icc.size(), profile)) { + return JXL_FAILURE("Failed to parse ICC profile with %zu bytes", + icc.size()); + } + return true; +} +#else // JPEGXL_ENABLE_SKCMS +Status DecodeProfile(const cmsContext context, const PaddedBytes& icc, + Profile* profile) { + profile->reset(cmsOpenProfileFromMemTHR(context, icc.data(), icc.size())); + if (profile->get() == nullptr) { + return JXL_FAILURE("Failed to decode profile"); + } + + // WARNING: due to the LCMS MD5 issue mentioned above, many existing + // profiles have incorrect MD5, so do not even bother checking them nor + // generating warning clutter. + + return true; +} +#endif // JPEGXL_ENABLE_SKCMS + +#if JPEGXL_ENABLE_SKCMS + +ColorSpace ColorSpaceFromProfile(const skcms_ICCProfile& profile) { + switch (profile.data_color_space) { + case skcms_Signature_RGB: + return ColorSpace::kRGB; + case skcms_Signature_Gray: + return ColorSpace::kGray; + default: + return ColorSpace::kUnknown; + } +} + +// "profile1" is pre-decoded to save time in DetectTransferFunction. +Status ProfileEquivalentToICC(const skcms_ICCProfile& profile1, + const PaddedBytes& icc) { + skcms_ICCProfile profile2; + JXL_RETURN_IF_ERROR(skcms_Parse(icc.data(), icc.size(), &profile2)); + return skcms_ApproximatelyEqualProfiles(&profile1, &profile2); +} + +// vector_out := matmul(matrix, vector_in) +void MatrixProduct(const skcms_Matrix3x3& matrix, const float vector_in[3], + float vector_out[3]) { + for (int i = 0; i < 3; ++i) { + vector_out[i] = 0; + for (int j = 0; j < 3; ++j) { + vector_out[i] += matrix.vals[i][j] * vector_in[j]; + } + } +} + +// Returns white point that was specified when creating the profile. +JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile, + CIExy* out) { + float media_white_point_XYZ[3]; + if (!skcms_GetWTPT(&profile, media_white_point_XYZ)) { + return JXL_FAILURE("ICC profile does not contain WhitePoint tag"); + } + skcms_Matrix3x3 CHAD; + if (!skcms_GetCHAD(&profile, &CHAD)) { + // If there is no chromatic adaptation matrix, it means that the white point + // is already unadapted. + *out = CIExyFromXYZ(media_white_point_XYZ); + return true; + } + // Otherwise, it has been adapted to the PCS white point using said matrix, + // and the adaptation needs to be undone. + skcms_Matrix3x3 inverse_CHAD; + if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) { + return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix"); + } + float unadapted_white_point_XYZ[3]; + MatrixProduct(inverse_CHAD, media_white_point_XYZ, unadapted_white_point_XYZ); + *out = CIExyFromXYZ(unadapted_white_point_XYZ); + return true; +} + +Status IdentifyPrimaries(const skcms_ICCProfile& profile, + const CIExy& wp_unadapted, ColorEncoding* c) { + if (!c->HasPrimaries()) return true; + + skcms_Matrix3x3 CHAD, inverse_CHAD; + if (skcms_GetCHAD(&profile, &CHAD)) { + JXL_RETURN_IF_ERROR(skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)); + } else { + static constexpr skcms_Matrix3x3 kLMSFromXYZ = { + {{0.8951, 0.2664, -0.1614}, + {-0.7502, 1.7135, 0.0367}, + {0.0389, -0.0685, 1.0296}}}; + static constexpr skcms_Matrix3x3 kXYZFromLMS = { + {{0.9869929, -0.1470543, 0.1599627}, + {0.4323053, 0.5183603, 0.0492912}, + {-0.0085287, 0.0400428, 0.9684867}}}; + static constexpr float kWpD50XYZ[3] = {0.96420288, 1.0, 0.82490540}; + float wp_unadapted_XYZ[3]; + JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp_unadapted, wp_unadapted_XYZ)); + float wp_D50_LMS[3], wp_unadapted_LMS[3]; + MatrixProduct(kLMSFromXYZ, kWpD50XYZ, wp_D50_LMS); + MatrixProduct(kLMSFromXYZ, wp_unadapted_XYZ, wp_unadapted_LMS); + inverse_CHAD = {{{wp_unadapted_LMS[0] / wp_D50_LMS[0], 0, 0}, + {0, wp_unadapted_LMS[1] / wp_D50_LMS[1], 0}, + {0, 0, wp_unadapted_LMS[2] / wp_D50_LMS[2]}}}; + inverse_CHAD = skcms_Matrix3x3_concat(&kXYZFromLMS, &inverse_CHAD); + inverse_CHAD = skcms_Matrix3x3_concat(&inverse_CHAD, &kLMSFromXYZ); + } + + float XYZ[3]; + PrimariesCIExy primaries; + CIExy* const chromaticities[] = {&primaries.r, &primaries.g, &primaries.b}; + for (int i = 0; i < 3; ++i) { + float RGB[3] = {}; + RGB[i] = 1; + skcms_Transform(RGB, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque, + &profile, XYZ, skcms_PixelFormat_RGB_fff, + skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1); + float unadapted_XYZ[3]; + MatrixProduct(inverse_CHAD, XYZ, unadapted_XYZ); + *chromaticities[i] = CIExyFromXYZ(unadapted_XYZ); + } + return c->SetPrimaries(primaries); +} + +void DetectTransferFunction(const skcms_ICCProfile& profile, + ColorEncoding* JXL_RESTRICT c) { + if (c->tf.SetImplicit()) return; + + for (TransferFunction tf : Values()) { + // Can only create profile from known transfer function. + if (tf == TransferFunction::kUnknown) continue; + + c->tf.SetTransferFunction(tf); + + skcms_ICCProfile profile_test; + PaddedBytes bytes; + if (MaybeCreateProfile(*c, &bytes) && DecodeProfile(bytes, &profile_test) && + skcms_ApproximatelyEqualProfiles(&profile, &profile_test)) { + return; + } + } + + c->tf.SetTransferFunction(TransferFunction::kUnknown); +} + +#else // JPEGXL_ENABLE_SKCMS + +uint32_t Type32(const ColorEncoding& c) { + if (c.IsGray()) return TYPE_GRAY_FLT; + return TYPE_RGB_FLT; +} + +uint32_t Type64(const ColorEncoding& c) { + if (c.IsGray()) return TYPE_GRAY_DBL; + return TYPE_RGB_DBL; +} + +ColorSpace ColorSpaceFromProfile(const Profile& profile) { + switch (cmsGetColorSpace(profile.get())) { + case cmsSigRgbData: + return ColorSpace::kRGB; + case cmsSigGrayData: + return ColorSpace::kGray; + default: + return ColorSpace::kUnknown; + } +} + +// "profile1" is pre-decoded to save time in DetectTransferFunction. +Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1, + const PaddedBytes& icc, const ColorEncoding& c) { + const uint32_t type_src = Type64(c); + + Profile profile2; + JXL_RETURN_IF_ERROR(DecodeProfile(context, icc, &profile2)); + + Profile profile_xyz; + JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz)); + + const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC; + const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION | + cmsFLAGS_HIGHRESPRECALC; + Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src, + profile_xyz.get(), TYPE_XYZ_DBL, + intent, flags)); + Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src, + profile_xyz.get(), TYPE_XYZ_DBL, + intent, flags)); + if (xform1 == nullptr || xform2 == nullptr) { + return JXL_FAILURE("Failed to create transform"); + } + + double in[3]; + double out1[3]; + double out2[3]; + + // Uniformly spaced samples from very dark to almost fully bright. + const double init = 1E-3; + const double step = 0.2; + + if (c.IsGray()) { + // Finer sampling and replicate each component. + for (in[0] = init; in[0] < 1.0; in[0] += step / 8) { + cmsDoTransform(xform1.get(), in, out1, 1); + cmsDoTransform(xform2.get(), in, out2, 1); + if (!ApproxEq(out1[0], out2[0], 2E-4)) { + return false; + } + } + } else { + for (in[0] = init; in[0] < 1.0; in[0] += step) { + for (in[1] = init; in[1] < 1.0; in[1] += step) { + for (in[2] = init; in[2] < 1.0; in[2] += step) { + cmsDoTransform(xform1.get(), in, out1, 1); + cmsDoTransform(xform2.get(), in, out2, 1); + for (size_t i = 0; i < 3; ++i) { + if (!ApproxEq(out1[i], out2[i], 2E-4)) { + return false; + } + } + } + } + } + } + + return true; +} + +// Returns white point that was specified when creating the profile. +// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation +// differs between ICC versions. +JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, + const Profile& profile, + const ColorEncoding& c) { + cmsCIEXYZ XYZ = {1.0, 1.0, 1.0}; + + Profile profile_xyz; + if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ; + // Array arguments are one per profile. + cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; + // Leave white point unchanged - that is what we're trying to extract. + cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC, + INTENT_ABSOLUTE_COLORIMETRIC}; + cmsBool black_compensation[2] = {0, 0}; + cmsFloat64Number adaption[2] = {0.0, 0.0}; + // Only transforming a single pixel, so skip expensive optimizations. + cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC; + Transform xform(cmsCreateExtendedTransform( + context, 2, profiles, black_compensation, intents, adaption, nullptr, 0, + Type64(c), TYPE_XYZ_DBL, flags)); + if (!xform) return XYZ; // TODO(lode): return error + + // xy are relative, so magnitude does not matter if we ignore output Y. + const cmsFloat64Number in[3] = {1.0, 1.0, 1.0}; + cmsDoTransform(xform.get(), in, &XYZ.X, 1); + return XYZ; +} + +Status IdentifyPrimaries(const Profile& profile, const cmsCIEXYZ& wp_unadapted, + ColorEncoding* c) { + if (!c->HasPrimaries()) return true; + if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true; + + // These were adapted to the profile illuminant before storing in the profile. + const cmsCIEXYZ* adapted_r = static_cast( + cmsReadTag(profile.get(), cmsSigRedColorantTag)); + const cmsCIEXYZ* adapted_g = static_cast( + cmsReadTag(profile.get(), cmsSigGreenColorantTag)); + const cmsCIEXYZ* adapted_b = static_cast( + cmsReadTag(profile.get(), cmsSigBlueColorantTag)); + if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) { + return JXL_FAILURE("Failed to retrieve colorants"); + } + + // TODO(janwas): no longer assume Bradford and D50. + // Undo the chromatic adaptation. + const cmsCIEXYZ d50 = D50_XYZ(); + + cmsCIEXYZ r, g, b; + cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r); + cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g); + cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b); + + const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g), + CIExyFromXYZ(b)}; + return c->SetPrimaries(rgb); +} + +void DetectTransferFunction(const cmsContext context, const Profile& profile, + ColorEncoding* JXL_RESTRICT c) { + if (c->tf.SetImplicit()) return; + + for (TransferFunction tf : Values()) { + // Can only create profile from known transfer function. + if (tf == TransferFunction::kUnknown) continue; + + c->tf.SetTransferFunction(tf); + + PaddedBytes icc_test; + if (MaybeCreateProfile(*c, &icc_test) && + ProfileEquivalentToICC(context, profile, icc_test, *c)) { + return; + } + } + + c->tf.SetTransferFunction(TransferFunction::kUnknown); +} + +void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) { + JXL_WARNING("LCMS error %u: %s", code, text); +} + +// Returns a context for the current thread, creating it if necessary. +cmsContext GetContext() { + static thread_local void* context_; + if (context_ == nullptr) { + context_ = cmsCreateContext(nullptr, nullptr); + JXL_ASSERT(context_ != nullptr); + + cmsSetLogErrorHandlerTHR(static_cast(context_), &ErrorHandler); + } + return static_cast(context_); +} + +#endif // JPEGXL_ENABLE_SKCMS + +} // namespace + +// All functions that call lcms directly (except ColorSpaceTransform::Run) must +// lock LcmsMutex(). + +Status ColorEncoding::SetFieldsFromICC() { + // In case parsing fails, mark the ColorEncoding as invalid. + SetColorSpace(ColorSpace::kUnknown); + tf.SetTransferFunction(TransferFunction::kUnknown); + + if (icc_.empty()) return JXL_FAILURE("Empty ICC profile"); + +#if JPEGXL_ENABLE_SKCMS + if (icc_.size() < 128) { + return JXL_FAILURE("ICC file too small"); + } + + skcms_ICCProfile profile; + JXL_RETURN_IF_ERROR(skcms_Parse(icc_.data(), icc_.size(), &profile)); + + // skcms does not return the rendering intent, so get it from the file. It + // is encoded as big-endian 32-bit integer in bytes 60..63. + uint32_t rendering_intent32 = icc_[67]; + if (rendering_intent32 > 3 || icc_[64] != 0 || icc_[65] != 0 || + icc_[66] != 0) { + return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32); + } + + SetColorSpace(ColorSpaceFromProfile(profile)); + + CIExy wp_unadapted; + JXL_RETURN_IF_ERROR(UnadaptedWhitePoint(profile, &wp_unadapted)); + JXL_RETURN_IF_ERROR(SetWhitePoint(wp_unadapted)); + + // Relies on color_space. + JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, this)); + + // Relies on color_space/white point/primaries being set already. + DetectTransferFunction(profile, this); + // ICC and RenderingIntent have the same values (0..3). + rendering_intent = static_cast(rendering_intent32); +#else // JPEGXL_ENABLE_SKCMS + + std::lock_guard guard(LcmsMutex()); + const cmsContext context = GetContext(); + + Profile profile; + JXL_RETURN_IF_ERROR(DecodeProfile(context, icc_, &profile)); + + const cmsUInt32Number rendering_intent32 = + cmsGetHeaderRenderingIntent(profile.get()); + if (rendering_intent32 > 3) { + return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32); + } + + SetColorSpace(ColorSpaceFromProfile(profile)); + + const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, *this); + JXL_RETURN_IF_ERROR(SetWhitePoint(CIExyFromXYZ(wp_unadapted))); + + // Relies on color_space. + JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, this)); + + // Relies on color_space/white point/primaries being set already. + DetectTransferFunction(context, profile, this); + + // ICC and RenderingIntent have the same values (0..3). + rendering_intent = static_cast(rendering_intent32); +#endif // JPEGXL_ENABLE_SKCMS + + return true; +} + +void ColorEncoding::DecideIfWantICC() { + PaddedBytes icc_new; + bool equivalent; +#if JPEGXL_ENABLE_SKCMS + skcms_ICCProfile profile; + if (!DecodeProfile(ICC(), &profile)) return; + if (!MaybeCreateProfile(*this, &icc_new)) return; + equivalent = ProfileEquivalentToICC(profile, icc_new); +#else // JPEGXL_ENABLE_SKCMS + const cmsContext context = GetContext(); + Profile profile; + if (!DecodeProfile(context, ICC(), &profile)) return; + if (!MaybeCreateProfile(*this, &icc_new)) return; + equivalent = ProfileEquivalentToICC(context, profile, icc_new, *this); +#endif // JPEGXL_ENABLE_SKCMS + + // Successfully created a profile => reconstruction should be equivalent. + JXL_ASSERT(equivalent); + want_icc_ = false; +} + +ColorSpaceTransform::~ColorSpaceTransform() { +#if !JPEGXL_ENABLE_SKCMS + std::lock_guard guard(LcmsMutex()); + TransformDeleter()(lcms_transform_); +#endif +} + +ColorSpaceTransform::ColorSpaceTransform() +#if JPEGXL_ENABLE_SKCMS + : skcms_icc_(new SkcmsICC()) +#endif // JPEGXL_ENABLE_SKCMS +{ +} + +Status ColorSpaceTransform::Init(const ColorEncoding& c_src, + const ColorEncoding& c_dst, + float intensity_target, size_t xsize, + const size_t num_threads) { + std::lock_guard guard(LcmsMutex()); +#if JXL_CMS_VERBOSE + printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str()); +#endif + +#if JPEGXL_ENABLE_SKCMS + skcms_icc_->icc_src_ = c_src.ICC(); + skcms_icc_->icc_dst_ = c_dst.ICC(); + JXL_RETURN_IF_ERROR( + DecodeProfile(skcms_icc_->icc_src_, &skcms_icc_->profile_src_)); + JXL_RETURN_IF_ERROR( + DecodeProfile(skcms_icc_->icc_dst_, &skcms_icc_->profile_dst_)); +#else // JPEGXL_ENABLE_SKCMS + const cmsContext context = GetContext(); + Profile profile_src, profile_dst; + JXL_RETURN_IF_ERROR(DecodeProfile(context, c_src.ICC(), &profile_src)); + JXL_RETURN_IF_ERROR(DecodeProfile(context, c_dst.ICC(), &profile_dst)); +#endif // JPEGXL_ENABLE_SKCMS + + skip_lcms_ = false; + if (c_src.SameColorEncoding(c_dst)) { + skip_lcms_ = true; +#if JXL_CMS_VERBOSE + printf("Skip CMS\n"); +#endif + } + + // Special-case for BT.2100 HLG/PQ and SRGB <=> linear: + const bool src_linear = c_src.tf.IsLinear(); + const bool dst_linear = c_dst.tf.IsLinear(); + if (((c_src.tf.IsPQ() || c_src.tf.IsHLG()) && dst_linear) || + ((c_dst.tf.IsPQ() || c_dst.tf.IsHLG()) && src_linear) || + ((c_src.tf.IsPQ() != c_dst.tf.IsPQ()) && intensity_target_ != 10000) || + (c_src.tf.IsSRGB() && dst_linear) || (c_dst.tf.IsSRGB() && src_linear)) { + // Construct new profiles as if the data were already/still linear. + ColorEncoding c_linear_src = c_src; + ColorEncoding c_linear_dst = c_dst; + c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear); + c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear); + PaddedBytes icc_src, icc_dst; +#if JPEGXL_ENABLE_SKCMS + skcms_ICCProfile new_src, new_dst; +#else // JPEGXL_ENABLE_SKCMS + Profile new_src, new_dst; +#endif // JPEGXL_ENABLE_SKCMS + // Only enable ExtraTF if profile creation succeeded. + if (MaybeCreateProfile(c_linear_src, &icc_src) && + MaybeCreateProfile(c_linear_dst, &icc_dst) && +#if JPEGXL_ENABLE_SKCMS + DecodeProfile(icc_src, &new_src) && DecodeProfile(icc_dst, &new_dst)) { +#else // JPEGXL_ENABLE_SKCMS + DecodeProfile(context, icc_src, &new_src) && + DecodeProfile(context, icc_dst, &new_dst)) { +#endif // JPEGXL_ENABLE_SKCMS + if (c_src.SameColorSpace(c_dst)) { + skip_lcms_ = true; + } +#if JXL_CMS_VERBOSE + printf("Special linear <-> HLG/PQ/sRGB; skip=%d\n", skip_lcms_); +#endif +#if JPEGXL_ENABLE_SKCMS + skcms_icc_->icc_src_ = PaddedBytes(); + skcms_icc_->profile_src_ = new_src; + skcms_icc_->icc_dst_ = PaddedBytes(); + skcms_icc_->profile_dst_ = new_dst; +#else // JPEGXL_ENABLE_SKCMS + profile_src.swap(new_src); + profile_dst.swap(new_dst); +#endif // JPEGXL_ENABLE_SKCMS + if (!c_src.tf.IsLinear()) { + preprocess_ = c_src.tf.IsSRGB() + ? ExtraTF::kSRGB + : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); + } + if (!c_dst.tf.IsLinear()) { + postprocess_ = c_dst.tf.IsSRGB() + ? ExtraTF::kSRGB + : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); + } + } else { + JXL_WARNING("Failed to create extra linear profiles"); + } + } + +#if JPEGXL_ENABLE_SKCMS + if (!skcms_MakeUsableAsDestination(&skcms_icc_->profile_dst_)) { + return JXL_FAILURE( + "Failed to make %s usable as a color transform destination", + Description(c_dst).c_str()); + } +#endif // JPEGXL_ENABLE_SKCMS + + // Not including alpha channel (copied separately). + const size_t channels_src = c_src.Channels(); + const size_t channels_dst = c_dst.Channels(); + JXL_CHECK(channels_src == channels_dst); +#if JXL_CMS_VERBOSE + printf("Channels: %zu; Threads: %zu\n", channels_src, num_threads); +#endif + +#if !JPEGXL_ENABLE_SKCMS + // Type includes color space (XYZ vs RGB), so can be different. + const uint32_t type_src = Type32(c_src); + const uint32_t type_dst = Type32(c_dst); + const uint32_t intent = static_cast(c_dst.rendering_intent); + // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling + // cmsDoTransform() thread-safe. + const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION | + cmsFLAGS_HIGHRESPRECALC; + lcms_transform_ = + cmsCreateTransformTHR(context, profile_src.get(), type_src, + profile_dst.get(), type_dst, intent, flags); + if (lcms_transform_ == nullptr) { + return JXL_FAILURE("Failed to create transform"); + } +#endif // !JPEGXL_ENABLE_SKCMS + + // Ideally LCMS would convert directly from External to Image3. However, + // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our + // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must + // be interleaved. Calling cmsDoTransform for each pixel is expensive + // (indirect call). We therefore transform rows, which requires per-thread + // buffers. To avoid separate allocations, we use the rows of an image. + // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit + // outputs (or vice versa), we use floating point input/output. +#if JPEGXL_ENABLE_SKCMS + // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB + // float buffers anyway. + buf_src_ = ImageF(xsize * 3, num_threads); + buf_dst_ = ImageF(xsize * 3, num_threads); +#else + buf_src_ = ImageF(xsize * channels_src, num_threads); + buf_dst_ = ImageF(xsize * channels_dst, num_threads); +#endif + intensity_target_ = intensity_target; + xsize_ = xsize; + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_color_management.h b/lib/jxl/enc_color_management.h new file mode 100644 index 0000000..9dbce85 --- /dev/null +++ b/lib/jxl/enc_color_management.h @@ -0,0 +1,70 @@ +// 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_ENC_COLOR_MANAGEMENT_H_ +#define LIB_JXL_ENC_COLOR_MANAGEMENT_H_ + +// ICC profiles and color space conversions. + +#include +#include + +#include + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// Run is thread-safe. +class ColorSpaceTransform { + public: + ColorSpaceTransform(); + ~ColorSpaceTransform(); + + // Cannot copy (transforms_ holds pointers). + ColorSpaceTransform(const ColorSpaceTransform&) = delete; + ColorSpaceTransform& operator=(const ColorSpaceTransform&) = delete; + + // "Constructor"; allocates for up to `num_threads`, or returns false. + // `intensity_target` is used for conversion to and from PQ, which is absolute + // (1 always represents 10000 cd/m²) and thus needs scaling in linear space if + // 1 is to represent another luminance level instead. + Status Init(const ColorEncoding& c_src, const ColorEncoding& c_dst, + float intensity_target, size_t xsize, size_t num_threads); + + float* BufSrc(const size_t thread) { return buf_src_.Row(thread); } + + float* BufDst(const size_t thread) { return buf_dst_.Row(thread); } + +#if JPEGXL_ENABLE_SKCMS + struct SkcmsICC; + std::unique_ptr skcms_icc_; +#else + void* lcms_transform_; +#endif + + ImageF buf_src_; + ImageF buf_dst_; + float intensity_target_; + size_t xsize_; + bool skip_lcms_ = false; + ExtraTF preprocess_ = ExtraTF::kNone; + ExtraTF postprocess_ = ExtraTF::kNone; +}; + +// buf_X can either be from BufX() or caller-allocated, interleaved storage. +// `thread` must be less than the `num_threads` passed to Init. +// `t` is non-const because buf_* may be modified. +void DoColorSpaceTransform(ColorSpaceTransform* t, size_t thread, + const float* buf_src, float* buf_dst); + +} // namespace jxl + +#endif // LIB_JXL_ENC_COLOR_MANAGEMENT_H_ diff --git a/lib/jxl/enc_comparator.cc b/lib/jxl/enc_comparator.cc new file mode 100644 index 0000000..f5b25f8 --- /dev/null +++ b/lib/jxl/enc_comparator.cc @@ -0,0 +1,140 @@ +// 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/enc_comparator.h" + +#include +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_gamma_correct.h" + +namespace jxl { +namespace { + +// color is linear, but blending happens in gamma-compressed space using +// (gamma-compressed) grayscale background color, alpha image represents +// weights of the sRGB colors in the [0 .. (1 << bit_depth) - 1] interval, +// output image is in linear space. +void AlphaBlend(const Image3F& in, const size_t c, float background_linear, + const ImageF& alpha, Image3F* out) { + const float background = LinearToSrgb8Direct(background_linear); + + for (size_t y = 0; y < out->ysize(); ++y) { + const float* JXL_RESTRICT row_a = alpha.ConstRow(y); + const float* JXL_RESTRICT row_i = in.ConstPlaneRow(c, y); + float* JXL_RESTRICT row_o = out->PlaneRow(c, y); + for (size_t x = 0; x < out->xsize(); ++x) { + const float a = row_a[x]; + if (a <= 0.f) { + row_o[x] = background_linear; + } else if (a >= 1.f) { + row_o[x] = row_i[x]; + } else { + const float w_fg = a; + const float w_bg = 1.0f - w_fg; + const float fg = w_fg * LinearToSrgb8Direct(row_i[x]); + const float bg = w_bg * background; + row_o[x] = Srgb8ToLinearDirect(fg + bg); + } + } + } +} + +const Image3F* AlphaBlend(const ImageBundle& ib, const Image3F& linear, + float background_linear, Image3F* copy) { + // No alpha => all opaque. + if (!ib.HasAlpha()) return &linear; + + *copy = Image3F(linear.xsize(), linear.ysize()); + for (size_t c = 0; c < 3; ++c) { + AlphaBlend(linear, c, background_linear, ib.alpha(), copy); + } + return copy; +} + +void AlphaBlend(float background_linear, ImageBundle* io_linear_srgb) { + // No alpha => all opaque. + if (!io_linear_srgb->HasAlpha()) return; + + for (size_t c = 0; c < 3; ++c) { + AlphaBlend(*io_linear_srgb->color(), c, background_linear, + *io_linear_srgb->alpha(), io_linear_srgb->color()); + } +} + +float ComputeScoreImpl(const ImageBundle& rgb0, const ImageBundle& rgb1, + Comparator* comparator, ImageF* distmap) { + JXL_CHECK(comparator->SetReferenceImage(rgb0)); + float score; + JXL_CHECK(comparator->CompareWith(rgb1, distmap, &score)); + return score; +} + +} // namespace + +float ComputeScore(const ImageBundle& rgb0, const ImageBundle& rgb1, + Comparator* comparator, ImageF* diffmap, ThreadPool* pool) { + PROFILER_FUNC; + // Convert to linear sRGB (unless already in that space) + ImageMetadata metadata0 = *rgb0.metadata(); + ImageBundle store0(&metadata0); + const ImageBundle* linear_srgb0; + JXL_CHECK(TransformIfNeeded(rgb0, ColorEncoding::LinearSRGB(rgb0.IsGray()), + pool, &store0, &linear_srgb0)); + ImageMetadata metadata1 = *rgb1.metadata(); + ImageBundle store1(&metadata1); + const ImageBundle* linear_srgb1; + JXL_CHECK(TransformIfNeeded(rgb1, ColorEncoding::LinearSRGB(rgb1.IsGray()), + pool, &store1, &linear_srgb1)); + + // No alpha: skip blending, only need a single call to Butteraugli. + if (!rgb0.HasAlpha() && !rgb1.HasAlpha()) { + return ComputeScoreImpl(*linear_srgb0, *linear_srgb1, comparator, diffmap); + } + + // Blend on black and white backgrounds + + const float black = 0.0f; + ImageBundle blended_black0 = linear_srgb0->Copy(); + ImageBundle blended_black1 = linear_srgb1->Copy(); + AlphaBlend(black, &blended_black0); + AlphaBlend(black, &blended_black1); + + const float white = 1.0f; + ImageBundle blended_white0 = linear_srgb0->Copy(); + ImageBundle blended_white1 = linear_srgb1->Copy(); + + AlphaBlend(white, &blended_white0); + AlphaBlend(white, &blended_white1); + + ImageF diffmap_black, diffmap_white; + const float dist_black = ComputeScoreImpl(blended_black0, blended_black1, + comparator, &diffmap_black); + const float dist_white = ComputeScoreImpl(blended_white0, blended_white1, + comparator, &diffmap_white); + + // diffmap and return values are the max of diffmap_black/white. + if (diffmap != nullptr) { + const size_t xsize = rgb0.xsize(); + const size_t ysize = rgb0.ysize(); + *diffmap = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* JXL_RESTRICT row_black = diffmap_black.ConstRow(y); + const float* JXL_RESTRICT row_white = diffmap_white.ConstRow(y); + float* JXL_RESTRICT row_out = diffmap->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = std::max(row_black[x], row_white[x]); + } + } + } + return std::max(dist_black, dist_white); +} + +} // namespace jxl diff --git a/lib/jxl/enc_comparator.h b/lib/jxl/enc_comparator.h new file mode 100644 index 0000000..e348a4e --- /dev/null +++ b/lib/jxl/enc_comparator.h @@ -0,0 +1,52 @@ +// 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_ENC_COMPARATOR_H_ +#define LIB_JXL_ENC_COMPARATOR_H_ + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +class Comparator { + public: + virtual ~Comparator() = default; + + // Sets the reference image, the first to compare + // Image must be in linear sRGB (gamma expanded) in range 0.0f-1.0f as + // the range from standard black point to standard white point, but values + // outside permitted. + virtual Status SetReferenceImage(const ImageBundle& ref) = 0; + + // Sets the actual image (with loss), the second to compare + // Image must be in linear sRGB (gamma expanded) in range 0.0f-1.0f as + // the range from standard black point to standard white point, but values + // outside permitted. + // In diffmap it outputs the local score per pixel, while in score it outputs + // a single score. Any one may be set to nullptr to not compute it. + virtual Status CompareWith(const ImageBundle& actual, ImageF* diffmap, + float* score) = 0; + + // Quality thresholds for diffmap and score values. + // The good score must represent a value where the images are considered to + // be perceptually indistinguishable (but not identical) + // The bad value must be larger than good to indicate "lower means better" + // and smaller than good to indicate "higher means better" + virtual float GoodQualityScore() const = 0; + virtual float BadQualityScore() const = 0; +}; + +// Computes the score given images in any RGB color model, optionally with +// alpha channel. +float ComputeScore(const ImageBundle& rgb0, const ImageBundle& rgb1, + Comparator* comparator, ImageF* diffmap = nullptr, + ThreadPool* pool = nullptr); + +} // namespace jxl + +#endif // LIB_JXL_ENC_COMPARATOR_H_ diff --git a/lib/jxl/enc_context_map.cc b/lib/jxl/enc_context_map.cc new file mode 100644 index 0000000..4719a25 --- /dev/null +++ b/lib/jxl/enc_context_map.cc @@ -0,0 +1,139 @@ +// 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. + +// Library to encode the context map. + +#include "lib/jxl/enc_context_map.h" + +#include + +#include +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/entropy_coder.h" + +namespace jxl { + +namespace { + +size_t IndexOf(const std::vector& v, uint8_t value) { + size_t i = 0; + for (; i < v.size(); ++i) { + if (v[i] == value) return i; + } + return i; +} + +void MoveToFront(std::vector* v, size_t index) { + uint8_t value = (*v)[index]; + for (size_t i = index; i != 0; --i) { + (*v)[i] = (*v)[i - 1]; + } + (*v)[0] = value; +} + +std::vector MoveToFrontTransform(const std::vector& v) { + if (v.empty()) return v; + uint8_t max_value = *std::max_element(v.begin(), v.end()); + std::vector mtf(max_value + 1); + for (size_t i = 0; i <= max_value; ++i) mtf[i] = i; + std::vector result(v.size()); + for (size_t i = 0; i < v.size(); ++i) { + size_t index = IndexOf(mtf, v[i]); + JXL_ASSERT(index < mtf.size()); + result[i] = static_cast(index); + MoveToFront(&mtf, index); + } + return result; +} + +} // namespace + +void EncodeContextMap(const std::vector& context_map, + size_t num_histograms, BitWriter* writer) { + if (num_histograms == 1) { + // Simple code + writer->Write(1, 1); + // 0 bits per entry. + writer->Write(2, 0); + return; + } + + std::vector transformed_symbols = MoveToFrontTransform(context_map); + std::vector> tokens(1), mtf_tokens(1); + EntropyEncodingData codes; + std::vector dummy_context_map; + for (size_t i = 0; i < context_map.size(); i++) { + tokens[0].emplace_back(0, context_map[i]); + } + for (size_t i = 0; i < transformed_symbols.size(); i++) { + mtf_tokens[0].emplace_back(0, transformed_symbols[i]); + } + HistogramParams params; + params.uint_method = HistogramParams::HybridUintMethod::kContextMap; + size_t ans_cost = BuildAndEncodeHistograms( + params, 1, tokens, &codes, &dummy_context_map, nullptr, 0, nullptr); + size_t mtf_cost = BuildAndEncodeHistograms( + params, 1, mtf_tokens, &codes, &dummy_context_map, nullptr, 0, nullptr); + bool use_mtf = mtf_cost < ans_cost; + // Rebuild token list. + tokens[0].clear(); + for (size_t i = 0; i < transformed_symbols.size(); i++) { + tokens[0].emplace_back(0, + use_mtf ? transformed_symbols[i] : context_map[i]); + } + size_t entry_bits = CeilLog2Nonzero(num_histograms); + size_t simple_cost = entry_bits * context_map.size(); + if (entry_bits < 4 && simple_cost < ans_cost && simple_cost < mtf_cost) { + writer->Write(1, 1); + writer->Write(2, entry_bits); + for (size_t i = 0; i < context_map.size(); i++) { + writer->Write(entry_bits, context_map[i]); + } + } else { + writer->Write(1, 0); + writer->Write(1, use_mtf); // Use/don't use MTF. + BuildAndEncodeHistograms(params, 1, tokens, &codes, &dummy_context_map, + writer, 0, nullptr); + WriteTokens(tokens[0], codes, dummy_context_map, writer); + } +} + +void EncodeBlockCtxMap(const BlockCtxMap& block_ctx_map, BitWriter* writer, + AuxOut* aux_out) { + auto& dct = block_ctx_map.dc_thresholds; + auto& qft = block_ctx_map.qf_thresholds; + auto& ctx_map = block_ctx_map.ctx_map; + BitWriter::Allotment allotment( + writer, + (dct[0].size() + dct[1].size() + dct[2].size() + qft.size()) * 34 + 1 + + 4 + 4 + ctx_map.size() * 10 + 1024); + if (dct[0].empty() && dct[1].empty() && dct[2].empty() && qft.empty() && + ctx_map.size() == 21 && + std::equal(ctx_map.begin(), ctx_map.end(), BlockCtxMap::kDefaultCtxMap)) { + writer->Write(1, 1); // default + ReclaimAndCharge(writer, &allotment, kLayerAC, aux_out); + return; + } + writer->Write(1, 0); + for (int j : {0, 1, 2}) { + writer->Write(4, dct[j].size()); + for (int i : dct[j]) { + JXL_CHECK(U32Coder::Write(kDCThresholdDist, PackSigned(i), writer)); + } + } + writer->Write(4, qft.size()); + for (uint32_t i : qft) { + JXL_CHECK(U32Coder::Write(kQFThresholdDist, i - 1, writer)); + } + EncodeContextMap(ctx_map, block_ctx_map.num_ctxs, writer); + ReclaimAndCharge(writer, &allotment, kLayerAC, aux_out); +} + +} // namespace jxl diff --git a/lib/jxl/enc_context_map.h b/lib/jxl/enc_context_map.h new file mode 100644 index 0000000..7f6c624 --- /dev/null +++ b/lib/jxl/enc_context_map.h @@ -0,0 +1,33 @@ +// 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_ENC_CONTEXT_MAP_H_ +#define LIB_JXL_ENC_CONTEXT_MAP_H_ + +#include +#include + +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +// Max limit is 255 because encoding assumes numbers < 255 +// More clusters can help compression, but makes encode/decode somewhat slower +static const size_t kClustersLimit = 128; + +// Encodes the given context map to the bit stream. The number of different +// histogram ids is given by num_histograms. +void EncodeContextMap(const std::vector& context_map, + size_t num_histograms, BitWriter* writer); + +void EncodeBlockCtxMap(const BlockCtxMap& block_ctx_map, BitWriter* writer, + AuxOut* aux_out); +} // namespace jxl + +#endif // LIB_JXL_ENC_CONTEXT_MAP_H_ diff --git a/lib/jxl/enc_detect_dots.cc b/lib/jxl/enc_detect_dots.cc new file mode 100644 index 0000000..07e4003 --- /dev/null +++ b/lib/jxl/enc_detect_dots.cc @@ -0,0 +1,620 @@ +// 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/enc_detect_dots.h" + +#include + +#include +#include +#include +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_detect_dots.cc" +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/common.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/linalg.h" +#include "lib/jxl/optimize.h" + +// Set JXL_DEBUG_DOT_DETECT to 1 to enable debugging. +#ifndef JXL_DEBUG_DOT_DETECT +#define JXL_DEBUG_DOT_DETECT 0 +#endif + +#if JXL_DEBUG_DOT_DETECT +#include "lib/jxl/aux_out.h" +#endif + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +ImageF SumOfSquareDifferences(const Image3F& forig, const Image3F& smooth, + ThreadPool* pool) { + const HWY_FULL(float) d; + const auto color_coef0 = Set(d, 0.0f); + const auto color_coef1 = Set(d, 10.0f); + const auto color_coef2 = Set(d, 0.0f); + + ImageF sum_of_squares(forig.xsize(), forig.ysize()); + RunOnPool( + pool, 0, forig.ysize(), ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = static_cast(task); + const float* JXL_RESTRICT orig_row0 = forig.Plane(0).ConstRow(y); + const float* JXL_RESTRICT orig_row1 = forig.Plane(1).ConstRow(y); + const float* JXL_RESTRICT orig_row2 = forig.Plane(2).ConstRow(y); + const float* JXL_RESTRICT smooth_row0 = smooth.Plane(0).ConstRow(y); + const float* JXL_RESTRICT smooth_row1 = smooth.Plane(1).ConstRow(y); + const float* JXL_RESTRICT smooth_row2 = smooth.Plane(2).ConstRow(y); + float* JXL_RESTRICT sos_row = sum_of_squares.Row(y); + + for (size_t x = 0; x < forig.xsize(); x += Lanes(d)) { + auto v0 = Load(d, orig_row0 + x) - Load(d, smooth_row0 + x); + auto v1 = Load(d, orig_row1 + x) - Load(d, smooth_row1 + x); + auto v2 = Load(d, orig_row2 + x) - Load(d, smooth_row2 + x); + v0 *= v0; + v1 *= v1; + v2 *= v2; + v0 *= color_coef0; // FMA doesn't help here. + v1 *= color_coef1; + v2 *= color_coef2; + const auto sos = v0 + v1 + v2; // weighted sum of square diffs + Store(sos, d, sos_row + x); + } + }, + "ComputeEnergyImage"); + return sum_of_squares; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(SumOfSquareDifferences); // Local function + +const int kEllipseWindowSize = 5; + +namespace { +struct GaussianEllipse { + double x; // position in x + double y; // position in y + double sigma_x; // scale in x + double sigma_y; // scale in y + double angle; // ellipse rotation in radians + std::array intensity; // intensity in each channel + + // The following variables do not need to be encoded + double l2_loss; // error after the Gaussian was fit + double l1_loss; + double ridge_loss; // the l2_loss plus regularization term + double custom_loss; // experimental custom loss + std::array bgColor; // best background color + size_t neg_pixels; // number of negative pixels when subtracting dot + std::array neg_value; // debt due to channel truncation +}; +double DotGaussianModel(double dx, double dy, double ct, double st, + double sigma_x, double sigma_y, double intensity) { + double rx = ct * dx + st * dy; + double ry = -st * dx + ct * dy; + double md = (rx * rx / sigma_x) + (ry * ry / sigma_y); + double value = intensity * exp(-0.5 * md); + return value; +} + +constexpr bool kOptimizeBackground = true; + +// Gaussian that smooths noise but preserves dots +const WeightsSeparable5& WeightsSeparable5Gaussian0_65() { + constexpr float w0 = 0.558311f; + constexpr float w1 = 0.210395f; + constexpr float w2 = 0.010449f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; +} + +// (Iterated) Gaussian that removes dots. +const WeightsSeparable5& WeightsSeparable5Gaussian3() { + constexpr float w0 = 0.222338f; + constexpr float w1 = 0.210431f; + constexpr float w2 = 0.1784f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; +} + +ImageF ComputeEnergyImage(const Image3F& orig, Image3F* smooth, + ThreadPool* pool) { + PROFILER_FUNC; + + // Prepare guidance images for dot selection. + Image3F forig(orig.xsize(), orig.ysize()); + Image3F tmp(orig.xsize(), orig.ysize()); + *smooth = Image3F(orig.xsize(), orig.ysize()); + + const auto& weights1 = WeightsSeparable5Gaussian0_65(); + const auto& weights3 = WeightsSeparable5Gaussian3(); + + Separable5_3(orig, Rect(orig), weights1, pool, &forig); + + Separable5_3(orig, Rect(orig), weights3, pool, &tmp); + Separable5_3(tmp, Rect(tmp), weights3, pool, smooth); + +#if JXL_DEBUG_DOT_DETECT + AuxOut aux; + aux.debug_prefix = "/tmp/sebastian/"; + aux.DumpImage("filtered", forig); + aux.DumpImage("sm", *smooth); +#endif + + return HWY_DYNAMIC_DISPATCH(SumOfSquareDifferences)(forig, *smooth, pool); +} + +struct Pixel { + int x; + int y; +}; + +Pixel operator+(const Pixel& a, const Pixel& b) { + return Pixel{a.x + b.x, a.y + b.y}; +} + +// Maximum area in pixels of a ellipse +const size_t kMaxCCSize = 1000; + +// Extracts a connected component from a Binary image where seed is part +// of the component +bool ExtractComponent(ImageF* img, std::vector* pixels, + const Pixel& seed, double threshold) { + PROFILER_FUNC; + static const std::vector neighbors{{1, -1}, {1, 0}, {1, 1}, {0, -1}, + {0, 1}, {-1, -1}, {-1, 1}, {1, 0}}; + std::vector q{seed}; + while (!q.empty()) { + Pixel current = q.back(); + q.pop_back(); + pixels->push_back(current); + if (pixels->size() > kMaxCCSize) return false; + for (const Pixel& delta : neighbors) { + Pixel child = current + delta; + if (child.x >= 0 && static_cast(child.x) < img->xsize() && + child.y >= 0 && static_cast(child.y) < img->ysize()) { + float* value = &img->Row(child.y)[child.x]; + if (*value > threshold) { + *value = 0.0; + q.push_back(child); + } + } + } + } + return true; +} + +inline bool PointInRect(const Rect& r, const Pixel& p) { + return (static_cast(p.x) >= r.x0() && + static_cast(p.x) < (r.x0() + r.xsize()) && + static_cast(p.y) >= r.y0() && + static_cast(p.y) < (r.y0() + r.ysize())); +} + +struct ConnectedComponent { + ConnectedComponent(const Rect& bounds, const std::vector&& pixels) + : bounds(bounds), pixels(pixels) {} + Rect bounds; + std::vector pixels; + float maxEnergy; + float meanEnergy; + float varEnergy; + float meanBg; + float varBg; + float score; + Pixel mode; + + void CompStats(const ImageF& energy, int extra) { + PROFILER_FUNC; + maxEnergy = 0.0; + meanEnergy = 0.0; + varEnergy = 0.0; + meanBg = 0.0; + varBg = 0.0; + int nIn = 0; + int nOut = 0; + mode.x = 0; + mode.y = 0; + for (int sy = -extra; sy < (static_cast(bounds.ysize()) + extra); + sy++) { + int y = sy + static_cast(bounds.y0()); + if (y < 0 || static_cast(y) >= energy.ysize()) continue; + const float* JXL_RESTRICT erow = energy.ConstRow(y); + for (int sx = -extra; sx < (static_cast(bounds.xsize()) + extra); + sx++) { + int x = sx + static_cast(bounds.x0()); + if (x < 0 || static_cast(x) >= energy.xsize()) continue; + if (erow[x] > maxEnergy) { + maxEnergy = erow[x]; + mode.x = x; + mode.y = y; + } + if (PointInRect(bounds, Pixel{x, y})) { + meanEnergy += erow[x]; + varEnergy += erow[x] * erow[x]; + nIn++; + } else { + meanBg += erow[x]; + varBg += erow[x] * erow[x]; + nOut++; + } + } + } + meanEnergy = meanEnergy / nIn; + meanBg = meanBg / nOut; + varEnergy = (varEnergy / nIn) - meanEnergy * meanEnergy; + varBg = (varBg / nOut) - meanBg * meanBg; + score = (meanEnergy - meanBg) / std::sqrt(varBg); + } +}; + +Rect BoundingRectangle(const std::vector& pixels) { + PROFILER_FUNC; + JXL_ASSERT(!pixels.empty()); + int low_x, high_x, low_y, high_y; + low_x = high_x = pixels[0].x; + low_y = high_y = pixels[0].y; + for (const Pixel& p : pixels) { + low_x = std::min(low_x, p.x); + high_x = std::max(high_x, p.x); + low_y = std::min(low_y, p.y); + high_y = std::max(high_y, p.y); + } + return Rect(low_x, low_y, high_x - low_x + 1, high_y - low_y + 1); +} + +std::vector FindCC(const ImageF& energy, double t_low, + double t_high, uint32_t maxWindow, + double minScore) { + PROFILER_FUNC; + const int kExtraRect = 4; + ImageF img = CopyImage(energy); + std::vector ans; + for (size_t y = 0; y < img.ysize(); y++) { + float* JXL_RESTRICT row = img.Row(y); + for (size_t x = 0; x < img.xsize(); x++) { + if (row[x] > t_high) { + std::vector pixels; + row[x] = 0.0; + bool success = ExtractComponent( + &img, &pixels, Pixel{static_cast(x), static_cast(y)}, + t_low); + if (!success) continue; +#if JXL_DEBUG_DOT_DETECT + for (size_t i = 0; i < pixels.size(); i++) { + fprintf(stderr, "(%d,%d) ", pixels[i].x, pixels[i].y); + } + fprintf(stderr, "\n"); +#endif // JXL_DEBUG_DOT_DETECT + Rect bounds = BoundingRectangle(pixels); + if (bounds.xsize() < maxWindow && bounds.ysize() < maxWindow) { + ConnectedComponent cc{bounds, std::move(pixels)}; + cc.CompStats(energy, kExtraRect); + if (cc.score < minScore) continue; + JXL_DEBUG(JXL_DEBUG_DOT_DETECT, + "cc mode: (%d,%d), max: %f, bgMean: %f bgVar: " + "%f bound:(%zu,%zu,%zu,%zu)\n", + cc.mode.x, cc.mode.y, cc.maxEnergy, cc.meanEnergy, + cc.varEnergy, cc.bounds.x0(), cc.bounds.y0(), + cc.bounds.xsize(), cc.bounds.ysize()); + ans.push_back(cc); + } + } + } + } + return ans; +} + +// TODO (sggonzalez): Adapt this function for the different color spaces or +// remove it if the color space with the best performance does not need it +void ComputeDotLosses(GaussianEllipse* ellipse, const ConnectedComponent& cc, + const Image3F& img, const Image3F& background) { + PROFILER_FUNC; + const int rectBounds = 2; + const double kIntensityR = 0.0; // 0.015; + const double kSigmaR = 0.0; // 0.01; + const double kZeroEpsilon = 0.1; // Tolerance to consider a value negative + double ct = cos(ellipse->angle), st = sin(ellipse->angle); + const std::array channelGains{{1.0, 1.0, 1.0}}; + int N = 0; + ellipse->l1_loss = 0.0; + ellipse->l2_loss = 0.0; + ellipse->neg_pixels = 0; + ellipse->neg_value.fill(0.0); + double distMeanModeSq = (cc.mode.x - ellipse->x) * (cc.mode.x - ellipse->x) + + (cc.mode.y - ellipse->y) * (cc.mode.y - ellipse->y); + ellipse->custom_loss = 0.0; + for (int c = 0; c < 3; c++) { + for (int sy = -rectBounds; + sy < (static_cast(cc.bounds.ysize()) + rectBounds); sy++) { + int y = sy + cc.bounds.y0(); + if (y < 0 || static_cast(y) >= img.ysize()) continue; + const float* JXL_RESTRICT row = img.ConstPlaneRow(c, y); + // bgrow is only used if kOptimizeBackground is false. + // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) + const float* JXL_RESTRICT bgrow = background.ConstPlaneRow(c, y); + for (int sx = -rectBounds; + sx < (static_cast(cc.bounds.xsize()) + rectBounds); sx++) { + int x = sx + cc.bounds.x0(); + if (x < 0 || static_cast(x) >= img.xsize()) continue; + double target = row[x]; + double dotDelta = DotGaussianModel( + x - ellipse->x, y - ellipse->y, ct, st, ellipse->sigma_x, + ellipse->sigma_y, ellipse->intensity[c]); + if (dotDelta > target + kZeroEpsilon) { + ellipse->neg_pixels++; + ellipse->neg_value[c] += dotDelta - target; + } + double bkg = kOptimizeBackground ? ellipse->bgColor[c] : bgrow[x]; + double pred = bkg + dotDelta; + double diff = target - pred; + double l2 = channelGains[c] * diff * diff; + double l1 = channelGains[c] * std::fabs(diff); + ellipse->l2_loss += l2; + ellipse->l1_loss += l1; + double w = DotGaussianModel(x - cc.mode.x, y - cc.mode.y, 1.0, 0.0, + 1.0 + ellipse->sigma_x, + 1.0 + ellipse->sigma_y, 1.0); + ellipse->custom_loss += w * l2; + N++; + } + } + } + ellipse->l2_loss /= N; + ellipse->custom_loss /= N; + ellipse->custom_loss += 20.0 * distMeanModeSq + ellipse->neg_value[1]; + ellipse->l1_loss /= N; + double ridgeTerm = kSigmaR * ellipse->sigma_x + kSigmaR * ellipse->sigma_y; + for (int c = 0; c < 3; c++) { + ridgeTerm += kIntensityR * ellipse->intensity[c] * ellipse->intensity[c]; + } + ellipse->ridge_loss = ellipse->l2_loss + ridgeTerm; +} + +GaussianEllipse FitGaussianFast(const ConnectedComponent& cc, + const ImageF& energy, const Image3F& img, + const Image3F& background) { + PROFILER_FUNC; + constexpr bool leastSqIntensity = true; + constexpr double kEpsilon = 1e-6; + GaussianEllipse ans; + constexpr int kRectBounds = (kEllipseWindowSize >> 1); + + // Compute the 1st and 2nd moments of the CC + double sum = 0.0; + int N = 0; + std::array m1{{0.0, 0.0, 0.0}}; + std::array m2{{0.0, 0.0, 0.0}}; + std::array color{{0.0, 0.0, 0.0}}; + std::array bgColor{{0.0, 0.0, 0.0}}; + + JXL_DEBUG(JXL_DEBUG_DOT_DETECT, "%zu %zu %zu %zu\n", cc.bounds.x0(), + cc.bounds.y0(), cc.bounds.xsize(), cc.bounds.ysize()); + for (int c = 0; c < 3; c++) { + color[c] = img.ConstPlaneRow(c, cc.mode.y)[cc.mode.x] - + background.ConstPlaneRow(c, cc.mode.y)[cc.mode.x]; + } + double sign = (color[1] > 0) ? 1 : -1; + for (int sy = -kRectBounds; sy <= kRectBounds; sy++) { + int y = sy + cc.mode.y; + if (y < 0 || static_cast(y) >= energy.ysize()) continue; + const float* JXL_RESTRICT row = img.ConstPlaneRow(1, y); + const float* JXL_RESTRICT bgrow = background.ConstPlaneRow(1, y); + for (int sx = -kRectBounds; sx <= kRectBounds; sx++) { + int x = sx + cc.mode.x; + if (x < 0 || static_cast(x) >= energy.xsize()) continue; + double w = std::max(kEpsilon, sign * (row[x] - bgrow[x])); + sum += w; + + m1[0] += w * x; + m1[1] += w * y; + m2[0] += w * x * x; + m2[1] += w * x * y; + m2[2] += w * y * y; + for (int c = 0; c < 3; c++) { + bgColor[c] += background.ConstPlaneRow(c, y)[x]; + } + N++; + } + } + JXL_CHECK(N > 0); + + for (int i = 0; i < 3; i++) { + m1[i] /= sum; + m2[i] /= sum; + bgColor[i] /= N; + } + + // Some magic constants + constexpr double kSigmaMult = 1.0; + constexpr std::array kScaleMult{{1.1, 1.1, 1.1}}; + + // Now set the parameters of the Gaussian + ans.x = m1[0]; + ans.y = m1[1]; + for (int j = 0; j < 3; j++) { + ans.intensity[j] = kScaleMult[j] * color[j]; + } + + ImageD Sigma(2, 2), D(1, 2), U(2, 2); + Sigma.Row(0)[0] = m2[0] - m1[0] * m1[0]; + Sigma.Row(1)[1] = m2[2] - m1[1] * m1[1]; + Sigma.Row(0)[1] = Sigma.Row(1)[0] = m2[1] - m1[0] * m1[1]; + ConvertToDiagonal(Sigma, &D, &U); + const double* JXL_RESTRICT d = D.ConstRow(0); + const double* JXL_RESTRICT u = U.ConstRow(1); + int p1 = 0, p2 = 1; + if (d[0] < d[1]) std::swap(p1, p2); + ans.sigma_x = kSigmaMult * d[p1]; + ans.sigma_y = kSigmaMult * d[p2]; + ans.angle = std::atan2(u[p1], u[p2]); + ans.l2_loss = 0.0; + ans.bgColor = bgColor; + if (leastSqIntensity) { + GaussianEllipse* ellipse = &ans; + double ct = cos(ans.angle), st = sin(ans.angle); + // Estimate intensity with least squares (fixed background) + for (int c = 0; c < 3; c++) { + double gg = 0.0; + double gd = 0.0; + int yc = static_cast(cc.mode.y); + int xc = static_cast(cc.mode.x); + for (int y = yc - kRectBounds; y <= yc + kRectBounds; y++) { + if (y < 0 || static_cast(y) >= img.ysize()) continue; + const float* JXL_RESTRICT row = img.ConstPlaneRow(c, y); + const float* JXL_RESTRICT bgrow = background.ConstPlaneRow(c, y); + for (int x = xc - kRectBounds; x <= xc + kRectBounds; x++) { + if (x < 0 || static_cast(x) >= img.xsize()) continue; + double target = row[x] - bgrow[x]; + double gaussian = + DotGaussianModel(x - ellipse->x, y - ellipse->y, ct, st, + ellipse->sigma_x, ellipse->sigma_y, 1.0); + gg += gaussian * gaussian; + gd += gaussian * target; + } + } + ans.intensity[c] = gd / (gg + 1e-6); // Regularized least squares + } + } + ComputeDotLosses(&ans, cc, img, background); + return ans; +} + +GaussianEllipse FitGaussian(const ConnectedComponent& cc, const ImageF& energy, + const Image3F& img, const Image3F& background) { + auto ellipse = FitGaussianFast(cc, energy, img, background); + if (ellipse.sigma_x < ellipse.sigma_y) { + std::swap(ellipse.sigma_x, ellipse.sigma_y); + ellipse.angle += kPi / 2.0; + } + ellipse.angle -= kPi * std::floor(ellipse.angle / kPi); + if (fabs(ellipse.angle - kPi) < 1e-6 || fabs(ellipse.angle) < 1e-6) { + ellipse.angle = 0.0; + } + JXL_CHECK(ellipse.angle >= 0 && ellipse.angle <= kPi && + ellipse.sigma_x >= ellipse.sigma_y); + JXL_DEBUG(JXL_DEBUG_DOT_DETECT, + "Ellipse mu=(%lf,%lf) sigma=(%lf,%lf) angle=%lf " + "intensity=(%lf,%lf,%lf) bg=(%lf,%lf,%lf) l2_loss=%lf " + "custom_loss=%lf, neg_pix=%zu, neg_v=(%lf,%lf,%lf)\n", + ellipse.x, ellipse.y, ellipse.sigma_x, ellipse.sigma_y, + ellipse.angle, ellipse.intensity[0], ellipse.intensity[1], + ellipse.intensity[2], ellipse.bgColor[0], ellipse.bgColor[1], + ellipse.bgColor[2], ellipse.l2_loss, ellipse.custom_loss, + ellipse.neg_pixels, ellipse.neg_value[0], ellipse.neg_value[1], + ellipse.neg_value[2]); + return ellipse; +} + +} // namespace + +std::vector DetectGaussianEllipses( + const Image3F& opsin, const GaussianDetectParams& params, + const EllipseQuantParams& qParams, ThreadPool* pool) { + PROFILER_FUNC; + std::vector dots; + Image3F smooth(opsin.xsize(), opsin.ysize()); + ImageF energy = ComputeEnergyImage(opsin, &smooth, pool); +#if JXL_DEBUG_DOT_DETECT + AuxOut aux; + aux.debug_prefix = "/tmp/sebastian/"; + aux.DumpXybImage("smooth", smooth); + aux.DumpPlaneNormalized("energy", energy); +#endif // JXL_DEBUG_DOT_DETECT + std::vector components = FindCC( + energy, params.t_low, params.t_high, params.maxWinSize, params.minScore); + size_t numCC = + std::min(params.maxCC, (components.size() * params.percCC) / 100); + if (components.size() > numCC) { + std::sort( + components.begin(), components.end(), + [](const ConnectedComponent& a, const ConnectedComponent& b) -> bool { + return a.score > b.score; + }); + components.erase(components.begin() + numCC, components.end()); + } + for (const auto& cc : components) { + GaussianEllipse ellipse = FitGaussian(cc, energy, opsin, smooth); + if (ellipse.x < 0.0 || + std::ceil(ellipse.x) >= static_cast(opsin.xsize()) || + ellipse.y < 0.0 || + std::ceil(ellipse.y) >= static_cast(opsin.ysize())) { + continue; + } + if (ellipse.neg_pixels > params.maxNegPixels) continue; + double intensity = 0.21 * ellipse.intensity[0] + + 0.72 * ellipse.intensity[1] + + 0.07 * ellipse.intensity[2]; + double intensitySq = intensity * intensity; + // for (int c = 0; c < 3; c++) { + // intensitySq += ellipse.intensity[c] * ellipse.intensity[c]; + //} + double sqDistMeanMode = (ellipse.x - cc.mode.x) * (ellipse.x - cc.mode.x) + + (ellipse.y - cc.mode.y) * (ellipse.y - cc.mode.y); + if (ellipse.l2_loss < params.maxL2Loss && + ellipse.custom_loss < params.maxCustomLoss && + intensitySq > (params.minIntensity * params.minIntensity) && + sqDistMeanMode < params.maxDistMeanMode * params.maxDistMeanMode) { + size_t x0 = cc.bounds.x0(); + size_t y0 = cc.bounds.y0(); + dots.emplace_back(); + dots.back().second.emplace_back(x0, y0); + QuantizedPatch& patch = dots.back().first; + patch.xsize = cc.bounds.xsize(); + patch.ysize = cc.bounds.ysize(); + for (size_t y = 0; y < patch.ysize; y++) { + for (size_t x = 0; x < patch.xsize; x++) { + for (size_t c = 0; c < 3; c++) { + patch.fpixels[c][y * patch.xsize + x] = + opsin.ConstPlaneRow(c, y0 + y)[x0 + x] - + smooth.ConstPlaneRow(c, y0 + y)[x0 + x]; + } + } + } + } + } +#if JXL_DEBUG_DOT_DETECT + JXL_DEBUG(JXL_DEBUG_DOT_DETECT, "Candidates: %zu, Dots: %zu\n", + components.size(), dots.size()); + ApplyGaussianEllipses(&smooth, dots, 1.0); + aux.DumpXybImage("draw", smooth); + ApplyGaussianEllipses(&smooth, dots, -1.0); + + auto qdots = QuantizeGaussianEllipses(dots, qParams); + auto deq = DequantizeGaussianEllipses(qdots, qParams); + ApplyGaussianEllipses(&smooth, deq, 1.0); + aux.DumpXybImage("qdraw", smooth); + ApplyGaussianEllipses(&smooth, deq, -1.0); +#endif // JXL_DEBUG_DOT_DETECT + return dots; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_detect_dots.h b/lib/jxl/enc_detect_dots.h new file mode 100644 index 0000000..6e06a16 --- /dev/null +++ b/lib/jxl/enc_detect_dots.h @@ -0,0 +1,66 @@ +// 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. + +// We attempt to remove dots, or speckle from images using Gaussian blur. +#ifndef LIB_JXL_ENC_DETECT_DOTS_H_ +#define LIB_JXL_ENC_DETECT_DOTS_H_ + +#include +#include + +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/image.h" + +namespace jxl { + +struct GaussianDetectParams { + double t_high = 0; // at least one pixel must have larger energy than t_high + double t_low = 0; // all pixels must have a larger energy than tLow + uint32_t maxWinSize = 0; // discard dots larger than this containing window + double maxL2Loss = 0; + double maxCustomLoss = 0; + double minIntensity = 0; // If the intensity is too low, discard it + double maxDistMeanMode = 0; // The mean and the mode must be close + size_t maxNegPixels = 0; // Maximum number of negative pixel + size_t minScore = 0; + size_t maxCC = 50; // Maximum number of CC to keep + size_t percCC = 15; // Percentage in [0,100] of CC to keep +}; + +// Ellipse Quantization Params +struct EllipseQuantParams { + size_t xsize; // Image size in x + size_t ysize; // Image size in y + size_t qPosition; // Position quantization delta + // Quantization for the Gaussian sigma parameters + double minSigma; + double maxSigma; + size_t qSigma; // number of quantization levels + // Quantization for the rotation angle (between -pi and pi) + size_t qAngle; + // Quantization for the intensity + std::array minIntensity; + std::array maxIntensity; + std::array qIntensity; // number of quantization levels + // Extra parameters for the encoding + bool subtractQuantized; // Should we subtract quantized or detected dots? + float ytox; + float ytob; + + void QuantPositionSize(size_t* xsize, size_t* ysize) const; +}; + +// Detects dots in XYB image. +std::vector DetectGaussianEllipses( + const Image3F& opsin, const GaussianDetectParams& params, + const EllipseQuantParams& qParams, ThreadPool* pool); + +} // namespace jxl + +#endif // LIB_JXL_ENC_DETECT_DOTS_H_ diff --git a/lib/jxl/enc_dot_dictionary.cc b/lib/jxl/enc_dot_dictionary.cc new file mode 100644 index 0000000..1b5413b --- /dev/null +++ b/lib/jxl/enc_dot_dictionary.cc @@ -0,0 +1,72 @@ +// 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/enc_dot_dictionary.h" + +#include +#include + +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_detect_dots.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// Private implementation of Dictionary Encode/Decode +namespace { + +/* Quantization constants for Ellipse dots */ +const size_t kEllipsePosQ = 2; // Quantization level for the position +const double kEllipseMinSigma = 0.1; // Minimum sigma value +const double kEllipseMaxSigma = 3.1; // Maximum Sigma value +const size_t kEllipseSigmaQ = 16; // Number of quantization levels for sigma +const size_t kEllipseAngleQ = 8; // Quantization level for the angle +// TODO: fix these values. +const std::array kEllipseMinIntensity{{-0.05, 0.0, -0.5}}; +const std::array kEllipseMaxIntensity{{0.05, 1.0, 0.4}}; +const std::array kEllipseIntensityQ{{10, 36, 10}}; +} // namespace + +std::vector FindDotDictionary(const CompressParams& cparams, + const Image3F& opsin, + const ColorCorrelationMap& cmap, + ThreadPool* pool) { + if (ApplyOverride(cparams.dots, + cparams.butteraugli_distance >= kMinButteraugliForDots)) { + GaussianDetectParams ellipse_params; + ellipse_params.t_high = 0.04; + ellipse_params.t_low = 0.02; + ellipse_params.maxWinSize = 5; + ellipse_params.maxL2Loss = 0.005; + ellipse_params.maxCustomLoss = 300; + ellipse_params.minIntensity = 0.12; + ellipse_params.maxDistMeanMode = 1.0; + ellipse_params.maxNegPixels = 0; + ellipse_params.minScore = 12.0; + ellipse_params.maxCC = 100; + ellipse_params.percCC = 100; + EllipseQuantParams qParams{ + opsin.xsize(), opsin.ysize(), kEllipsePosQ, + kEllipseMinSigma, kEllipseMaxSigma, kEllipseSigmaQ, + kEllipseAngleQ, kEllipseMinIntensity, kEllipseMaxIntensity, + kEllipseIntensityQ, kEllipsePosQ <= 5, cmap.YtoXRatio(0), + cmap.YtoBRatio(0)}; + + return DetectGaussianEllipses(opsin, ellipse_params, qParams, pool); + } + return {}; +} +} // namespace jxl diff --git a/lib/jxl/enc_dot_dictionary.h b/lib/jxl/enc_dot_dictionary.h new file mode 100644 index 0000000..f89791e --- /dev/null +++ b/lib/jxl/enc_dot_dictionary.h @@ -0,0 +1,35 @@ +// 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_ENC_DOT_DICTIONARY_H_ +#define LIB_JXL_ENC_DOT_DICTIONARY_H_ + +// Dots are stored in a dictionary to avoid storing similar dots multiple +// times. + +#include + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" + +namespace jxl { + +std::vector FindDotDictionary(const CompressParams& cparams, + const Image3F& opsin, + const ColorCorrelationMap& cmap, + ThreadPool* pool); + +} // namespace jxl + +#endif // LIB_JXL_ENC_DOT_DICTIONARY_H_ diff --git a/lib/jxl/enc_entropy_coder.cc b/lib/jxl/enc_entropy_coder.cc new file mode 100644 index 0000000..0946300 --- /dev/null +++ b/lib/jxl/enc_entropy_coder.cc @@ -0,0 +1,268 @@ +// 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/enc_entropy_coder.h" + +#include +#include + +#include +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_entropy_coder.cc" +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_context_map.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// Returns number of non-zero coefficients (but skip LLF). +// We cannot rely on block[] being all-zero bits, so first truncate to integer. +// Also writes the per-8x8 block nzeros starting at nzeros_pos. +int32_t NumNonZeroExceptLLF(const size_t cx, const size_t cy, + const AcStrategy acs, const size_t covered_blocks, + const size_t log2_covered_blocks, + const int32_t* JXL_RESTRICT block, + const size_t nzeros_stride, + int32_t* JXL_RESTRICT nzeros_pos) { + const HWY_CAPPED(int32_t, kBlockDim) di; + + const auto zero = Zero(di); + // Add FF..FF for every zero coefficient, negate to get #zeros. + auto neg_sum_zero = zero; + + { + // Mask sufficient for one row of coefficients. + HWY_ALIGN const int32_t + llf_mask_lanes[AcStrategy::kMaxCoeffBlocks * (1 + kBlockDim)] = { + -1, -1, -1, -1}; + // First cx=1,2,4 elements are FF..FF, others 0. + const int32_t* llf_mask_pos = + llf_mask_lanes + AcStrategy::kMaxCoeffBlocks - cx; + + // Rows with LLF: mask out the LLF + for (size_t y = 0; y < cy; y++) { + for (size_t x = 0; x < cx * kBlockDim; x += Lanes(di)) { + const auto llf_mask = LoadU(di, llf_mask_pos + x); + + // LLF counts as zero so we don't include it in nzeros. + const auto coef = + AndNot(llf_mask, Load(di, &block[y * cx * kBlockDim + x])); + + neg_sum_zero += VecFromMask(di, coef == zero); + } + } + } + + // Remaining rows: no mask + for (size_t y = cy; y < cy * kBlockDim; y++) { + for (size_t x = 0; x < cx * kBlockDim; x += Lanes(di)) { + const auto coef = Load(di, &block[y * cx * kBlockDim + x]); + neg_sum_zero += VecFromMask(di, coef == zero); + } + } + + // We want area - sum_zero, add because neg_sum_zero is already negated. + const int32_t nzeros = + int32_t(cx * cy * kDCTBlockSize) + GetLane(SumOfLanes(neg_sum_zero)); + + const int32_t shifted_nzeros = static_cast( + (nzeros + covered_blocks - 1) >> log2_covered_blocks); + // Need non-canonicalized dimensions! + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + nzeros_pos[x + y * nzeros_stride] = shifted_nzeros; + } + } + + return nzeros; +} + +// Specialization for 8x8, where only top-left is LLF/DC. +// About 1% overall speedup vs. NumNonZeroExceptLLF. +int32_t NumNonZero8x8ExceptDC(const int32_t* JXL_RESTRICT block, + int32_t* JXL_RESTRICT nzeros_pos) { + const HWY_CAPPED(int32_t, kBlockDim) di; + + const auto zero = Zero(di); + // Add FF..FF for every zero coefficient, negate to get #zeros. + auto neg_sum_zero = zero; + + { + // First row has DC, so mask + const size_t y = 0; + HWY_ALIGN const int32_t dc_mask_lanes[kBlockDim] = {-1}; + + for (size_t x = 0; x < kBlockDim; x += Lanes(di)) { + const auto dc_mask = Load(di, dc_mask_lanes + x); + + // DC counts as zero so we don't include it in nzeros. + const auto coef = AndNot(dc_mask, Load(di, &block[y * kBlockDim + x])); + + neg_sum_zero += VecFromMask(di, coef == zero); + } + } + + // Remaining rows: no mask + for (size_t y = 1; y < kBlockDim; y++) { + for (size_t x = 0; x < kBlockDim; x += Lanes(di)) { + const auto coef = Load(di, &block[y * kBlockDim + x]); + neg_sum_zero += VecFromMask(di, coef == zero); + } + } + + // We want 64 - sum_zero, add because neg_sum_zero is already negated. + const int32_t nzeros = + int32_t(kDCTBlockSize) + GetLane(SumOfLanes(neg_sum_zero)); + + *nzeros_pos = nzeros; + + return nzeros; +} + +// The number of nonzeros of each block is predicted from the top and the left +// blocks, with opportune scaling to take into account the number of blocks of +// each strategy. The predicted number of nonzeros divided by two is used as a +// context; if this number is above 63, a specific context is used. If the +// number of nonzeros of a strategy is above 63, it is written directly using a +// fixed number of bits (that depends on the size of the strategy). +void TokenizeCoefficients(const coeff_order_t* JXL_RESTRICT orders, + const Rect& rect, + const int32_t* JXL_RESTRICT* JXL_RESTRICT ac_rows, + const AcStrategyImage& ac_strategy, + YCbCrChromaSubsampling cs, + Image3I* JXL_RESTRICT tmp_num_nzeroes, + std::vector* JXL_RESTRICT output, + const ImageB& qdc, const ImageI& qf, + const BlockCtxMap& block_ctx_map) { + const size_t xsize_blocks = rect.xsize(); + const size_t ysize_blocks = rect.ysize(); + + // TODO(user): update the estimate: usually less coefficients are used. + output->reserve(output->size() + + 3 * xsize_blocks * ysize_blocks * kDCTBlockSize); + + size_t offset[3] = {}; + const size_t nzeros_stride = tmp_num_nzeroes->PixelsPerRow(); + for (size_t by = 0; by < ysize_blocks; ++by) { + size_t sby[3] = {by >> cs.VShift(0), by >> cs.VShift(1), + by >> cs.VShift(2)}; + int32_t* JXL_RESTRICT row_nzeros[3] = { + tmp_num_nzeroes->PlaneRow(0, sby[0]), + tmp_num_nzeroes->PlaneRow(1, sby[1]), + tmp_num_nzeroes->PlaneRow(2, sby[2]), + }; + const int32_t* JXL_RESTRICT row_nzeros_top[3] = { + sby[0] == 0 ? nullptr : tmp_num_nzeroes->ConstPlaneRow(0, sby[0] - 1), + sby[1] == 0 ? nullptr : tmp_num_nzeroes->ConstPlaneRow(1, sby[1] - 1), + sby[2] == 0 ? nullptr : tmp_num_nzeroes->ConstPlaneRow(2, sby[2] - 1), + }; + const uint8_t* JXL_RESTRICT row_qdc = + qdc.ConstRow(rect.y0() + by) + rect.x0(); + const int32_t* JXL_RESTRICT row_qf = rect.ConstRow(qf, by); + AcStrategyRow acs_row = ac_strategy.ConstRow(rect, by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + size_t sbx[3] = {bx >> cs.HShift(0), bx >> cs.HShift(1), + bx >> cs.HShift(2)}; + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + const size_t covered_blocks = cx * cy; // = #LLF coefficients + const size_t log2_covered_blocks = + Num0BitsBelowLS1Bit_Nonzero(covered_blocks); + const size_t size = covered_blocks * kDCTBlockSize; + + CoefficientLayout(&cy, &cx); // swap cx/cy to canonical order + + for (int c : {1, 0, 2}) { + if (sbx[c] << cs.HShift(c) != bx) continue; + if (sby[c] << cs.VShift(c) != by) continue; + const int32_t* JXL_RESTRICT block = ac_rows[c] + offset[c]; + + int32_t nzeros = + (covered_blocks == 1) + ? NumNonZero8x8ExceptDC(block, row_nzeros[c] + sbx[c]) + : NumNonZeroExceptLLF(cx, cy, acs, covered_blocks, + log2_covered_blocks, block, nzeros_stride, + row_nzeros[c] + sbx[c]); + + int ord = kStrategyOrder[acs.RawStrategy()]; + const coeff_order_t* JXL_RESTRICT order = + &orders[CoeffOrderOffset(ord, c)]; + + int32_t predicted_nzeros = + PredictFromTopAndLeft(row_nzeros_top[c], row_nzeros[c], sbx[c], 32); + size_t block_ctx = + block_ctx_map.Context(row_qdc[bx], row_qf[sbx[c]], ord, c); + const int32_t nzero_ctx = + block_ctx_map.NonZeroContext(predicted_nzeros, block_ctx); + + output->emplace_back(nzero_ctx, nzeros); + const size_t histo_offset = + block_ctx_map.ZeroDensityContextsOffset(block_ctx); + // Skip LLF. + size_t prev = (nzeros > static_cast(size / 16) ? 0 : 1); + for (size_t k = covered_blocks; k < size && nzeros != 0; ++k) { + int32_t coeff = block[order[k]]; + size_t ctx = + histo_offset + ZeroDensityContext(nzeros, k, covered_blocks, + log2_covered_blocks, prev); + uint32_t u_coeff = PackSigned(coeff); + output->emplace_back(ctx, u_coeff); + prev = coeff != 0; + nzeros -= prev; + } + JXL_DASSERT(nzeros == 0); + offset[c] += size; + } + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(TokenizeCoefficients); +void TokenizeCoefficients(const coeff_order_t* JXL_RESTRICT orders, + const Rect& rect, + const int32_t* JXL_RESTRICT* JXL_RESTRICT ac_rows, + const AcStrategyImage& ac_strategy, + YCbCrChromaSubsampling cs, + Image3I* JXL_RESTRICT tmp_num_nzeroes, + std::vector* JXL_RESTRICT output, + const ImageB& qdc, const ImageI& qf, + const BlockCtxMap& block_ctx_map) { + return HWY_DYNAMIC_DISPATCH(TokenizeCoefficients)( + orders, rect, ac_rows, ac_strategy, cs, tmp_num_nzeroes, output, qdc, qf, + block_ctx_map); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_entropy_coder.h b/lib/jxl/enc_entropy_coder.h new file mode 100644 index 0000000..7dfc71c --- /dev/null +++ b/lib/jxl/enc_entropy_coder.h @@ -0,0 +1,46 @@ +// 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_ENC_ENTROPY_CODER_H_ +#define LIB_JXL_ENC_ENTROPY_CODER_H_ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "lib/jxl/ac_context.h" // BlockCtxMap +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/frame_header.h" // YCbCrChromaSubsampling +#include "lib/jxl/image.h" + +// Entropy coding and context modeling of DC and AC coefficients, as well as AC +// strategy and quantization field. + +namespace jxl { + +// Generate DCT NxN quantized AC values tokens. +// Only the subset "rect" [in units of blocks] within all images. +// See also DecodeACVarBlock. +void TokenizeCoefficients(const coeff_order_t* JXL_RESTRICT orders, + const Rect& rect, + const int32_t* JXL_RESTRICT* JXL_RESTRICT ac_rows, + const AcStrategyImage& ac_strategy, + YCbCrChromaSubsampling cs, + Image3I* JXL_RESTRICT tmp_num_nzeroes, + std::vector* JXL_RESTRICT output, + const ImageB& qdc, const ImageI& qf, + const BlockCtxMap& block_ctx_map); + +} // namespace jxl + +#endif // LIB_JXL_ENC_ENTROPY_CODER_H_ diff --git a/lib/jxl/enc_external_image.cc b/lib/jxl/enc_external_image.cc new file mode 100644 index 0000000..6476b80 --- /dev/null +++ b/lib/jxl/enc_external_image.cc @@ -0,0 +1,340 @@ +// 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/enc_external_image.h" + +#include + +#include +#include +#include +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" + +namespace jxl { +namespace { + +// Based on highway scalar implementation, for testing +float LoadFloat16(uint16_t bits16) { + const uint32_t sign = bits16 >> 15; + const uint32_t biased_exp = (bits16 >> 10) & 0x1F; + const uint32_t mantissa = bits16 & 0x3FF; + + // Subnormal or zero + if (biased_exp == 0) { + const float subnormal = (1.0f / 16384) * (mantissa * (1.0f / 1024)); + return sign ? -subnormal : subnormal; + } + + // Normalized: convert the representation directly (faster than ldexp/tables). + const uint32_t biased_exp32 = biased_exp + (127 - 15); + const uint32_t mantissa32 = mantissa << (23 - 10); + const uint32_t bits32 = (sign << 31) | (biased_exp32 << 23) | mantissa32; + + float result; + memcpy(&result, &bits32, 4); + return result; +} + +float LoadLEFloat16(const uint8_t* p) { + uint16_t bits16 = LoadLE16(p); + return LoadFloat16(bits16); +} + +float LoadBEFloat16(const uint8_t* p) { + uint16_t bits16 = LoadBE16(p); + return LoadFloat16(bits16); +} + +// Loads a float in big endian +float LoadBEFloat(const uint8_t* p) { + float value; + const uint32_t u = LoadBE32(p); + memcpy(&value, &u, 4); + return value; +} + +// Loads a float in little endian +float LoadLEFloat(const uint8_t* p) { + float value; + const uint32_t u = LoadLE32(p); + memcpy(&value, &u, 4); + return value; +} + +typedef uint32_t(LoadFuncType)(const uint8_t* p); +template +void JXL_INLINE LoadFloatRow(float* JXL_RESTRICT row_out, const uint8_t* in, + float mul, size_t xsize, size_t bytes_per_pixel) { + size_t i = 0; + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = mul * LoadFunc(in + i); + i += bytes_per_pixel; + } +} + +uint32_t JXL_INLINE Load8(const uint8_t* p) { return *p; } + +} // namespace + +Status ConvertFromExternal(Span bytes, size_t xsize, + size_t ysize, const ColorEncoding& c_current, + bool has_alpha, bool alpha_is_premultiplied, + size_t bits_per_sample, JxlEndianness endianness, + bool flipped_y, ThreadPool* pool, ImageBundle* ib, + bool float_in) { + 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"); + } + + const size_t color_channels = c_current.Channels(); + const size_t channels = color_channels + has_alpha; + + // 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; + + const size_t row_size = xsize * bytes_per_pixel; + if (ysize && bytes.size() / ysize < row_size) { + return JXL_FAILURE("Buffer size is too small"); + } + + const bool little_endian = + endianness == JXL_LITTLE_ENDIAN || + (endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()); + + const uint8_t* const in = bytes.data(); + + Image3F color(xsize, ysize); + ImageF alpha; + if (has_alpha) { + alpha = ImageF(xsize, ysize); + } + + const auto get_y = [flipped_y, ysize](const size_t y) { + return flipped_y ? ysize - 1 - y : y; + }; + + if (float_in) { + for (size_t c = 0; c < color_channels; ++c) { + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t y = get_y(task); + 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 (little_endian) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadLEFloat16(in + i); + i += bytes_per_pixel; + } + } else { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadBEFloat16(in + i); + i += bytes_per_pixel; + } + } + } else { + if (little_endian) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadLEFloat(in + i); + i += bytes_per_pixel; + } + } else { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadBEFloat(in + i); + i += bytes_per_pixel; + } + } + } + }, + "ConvertRGBFloat"); + } + } else { + // Multiplier to convert from the integer range to floating point 0-1 range. + float mul = 1. / ((1ull << bits_per_sample) - 1); + for (size_t c = 0; c < color_channels; ++c) { + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + 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(row_out, in + i, mul, xsize, bytes_per_pixel); + } else if (bits_per_sample <= 16) { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } else if (bits_per_sample <= 24) { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } else { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } + }, + "ConvertRGBUint"); + } + } + + if (color_channels == 1) { + CopyImageTo(color.Plane(0), &color.Plane(1)); + CopyImageTo(color.Plane(0), &color.Plane(2)); + } + + ib->SetFromImage(std::move(color), c_current); + + if (has_alpha) { + if (float_in) { + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t y = get_y(task); + size_t i = row_size * task + + (color_channels * bits_per_sample / jxl::kBitsPerByte); + float* JXL_RESTRICT row_out = alpha.Row(y); + if (bits_per_sample <= 16) { + if (little_endian) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadLEFloat16(in + i); + i += bytes_per_pixel; + } + } else { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadBEFloat16(in + i); + i += bytes_per_pixel; + } + } + } else { + if (little_endian) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadLEFloat(in + i); + i += bytes_per_pixel; + } + } else { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = LoadBEFloat(in + i); + i += bytes_per_pixel; + } + } + } + }, + "ConvertAlphaFloat"); + } else { + float mul = 1. / ((1ull << bits_per_sample) - 1); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t y = get_y(task); + size_t i = row_size * task + color_channels * 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(row_out, in + i, mul, xsize, bytes_per_pixel); + } else if (bits_per_sample <= 16) { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } else if (bits_per_sample <= 24) { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } else { + if (little_endian) { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } else { + LoadFloatRow(row_out, in + i, mul, xsize, + bytes_per_pixel); + } + } + }, + "ConvertAlphaUint"); + } + + ib->SetAlpha(std::move(alpha), alpha_is_premultiplied); + } + + return true; +} + +Status BufferToImageBundle(const JxlPixelFormat& pixel_format, uint32_t xsize, + uint32_t ysize, const void* buffer, size_t size, + jxl::ThreadPool* pool, + const jxl::ColorEncoding& c_current, + jxl::ImageBundle* ib) { + 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; + } else if (pixel_format.data_type == JXL_TYPE_FLOAT16) { + bitdepth = 16; + float_in = true; + } else if (pixel_format.data_type == JXL_TYPE_UINT8) { + bitdepth = 8; + float_in = false; + } else if (pixel_format.data_type == JXL_TYPE_UINT16) { + bitdepth = 16; + float_in = false; + } else { + return JXL_FAILURE("unsupported bitdepth"); + } + + JXL_RETURN_IF_ERROR(ConvertFromExternal( + jxl::Span(static_cast(buffer), size), + xsize, ysize, c_current, + /*has_alpha=*/pixel_format.num_channels == 2 || + pixel_format.num_channels == 4, + /*alpha_is_premultiplied=*/false, bitdepth, pixel_format.endianness, + /*flipped_y=*/false, pool, ib, float_in)); + ib->VerifyMetadata(); + + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_external_image.h b/lib/jxl/enc_external_image.h new file mode 100644 index 0000000..ee5767f --- /dev/null +++ b/lib/jxl/enc_external_image.h @@ -0,0 +1,51 @@ +// 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_ENC_EXTERNAL_IMAGE_H_ +#define LIB_JXL_ENC_EXTERNAL_IMAGE_H_ + +// Interleaved image for color transforms and Codec. + +#include +#include + +#include "jxl/types.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Return the size in bytes of a given xsize, channels and bits_per_sample +// interleaved image. +constexpr size_t RowSize(size_t xsize, size_t channels, + size_t bits_per_sample) { + return bits_per_sample == 1 + ? DivCeil(xsize, kBitsPerByte) + : xsize * channels * DivCeil(bits_per_sample, kBitsPerByte); +} + +// Convert an interleaved pixel buffer to the internal ImageBundle +// representation. This is the opposite of ConvertToExternal(). +Status ConvertFromExternal(Span bytes, size_t xsize, + size_t ysize, const ColorEncoding& c_current, + bool has_alpha, bool alpha_is_premultiplied, + size_t bits_per_sample, JxlEndianness endianness, + bool flipped_y, ThreadPool* pool, ImageBundle* ib, + bool float_in); + +Status BufferToImageBundle(const JxlPixelFormat& pixel_format, uint32_t xsize, + uint32_t ysize, const void* buffer, size_t size, + jxl::ThreadPool* pool, + const jxl::ColorEncoding& c_current, + jxl::ImageBundle* ib); + +} // namespace jxl + +#endif // LIB_JXL_ENC_EXTERNAL_IMAGE_H_ diff --git a/lib/jxl/enc_external_image_gbench.cc b/lib/jxl/enc_external_image_gbench.cc new file mode 100644 index 0000000..612b6e8 --- /dev/null +++ b/lib/jxl/enc_external_image_gbench.cc @@ -0,0 +1,49 @@ +// 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 "benchmark/benchmark.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { +namespace { + +// Encoder case, deinterleaves a buffer. +void BM_EncExternalImage_ConvertImageRGBA(benchmark::State& state) { + const size_t kNumIter = 5; + size_t xsize = state.range(); + size_t ysize = state.range(); + + ImageMetadata im; + im.SetAlphaBits(8); + ImageBundle ib(&im); + + std::vector interleaved(xsize * ysize * 4); + + for (auto _ : state) { + for (size_t i = 0; i < kNumIter; ++i) { + JXL_CHECK(ConvertFromExternal( + Span(interleaved.data(), interleaved.size()), xsize, + ysize, + /*c_current=*/ColorEncoding::SRGB(), + /*has_alpha=*/true, + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/8, JXL_NATIVE_ENDIAN, + /*flipped_y=*/false, + /*pool=*/nullptr, &ib, /*float_in=*/false)); + } + } + + // Pixels per second. + state.SetItemsProcessed(kNumIter * state.iterations() * xsize * ysize); + state.SetBytesProcessed(kNumIter * state.iterations() * interleaved.size()); +} + +BENCHMARK(BM_EncExternalImage_ConvertImageRGBA) + ->RangeMultiplier(2) + ->Range(256, 2048); + +} // namespace +} // namespace jxl diff --git a/lib/jxl/enc_external_image_test.cc b/lib/jxl/enc_external_image_test.cc new file mode 100644 index 0000000..5ec66af --- /dev/null +++ b/lib/jxl/enc_external_image_test.cc @@ -0,0 +1,49 @@ +// 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/enc_external_image.h" + +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +#if !defined(JXL_CRASH_ON_ERROR) +TEST(ExternalImageTest, InvalidSize) { + ImageMetadata im; + im.SetAlphaBits(8); + ImageBundle ib(&im); + + const uint8_t buf[10 * 100 * 8] = {}; + EXPECT_FALSE(ConvertFromExternal( + Span(buf, 10), /*xsize=*/10, /*ysize=*/100, + /*c_current=*/ColorEncoding::SRGB(), /*has_alpha=*/true, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false)); + EXPECT_FALSE(ConvertFromExternal( + Span(buf, sizeof(buf) - 1), /*xsize=*/10, /*ysize=*/100, + /*c_current=*/ColorEncoding::SRGB(), /*has_alpha=*/true, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false)); + EXPECT_TRUE(ConvertFromExternal( + Span(buf, sizeof(buf)), /*xsize=*/10, + /*ysize=*/100, /*c_current=*/ColorEncoding::SRGB(), + /*has_alpha=*/true, /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*flipped_y=*/false, nullptr, &ib, /*float_in=*/false)); +} +#endif + +} // namespace +} // namespace jxl diff --git a/lib/jxl/enc_fast_heuristics.cc b/lib/jxl/enc_fast_heuristics.cc new file mode 100644 index 0000000..16f7670 --- /dev/null +++ b/lib/jxl/enc_fast_heuristics.cc @@ -0,0 +1,361 @@ +// 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 +#include + +#include +#include +#include + +#include "lib/jxl/convolve.h" +#include "lib/jxl/enc_ac_strategy.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_ar_control_field.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_heuristics.h" +#include "lib/jxl/enc_noise.h" +#include "lib/jxl/gaborish.h" +#include "lib/jxl/gauss_blur.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_fast_heuristics.cc" +#include +#include + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { +using DF4 = HWY_CAPPED(float, 4); +DF4 df4; +HWY_FULL(float) df; + +Status Heuristics(PassesEncoderState* enc_state, + ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* linear, Image3F* opsin, ThreadPool* pool, + AuxOut* aux_out) { + PROFILER_ZONE("JxlLossyFrameHeuristics uninstrumented"); + CompressParams& cparams = enc_state->cparams; + PassesSharedState& shared = enc_state->shared; + const FrameDimensions& frame_dim = enc_state->shared.frame_dim; + JXL_CHECK(cparams.butteraugli_distance > 0); + + // TODO(veluca): make this tiled. + if (shared.frame_header.loop_filter.gab) { + GaborishInverse(opsin, 0.9908511000000001f, pool); + } + // Compute image of high frequencies by removing a blurred version. + // TODO(veluca): certainly can be made faster, and use less memory... + constexpr size_t pad = 16; + Image3F padded = PadImageMirror(*opsin, pad, pad); + // Make the image (X, Y, B-Y) + // TODO(veluca): SubtractFrom is not parallel *and* not SIMD-fied. + SubtractFrom(padded.Plane(1), &padded.Plane(2)); + // Ensure that OOB access for CfL does nothing. Not necessary if doing things + // properly... + Image3F hf(padded.xsize() + 64, padded.ysize()); + ZeroFillImage(&hf); + hf.ShrinkTo(padded.xsize(), padded.ysize()); + ImageF temp(padded.xsize(), padded.ysize()); + // TODO(veluca): consider some faster blurring method. + auto g = CreateRecursiveGaussian(11.415258091746161); + for (size_t c = 0; c < 3; c++) { + FastGaussian(g, padded.Plane(c), pool, &temp, &hf.Plane(c)); + SubtractFrom(padded.Plane(c), &hf.Plane(c)); + } + // TODO(veluca): DC CfL? + size_t xcolortiles = DivCeil(frame_dim.xsize_blocks, kColorTileDimInBlocks); + size_t ycolortiles = DivCeil(frame_dim.ysize_blocks, kColorTileDimInBlocks); + RunOnPool( + pool, 0, xcolortiles * ycolortiles, ThreadPool::SkipInit(), + [&](size_t tile_id, size_t _) { + size_t tx = tile_id % xcolortiles; + size_t ty = tile_id / xcolortiles; + size_t x0 = tx * kColorTileDim; + size_t x1 = std::min(x0 + kColorTileDim, hf.xsize()); + size_t y0 = ty * kColorTileDim; + size_t y1 = std::min(y0 + kColorTileDim, hf.ysize()); + for (size_t c : {0, 2}) { + static constexpr float kInvColorFactor = 1.0f / kDefaultColorFactor; + auto ca = Zero(df); + auto cb = Zero(df); + const auto inv_color_factor = Set(df, kInvColorFactor); + for (size_t y = y0; y < y1; y++) { + const float* row_m = hf.PlaneRow(1, y); + const float* row_s = hf.PlaneRow(c, y); + for (size_t x = x0; x < x1; x += Lanes(df)) { + // color residual = ax + b + const auto a = inv_color_factor * Load(df, row_m + x); + const auto b = Zero(df) - Load(df, row_s + x); + ca = MulAdd(a, a, ca); + cb = MulAdd(a, b, cb); + } + } + float best = + -GetLane(SumOfLanes(cb)) / (GetLane(SumOfLanes(ca)) + 1e-9f); + int8_t& res = (c == 0 ? shared.cmap.ytox_map : shared.cmap.ytob_map) + .Row(ty)[tx]; + res = std::max(-128.0f, std::min(127.0f, roundf(best))); + } + }, + "CfL"); + Image3F pooled(frame_dim.xsize_padded / 4, frame_dim.ysize_padded / 4); + Image3F summed(frame_dim.xsize_padded / 4, frame_dim.ysize_padded / 4); + RunOnPool( + pool, 0, frame_dim.ysize_padded / 4, ThreadPool::SkipInit(), + [&](size_t y, size_t _) { + for (size_t c = 0; c < 3; c++) { + float* JXL_RESTRICT row_out = pooled.PlaneRow(c, y); + float* JXL_RESTRICT row_out_avg = summed.PlaneRow(c, y); + const float* JXL_RESTRICT row_in[4]; + for (size_t iy = 0; iy < 4; iy++) { + row_in[iy] = hf.PlaneRow(c, 4 * y + pad + iy); + } + for (size_t x = 0; x < frame_dim.xsize_padded / 4; x++) { + auto max = Zero(df4); + auto sum = Zero(df4); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix += Lanes(df4)) { + const auto nn = Abs(Load(df4, row_in[iy] + x * 4 + ix + pad)); + sum += nn; + max = IfThenElse(max > nn, max, nn); + } + } + row_out_avg[x] = GetLane(SumOfLanes(sum)); + row_out[x] = GetLane(MaxOfLanes(max)); + } + } + }, + "MaxPool"); + // TODO(veluca): better handling of the border + // TODO(veluca): consider some faster blurring method. + // TODO(veluca): parallelize. + // Remove noise from the resulting image. + auto g2 = CreateRecursiveGaussian(2.0849544429861884); + constexpr size_t pad2 = 16; + Image3F summed_pad = PadImageMirror(summed, pad2, pad2); + ImageF tmp_out(summed_pad.xsize(), summed_pad.ysize()); + ImageF tmp2(summed_pad.xsize(), summed_pad.ysize()); + Image3F pooled_pad = PadImageMirror(pooled, pad2, pad2); + for (size_t c = 0; c < 3; c++) { + FastGaussian(g2, summed_pad.Plane(c), pool, &tmp2, &tmp_out); + const auto unblurred_multiplier = Set(df, 0.5f); + for (size_t y = 0; y < summed.ysize(); y++) { + float* row = summed.PlaneRow(c, y); + const float* row_blur = tmp_out.Row(y + pad2); + for (size_t x = 0; x < summed.xsize(); x += Lanes(df)) { + const auto b = Load(df, row_blur + x + pad2); + const auto o = Load(df, row + x) * unblurred_multiplier; + const auto m = IfThenElse(b > o, b, o); + Store(m, df, row + x); + } + } + } + for (size_t c = 0; c < 3; c++) { + FastGaussian(g2, pooled_pad.Plane(c), pool, &tmp2, &tmp_out); + const auto unblurred_multiplier = Set(df, 0.5f); + for (size_t y = 0; y < pooled.ysize(); y++) { + float* row = pooled.PlaneRow(c, y); + const float* row_blur = tmp_out.Row(y + pad2); + for (size_t x = 0; x < pooled.xsize(); x += Lanes(df)) { + const auto b = Load(df, row_blur + x + pad2); + const auto o = Load(df, row + x) * unblurred_multiplier; + const auto m = IfThenElse(b > o, b, o); + Store(m, df, row + x); + } + } + } + const static float kChannelMul[3] = { + 7.9644294909680253f, + 0.5700000183257159f, + 0.20267448837597055f, + }; + ImageF pooledhf44(pooled.xsize(), pooled.ysize()); + for (size_t y = 0; y < pooled.ysize(); y++) { + const float* row_in_x = pooled.ConstPlaneRow(0, y); + const float* row_in_y = pooled.ConstPlaneRow(1, y); + const float* row_in_b = pooled.ConstPlaneRow(2, y); + float* row_out = pooledhf44.Row(y); + for (size_t x = 0; x < pooled.xsize(); x += Lanes(df)) { + auto v = Set(df, kChannelMul[0]) * Load(df, row_in_x + x); + v = MulAdd(Set(df, kChannelMul[1]), Load(df, row_in_y + x), v); + v = MulAdd(Set(df, kChannelMul[2]), Load(df, row_in_b + x), v); + Store(v, df, row_out + x); + } + } + ImageF summedhf44(summed.xsize(), summed.ysize()); + for (size_t y = 0; y < summed.ysize(); y++) { + const float* row_in_x = summed.ConstPlaneRow(0, y); + const float* row_in_y = summed.ConstPlaneRow(1, y); + const float* row_in_b = summed.ConstPlaneRow(2, y); + float* row_out = summedhf44.Row(y); + for (size_t x = 0; x < summed.xsize(); x += Lanes(df)) { + auto v = Set(df, kChannelMul[0]) * Load(df, row_in_x + x); + v = MulAdd(Set(df, kChannelMul[1]), Load(df, row_in_y + x), v); + v = MulAdd(Set(df, kChannelMul[2]), Load(df, row_in_b + x), v); + Store(v, df, row_out + x); + } + } + aux_out->DumpPlaneNormalized("pooledhf44", pooledhf44); + aux_out->DumpPlaneNormalized("summedhf44", summedhf44); + + static const float kDcQuantMul = 0.88170190420916206; + static const float kAcQuantMul = 2.5165738934721524; + + float dc_quant = kDcQuantMul * InitialQuantDC(cparams.butteraugli_distance); + float ac_quant_base = kAcQuantMul / cparams.butteraugli_distance; + ImageF quant_field(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + + static_assert(kColorTileDim == 64, "Fix the code below"); + auto mmacs = [&](size_t bx, size_t by, AcStrategy acs, float& min, + float& max) { + min = 1e10; + max = 0; + for (size_t y = 2 * by; y < 2 * (by + acs.covered_blocks_y()); y++) { + const float* row = summedhf44.Row(y); + for (size_t x = 2 * bx; x < 2 * (bx + acs.covered_blocks_x()); x++) { + min = std::min(min, row[x]); + max = std::max(max, row[x]); + } + } + }; + // Multipliers for allowed range of summedhf44. + std::pair candidates[] = { + // The order is such that, in case of ties, 8x8 is favoured over 4x4 which + // is favoured over 2x2. Similarly, we prefer square transforms over + // same-area rectangular ones. + {AcStrategy::Type::DCT2X2, 1.5f}, + {AcStrategy::Type::DCT4X4, 1.4f}, + {AcStrategy::Type::DCT4X8, 1.2f}, + {AcStrategy::Type::DCT8X4, 1.2f}, + {AcStrategy::Type::AFV0, + 1.15f}, // doesn't really work with these heuristics + {AcStrategy::Type::AFV1, 1.15f}, + {AcStrategy::Type::AFV2, 1.15f}, + {AcStrategy::Type::AFV3, 1.15f}, + {AcStrategy::Type::DCT, 1.0f}, + {AcStrategy::Type::DCT16X8, 0.8f}, + {AcStrategy::Type::DCT8X16, 0.8f}, + {AcStrategy::Type::DCT16X16, 0.2f}, + {AcStrategy::Type::DCT16X32, 0.2f}, + {AcStrategy::Type::DCT32X16, 0.2f}, + {AcStrategy::Type::DCT32X32, 0.2f}, + {AcStrategy::Type::DCT32X64, 0.1f}, + {AcStrategy::Type::DCT64X32, 0.1f}, + {AcStrategy::Type::DCT64X64, 0.04f}, + +#if 0 + {AcStrategy::Type::DCT2X2, 1e+10}, {AcStrategy::Type::DCT4X4, 2.0f}, + {AcStrategy::Type::DCT, 1.0f}, {AcStrategy::Type::DCT16X8, 1.0f}, + {AcStrategy::Type::DCT8X16, 1.0f}, {AcStrategy::Type::DCT32X8, 1.0f}, + {AcStrategy::Type::DCT8X32, 1.0f}, {AcStrategy::Type::DCT32X16, 1.0f}, + {AcStrategy::Type::DCT16X32, 1.0f}, {AcStrategy::Type::DCT64X32, 1.0f}, + {AcStrategy::Type::DCT32X64, 1.0f}, {AcStrategy::Type::DCT16X16, 1.0f}, + {AcStrategy::Type::DCT32X32, 1.0f}, {AcStrategy::Type::DCT64X64, 1.0f}, +#endif + // TODO(veluca): figure out if we want 4x8 and/or AVF. + }; + float max_range = 1e-8f + 0.5f * std::pow(cparams.butteraugli_distance, 0.5f); + // Change quant field and sharpness amounts based on (pooled|summed)hf44, and + // compute block sizes. + // TODO(veluca): maybe this could be done per group: it would allow choosing + // floating blocks better. + RunOnPool( + pool, 0, xcolortiles * ycolortiles, ThreadPool::SkipInit(), + [&](size_t tile_id, size_t _) { + size_t tx = tile_id % xcolortiles; + size_t ty = tile_id / xcolortiles; + size_t x0 = tx * kColorTileDim / kBlockDim; + size_t x1 = std::min(x0 + kColorTileDimInBlocks, quant_field.xsize()); + size_t y0 = ty * kColorTileDim / kBlockDim; + size_t y1 = std::min(y0 + kColorTileDimInBlocks, quant_field.ysize()); + size_t qf_stride = quant_field.PixelsPerRow(); + size_t epf_stride = shared.epf_sharpness.PixelsPerRow(); + bool chosen_mask[64] = {}; + for (size_t y = y0; y < y1; y++) { + uint8_t* epf_row = shared.epf_sharpness.Row(y); + float* qf_row = quant_field.Row(y); + for (size_t x = x0; x < x1; x++) { + if (chosen_mask[(y - y0) * 8 + (x - x0)]) continue; + // Default to DCT8 just in case something funny happens in the loop + // below. + AcStrategy::Type best = AcStrategy::DCT; + size_t best_covered = 1; + float qf = ac_quant_base; + for (size_t i = 0; i < sizeof(candidates) / sizeof(*candidates); + i++) { + AcStrategy acs = AcStrategy::FromRawStrategy(candidates[i].first); + if (y + acs.covered_blocks_y() > y1) continue; + if (x + acs.covered_blocks_x() > x1) continue; + bool fits = true; + for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { + for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { + if (chosen_mask[(iy - y0) * 8 + (ix - x0)]) { + fits = false; + break; + } + } + } + if (!fits) continue; + float min, max; + mmacs(x, y, acs, min, max); + if (max - min > max_range * candidates[i].second) continue; + size_t cb = acs.covered_blocks_x() * acs.covered_blocks_y(); + if (cb >= best_covered) { + best_covered = cb; + best = candidates[i].first; + // TODO(veluca): make this better. + qf = ac_quant_base / + (3.9312946339134007f + 2.6011435675118082f * min); + } + } + shared.ac_strategy.Set(x, y, best); + AcStrategy acs = AcStrategy::FromRawStrategy(best); + for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { + for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { + chosen_mask[(iy - y0) * 8 + (ix - x0)] = 1; + qf_row[ix + (iy - y) * qf_stride] = qf; + } + } + // TODO + for (size_t iy = y; iy < y + acs.covered_blocks_y(); iy++) { + for (size_t ix = x; ix < x + acs.covered_blocks_x(); ix++) { + epf_row[ix + (iy - y) * epf_stride] = 4; + } + } + } + } + }, + "QF+ACS+EPF"); + aux_out->DumpPlaneNormalized("qf", quant_field); + aux_out->DumpPlaneNormalized("epf", shared.epf_sharpness); + DumpAcStrategy(shared.ac_strategy, frame_dim.xsize_padded, + frame_dim.ysize_padded, "acs", aux_out); + + shared.quantizer.SetQuantField(dc_quant, quant_field, + &shared.raw_quant_field); + + return true; +} +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(Heuristics); +Status FastEncoderHeuristics::LossyFrameHeuristics( + PassesEncoderState* enc_state, ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* linear, Image3F* opsin, ThreadPool* pool, + AuxOut* aux_out) { + return HWY_DYNAMIC_DISPATCH(Heuristics)(enc_state, modular_frame_encoder, + linear, opsin, pool, aux_out); +} + +} // namespace jxl +#endif diff --git a/lib/jxl/enc_file.cc b/lib/jxl/enc_file.cc new file mode 100644 index 0000000..9407a7d --- /dev/null +++ b/lib/jxl/enc_file.cc @@ -0,0 +1,295 @@ +// 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/enc_file.h" + +#include + +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_frame.h" +#include "lib/jxl/enc_icc_codec.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +namespace { + +// DC + 'Very Low Frequency' +PassDefinition progressive_passes_dc_vlf[] = { + {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/4}}; + +PassDefinition progressive_passes_dc_lf[] = { + {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/4}, + {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/2}}; + +PassDefinition progressive_passes_dc_lf_salient_ac[] = { + {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/4}, + {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/2}, + {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/true, + /*suitable_for_downsampling_of_at_least=*/0}}; + +PassDefinition progressive_passes_dc_lf_salient_ac_other_ac[] = { + {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/4}, + {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/2}, + {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/true, + /*suitable_for_downsampling_of_at_least=*/0}, + {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/0}}; + +PassDefinition progressive_passes_dc_quant_ac_full_ac[] = { + {/*num_coefficients=*/8, /*shift=*/1, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/2}, + {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false, + /*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) { + *metadata = io->metadata; + size_t ups = 1; + if (cparams.already_downsampled) ups = cparams.resampling; + + JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize() * ups, io->ysize() * ups)); + + // Keep ICC profile in lossless modes because a reconstructed profile may be + // slightly different (quantization). + // Also keep ICC in JPEG reconstruction mode as we need byte-exact profiles. + const bool lossless_modular = + cparams.modular_mode && cparams.quality_pair.first == 100.0f; + if (!lossless_modular && !io->Main().IsJPEG()) { + metadata->m.color_encoding.DecideIfWantICC(); + } + + metadata->m.xyb_encoded = + cparams.color_transform == ColorTransform::kXYB ? true : false; + + InterpretExif(io->blobs.exif, metadata); + + return true; +} + +} // namespace + +Status EncodePreview(const CompressParams& cparams, const ImageBundle& ib, + const CodecMetadata* metadata, ThreadPool* pool, + BitWriter* JXL_RESTRICT writer) { + BitWriter preview_writer; + // TODO(janwas): also support generating preview by downsampling + if (ib.HasColor()) { + AuxOut aux_out; + PassesEncoderState passes_enc_state; + // TODO(lode): check if we want all extra channels and matching xyb_encoded + // for the preview, such that using the main ImageMetadata object for + // encoding this frame is warrented. + FrameInfo frame_info; + frame_info.is_preview = true; + JXL_RETURN_IF_ERROR(EncodeFrame(cparams, frame_info, metadata, ib, + &passes_enc_state, pool, &preview_writer, + &aux_out)); + preview_writer.ZeroPadToByte(); + } + + if (preview_writer.BitsWritten() != 0) { + writer->ZeroPadToByte(); + writer->AppendByteAligned(preview_writer); + } + + return true; +} + +Status WriteHeaders(CodecMetadata* metadata, BitWriter* writer, + AuxOut* aux_out) { + // Marker/signature + BitWriter::Allotment allotment(writer, 16); + writer->Write(8, 0xFF); + writer->Write(8, kCodestreamMarker); + ReclaimAndCharge(writer, &allotment, kLayerHeader, aux_out); + + JXL_RETURN_IF_ERROR( + WriteSizeHeader(metadata->size, writer, kLayerHeader, aux_out)); + + JXL_RETURN_IF_ERROR( + WriteImageMetadata(metadata->m, writer, kLayerHeader, aux_out)); + + metadata->transform_data.nonserialized_xyb_encoded = metadata->m.xyb_encoded; + JXL_RETURN_IF_ERROR( + Bundle::Write(metadata->transform_data, writer, kLayerHeader, aux_out)); + + return true; +} + +Status EncodeFile(const CompressParams& cparams_orig, const CodecInOut* io, + PassesEncoderState* passes_enc_state, PaddedBytes* compressed, + AuxOut* aux_out, ThreadPool* pool) { + io->CheckMetadata(); + BitWriter writer; + + CompressParams cparams = cparams_orig; + if (io->Main().color_transform != ColorTransform::kNone) { + // Set the color transform to YCbCr or XYB if the original image is such. + cparams.color_transform = io->Main().color_transform; + } + + // TODO(lode): move this to a common CompressParam post-initializer that is + // mandatory to be called, so that the encode API can also use it, once the + // encode API has the settings for resampling in the first place. + if (cparams.resampling == 0) { + cparams.resampling = 1; + // For very low bit rates, using 2x2 resampling gives better results on + // most photographic images, with an adjusted butteraugli score chosen to + // give roughly the same amount of bits per pixel. + if (!cparams.already_downsampled && cparams.butteraugli_distance >= 20) { + cparams.resampling = 2; + cparams.butteraugli_distance = + 6 + ((cparams.butteraugli_distance - 20) * 0.25); + } + } + + std::unique_ptr metadata = jxl::make_unique(); + JXL_RETURN_IF_ERROR(PrepareCodecMetadataFromIO(cparams, io, metadata.get())); + JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, aux_out)); + + // Only send ICC (at least several hundred bytes) if fields aren't enough. + if (metadata->m.color_encoding.WantICC()) { + JXL_RETURN_IF_ERROR(WriteICC(metadata->m.color_encoding.ICC(), &writer, + kLayerHeader, aux_out)); + } + + if (metadata->m.have_preview) { + JXL_RETURN_IF_ERROR(EncodePreview(cparams, io->preview_frame, + metadata.get(), pool, &writer)); + } + + // Each frame should start on byte boundaries. + writer.ZeroPadToByte(); + + if (cparams.progressive_mode || cparams.qprogressive_mode) { + if (cparams.saliency_map != nullptr) { + passes_enc_state->progressive_splitter.SetSaliencyMap( + cparams.saliency_map); + } + passes_enc_state->progressive_splitter.SetSaliencyThreshold( + cparams.saliency_threshold); + if (cparams.qprogressive_mode) { + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_quant_ac_full_ac}); + } else { + switch (cparams.saliency_num_progressive_steps) { + case 1: + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_vlf}); + break; + case 2: + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_lf}); + break; + case 3: + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_lf_salient_ac}); + break; + case 4: + if (cparams.saliency_threshold == 0.0f) { + // No need for a 4th pass if saliency-threshold regards everything + // as salient. + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_lf_salient_ac}); + } else { + passes_enc_state->progressive_splitter.SetProgressiveMode( + ProgressiveMode{progressive_passes_dc_lf_salient_ac_other_ac}); + } + break; + default: + return JXL_FAILURE("Invalid saliency_num_progressive_steps."); + } + } + } + + for (size_t i = 0; i < io->frames.size(); i++) { + FrameInfo info; + info.is_last = i == io->frames.size() - 1; + if (io->frames[i].use_for_next_frame) { + info.save_as_reference = 1; + } + JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(), + io->frames[i], passes_enc_state, pool, + &writer, aux_out)); + } + + // Clean up passes_enc_state in case it gets reused. + for (size_t i = 0; i < 4; i++) { + passes_enc_state->shared.dc_frames[i] = Image3F(); + passes_enc_state->shared.reference_frames[i].storage = ImageBundle(); + } + + *compressed = std::move(writer).TakeBytes(); + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_file.h b/lib/jxl/enc_file.h new file mode 100644 index 0000000..f729d7b --- /dev/null +++ b/lib/jxl/enc_file.h @@ -0,0 +1,51 @@ +// 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_ENC_FILE_H_ +#define LIB_JXL_ENC_FILE_H_ + +// Facade for JXL encoding. + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" + +namespace jxl { + +// Write preview from `io`. +Status EncodePreview(const CompressParams& cparams, const ImageBundle& ib, + const CodecMetadata* metadata, ThreadPool* pool, + BitWriter* JXL_RESTRICT writer); + +// Write headers from the CodecMetadata. Also may modify nonserialized_... +// fields of the metadata. +Status WriteHeaders(CodecMetadata* metadata, BitWriter* writer, + AuxOut* aux_out); + +// Compresses pixels from `io` (given in any ColorEncoding). +// `io->metadata.m.original` must be set. +Status EncodeFile(const CompressParams& params, const CodecInOut* io, + PassesEncoderState* passes_enc_state, PaddedBytes* compressed, + AuxOut* aux_out = nullptr, ThreadPool* pool = nullptr); + +// Backwards-compatible interface. Don't use in new code. +// TODO(deymo): Remove this function once we migrate users to C encoder API. +struct FrameEncCache {}; +JXL_INLINE Status EncodeFile(const CompressParams& params, const CodecInOut* io, + FrameEncCache* /* unused */, + PaddedBytes* compressed, AuxOut* aux_out = nullptr, + ThreadPool* pool = nullptr) { + PassesEncoderState passes_enc_state; + return EncodeFile(params, io, &passes_enc_state, compressed, aux_out, pool); +} + +} // namespace jxl + +#endif // LIB_JXL_ENC_FILE_H_ diff --git a/lib/jxl/enc_frame.cc b/lib/jxl/enc_frame.cc new file mode 100644 index 0000000..23081a5 --- /dev/null +++ b/lib/jxl/enc_frame.cc @@ -0,0 +1,1423 @@ +// 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/enc_frame.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/compressed_dc.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_chroma_from_luma.h" +#include "lib/jxl/enc_coeff_order.h" +#include "lib/jxl/enc_context_map.h" +#include "lib/jxl/enc_entropy_coder.h" +#include "lib/jxl/enc_group.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/enc_noise.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_patch_dictionary.h" +#include "lib/jxl/enc_quant_weights.h" +#include "lib/jxl/enc_splines.h" +#include "lib/jxl/enc_toc.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/gaborish.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/splines.h" +#include "lib/jxl/toc.h" + +namespace jxl { +namespace { + +void ClusterGroups(PassesEncoderState* enc_state) { + if (enc_state->shared.frame_header.passes.num_passes > 1) { + // TODO(veluca): implement this for progressive modes. + return; + } + // This only considers pass 0 for now. + std::vector context_map; + EntropyEncodingData codes; + auto& ac = enc_state->passes[0].ac_tokens; + size_t limit = std::ceil(std::sqrt(ac.size())); + if (limit == 1) return; + size_t num_contexts = enc_state->shared.block_ctx_map.NumACContexts(); + std::vector costs(ac.size()); + HistogramParams params; + params.uint_method = HistogramParams::HybridUintMethod::kNone; + params.lz77_method = HistogramParams::LZ77Method::kNone; + params.ans_histogram_strategy = + HistogramParams::ANSHistogramStrategy::kApproximate; + size_t max = 0; + auto token_cost = [&](std::vector>& tokens, size_t num_ctx, + bool estimate = true) { + // TODO(veluca): not estimating is very expensive. + BitWriter writer; + size_t c = BuildAndEncodeHistograms( + params, num_ctx, tokens, &codes, &context_map, + estimate ? nullptr : &writer, 0, /*aux_out=*/0); + if (estimate) return c; + for (size_t i = 0; i < tokens.size(); i++) { + WriteTokens(tokens[i], codes, context_map, &writer, 0, nullptr); + } + return writer.BitsWritten(); + }; + for (size_t i = 0; i < ac.size(); i++) { + std::vector> tokens{ac[i]}; + costs[i] = + token_cost(tokens, enc_state->shared.block_ctx_map.NumACContexts()); + if (costs[i] > costs[max]) { + max = i; + } + } + auto dist = [&](int i, int j) { + std::vector> tokens{ac[i], ac[j]}; + return token_cost(tokens, num_contexts) - costs[i] - costs[j]; + }; + std::vector out{max}; + std::vector old_map(ac.size()); + std::vector dists(ac.size()); + size_t farthest = 0; + for (size_t i = 0; i < ac.size(); i++) { + if (i == max) continue; + dists[i] = dist(max, i); + if (dists[i] > dists[farthest]) { + farthest = i; + } + } + + while (dists[farthest] > 0 && out.size() < limit) { + out.push_back(farthest); + dists[farthest] = 0; + enc_state->histogram_idx[farthest] = out.size() - 1; + for (size_t i = 0; i < ac.size(); i++) { + float d = dist(out.back(), i); + if (d < dists[i]) { + dists[i] = d; + old_map[i] = enc_state->histogram_idx[i]; + enc_state->histogram_idx[i] = out.size() - 1; + } + if (dists[i] > dists[farthest]) { + farthest = i; + } + } + } + + std::vector remap(out.size()); + std::iota(remap.begin(), remap.end(), 0); + for (size_t i = 0; i < enc_state->histogram_idx.size(); i++) { + enc_state->histogram_idx[i] = remap[enc_state->histogram_idx[i]]; + } + auto remap_cost = [&](std::vector remap) { + std::vector re_remap(remap.size(), remap.size()); + size_t r = 0; + for (size_t i = 0; i < remap.size(); i++) { + if (re_remap[remap[i]] == remap.size()) { + re_remap[remap[i]] = r++; + } + remap[i] = re_remap[remap[i]]; + } + auto tokens = ac; + size_t max_hist = 0; + for (size_t i = 0; i < tokens.size(); i++) { + for (size_t j = 0; j < tokens[i].size(); j++) { + size_t hist = remap[enc_state->histogram_idx[i]]; + tokens[i][j].context += hist * num_contexts; + max_hist = std::max(hist + 1, max_hist); + } + } + return token_cost(tokens, max_hist * num_contexts, /*estimate=*/false); + }; + + for (size_t src = 0; src < out.size(); src++) { + float cost = remap_cost(remap); + size_t best = src; + for (size_t j = src + 1; j < out.size(); j++) { + if (remap[src] == remap[j]) continue; + auto remap_c = remap; + std::replace(remap_c.begin(), remap_c.end(), remap[src], remap[j]); + float c = remap_cost(remap_c); + if (c < cost) { + best = j; + cost = c; + } + } + if (src != best) { + std::replace(remap.begin(), remap.end(), remap[src], remap[best]); + } + } + std::vector re_remap(remap.size(), remap.size()); + size_t r = 0; + for (size_t i = 0; i < remap.size(); i++) { + if (re_remap[remap[i]] == remap.size()) { + re_remap[remap[i]] = r++; + } + remap[i] = re_remap[remap[i]]; + } + + enc_state->shared.num_histograms = + *std::max_element(remap.begin(), remap.end()) + 1; + for (size_t i = 0; i < enc_state->histogram_idx.size(); i++) { + enc_state->histogram_idx[i] = remap[enc_state->histogram_idx[i]]; + } + for (size_t i = 0; i < ac.size(); i++) { + for (size_t j = 0; j < ac[i].size(); j++) { + ac[i][j].context += enc_state->histogram_idx[i] * num_contexts; + } + } +} + +uint64_t FrameFlagsFromParams(const CompressParams& cparams) { + uint64_t flags = 0; + + const float dist = cparams.butteraugli_distance; + + // We don't add noise at low butteraugli distances because the original + // noise is stored within the compressed image and adding noise makes things + // worse. + if (ApplyOverride(cparams.noise, dist >= kMinButteraugliForNoise) || + cparams.photon_noise_iso > 0) { + flags |= FrameHeader::kNoise; + } + + if (cparams.progressive_dc > 0 && cparams.modular_mode == false) { + flags |= FrameHeader::kUseDcFrame; + } + + return flags; +} + +Status LoopFilterFromParams(const CompressParams& cparams, + FrameHeader* JXL_RESTRICT frame_header) { + LoopFilter* loop_filter = &frame_header->loop_filter; + + // Gaborish defaults to enabled in Hare or slower. + loop_filter->gab = ApplyOverride( + cparams.gaborish, cparams.speed_tier <= SpeedTier::kHare && + frame_header->encoding == FrameEncoding::kVarDCT && + cparams.decoding_speed_tier < 4); + + if (cparams.epf != -1) { + loop_filter->epf_iters = cparams.epf; + } else { + if (frame_header->encoding == FrameEncoding::kModular) { + loop_filter->epf_iters = 0; + } else { + constexpr float kThresholds[3] = {0.7, 1.5, 4.0}; + loop_filter->epf_iters = 0; + if (cparams.decoding_speed_tier < 3) { + for (size_t i = cparams.decoding_speed_tier == 2 ? 1 : 0; i < 3; i++) { + if (cparams.butteraugli_distance >= kThresholds[i]) { + loop_filter->epf_iters++; + } + } + } + } + } + // Strength of EPF in modular mode. + if (frame_header->encoding == FrameEncoding::kModular && + cparams.quality_pair.first < 100) { + // TODO(veluca): this formula is nonsense. + loop_filter->epf_sigma_for_modular = + 20.0f * (1.0f - cparams.quality_pair.first / 100); + } + if (frame_header->encoding == FrameEncoding::kModular && + cparams.lossy_palette) { + loop_filter->epf_sigma_for_modular = 1.0f; + } + + return true; +} + +Status MakeFrameHeader(const CompressParams& cparams, + const ProgressiveSplitter& progressive_splitter, + const FrameInfo& frame_info, const ImageBundle& ib, + FrameHeader* JXL_RESTRICT frame_header) { + frame_header->nonserialized_is_preview = frame_info.is_preview; + frame_header->is_last = frame_info.is_last; + frame_header->save_before_color_transform = + frame_info.save_before_color_transform; + frame_header->frame_type = frame_info.frame_type; + frame_header->name = ib.name; + + progressive_splitter.InitPasses(&frame_header->passes); + + if (cparams.modular_mode) { + frame_header->encoding = FrameEncoding::kModular; + frame_header->group_size_shift = cparams.modular_group_size_shift; + } + + frame_header->chroma_subsampling = ib.chroma_subsampling; + if (ib.IsJPEG()) { + // we are transcoding a JPEG, so we don't get to choose + frame_header->encoding = FrameEncoding::kVarDCT; + frame_header->color_transform = ib.color_transform; + } else { + frame_header->color_transform = cparams.color_transform; + if (!cparams.modular_mode && + (frame_header->chroma_subsampling.MaxHShift() != 0 || + frame_header->chroma_subsampling.MaxVShift() != 0)) { + return JXL_FAILURE( + "Chroma subsampling is not supported in VarDCT mode when not " + "recompressing JPEGs"); + } + } + + frame_header->flags = FrameFlagsFromParams(cparams); + // Noise is not supported in the Modular encoder for now. + if (frame_header->encoding != FrameEncoding::kVarDCT) { + frame_header->UpdateFlag(false, FrameHeader::Flags::kNoise); + } + + JXL_RETURN_IF_ERROR(LoopFilterFromParams(cparams, frame_header)); + + frame_header->dc_level = frame_info.dc_level; + if (frame_header->dc_level > 2) { + // With 3 or more progressive_dc frames, the implementation does not yet + // work, see enc_cache.cc. + return JXL_FAILURE("progressive_dc > 2 is not yet supported"); + } + if (cparams.progressive_dc > 0 && + (cparams.ec_resampling != 1 || cparams.resampling != 1)) { + return JXL_FAILURE("Resampling not supported with DC frames"); + } + if (cparams.resampling != 1 && cparams.resampling != 2 && + cparams.resampling != 4 && cparams.resampling != 8) { + return JXL_FAILURE("Invalid resampling factor"); + } + if (cparams.ec_resampling != 1 && cparams.ec_resampling != 2 && + cparams.ec_resampling != 4 && cparams.ec_resampling != 8) { + return JXL_FAILURE("Invalid ec_resampling factor"); + } + // Resized frames. + if (frame_info.frame_type != FrameType::kDCFrame) { + frame_header->frame_origin = ib.origin; + size_t ups = 1; + if (cparams.already_downsampled) ups = cparams.resampling; + frame_header->frame_size.xsize = ib.xsize() * ups; + frame_header->frame_size.ysize = ib.ysize() * ups; + if (ib.origin.x0 != 0 || ib.origin.y0 != 0 || + frame_header->frame_size.xsize != frame_header->default_xsize() || + frame_header->frame_size.ysize != frame_header->default_ysize()) { + frame_header->custom_size_or_origin = true; + } + } + // Upsampling. + frame_header->upsampling = cparams.resampling; + const std::vector& extra_channels = + frame_header->nonserialized_metadata->m.extra_channel_info; + frame_header->extra_channel_upsampling.clear(); + frame_header->extra_channel_upsampling.resize(extra_channels.size(), + cparams.ec_resampling); + frame_header->save_as_reference = frame_info.save_as_reference; + + // Set blending-related information. + if (ib.blend || frame_header->custom_size_or_origin) { + // Set blend_channel to the first alpha channel. These values are only + // encoded in case a blend mode involving alpha is used and there are more + // than one extra channels. + size_t index = 0; + if (extra_channels.size() > 1) { + for (size_t i = 0; i < extra_channels.size(); i++) { + if (extra_channels[i].type == ExtraChannel::kAlpha) { + index = i; + break; + } + } + } + frame_header->blending_info.alpha_channel = index; + frame_header->blending_info.mode = + ib.blend ? ib.blendmode : BlendMode::kReplace; + // previous frames are saved with ID 1. + frame_header->blending_info.source = 1; + for (size_t i = 0; i < extra_channels.size(); i++) { + frame_header->extra_channel_blending_info[i].alpha_channel = index; + BlendMode default_blend = ib.blendmode; + if (extra_channels[i].type != ExtraChannel::kBlack && i != index) { + // K needs to be blended, spot colors and other stuff gets added + default_blend = BlendMode::kAdd; + } + frame_header->extra_channel_blending_info[i].mode = + ib.blend ? default_blend : BlendMode::kReplace; + frame_header->extra_channel_blending_info[i].source = 1; + } + } + + frame_header->animation_frame.duration = ib.duration; + + // TODO(veluca): timecode. + + return true; +} + +// Invisible (alpha = 0) pixels tend to be a mess in optimized PNGs. +// Since they have no visual impact whatsoever, we can replace them with +// something that compresses better and reduces artifacts near the edges. This +// does some kind of smooth stuff that seems to work. +// Replace invisible pixels with a weighted average of the pixel to the left, +// the pixel to the topright, and non-invisible neighbours. +// Produces downward-blurry smears, with in the upwards direction only a 1px +// edge duplication but not more. It would probably be better to smear in all +// directions. That requires an alpha-weighed convolution with a large enough +// kernel though, which might be overkill... +void SimplifyInvisible(Image3F* image, const ImageF& alpha, bool lossless) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image->ysize(); ++y) { + float* JXL_RESTRICT row = image->PlaneRow(c, y); + const float* JXL_RESTRICT prow = + (y > 0 ? image->PlaneRow(c, y - 1) : nullptr); + const float* JXL_RESTRICT nrow = + (y + 1 < image->ysize() ? image->PlaneRow(c, y + 1) : nullptr); + const float* JXL_RESTRICT a = alpha.Row(y); + const float* JXL_RESTRICT pa = (y > 0 ? alpha.Row(y - 1) : nullptr); + const float* JXL_RESTRICT na = + (y + 1 < image->ysize() ? alpha.Row(y + 1) : nullptr); + for (size_t x = 0; x < image->xsize(); ++x) { + if (a[x] == 0) { + if (lossless) { + row[x] = 0; + continue; + } + float d = 0.f; + row[x] = 0; + if (x > 0) { + row[x] += row[x - 1]; + d++; + if (a[x - 1] > 0.f) { + row[x] += row[x - 1]; + d++; + } + } + if (x + 1 < image->xsize()) { + if (y > 0) { + row[x] += prow[x + 1]; + d++; + } + if (a[x + 1] > 0.f) { + row[x] += 2.f * row[x + 1]; + d += 2.f; + } + if (y > 0 && pa[x + 1] > 0.f) { + row[x] += 2.f * prow[x + 1]; + d += 2.f; + } + if (y + 1 < image->ysize() && na[x + 1] > 0.f) { + row[x] += 2.f * nrow[x + 1]; + d += 2.f; + } + } + if (y > 0 && pa[x] > 0.f) { + row[x] += 2.f * prow[x]; + d += 2.f; + } + if (y + 1 < image->ysize() && na[x] > 0.f) { + row[x] += 2.f * nrow[x]; + d += 2.f; + } + if (d > 1.f) row[x] /= d; + } + } + } + } +} + +} // namespace + +class LossyFrameEncoder { + public: + LossyFrameEncoder(const CompressParams& cparams, + const FrameHeader& frame_header, + PassesEncoderState* JXL_RESTRICT enc_state, + ThreadPool* pool, AuxOut* aux_out) + : enc_state_(enc_state), pool_(pool), aux_out_(aux_out) { + JXL_CHECK(InitializePassesSharedState(frame_header, &enc_state_->shared, + /*encoder=*/true)); + enc_state_->cparams = cparams; + enc_state_->passes.clear(); + } + + Status ComputeEncodingData(const ImageBundle* linear, + Image3F* JXL_RESTRICT opsin, ThreadPool* pool, + ModularFrameEncoder* modular_frame_encoder, + BitWriter* JXL_RESTRICT writer, + FrameHeader* frame_header) { + PROFILER_ZONE("ComputeEncodingData uninstrumented"); + JXL_ASSERT((opsin->xsize() % kBlockDim) == 0 && + (opsin->ysize() % kBlockDim) == 0); + PassesSharedState& shared = enc_state_->shared; + + if (!enc_state_->cparams.max_error_mode) { + float x_qm_scale_steps[3] = {0.65f, 1.25f, 9.0f}; + shared.frame_header.x_qm_scale = 1; + for (float x_qm_scale_step : x_qm_scale_steps) { + if (enc_state_->cparams.butteraugli_distance > x_qm_scale_step) { + shared.frame_header.x_qm_scale++; + } + } + } + + JXL_RETURN_IF_ERROR(enc_state_->heuristics->LossyFrameHeuristics( + enc_state_, modular_frame_encoder, linear, opsin, pool_, aux_out_)); + + InitializePassesEncoder(*opsin, pool_, enc_state_, modular_frame_encoder, + aux_out_); + + enc_state_->passes.resize(enc_state_->progressive_splitter.GetNumPasses()); + for (PassesEncoderState::PassData& pass : enc_state_->passes) { + pass.ac_tokens.resize(shared.frame_dim.num_groups); + } + + ComputeAllCoeffOrders(shared.frame_dim); + shared.num_histograms = 1; + + const auto tokenize_group_init = [&](const size_t num_threads) { + group_caches_.resize(num_threads); + return true; + }; + const auto tokenize_group = [&](const int group_index, const int thread) { + // Tokenize coefficients. + const Rect rect = shared.BlockGroupRect(group_index); + for (size_t idx_pass = 0; idx_pass < enc_state_->passes.size(); + idx_pass++) { + JXL_ASSERT(enc_state_->coeffs[idx_pass]->Type() == ACType::k32); + const int32_t* JXL_RESTRICT ac_rows[3] = { + enc_state_->coeffs[idx_pass]->PlaneRow(0, group_index, 0).ptr32, + enc_state_->coeffs[idx_pass]->PlaneRow(1, group_index, 0).ptr32, + enc_state_->coeffs[idx_pass]->PlaneRow(2, group_index, 0).ptr32, + }; + // Ensure group cache is initialized. + group_caches_[thread].InitOnce(); + TokenizeCoefficients( + &shared.coeff_orders[idx_pass * shared.coeff_order_size], rect, + ac_rows, shared.ac_strategy, frame_header->chroma_subsampling, + &group_caches_[thread].num_nzeroes, + &enc_state_->passes[idx_pass].ac_tokens[group_index], + enc_state_->shared.quant_dc, enc_state_->shared.raw_quant_field, + enc_state_->shared.block_ctx_map); + } + }; + RunOnPool(pool_, 0, shared.frame_dim.num_groups, tokenize_group_init, + tokenize_group, "TokenizeGroup"); + + *frame_header = shared.frame_header; + return true; + } + + Status ComputeJPEGTranscodingData(const jpeg::JPEGData& jpeg_data, + ModularFrameEncoder* modular_frame_encoder, + FrameHeader* frame_header) { + PROFILER_ZONE("ComputeJPEGTranscodingData uninstrumented"); + PassesSharedState& shared = enc_state_->shared; + + frame_header->x_qm_scale = 2; + frame_header->b_qm_scale = 2; + + FrameDimensions frame_dim = frame_header->ToFrameDimensions(); + + const size_t xsize = frame_dim.xsize_padded; + const size_t ysize = frame_dim.ysize_padded; + const size_t xsize_blocks = frame_dim.xsize_blocks; + const size_t ysize_blocks = frame_dim.ysize_blocks; + + // no-op chroma from luma + shared.cmap = ColorCorrelationMap(xsize, ysize, false); + shared.ac_strategy.FillDCT8(); + FillImage(uint8_t(0), &shared.epf_sharpness); + + enc_state_->coeffs.clear(); + enc_state_->coeffs.emplace_back(make_unique>( + kGroupDim * kGroupDim, frame_dim.num_groups)); + + // convert JPEG quantization table to a Quantizer object + float dcquantization[3]; + std::vector qe(DequantMatrices::kNum, + QuantEncoding::Library(0)); + + auto jpeg_c_map = JpegOrder(frame_header->color_transform, + jpeg_data.components.size() == 1); + + std::vector qt(192); + for (size_t c = 0; c < 3; c++) { + size_t jpeg_c = jpeg_c_map[c]; + const int* quant = + jpeg_data.quant[jpeg_data.components[jpeg_c].quant_idx].values.data(); + + dcquantization[c] = 255 * 8.0f / quant[0]; + for (size_t y = 0; y < 8; y++) { + for (size_t x = 0; x < 8; x++) { + // JPEG XL transposes the DCT, JPEG doesn't. + qt[c * 64 + 8 * x + y] = quant[8 * y + x]; + } + } + } + DequantMatricesSetCustomDC(&shared.matrices, dcquantization); + float dcquantization_r[3] = {1.0f / dcquantization[0], + 1.0f / dcquantization[1], + 1.0f / dcquantization[2]}; + + qe[AcStrategy::Type::DCT] = QuantEncoding::RAW(qt); + DequantMatricesSetCustom(&shared.matrices, qe, modular_frame_encoder); + + // Ensure that InvGlobalScale() is 1. + shared.quantizer = Quantizer(&shared.matrices, 1, kGlobalScaleDenom); + // Recompute MulDC() and InvMulDC(). + shared.quantizer.RecomputeFromGlobalScale(); + + // Per-block dequant scaling should be 1. + FillImage(static_cast(shared.quantizer.InvGlobalScale()), + &shared.raw_quant_field); + + std::vector scaled_qtable(192); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 64; i++) { + scaled_qtable[64 * c + i] = + (1 << kCFLFixedPointPrecision) * qt[64 + i] / qt[64 * c + i]; + } + } + + auto jpeg_row = [&](size_t c, size_t y) { + return jpeg_data.components[jpeg_c_map[c]].coeffs.data() + + jpeg_data.components[jpeg_c_map[c]].width_in_blocks * + kDCTBlockSize * y; + }; + + Image3F dc = Image3F(xsize_blocks, ysize_blocks); + bool DCzero = + (shared.frame_header.color_transform == ColorTransform::kYCbCr); + // Compute chroma-from-luma for AC (doesn't seem to be useful for DC) + if (frame_header->chroma_subsampling.Is444() && + enc_state_->cparams.force_cfl_jpeg_recompression && + jpeg_data.components.size() == 3) { + for (size_t c : {0, 2}) { + ImageSB* map = (c == 0 ? &shared.cmap.ytox_map : &shared.cmap.ytob_map); + const float kScale = kDefaultColorFactor; + const int kOffset = 127; + const float kBase = + c == 0 ? shared.cmap.YtoXRatio(0) : shared.cmap.YtoBRatio(0); + const float kZeroThresh = + kScale * kZeroBiasDefault[c] * + 0.9999f; // just epsilon less for better rounding + + auto process_row = [&](int task, int thread) { + size_t ty = task; + int8_t* JXL_RESTRICT row_out = map->Row(ty); + for (size_t tx = 0; tx < map->xsize(); ++tx) { + const size_t y0 = ty * kColorTileDimInBlocks; + const size_t x0 = tx * kColorTileDimInBlocks; + const size_t y1 = std::min(frame_dim.ysize_blocks, + (ty + 1) * kColorTileDimInBlocks); + const size_t x1 = std::min(frame_dim.xsize_blocks, + (tx + 1) * kColorTileDimInBlocks); + int32_t d_num_zeros[257] = {0}; + // TODO(veluca): this needs SIMD + fixed point adaptation, and/or + // conversion to the new CfL algorithm. + for (size_t y = y0; y < y1; ++y) { + const int16_t* JXL_RESTRICT row_m = jpeg_row(1, y); + const int16_t* JXL_RESTRICT row_s = jpeg_row(c, y); + for (size_t x = x0; x < x1; ++x) { + for (size_t coeffpos = 1; coeffpos < kDCTBlockSize; + coeffpos++) { + const float scaled_m = + row_m[x * kDCTBlockSize + coeffpos] * + scaled_qtable[64 * c + coeffpos] * + (1.0f / (1 << kCFLFixedPointPrecision)); + const float scaled_s = + kScale * row_s[x * kDCTBlockSize + coeffpos] + + (kOffset - kBase * kScale) * scaled_m; + if (std::abs(scaled_m) > 1e-8f) { + float from, to; + if (scaled_m > 0) { + from = (scaled_s - kZeroThresh) / scaled_m; + to = (scaled_s + kZeroThresh) / scaled_m; + } else { + from = (scaled_s + kZeroThresh) / scaled_m; + to = (scaled_s - kZeroThresh) / scaled_m; + } + if (from < 0.0f) { + from = 0.0f; + } + if (to > 255.0f) { + to = 255.0f; + } + // Instead of clamping the both values + // we just check that range is sane. + if (from <= to) { + d_num_zeros[static_cast(std::ceil(from))]++; + d_num_zeros[static_cast(std::floor(to + 1))]--; + } + } + } + } + } + int best = 0; + int32_t best_sum = 0; + FindIndexOfSumMaximum(d_num_zeros, 256, &best, &best_sum); + int32_t offset_sum = 0; + for (int i = 0; i < 256; ++i) { + if (i <= kOffset) { + offset_sum += d_num_zeros[i]; + } + } + row_out[tx] = 0; + if (best_sum > offset_sum + 1) { + row_out[tx] = best - kOffset; + } + } + }; + + RunOnPool(pool_, 0, map->ysize(), ThreadPool::SkipInit(), process_row, + "FindCorrelation"); + } + } + if (!frame_header->chroma_subsampling.Is444()) { + ZeroFillImage(&dc); + enc_state_->coeffs[0]->ZeroFill(); + } + // JPEG DC is from -1024 to 1023. + std::vector dc_counts[3] = {}; + dc_counts[0].resize(2048); + dc_counts[1].resize(2048); + dc_counts[2].resize(2048); + size_t total_dc[3] = {}; + for (size_t c : {1, 0, 2}) { + if (jpeg_data.components.size() == 1 && c != 1) { + enc_state_->coeffs[0]->ZeroFillPlane(c); + ZeroFillImage(&dc.Plane(c)); + // Ensure no division by 0. + dc_counts[c][1024] = 1; + total_dc[c] = 1; + continue; + } + size_t hshift = frame_header->chroma_subsampling.HShift(c); + size_t vshift = frame_header->chroma_subsampling.VShift(c); + ImageSB& map = (c == 0 ? shared.cmap.ytox_map : shared.cmap.ytob_map); + for (size_t group_index = 0; group_index < frame_dim.num_groups; + group_index++) { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + size_t offset = 0; + int32_t* JXL_RESTRICT ac = + enc_state_->coeffs[0]->PlaneRow(c, group_index, 0).ptr32; + for (size_t by = gy * kGroupDimInBlocks; + by < ysize_blocks && by < (gy + 1) * kGroupDimInBlocks; ++by) { + if ((by >> vshift) << vshift != by) continue; + const int16_t* JXL_RESTRICT inputjpeg = jpeg_row(c, by >> vshift); + const int16_t* JXL_RESTRICT inputjpegY = jpeg_row(1, by); + float* JXL_RESTRICT fdc = dc.PlaneRow(c, by >> vshift); + const int8_t* JXL_RESTRICT cm = + map.ConstRow(by / kColorTileDimInBlocks); + for (size_t bx = gx * kGroupDimInBlocks; + bx < xsize_blocks && bx < (gx + 1) * kGroupDimInBlocks; ++bx) { + if ((bx >> hshift) << hshift != bx) continue; + size_t base = (bx >> hshift) * kDCTBlockSize; + int idc; + if (DCzero) { + idc = inputjpeg[base]; + } else { + idc = inputjpeg[base] + 1024 / qt[c * 64]; + } + dc_counts[c][std::min(static_cast(idc + 1024), + uint32_t(2047))]++; + total_dc[c]++; + fdc[bx >> hshift] = idc * dcquantization_r[c]; + if (c == 1 || !enc_state_->cparams.force_cfl_jpeg_recompression || + !frame_header->chroma_subsampling.Is444()) { + for (size_t y = 0; y < 8; y++) { + for (size_t x = 0; x < 8; x++) { + ac[offset + y * 8 + x] = inputjpeg[base + x * 8 + y]; + } + } + } else { + const int32_t scale = + shared.cmap.RatioJPEG(cm[bx / kColorTileDimInBlocks]); + + for (size_t y = 0; y < 8; y++) { + for (size_t x = 0; x < 8; x++) { + int Y = inputjpegY[kDCTBlockSize * bx + x * 8 + y]; + int QChroma = inputjpeg[kDCTBlockSize * bx + x * 8 + y]; + // Fixed-point multiply of CfL scale with quant table ratio + // first, and Y value second. + int coeff_scale = (scale * scaled_qtable[64 * c + y * 8 + x] + + (1 << (kCFLFixedPointPrecision - 1))) >> + kCFLFixedPointPrecision; + int cfl_factor = (Y * coeff_scale + + (1 << (kCFLFixedPointPrecision - 1))) >> + kCFLFixedPointPrecision; + int QCR = QChroma - cfl_factor; + ac[offset + y * 8 + x] = QCR; + } + } + } + offset += 64; + } + } + } + } + + auto& dct = enc_state_->shared.block_ctx_map.dc_thresholds; + auto& num_dc_ctxs = enc_state_->shared.block_ctx_map.num_dc_ctxs; + enc_state_->shared.block_ctx_map.num_dc_ctxs = 1; + for (size_t i = 0; i < 3; i++) { + dct[i].clear(); + int num_thresholds = (CeilLog2Nonzero(total_dc[i]) - 10) / 2; + // up to 3 buckets per channel: + // dark/medium/bright, yellow/unsat/blue, green/unsat/red + num_thresholds = std::min(std::max(num_thresholds, 0), 2); + size_t cumsum = 0; + size_t cut = total_dc[i] / (num_thresholds + 1); + for (int j = 0; j < 2048; j++) { + cumsum += dc_counts[i][j]; + if (cumsum > cut) { + dct[i].push_back(j - 1025); + cut = total_dc[i] * (dct[i].size() + 1) / (num_thresholds + 1); + } + } + num_dc_ctxs *= dct[i].size() + 1; + } + + auto& ctx_map = enc_state_->shared.block_ctx_map.ctx_map; + ctx_map.clear(); + ctx_map.resize(3 * kNumOrders * num_dc_ctxs, 0); + + int lbuckets = (dct[1].size() + 1); + for (size_t i = 0; i < num_dc_ctxs; i++) { + // up to 9 contexts for luma + ctx_map[i] = i / lbuckets; + // up to 3 contexts for chroma + ctx_map[kNumOrders * num_dc_ctxs + i] = + num_dc_ctxs / lbuckets + (i % lbuckets); + ctx_map[2 * kNumOrders * num_dc_ctxs + i] = + num_dc_ctxs / lbuckets + (i % lbuckets); + } + enc_state_->shared.block_ctx_map.num_ctxs = + *std::max_element(ctx_map.begin(), ctx_map.end()) + 1; + + enc_state_->histogram_idx.resize(shared.frame_dim.num_groups); + + // disable DC frame for now + shared.frame_header.UpdateFlag(false, FrameHeader::kUseDcFrame); + auto compute_dc_coeffs = [&](int group_index, int /* thread */) { + modular_frame_encoder->AddVarDCTDC(dc, group_index, /*nl_dc=*/false, + enc_state_); + modular_frame_encoder->AddACMetadata(group_index, /*jpeg_transcode=*/true, + enc_state_); + }; + RunOnPool(pool_, 0, shared.frame_dim.num_dc_groups, ThreadPool::SkipInit(), + compute_dc_coeffs, "Compute DC coeffs"); + + // Must happen before WriteFrameHeader! + shared.frame_header.UpdateFlag(true, FrameHeader::kSkipAdaptiveDCSmoothing); + + enc_state_->passes.resize(enc_state_->progressive_splitter.GetNumPasses()); + for (PassesEncoderState::PassData& pass : enc_state_->passes) { + pass.ac_tokens.resize(shared.frame_dim.num_groups); + } + + JXL_CHECK(enc_state_->passes.size() == + 1); // skipping coeff splitting so need to have only one pass + + ComputeAllCoeffOrders(frame_dim); + shared.num_histograms = 1; + + const auto tokenize_group_init = [&](const size_t num_threads) { + group_caches_.resize(num_threads); + return true; + }; + const auto tokenize_group = [&](const int group_index, const int thread) { + // Tokenize coefficients. + const Rect rect = shared.BlockGroupRect(group_index); + for (size_t idx_pass = 0; idx_pass < enc_state_->passes.size(); + idx_pass++) { + JXL_ASSERT(enc_state_->coeffs[idx_pass]->Type() == ACType::k32); + const int32_t* JXL_RESTRICT ac_rows[3] = { + enc_state_->coeffs[idx_pass]->PlaneRow(0, group_index, 0).ptr32, + enc_state_->coeffs[idx_pass]->PlaneRow(1, group_index, 0).ptr32, + enc_state_->coeffs[idx_pass]->PlaneRow(2, group_index, 0).ptr32, + }; + // Ensure group cache is initialized. + group_caches_[thread].InitOnce(); + TokenizeCoefficients( + &shared.coeff_orders[idx_pass * shared.coeff_order_size], rect, + ac_rows, shared.ac_strategy, frame_header->chroma_subsampling, + &group_caches_[thread].num_nzeroes, + &enc_state_->passes[idx_pass].ac_tokens[group_index], + enc_state_->shared.quant_dc, enc_state_->shared.raw_quant_field, + enc_state_->shared.block_ctx_map); + } + }; + RunOnPool(pool_, 0, shared.frame_dim.num_groups, tokenize_group_init, + tokenize_group, "TokenizeGroup"); + *frame_header = shared.frame_header; + return true; + } + + Status EncodeGlobalDCInfo(const FrameHeader& frame_header, + BitWriter* writer) const { + // Encode quantizer DC and global scale. + JXL_RETURN_IF_ERROR( + enc_state_->shared.quantizer.Encode(writer, kLayerQuant, aux_out_)); + EncodeBlockCtxMap(enc_state_->shared.block_ctx_map, writer, aux_out_); + ColorCorrelationMapEncodeDC(&enc_state_->shared.cmap, writer, kLayerDC, + aux_out_); + return true; + } + + Status EncodeGlobalACInfo(BitWriter* writer, + ModularFrameEncoder* modular_frame_encoder) { + JXL_RETURN_IF_ERROR(DequantMatricesEncode(&enc_state_->shared.matrices, + writer, kLayerDequantTables, + aux_out_, modular_frame_encoder)); + if (enc_state_->cparams.speed_tier <= SpeedTier::kTortoise) { + ClusterGroups(enc_state_); + } + size_t num_histo_bits = + CeilLog2Nonzero(enc_state_->shared.frame_dim.num_groups); + if (num_histo_bits != 0) { + BitWriter::Allotment allotment(writer, num_histo_bits); + writer->Write(num_histo_bits, enc_state_->shared.num_histograms - 1); + ReclaimAndCharge(writer, &allotment, kLayerAC, aux_out_); + } + + for (size_t i = 0; i < enc_state_->progressive_splitter.GetNumPasses(); + i++) { + // Encode coefficient orders. + size_t order_bits = 0; + JXL_RETURN_IF_ERROR(U32Coder::CanEncode( + kOrderEnc, enc_state_->used_orders[i], &order_bits)); + BitWriter::Allotment allotment(writer, order_bits); + JXL_CHECK(U32Coder::Write(kOrderEnc, enc_state_->used_orders[i], writer)); + ReclaimAndCharge(writer, &allotment, kLayerOrder, aux_out_); + EncodeCoeffOrders( + enc_state_->used_orders[i], + &enc_state_->shared + .coeff_orders[i * enc_state_->shared.coeff_order_size], + writer, kLayerOrder, aux_out_); + + // Encode histograms. + HistogramParams hist_params( + enc_state_->cparams.speed_tier, + enc_state_->shared.block_ctx_map.NumACContexts()); + if (enc_state_->cparams.speed_tier > SpeedTier::kTortoise) { + hist_params.lz77_method = HistogramParams::LZ77Method::kNone; + } + if (enc_state_->cparams.decoding_speed_tier >= 1) { + hist_params.max_histograms = 6; + } + BuildAndEncodeHistograms( + hist_params, + enc_state_->shared.num_histograms * + enc_state_->shared.block_ctx_map.NumACContexts(), + enc_state_->passes[i].ac_tokens, &enc_state_->passes[i].codes, + &enc_state_->passes[i].context_map, writer, kLayerAC, aux_out_); + } + + return true; + } + + Status EncodeACGroup(size_t pass, size_t group_index, BitWriter* group_code, + AuxOut* local_aux_out) { + return EncodeGroupTokenizedCoefficients( + group_index, pass, enc_state_->histogram_idx[group_index], *enc_state_, + group_code, local_aux_out); + } + + PassesEncoderState* State() { return enc_state_; } + + private: + void ComputeAllCoeffOrders(const FrameDimensions& frame_dim) { + PROFILER_FUNC; + enc_state_->used_orders.resize( + enc_state_->progressive_splitter.GetNumPasses()); + for (size_t i = 0; i < enc_state_->progressive_splitter.GetNumPasses(); + i++) { + // No coefficient reordering in Falcon or faster. + if (enc_state_->cparams.speed_tier < SpeedTier::kFalcon) { + enc_state_->used_orders[i] = ComputeUsedOrders( + enc_state_->cparams.speed_tier, enc_state_->shared.ac_strategy, + Rect(enc_state_->shared.raw_quant_field)); + } + ComputeCoeffOrder( + enc_state_->cparams.speed_tier, *enc_state_->coeffs[i], + enc_state_->shared.ac_strategy, frame_dim, enc_state_->used_orders[i], + &enc_state_->shared + .coeff_orders[i * enc_state_->shared.coeff_order_size]); + } + } + + template + static inline void FindIndexOfSumMaximum(const V* array, const size_t len, + R* idx, V* sum) { + JXL_ASSERT(len > 0); + V maxval = 0; + V val = 0; + R maxidx = 0; + for (size_t i = 0; i < len; ++i) { + val += array[i]; + if (val > maxval) { + maxval = val; + maxidx = i; + } + } + *idx = maxidx; + *sum = maxval; + } + + PassesEncoderState* JXL_RESTRICT enc_state_; + ThreadPool* pool_; + AuxOut* aux_out_; + std::vector group_caches_; +}; + +Status EncodeFrame(const CompressParams& cparams_orig, + const FrameInfo& frame_info, const CodecMetadata* metadata, + const ImageBundle& ib, PassesEncoderState* passes_enc_state, + ThreadPool* pool, BitWriter* writer, AuxOut* aux_out) { + ib.VerifyMetadata(); + + passes_enc_state->special_frames.clear(); + + CompressParams cparams = cparams_orig; + + if (cparams.progressive_dc < 0) { + if (cparams.progressive_dc != -1) { + return JXL_FAILURE("Invalid progressive DC setting value (%d)", + cparams.progressive_dc); + } + cparams.progressive_dc = 0; + // Enable progressive_dc for lower qualities. + if (cparams.butteraugli_distance >= + kMinButteraugliDistanceForProgressiveDc) { + cparams.progressive_dc = 1; + } + } + if (cparams.ec_resampling < cparams.resampling) { + cparams.ec_resampling = cparams.resampling; + } + if (cparams.resampling > 1) cparams.progressive_dc = 0; + + if (frame_info.dc_level + cparams.progressive_dc > 4) { + return JXL_FAILURE("Too many levels of progressive DC"); + } + + if (cparams.butteraugli_distance != 0 && + cparams.butteraugli_distance < kMinButteraugliDistance) { + 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; + cparams.epf = 0; + cparams.modular_mode = false; + } + + if (ib.xsize() == 0 || ib.ysize() == 0) return JXL_FAILURE("Empty image"); + + // Assert that this metadata is correctly set up for the compression params, + // this should have been done by enc_file.cc + JXL_ASSERT(metadata->m.xyb_encoded == + (cparams.color_transform == ColorTransform::kXYB)); + std::unique_ptr frame_header = + jxl::make_unique(metadata); + JXL_RETURN_IF_ERROR(MakeFrameHeader(cparams, + passes_enc_state->progressive_splitter, + frame_info, ib, frame_header.get())); + // Check that if the codestream header says xyb_encoded, the color_transform + // matches the requirement. This is checked from the cparams here, even though + // optimally we'd be able to check this against what has actually been written + // in the main codestream header, but since ib is a const object and the data + // written to the main codestream header is (in modified form) in ib, the + // encoder cannot indicate this fact in the ib's metadata. + if (cparams_orig.color_transform == ColorTransform::kXYB) { + if (frame_header->color_transform != ColorTransform::kXYB) { + return JXL_FAILURE( + "The color transform of frames must be xyb if the codestream is xyb " + "encoded"); + } + } else { + if (frame_header->color_transform == ColorTransform::kXYB) { + return JXL_FAILURE( + "The color transform of frames cannot be xyb if the codestream is " + "not xyb encoded"); + } + } + + FrameDimensions frame_dim = frame_header->ToFrameDimensions(); + + const size_t num_groups = frame_dim.num_groups; + + Image3F opsin; + const ColorEncoding& c_linear = ColorEncoding::LinearSRGB(ib.IsGray()); + std::unique_ptr metadata_linear = + jxl::make_unique(); + metadata_linear->xyb_encoded = + (cparams.color_transform == ColorTransform::kXYB); + metadata_linear->color_encoding = c_linear; + ImageBundle linear_storage(metadata_linear.get()); + + std::vector aux_outs; + // LossyFrameEncoder stores a reference to a std::function + // so we need to keep the std::function being referenced + // alive while lossy_frame_encoder is used. We could make resize_aux_outs a + // lambda type by making LossyFrameEncoder a template instead, but this is + // simpler. + const std::function resize_aux_outs = + [&aux_outs, aux_out](size_t num_threads) -> Status { + if (aux_out != nullptr) { + size_t old_size = aux_outs.size(); + for (size_t i = num_threads; i < old_size; i++) { + aux_out->Assimilate(aux_outs[i]); + } + aux_outs.resize(num_threads); + // Each thread needs these INPUTS. Don't copy the entire AuxOut + // because it may contain stats which would be Assimilated multiple + // times below. + for (size_t i = old_size; i < aux_outs.size(); i++) { + aux_outs[i].dump_image = aux_out->dump_image; + aux_outs[i].debug_prefix = aux_out->debug_prefix; + } + } + return true; + }; + + LossyFrameEncoder lossy_frame_encoder(cparams, *frame_header, + passes_enc_state, pool, aux_out); + std::unique_ptr modular_frame_encoder = + jxl::make_unique(*frame_header, cparams); + + const std::vector* extra_channels = &ib.extra_channels(); + std::vector extra_channels_storage; + // Clear patches + passes_enc_state->shared.image_features.patches = PatchDictionary(); + passes_enc_state->shared.image_features.patches.SetPassesSharedState( + &passes_enc_state->shared); + + if (ib.IsJPEG()) { + JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeJPEGTranscodingData( + *ib.jpeg_data, modular_frame_encoder.get(), frame_header.get())); + } else if (!lossy_frame_encoder.State()->heuristics->HandlesColorConversion( + cparams, ib) || + frame_header->encoding != FrameEncoding::kVarDCT) { + // Allocating a large enough image avoids a copy when padding. + opsin = + Image3F(RoundUpToBlockDim(ib.xsize()), RoundUpToBlockDim(ib.ysize())); + opsin.ShrinkTo(ib.xsize(), ib.ysize()); + + const bool want_linear = frame_header->encoding == FrameEncoding::kVarDCT && + cparams.speed_tier <= SpeedTier::kKitten; + const ImageBundle* JXL_RESTRICT ib_or_linear = &ib; + + if (frame_header->color_transform == ColorTransform::kXYB && + frame_info.ib_needs_color_transform) { + // linear_storage would only be used by the Butteraugli loop (passing + // linear sRGB avoids a color conversion there). Otherwise, don't + // fill it to reduce memory usage. + ib_or_linear = + ToXYB(ib, pool, &opsin, want_linear ? &linear_storage : nullptr); + } else { // RGB or YCbCr: don't do anything (forward YCbCr is not + // implemented, this is only used when the input is already in + // YCbCr) + // If encoding a special DC or reference frame, don't do anything: + // input is already in XYB. + CopyImageTo(ib.color(), &opsin); + } + bool lossless = (frame_header->encoding == FrameEncoding::kModular && + cparams.quality_pair.first == 100); + if (ib.HasAlpha() && !ib.AlphaIsPremultiplied() && + frame_header->frame_type == FrameType::kRegularFrame && + !ApplyOverride(cparams.keep_invisible, lossless) && + cparams.ec_resampling == cparams.resampling) { + // simplify invisible pixels + SimplifyInvisible(&opsin, ib.alpha(), lossless); + if (want_linear) { + SimplifyInvisible(const_cast(&ib_or_linear->color()), + ib.alpha(), lossless); + } + } + if (aux_out != nullptr) { + JXL_RETURN_IF_ERROR( + aux_out->InspectImage3F("enc_frame:OpsinDynamicsImage", opsin)); + } + if (frame_header->encoding == FrameEncoding::kVarDCT) { + PadImageToBlockMultipleInPlace(&opsin); + JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( + ib_or_linear, &opsin, pool, modular_frame_encoder.get(), writer, + frame_header.get())); + } else if (frame_header->upsampling != 1 && !cparams.already_downsampled) { + // In VarDCT mode, LossyFrameHeuristics takes care of running downsampling + // after noise, if necessary. + DownsampleImage(&opsin, frame_header->upsampling); + } + } else { + JXL_RETURN_IF_ERROR(lossy_frame_encoder.ComputeEncodingData( + &ib, &opsin, pool, modular_frame_encoder.get(), writer, + frame_header.get())); + } + if (cparams.ec_resampling != 1 && !cparams.already_downsampled) { + extra_channels = &extra_channels_storage; + for (size_t i = 0; i < ib.extra_channels().size(); i++) { + extra_channels_storage.emplace_back(CopyImage(ib.extra_channels()[i])); + DownsampleImage(&extra_channels_storage.back(), cparams.ec_resampling); + } + } + // needs to happen *AFTER* VarDCT-ComputeEncodingData. + JXL_RETURN_IF_ERROR(modular_frame_encoder->ComputeEncodingData( + *frame_header, *ib.metadata(), &opsin, *extra_channels, + lossy_frame_encoder.State(), pool, aux_out, + /* do_color=*/frame_header->encoding == FrameEncoding::kModular)); + + writer->AppendByteAligned(lossy_frame_encoder.State()->special_frames); + frame_header->UpdateFlag( + lossy_frame_encoder.State()->shared.image_features.patches.HasAny(), + FrameHeader::kPatches); + frame_header->UpdateFlag( + lossy_frame_encoder.State()->shared.image_features.splines.HasAny(), + FrameHeader::kSplines); + JXL_RETURN_IF_ERROR(WriteFrameHeader(*frame_header, writer, aux_out)); + + const size_t num_passes = + passes_enc_state->progressive_splitter.GetNumPasses(); + + // DC global info + DC groups + AC global info + AC groups * + // num_passes. + const bool has_ac_global = true; + std::vector group_codes(NumTocEntries(frame_dim.num_groups, + frame_dim.num_dc_groups, + num_passes, has_ac_global)); + const size_t global_ac_index = frame_dim.num_dc_groups + 1; + const bool is_small_image = frame_dim.num_groups == 1 && num_passes == 1; + const auto get_output = [&](const size_t index) { + return &group_codes[is_small_image ? 0 : index]; + }; + auto ac_group_code = [&](size_t pass, size_t group) { + return get_output(AcGroupIndex(pass, group, frame_dim.num_groups, + frame_dim.num_dc_groups, has_ac_global)); + }; + + if (frame_header->flags & FrameHeader::kPatches) { + PatchDictionaryEncoder::Encode( + lossy_frame_encoder.State()->shared.image_features.patches, + get_output(0), kLayerDictionary, aux_out); + } + + if (frame_header->flags & FrameHeader::kSplines) { + EncodeSplines(lossy_frame_encoder.State()->shared.image_features.splines, + get_output(0), kLayerSplines, HistogramParams(), aux_out); + } + + if (frame_header->flags & FrameHeader::kNoise) { + EncodeNoise(lossy_frame_encoder.State()->shared.image_features.noise_params, + get_output(0), kLayerNoise, aux_out); + } + + JXL_RETURN_IF_ERROR( + DequantMatricesEncodeDC(&lossy_frame_encoder.State()->shared.matrices, + get_output(0), kLayerDequantTables, aux_out)); + if (frame_header->encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR( + lossy_frame_encoder.EncodeGlobalDCInfo(*frame_header, get_output(0))); + } + JXL_RETURN_IF_ERROR( + modular_frame_encoder->EncodeGlobalInfo(get_output(0), aux_out)); + JXL_RETURN_IF_ERROR(modular_frame_encoder->EncodeStream( + get_output(0), aux_out, kLayerModularGlobal, ModularStreamId::Global())); + + const auto process_dc_group = [&](const int group_index, const int thread) { + AuxOut* my_aux_out = aux_out ? &aux_outs[thread] : nullptr; + BitWriter* output = get_output(group_index + 1); + if (frame_header->encoding == FrameEncoding::kVarDCT && + !(frame_header->flags & FrameHeader::kUseDcFrame)) { + BitWriter::Allotment allotment(output, 2); + output->Write(2, modular_frame_encoder->extra_dc_precision[group_index]); + ReclaimAndCharge(output, &allotment, kLayerDC, my_aux_out); + JXL_CHECK(modular_frame_encoder->EncodeStream( + output, my_aux_out, kLayerDC, + ModularStreamId::VarDCTDC(group_index))); + } + JXL_CHECK(modular_frame_encoder->EncodeStream( + output, my_aux_out, kLayerModularDcGroup, + ModularStreamId::ModularDC(group_index))); + if (frame_header->encoding == FrameEncoding::kVarDCT) { + const Rect& rect = + lossy_frame_encoder.State()->shared.DCGroupRect(group_index); + size_t nb_bits = CeilLog2Nonzero(rect.xsize() * rect.ysize()); + if (nb_bits != 0) { + BitWriter::Allotment allotment(output, nb_bits); + output->Write(nb_bits, + modular_frame_encoder->ac_metadata_size[group_index] - 1); + ReclaimAndCharge(output, &allotment, kLayerControlFields, my_aux_out); + } + JXL_CHECK(modular_frame_encoder->EncodeStream( + output, my_aux_out, kLayerControlFields, + ModularStreamId::ACMetadata(group_index))); + } + }; + RunOnPool(pool, 0, frame_dim.num_dc_groups, resize_aux_outs, process_dc_group, + "EncodeDCGroup"); + + if (frame_header->encoding == FrameEncoding::kVarDCT) { + JXL_RETURN_IF_ERROR(lossy_frame_encoder.EncodeGlobalACInfo( + get_output(global_ac_index), modular_frame_encoder.get())); + } + + std::atomic num_errors{0}; + const auto process_group = [&](const int group_index, const int thread) { + AuxOut* my_aux_out = aux_out ? &aux_outs[thread] : nullptr; + + for (size_t i = 0; i < num_passes; i++) { + if (frame_header->encoding == FrameEncoding::kVarDCT) { + if (!lossy_frame_encoder.EncodeACGroup( + i, group_index, ac_group_code(i, group_index), my_aux_out)) { + num_errors.fetch_add(1, std::memory_order_relaxed); + return; + } + } + // Write all modular encoded data (color?, alpha, depth, extra channels) + if (!modular_frame_encoder->EncodeStream( + ac_group_code(i, group_index), my_aux_out, kLayerModularAcGroup, + ModularStreamId::ModularAC(group_index, i))) { + num_errors.fetch_add(1, std::memory_order_relaxed); + return; + } + } + }; + RunOnPool(pool, 0, num_groups, resize_aux_outs, process_group, + "EncodeGroupCoefficients"); + + // Resizing aux_outs to 0 also Assimilates the array. + static_cast(resize_aux_outs(0)); + JXL_RETURN_IF_ERROR(num_errors.load(std::memory_order_relaxed) == 0); + + for (BitWriter& bw : group_codes) { + bw.ZeroPadToByte(); // end of group. + } + + std::vector* permutation_ptr = nullptr; + std::vector permutation; + if (cparams.centerfirst && !(num_passes == 1 && num_groups == 1)) { + permutation_ptr = &permutation; + // Don't permute global DC/AC or DC. + permutation.resize(global_ac_index + 1); + std::iota(permutation.begin(), permutation.end(), 0); + std::vector ac_group_order(num_groups); + std::iota(ac_group_order.begin(), ac_group_order.end(), 0); + size_t group_dim = frame_dim.group_dim; + + // The center of the image is either given by parameters or chosen + // to be the middle of the image by default if center_x, center_y resp. + // are not provided. + + int64_t imag_cx; + if (cparams.center_x != static_cast(-1)) { + JXL_RETURN_IF_ERROR(cparams.center_x < ib.xsize()); + imag_cx = cparams.center_x; + } else { + imag_cx = ib.xsize() / 2; + } + + int64_t imag_cy; + if (cparams.center_y != static_cast(-1)) { + JXL_RETURN_IF_ERROR(cparams.center_y < ib.ysize()); + imag_cy = cparams.center_y; + } else { + imag_cy = ib.ysize() / 2; + } + + // The center of the group containing the center of the image. + int64_t cx = (imag_cx / group_dim) * group_dim + group_dim / 2; + int64_t cy = (imag_cy / group_dim) * group_dim + group_dim / 2; + // This identifies in what area of the central group the center of the image + // lies in. + double direction = -std::atan2(imag_cy - cy, imag_cx - cx); + // This identifies the side of the central group the center of the image + // lies closest to. This can take values 0, 1, 2, 3 corresponding to left, + // bottom, right, top. + int64_t side = std::fmod((direction + 5 * kPi / 4), 2 * kPi) * 2 / kPi; + auto get_distance_from_center = [&](size_t gid) { + Rect r = passes_enc_state->shared.GroupRect(gid); + int64_t gcx = r.x0() + group_dim / 2; + int64_t gcy = r.y0() + group_dim / 2; + int64_t dx = gcx - cx; + int64_t dy = gcy - cy; + // The angle is determined by taking atan2 and adding an appropriate + // starting point depending on the side we want to start on. + double angle = std::remainder( + std::atan2(dy, dx) + kPi / 4 + side * (kPi / 2), 2 * kPi); + // Concentric squares in clockwise order. + return std::make_pair(std::max(std::abs(dx), std::abs(dy)), angle); + }; + std::sort(ac_group_order.begin(), ac_group_order.end(), + [&](coeff_order_t a, coeff_order_t b) { + return get_distance_from_center(a) < + get_distance_from_center(b); + }); + std::vector inv_ac_group_order(ac_group_order.size(), 0); + for (size_t i = 0; i < ac_group_order.size(); i++) { + inv_ac_group_order[ac_group_order[i]] = i; + } + for (size_t i = 0; i < num_passes; i++) { + size_t pass_start = permutation.size(); + for (coeff_order_t v : inv_ac_group_order) { + permutation.push_back(pass_start + v); + } + } + std::vector new_group_codes(group_codes.size()); + for (size_t i = 0; i < permutation.size(); i++) { + new_group_codes[permutation[i]] = std::move(group_codes[i]); + } + group_codes = std::move(new_group_codes); + } + + JXL_RETURN_IF_ERROR( + WriteGroupOffsets(group_codes, permutation_ptr, writer, aux_out)); + writer->AppendByteAligned(group_codes); + writer->ZeroPadToByte(); // end of frame. + + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_frame.h b/lib/jxl/enc_frame.h new file mode 100644 index 0000000..ba99991 --- /dev/null +++ b/lib/jxl/enc_frame.h @@ -0,0 +1,51 @@ +// 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_ENC_FRAME_H_ +#define LIB_JXL_ENC_FRAME_H_ + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Information needed for encoding a frame that is not contained elsewhere and +// does not belong to `cparams`. +struct FrameInfo { + // TODO(veluca): consider adding more parameters, such as custom patches. + bool save_before_color_transform = false; + // Whether or not the input image bundle is already in the codestream + // colorspace (as deduced by cparams). + // TODO(veluca): this is a hack - ImageBundle doesn't have a simple way to say + // "this is already in XYB". + bool ib_needs_color_transform = true; + FrameType frame_type = FrameType::kRegularFrame; + size_t dc_level = 0; + // Only used for kRegularFrame. + bool is_last = true; + bool is_preview = false; + // Information for storing this frame for future use (only for non-DC frames). + size_t save_as_reference = 0; +}; + +// Encodes a single frame (including its header) into a byte stream. Groups may +// be processed in parallel by `pool`. metadata is the ImageMetadata encoded in +// the codestream, and must be used for the FrameHeaders, do not use +// ib.metadata. +Status EncodeFrame(const CompressParams& cparams_orig, + const FrameInfo& frame_info, const CodecMetadata* metadata, + const ImageBundle& ib, PassesEncoderState* passes_enc_state, + ThreadPool* pool, BitWriter* writer, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_FRAME_H_ diff --git a/lib/jxl/enc_gamma_correct.h b/lib/jxl/enc_gamma_correct.h new file mode 100644 index 0000000..0db7012 --- /dev/null +++ b/lib/jxl/enc_gamma_correct.h @@ -0,0 +1,36 @@ +// 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_ENC_GAMMA_CORRECT_H_ +#define LIB_JXL_ENC_GAMMA_CORRECT_H_ + +// Deprecated: sRGB transfer function. Use color_management.h instead. + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/transfer_functions-inl.h" + +namespace jxl { + +// Values are in [0, 1]. +static JXL_INLINE double Srgb8ToLinearDirect(double srgb) { + if (srgb <= 0.0) return 0.0; + if (srgb <= 0.04045) return srgb / 12.92; + if (srgb >= 1.0) return 1.0; + return std::pow((srgb + 0.055) / 1.055, 2.4); +} + +// Values are in [0, 1]. +static JXL_INLINE double LinearToSrgb8Direct(double linear) { + if (linear <= 0.0) return 0.0; + if (linear >= 1.0) return 1.0; + if (linear <= 0.0031308) return linear * 12.92; + return std::pow(linear, 1.0 / 2.4) * 1.055 - 0.055; +} + +} // namespace jxl + +#endif // LIB_JXL_ENC_GAMMA_CORRECT_H_ diff --git a/lib/jxl/enc_group.cc b/lib/jxl/enc_group.cc new file mode 100644 index 0000000..91357dc --- /dev/null +++ b/lib/jxl/enc_group.cc @@ -0,0 +1,342 @@ +// 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/enc_group.h" + +#include + +#include "hwy/aligned_allocator.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_group.cc" +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_transforms-inl.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_transforms-inl.h" +#include "lib/jxl/image.h" +#include "lib/jxl/quantizer-inl.h" +#include "lib/jxl/quantizer.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// NOTE: caller takes care of extracting quant from rect of RawQuantField. +void QuantizeBlockAC(const Quantizer& quantizer, const bool error_diffusion, + size_t c, int32_t quant, float qm_multiplier, + size_t quant_kind, size_t xsize, size_t ysize, + const float* JXL_RESTRICT block_in, + int32_t* JXL_RESTRICT block_out) { + PROFILER_FUNC; + const float* JXL_RESTRICT qm = quantizer.InvDequantMatrix(quant_kind, c); + const float qac = quantizer.Scale() * quant; + // Not SIMD-fied for now. + float thres[4] = {0.5f, 0.6f, 0.6f, 0.65f}; + if (c != 1) { + for (int i = 1; i < 4; ++i) { + thres[i] = 0.75f; + } + } + + if (!error_diffusion) { + HWY_CAPPED(float, kBlockDim) df; + HWY_CAPPED(int32_t, kBlockDim) di; + HWY_CAPPED(uint32_t, kBlockDim) du; + const auto quant = Set(df, qac * qm_multiplier); + + for (size_t y = 0; y < ysize * kBlockDim; y++) { + size_t yfix = static_cast(y >= ysize * kBlockDim / 2) * 2; + const size_t off = y * kBlockDim * xsize; + for (size_t x = 0; x < xsize * kBlockDim; x += Lanes(df)) { + auto thr = Zero(df); + if (xsize == 1) { + HWY_ALIGN uint32_t kMask[kBlockDim] = {0, 0, 0, 0, + ~0u, ~0u, ~0u, ~0u}; + const auto mask = MaskFromVec(BitCast(df, Load(du, kMask + x))); + thr = + IfThenElse(mask, Set(df, thres[yfix + 1]), Set(df, thres[yfix])); + } else { + // Same for all lanes in the vector. + thr = Set( + df, + thres[yfix + static_cast(x >= xsize * kBlockDim / 2)]); + } + + const auto q = Load(df, qm + off + x) * quant; + const auto in = Load(df, block_in + off + x); + const auto val = q * in; + const auto nzero_mask = Abs(val) >= thr; + const auto v = ConvertTo(di, IfThenElseZero(nzero_mask, Round(val))); + Store(v, di, block_out + off + x); + } + } + return; + } + +retry: + int hfNonZeros[4] = {}; + float hfError[4] = {}; + float hfMaxError[4] = {}; + size_t hfMaxErrorIx[4] = {}; + for (size_t y = 0; y < ysize * kBlockDim; y++) { + for (size_t x = 0; x < xsize * kBlockDim; x++) { + const size_t pos = y * kBlockDim * xsize + x; + if (x < xsize && y < ysize) { + // Ensure block is initialized + block_out[pos] = 0; + continue; + } + const size_t hfix = (static_cast(y >= ysize * kBlockDim / 2) * 2 + + static_cast(x >= xsize * kBlockDim / 2)); + const float val = block_in[pos] * (qm[pos] * qac * qm_multiplier); + float v = (std::abs(val) < thres[hfix]) ? 0 : rintf(val); + const float error = std::abs(val) - std::abs(v); + hfError[hfix] += error; + if (hfMaxError[hfix] < error) { + hfMaxError[hfix] = error; + hfMaxErrorIx[hfix] = pos; + } + if (v != 0.0f) { + hfNonZeros[hfix] += std::abs(v); + } + block_out[pos] = static_cast(rintf(v)); + } + } + if (c != 1) return; + // TODO(veluca): include AFV? + const size_t kPartialBlockKinds = + (1 << AcStrategy::Type::IDENTITY) | (1 << AcStrategy::Type::DCT2X2) | + (1 << AcStrategy::Type::DCT4X4) | (1 << AcStrategy::Type::DCT4X8) | + (1 << AcStrategy::Type::DCT8X4); + if ((1 << quant_kind) & kPartialBlockKinds) return; + float hfErrorLimit = 0.1f * (xsize * ysize) * kDCTBlockSize * 0.25f; + bool goretry = false; + for (int i = 1; i < 4; ++i) { + if (hfError[i] >= hfErrorLimit && + hfNonZeros[i] <= (xsize + ysize) * 0.25f) { + if (thres[i] >= 0.4f) { + thres[i] -= 0.01f; + goretry = true; + } + } + } + if (goretry) goto retry; + for (int i = 1; i < 4; ++i) { + if (hfError[i] >= hfErrorLimit && hfNonZeros[i] == 0) { + const size_t pos = hfMaxErrorIx[i]; + if (hfMaxError[i] >= 0.4f) { + block_out[pos] = block_in[pos] > 0.0f ? 1.0f : -1.0f; + } + } + } +} + +// NOTE: caller takes care of extracting quant from rect of RawQuantField. +void QuantizeRoundtripYBlockAC(const Quantizer& quantizer, + const bool error_diffusion, int32_t quant, + size_t quant_kind, size_t xsize, size_t ysize, + const float* JXL_RESTRICT biases, + float* JXL_RESTRICT inout, + int32_t* JXL_RESTRICT quantized) { + QuantizeBlockAC(quantizer, error_diffusion, 1, quant, 1.0f, quant_kind, xsize, + ysize, inout, quantized); + + PROFILER_ZONE("enc quant adjust bias"); + const float* JXL_RESTRICT dequant_matrix = + quantizer.DequantMatrix(quant_kind, 1); + + HWY_CAPPED(float, kDCTBlockSize) df; + HWY_CAPPED(int32_t, kDCTBlockSize) di; + const auto inv_qac = Set(df, quantizer.inv_quant_ac(quant)); + for (size_t k = 0; k < kDCTBlockSize * xsize * ysize; k += Lanes(df)) { + const auto quant = Load(di, quantized + k); + const auto adj_quant = AdjustQuantBias(di, 1, quant, biases); + const auto dequantm = Load(df, dequant_matrix + k); + Store(adj_quant * dequantm * inv_qac, df, inout + k); + } +} + +void ComputeCoefficients(size_t group_idx, PassesEncoderState* enc_state, + const Image3F& opsin, Image3F* dc) { + PROFILER_FUNC; + const Rect block_group_rect = enc_state->shared.BlockGroupRect(group_idx); + const Rect group_rect = enc_state->shared.GroupRect(group_idx); + const Rect cmap_rect( + block_group_rect.x0() / kColorTileDimInBlocks, + block_group_rect.y0() / kColorTileDimInBlocks, + DivCeil(block_group_rect.xsize(), kColorTileDimInBlocks), + DivCeil(block_group_rect.ysize(), kColorTileDimInBlocks)); + + const size_t xsize_blocks = block_group_rect.xsize(); + const size_t ysize_blocks = block_group_rect.ysize(); + + const size_t dc_stride = static_cast(dc->PixelsPerRow()); + const size_t opsin_stride = static_cast(opsin.PixelsPerRow()); + + const ImageI& full_quant_field = enc_state->shared.raw_quant_field; + const CompressParams& cparams = enc_state->cparams; + + // TODO(veluca): consider strategies to reduce this memory. + auto mem = hwy::AllocateAligned(3 * AcStrategy::kMaxCoeffArea); + auto fmem = hwy::AllocateAligned(5 * AcStrategy::kMaxCoeffArea); + float* JXL_RESTRICT scratch_space = + fmem.get() + 3 * AcStrategy::kMaxCoeffArea; + { + // Only use error diffusion in Squirrel mode or slower. + const bool error_diffusion = cparams.speed_tier <= SpeedTier::kSquirrel; + constexpr HWY_CAPPED(float, kDCTBlockSize) d; + + int32_t* JXL_RESTRICT coeffs[kMaxNumPasses][3] = {}; + size_t num_passes = enc_state->progressive_splitter.GetNumPasses(); + JXL_DASSERT(num_passes > 0); + for (size_t i = 0; i < num_passes; i++) { + // TODO(veluca): 16-bit quantized coeffs are not implemented yet. + JXL_ASSERT(enc_state->coeffs[i]->Type() == ACType::k32); + for (size_t c = 0; c < 3; c++) { + coeffs[i][c] = enc_state->coeffs[i]->PlaneRow(c, group_idx, 0).ptr32; + } + } + + HWY_ALIGN float* coeffs_in = fmem.get(); + HWY_ALIGN int32_t* quantized = mem.get(); + + size_t offset = 0; + + for (size_t by = 0; by < ysize_blocks; ++by) { + const int32_t* JXL_RESTRICT row_quant_ac = + block_group_rect.ConstRow(full_quant_field, by); + size_t ty = by / kColorTileDimInBlocks; + const int8_t* JXL_RESTRICT row_cmap[3] = { + cmap_rect.ConstRow(enc_state->shared.cmap.ytox_map, ty), + nullptr, + cmap_rect.ConstRow(enc_state->shared.cmap.ytob_map, ty), + }; + const float* JXL_RESTRICT opsin_rows[3] = { + group_rect.ConstPlaneRow(opsin, 0, by * kBlockDim), + group_rect.ConstPlaneRow(opsin, 1, by * kBlockDim), + group_rect.ConstPlaneRow(opsin, 2, by * kBlockDim), + }; + float* JXL_RESTRICT dc_rows[3] = { + block_group_rect.PlaneRow(dc, 0, by), + block_group_rect.PlaneRow(dc, 1, by), + block_group_rect.PlaneRow(dc, 2, by), + }; + AcStrategyRow ac_strategy_row = + enc_state->shared.ac_strategy.ConstRow(block_group_rect, by); + for (size_t tx = 0; tx < DivCeil(xsize_blocks, kColorTileDimInBlocks); + tx++) { + const auto x_factor = + Set(d, enc_state->shared.cmap.YtoXRatio(row_cmap[0][tx])); + const auto b_factor = + Set(d, enc_state->shared.cmap.YtoBRatio(row_cmap[2][tx])); + for (size_t bx = tx * kColorTileDimInBlocks; + bx < xsize_blocks && bx < (tx + 1) * kColorTileDimInBlocks; ++bx) { + const AcStrategy acs = ac_strategy_row[bx]; + if (!acs.IsFirstBlock()) continue; + + size_t xblocks = acs.covered_blocks_x(); + size_t yblocks = acs.covered_blocks_y(); + + CoefficientLayout(&yblocks, &xblocks); + + size_t size = kDCTBlockSize * xblocks * yblocks; + + // DCT Y channel, roundtrip-quantize it and set DC. + const int32_t quant_ac = row_quant_ac[bx]; + TransformFromPixels(acs.Strategy(), opsin_rows[1] + bx * kBlockDim, + opsin_stride, coeffs_in + size, scratch_space); + DCFromLowestFrequencies(acs.Strategy(), coeffs_in + size, + dc_rows[1] + bx, dc_stride); + QuantizeRoundtripYBlockAC( + enc_state->shared.quantizer, error_diffusion, quant_ac, + acs.RawStrategy(), xblocks, yblocks, kDefaultQuantBias, + coeffs_in + size, quantized + size); + + // DCT X and B channels + for (size_t c : {0, 2}) { + TransformFromPixels(acs.Strategy(), opsin_rows[c] + bx * kBlockDim, + opsin_stride, coeffs_in + c * size, + scratch_space); + } + + // Unapply color correlation + for (size_t k = 0; k < size; k += Lanes(d)) { + const auto in_x = Load(d, coeffs_in + k); + const auto in_y = Load(d, coeffs_in + size + k); + const auto in_b = Load(d, coeffs_in + 2 * size + k); + const auto out_x = in_x - x_factor * in_y; + const auto out_b = in_b - b_factor * in_y; + Store(out_x, d, coeffs_in + k); + Store(out_b, d, coeffs_in + 2 * size + k); + } + + // Quantize X and B channels and set DC. + for (size_t c : {0, 2}) { + QuantizeBlockAC(enc_state->shared.quantizer, error_diffusion, c, + quant_ac, + c == 0 ? enc_state->x_qm_multiplier + : enc_state->b_qm_multiplier, + acs.RawStrategy(), xblocks, yblocks, + coeffs_in + c * size, quantized + c * size); + DCFromLowestFrequencies(acs.Strategy(), coeffs_in + c * size, + dc_rows[c] + bx, dc_stride); + } + enc_state->progressive_splitter.SplitACCoefficients( + quantized, size, acs, bx, by, offset, coeffs); + offset += size; + } + } + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ComputeCoefficients); +void ComputeCoefficients(size_t group_idx, PassesEncoderState* enc_state, + const Image3F& opsin, Image3F* dc) { + return HWY_DYNAMIC_DISPATCH(ComputeCoefficients)(group_idx, enc_state, opsin, + dc); +} + +Status EncodeGroupTokenizedCoefficients(size_t group_idx, size_t pass_idx, + size_t histogram_idx, + const PassesEncoderState& enc_state, + BitWriter* writer, AuxOut* aux_out) { + // Select which histogram to use among those of the current pass. + const size_t num_histograms = enc_state.shared.num_histograms; + // num_histograms is 0 only for lossless. + JXL_ASSERT(num_histograms == 0 || histogram_idx < num_histograms); + size_t histo_selector_bits = CeilLog2Nonzero(num_histograms); + + if (histo_selector_bits != 0) { + BitWriter::Allotment allotment(writer, histo_selector_bits); + writer->Write(histo_selector_bits, histogram_idx); + ReclaimAndCharge(writer, &allotment, kLayerAC, aux_out); + } + WriteTokens(enc_state.passes[pass_idx].ac_tokens[group_idx], + enc_state.passes[pass_idx].codes, + enc_state.passes[pass_idx].context_map, writer, kLayerACTokens, + aux_out); + + return true; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_group.h b/lib/jxl/enc_group.h new file mode 100644 index 0000000..62468dd --- /dev/null +++ b/lib/jxl/enc_group.h @@ -0,0 +1,30 @@ +// 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_ENC_GROUP_H_ +#define LIB_JXL_ENC_GROUP_H_ + +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" + +namespace jxl { + +// Fills DC +void ComputeCoefficients(size_t group_idx, PassesEncoderState* enc_state, + const Image3F& opsin, Image3F* dc); + +Status EncodeGroupTokenizedCoefficients(size_t group_idx, size_t pass_idx, + size_t histogram_idx, + const PassesEncoderState& enc_state, + BitWriter* writer, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_GROUP_H_ diff --git a/lib/jxl/enc_heuristics.cc b/lib/jxl/enc_heuristics.cc new file mode 100644 index 0000000..b943521 --- /dev/null +++ b/lib/jxl/enc_heuristics.cc @@ -0,0 +1,946 @@ +// 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/enc_heuristics.h" + +#include +#include + +#include +#include +#include + +#include "lib/jxl/enc_ac_strategy.h" +#include "lib/jxl/enc_adaptive_quantization.h" +#include "lib/jxl/enc_ar_control_field.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_chroma_from_luma.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/enc_noise.h" +#include "lib/jxl/enc_patch_dictionary.h" +#include "lib/jxl/enc_photon_noise.h" +#include "lib/jxl/enc_quant_weights.h" +#include "lib/jxl/enc_splines.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/gaborish.h" + +namespace jxl { +namespace { +void FindBestBlockEntropyModel(PassesEncoderState& enc_state) { + if (enc_state.cparams.decoding_speed_tier >= 1) { + static constexpr uint8_t kSimpleCtxMap[] = { + // Cluster all blocks together + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + }; + static_assert( + 3 * kNumOrders == sizeof(kSimpleCtxMap) / sizeof *kSimpleCtxMap, + "Update simple context map"); + + auto bcm = enc_state.shared.block_ctx_map; + bcm.ctx_map.assign(std::begin(kSimpleCtxMap), std::end(kSimpleCtxMap)); + bcm.num_ctxs = 2; + bcm.num_dc_ctxs = 1; + return; + } + if (enc_state.cparams.speed_tier >= SpeedTier::kFalcon) { + return; + } + const ImageI& rqf = enc_state.shared.raw_quant_field; + // No need to change context modeling for small images. + size_t tot = rqf.xsize() * rqf.ysize(); + size_t size_for_ctx_model = + (1 << 10) * enc_state.cparams.butteraugli_distance; + if (tot < size_for_ctx_model) return; + + struct OccCounters { + // count the occurrences of each qf value and each strategy type. + OccCounters(const ImageI& rqf, const AcStrategyImage& ac_strategy) { + for (size_t y = 0; y < rqf.ysize(); y++) { + const int32_t* qf_row = rqf.Row(y); + AcStrategyRow acs_row = ac_strategy.ConstRow(y); + for (size_t x = 0; x < rqf.xsize(); x++) { + int ord = kStrategyOrder[acs_row[x].RawStrategy()]; + int qf = qf_row[x] - 1; + qf_counts[qf]++; + qf_ord_counts[ord][qf]++; + ord_counts[ord]++; + } + } + } + + size_t qf_counts[256] = {}; + size_t qf_ord_counts[kNumOrders][256] = {}; + size_t ord_counts[kNumOrders] = {}; + }; + // The OccCounters struct is too big to allocate on the stack. + std::unique_ptr counters( + new OccCounters(rqf, enc_state.shared.ac_strategy)); + + // Splitting the context model according to the quantization field seems to + // mostly benefit only large images. + size_t size_for_qf_split = (1 << 13) * enc_state.cparams.butteraugli_distance; + size_t num_qf_segments = tot < size_for_qf_split ? 1 : 2; + std::vector& qft = enc_state.shared.block_ctx_map.qf_thresholds; + qft.clear(); + // Divide the quant field in up to num_qf_segments segments. + size_t cumsum = 0; + size_t next = 1; + size_t last_cut = 256; + size_t cut = tot * next / num_qf_segments; + for (uint32_t j = 0; j < 256; j++) { + cumsum += counters->qf_counts[j]; + if (cumsum > cut) { + if (j != 0) { + qft.push_back(j); + } + last_cut = j; + while (cumsum > cut) { + next++; + cut = tot * next / num_qf_segments; + } + } else if (next > qft.size() + 1) { + if (j - 1 == last_cut && j != 0) { + qft.push_back(j); + } + } + } + + // Count the occurrences of each segment. + std::vector counts(kNumOrders * (qft.size() + 1)); + size_t qft_pos = 0; + for (size_t j = 0; j < 256; j++) { + if (qft_pos < qft.size() && j == qft[qft_pos]) { + qft_pos++; + } + for (size_t i = 0; i < kNumOrders; i++) { + counts[qft_pos + i * (qft.size() + 1)] += counters->qf_ord_counts[i][j]; + } + } + + // Repeatedly merge the lowest-count pair. + std::vector remap((qft.size() + 1) * kNumOrders); + std::iota(remap.begin(), remap.end(), 0); + std::vector clusters(remap); + size_t nb_clusters = Clamp1((int)(tot / size_for_ctx_model / 2), 4, 8); + // This is O(n^2 log n), but n <= 14. + while (clusters.size() > nb_clusters) { + std::sort(clusters.begin(), clusters.end(), + [&](int a, int b) { return counts[a] > counts[b]; }); + counts[clusters[clusters.size() - 2]] += counts[clusters.back()]; + counts[clusters.back()] = 0; + remap[clusters.back()] = clusters[clusters.size() - 2]; + clusters.pop_back(); + } + for (size_t i = 0; i < remap.size(); i++) { + while (remap[remap[i]] != remap[i]) { + remap[i] = remap[remap[i]]; + } + } + // Relabel starting from 0. + std::vector remap_remap(remap.size(), remap.size()); + size_t num = 0; + for (size_t i = 0; i < remap.size(); i++) { + if (remap_remap[remap[i]] == remap.size()) { + remap_remap[remap[i]] = num++; + } + remap[i] = remap_remap[remap[i]]; + } + // Write the block context map. + auto& ctx_map = enc_state.shared.block_ctx_map.ctx_map; + ctx_map = remap; + ctx_map.resize(remap.size() * 3); + for (size_t i = remap.size(); i < remap.size() * 3; i++) { + ctx_map[i] = remap[i % remap.size()] + num; + } + enc_state.shared.block_ctx_map.num_ctxs = + *std::max_element(ctx_map.begin(), ctx_map.end()) + 1; +} + +// Returns the target size based on whether bitrate or direct targetsize is +// given. +size_t TargetSize(const CompressParams& cparams, + const FrameDimensions& frame_dim) { + if (cparams.target_size > 0) { + return cparams.target_size; + } + if (cparams.target_bitrate > 0.0) { + return 0.5 + cparams.target_bitrate * frame_dim.xsize * frame_dim.ysize / + kBitsPerByte; + } + return 0; +} +} // namespace + +void FindBestDequantMatrices(const CompressParams& cparams, + const Image3F& opsin, + ModularFrameEncoder* modular_frame_encoder, + DequantMatrices* dequant_matrices) { + // TODO(veluca): quant matrices for no-gaborish. + // TODO(veluca): heuristics for in-bitstream quant tables. + *dequant_matrices = DequantMatrices(); + if (cparams.max_error_mode) { + // Set numerators of all quantization matrices to constant values. + float weights[3][1] = {{1.0f / cparams.max_error[0]}, + {1.0f / cparams.max_error[1]}, + {1.0f / cparams.max_error[2]}}; + DctQuantWeightParams dct_params(weights); + std::vector encodings(DequantMatrices::kNum, + QuantEncoding::DCT(dct_params)); + DequantMatricesSetCustom(dequant_matrices, encodings, + modular_frame_encoder); + float dc_weights[3] = {1.0f / cparams.max_error[0], + 1.0f / cparams.max_error[1], + 1.0f / cparams.max_error[2]}; + DequantMatricesSetCustomDC(dequant_matrices, dc_weights); + } +} + +bool DefaultEncoderHeuristics::HandlesColorConversion( + const CompressParams& cparams, const ImageBundle& ib) { + return cparams.noise != Override::kOn && cparams.patches != Override::kOn && + cparams.speed_tier >= SpeedTier::kWombat && cparams.resampling == 1 && + cparams.color_transform == ColorTransform::kXYB && + !cparams.modular_mode && !ib.HasAlpha(); +} + +namespace { + +void StoreMin2(const float v, float& min1, float& min2) { + if (v < min2) { + if (v < min1) { + min2 = min1; + min1 = v; + } else { + min2 = v; + } + } +} + +void CreateMask(const ImageF& image, ImageF& mask) { + for (size_t y = 0; y < image.ysize(); y++) { + auto* row_n = y > 0 ? image.Row(y - 1) : image.Row(y); + auto* row_in = image.Row(y); + auto* row_s = y + 1 < image.ysize() ? image.Row(y + 1) : image.Row(y); + auto* row_out = mask.Row(y); + for (size_t x = 0; x < image.xsize(); x++) { + // Center, west, east, north, south values and their absolute difference + float c = row_in[x]; + float w = x > 0 ? row_in[x - 1] : row_in[x]; + float e = x + 1 < image.xsize() ? row_in[x + 1] : row_in[x]; + float n = row_n[x]; + float s = row_s[x]; + float dw = std::abs(c - w); + float de = std::abs(c - e); + float dn = std::abs(c - n); + float ds = std::abs(c - s); + float min = std::numeric_limits::max(); + float min2 = std::numeric_limits::max(); + StoreMin2(dw, min, min2); + StoreMin2(de, min, min2); + StoreMin2(dn, min, min2); + StoreMin2(ds, min, min2); + row_out[x] = min2; + } + } +} + +// Downsamples the image by a factor of 2 with a kernel that's sharper than +// the standard 2x2 box kernel used by DownsampleImage. +// The kernel is optimized against the result of the 2x2 upsampling kernel used +// by the decoder. Ringing is slightly reduced by clamping the values of the +// resulting pixels within certain bounds of a small region in the original +// image. +void DownsampleImage2_Sharper(const ImageF& input, ImageF* output) { + const int64_t kernelx = 12; + const int64_t kernely = 12; + + static const float kernel[144] = { + -0.000314256996835, -0.000314256996835, -0.000897597057705, + -0.000562751488849, -0.000176807273646, 0.001864627368902, + 0.001864627368902, -0.000176807273646, -0.000562751488849, + -0.000897597057705, -0.000314256996835, -0.000314256996835, + -0.000314256996835, -0.001527942804748, -0.000121760530512, + 0.000191123989093, 0.010193185932466, 0.058637519197110, + 0.058637519197110, 0.010193185932466, 0.000191123989093, + -0.000121760530512, -0.001527942804748, -0.000314256996835, + -0.000897597057705, -0.000121760530512, 0.000946363683751, + 0.007113577630288, 0.000437956841058, -0.000372823835211, + -0.000372823835211, 0.000437956841058, 0.007113577630288, + 0.000946363683751, -0.000121760530512, -0.000897597057705, + -0.000562751488849, 0.000191123989093, 0.007113577630288, + 0.044592622228814, 0.000222278879007, -0.162864473015945, + -0.162864473015945, 0.000222278879007, 0.044592622228814, + 0.007113577630288, 0.000191123989093, -0.000562751488849, + -0.000176807273646, 0.010193185932466, 0.000437956841058, + 0.000222278879007, -0.000913092543974, -0.017071696107902, + -0.017071696107902, -0.000913092543974, 0.000222278879007, + 0.000437956841058, 0.010193185932466, -0.000176807273646, + 0.001864627368902, 0.058637519197110, -0.000372823835211, + -0.162864473015945, -0.017071696107902, 0.414660099370354, + 0.414660099370354, -0.017071696107902, -0.162864473015945, + -0.000372823835211, 0.058637519197110, 0.001864627368902, + 0.001864627368902, 0.058637519197110, -0.000372823835211, + -0.162864473015945, -0.017071696107902, 0.414660099370354, + 0.414660099370354, -0.017071696107902, -0.162864473015945, + -0.000372823835211, 0.058637519197110, 0.001864627368902, + -0.000176807273646, 0.010193185932466, 0.000437956841058, + 0.000222278879007, -0.000913092543974, -0.017071696107902, + -0.017071696107902, -0.000913092543974, 0.000222278879007, + 0.000437956841058, 0.010193185932466, -0.000176807273646, + -0.000562751488849, 0.000191123989093, 0.007113577630288, + 0.044592622228814, 0.000222278879007, -0.162864473015945, + -0.162864473015945, 0.000222278879007, 0.044592622228814, + 0.007113577630288, 0.000191123989093, -0.000562751488849, + -0.000897597057705, -0.000121760530512, 0.000946363683751, + 0.007113577630288, 0.000437956841058, -0.000372823835211, + -0.000372823835211, 0.000437956841058, 0.007113577630288, + 0.000946363683751, -0.000121760530512, -0.000897597057705, + -0.000314256996835, -0.001527942804748, -0.000121760530512, + 0.000191123989093, 0.010193185932466, 0.058637519197110, + 0.058637519197110, 0.010193185932466, 0.000191123989093, + -0.000121760530512, -0.001527942804748, -0.000314256996835, + -0.000314256996835, -0.000314256996835, -0.000897597057705, + -0.000562751488849, -0.000176807273646, 0.001864627368902, + 0.001864627368902, -0.000176807273646, -0.000562751488849, + -0.000897597057705, -0.000314256996835, -0.000314256996835}; + + int64_t xsize = input.xsize(); + int64_t ysize = input.ysize(); + + ImageF box_downsample = CopyImage(input); + DownsampleImage(&box_downsample, 2); + + ImageF mask(box_downsample.xsize(), box_downsample.ysize()); + CreateMask(box_downsample, mask); + + for (size_t y = 0; y < output->ysize(); y++) { + float* row_out = output->Row(y); + const float* row_in[kernely]; + const float* row_mask = mask.Row(y); + // get the rows in the support + for (size_t ky = 0; ky < kernely; ky++) { + int64_t iy = y * 2 + ky - (kernely - 1) / 2; + if (iy < 0) iy = 0; + if (iy >= ysize) iy = ysize - 1; + row_in[ky] = input.Row(iy); + } + + for (size_t x = 0; x < output->xsize(); x++) { + // get min and max values of the original image in the support + float min = std::numeric_limits::max(); + float max = std::numeric_limits::min(); + // kernelx - R and kernely - R are the radius of a rectangular region in + // which the values of a pixel are bounded to reduce ringing. + static constexpr int64_t R = 5; + for (int64_t ky = R; ky + R < kernely; ky++) { + for (int64_t kx = R; kx + R < kernelx; kx++) { + int64_t ix = x * 2 + kx - (kernelx - 1) / 2; + if (ix < 0) ix = 0; + if (ix >= xsize) ix = xsize - 1; + min = std::min(min, row_in[ky][ix]); + max = std::max(max, row_in[ky][ix]); + } + } + + float sum = 0; + for (int64_t ky = 0; ky < kernely; ky++) { + for (int64_t kx = 0; kx < kernelx; kx++) { + int64_t ix = x * 2 + kx - (kernelx - 1) / 2; + if (ix < 0) ix = 0; + if (ix >= xsize) ix = xsize - 1; + sum += row_in[ky][ix] * kernel[ky * kernelx + kx]; + } + } + + row_out[x] = sum; + + // Clamp the pixel within the value of a small area to prevent ringning. + // The mask determines how much to clamp, clamp more to reduce more + // ringing in smooth areas, clamp less in noisy areas to get more + // sharpness. Higher mask_multiplier gives less clamping, so less + // ringing reduction. + const constexpr float mask_multiplier = 1; + float a = row_mask[x] * mask_multiplier; + float clip_min = min - a; + float clip_max = max + a; + if (row_out[x] < clip_min) { + row_out[x] = clip_min; + } else if (row_out[x] > clip_max) { + row_out[x] = clip_max; + } + } + } +} + +void DownsampleImage2_Sharper(Image3F* opsin) { + // Allocate extra space to avoid a reallocation when padding. + Image3F downsampled(DivCeil(opsin->xsize(), 2) + kBlockDim, + DivCeil(opsin->ysize(), 2) + kBlockDim); + downsampled.ShrinkTo(downsampled.xsize() - kBlockDim, + downsampled.ysize() - kBlockDim); + + for (size_t c = 0; c < 3; c++) { + DownsampleImage2_Sharper(opsin->Plane(c), &downsampled.Plane(c)); + } + *opsin = std::move(downsampled); +} + +// The default upsampling kernels used by Upsampler in the decoder. +static const constexpr int64_t kSize = 5; + +static const float kernel00[25] = { + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, + -0.03452303f, 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, + -0.04022174f, 0.28896755f, 0.56661550f, 0.03777607f, -0.01986694f, + -0.02921014f, 0.00278718f, 0.03777607f, -0.03144731f, -0.01185068f, + -0.00624645f, -0.01610267f, -0.01986694f, -0.01185068f, -0.00213539f, +}; +static const float kernel01[25] = { + -0.00624645f, -0.01610267f, -0.01986694f, -0.01185068f, -0.00213539f, + -0.02921014f, 0.00278718f, 0.03777607f, -0.03144731f, -0.01185068f, + -0.04022174f, 0.28896755f, 0.56661550f, 0.03777607f, -0.01986694f, + -0.03452303f, 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, +}; +static const float kernel10[25] = { + -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, + -0.01610267f, 0.00278718f, 0.28896755f, 0.14111091f, -0.03452303f, + -0.01986694f, 0.03777607f, 0.56661550f, 0.28896755f, -0.04022174f, + -0.01185068f, -0.03144731f, 0.03777607f, 0.00278718f, -0.02921014f, + -0.00213539f, -0.01185068f, -0.01986694f, -0.01610267f, -0.00624645f, +}; +static const float kernel11[25] = { + -0.00213539f, -0.01185068f, -0.01986694f, -0.01610267f, -0.00624645f, + -0.01185068f, -0.03144731f, 0.03777607f, 0.00278718f, -0.02921014f, + -0.01986694f, 0.03777607f, 0.56661550f, 0.28896755f, -0.04022174f, + -0.01610267f, 0.00278718f, 0.28896755f, 0.14111091f, -0.03452303f, + -0.00624645f, -0.02921014f, -0.04022174f, -0.03452303f, -0.01716200f, +}; + +// Does exactly the same as the Upsampler in dec_upsampler for 2x2 pixels, with +// default CustomTransformData. +// TODO(lode): use Upsampler instead. However, it requires pre-initialization +// and padding on the left side of the image which requires refactoring the +// other code using this. +static void UpsampleImage(const ImageF& input, ImageF* output) { + int64_t xsize = input.xsize(); + int64_t ysize = input.ysize(); + int64_t xsize2 = output->xsize(); + int64_t ysize2 = output->ysize(); + for (int64_t y = 0; y < ysize2; y++) { + for (int64_t x = 0; x < xsize2; x++) { + auto kernel = kernel00; + if ((x & 1) && (y & 1)) + kernel = kernel11; + else if (x & 1) + kernel = kernel10; + else if (y & 1) + kernel = kernel01; + float sum = 0; + int64_t x2 = x / 2; + int64_t y2 = y / 2; + + // get min and max values of the original image in the support + float min = std::numeric_limits::max(); + float max = std::numeric_limits::min(); + + for (int64_t ky = 0; ky < kSize; ky++) { + for (int64_t kx = 0; kx < kSize; kx++) { + int64_t xi = x2 - kSize / 2 + kx; + int64_t yi = y2 - kSize / 2 + ky; + if (xi < 0) xi = 0; + if (xi >= xsize) xi = input.xsize() - 1; + if (yi < 0) yi = 0; + if (yi >= ysize) yi = input.ysize() - 1; + min = std::min(min, input.Row(yi)[xi]); + max = std::max(max, input.Row(yi)[xi]); + } + } + + for (int64_t ky = 0; ky < kSize; ky++) { + for (int64_t kx = 0; kx < kSize; kx++) { + int64_t xi = x2 - kSize / 2 + kx; + int64_t yi = y2 - kSize / 2 + ky; + if (xi < 0) xi = 0; + if (xi >= xsize) xi = input.xsize() - 1; + if (yi < 0) yi = 0; + if (yi >= ysize) yi = input.ysize() - 1; + sum += input.Row(yi)[xi] * kernel[ky * kSize + kx]; + } + } + output->Row(y)[x] = sum; + if (output->Row(y)[x] < min) output->Row(y)[x] = min; + if (output->Row(y)[x] > max) output->Row(y)[x] = max; + } + } +} + +// Returns the derivative of Upsampler, with respect to input pixel x2, y2, to +// output pixel x, y (ignoring the clamping). +float UpsamplerDeriv(int64_t x2, int64_t y2, int64_t x, int64_t y) { + auto kernel = kernel00; + if ((x & 1) && (y & 1)) + kernel = kernel11; + else if (x & 1) + kernel = kernel10; + else if (y & 1) + kernel = kernel01; + + int64_t ix = x / 2; + int64_t iy = y / 2; + int64_t kx = x2 - ix + kSize / 2; + int64_t ky = y2 - iy + kSize / 2; + + // This should not happen. + if (kx < 0 || kx >= kSize || ky < 0 || ky >= kSize) return 0; + + return kernel[ky * kSize + kx]; +} + +// Apply the derivative of the Upsampler to the input, reversing the effect of +// its coefficients. The output image is 2x2 times smaller than the input. +void AntiUpsample(const ImageF& input, ImageF* d) { + int64_t xsize = input.xsize(); + int64_t ysize = input.ysize(); + int64_t xsize2 = d->xsize(); + int64_t ysize2 = d->ysize(); + int64_t k0 = kSize - 1; + int64_t k1 = kSize; + for (int64_t y2 = 0; y2 < ysize2; ++y2) { + auto* row = d->Row(y2); + for (int64_t x2 = 0; x2 < xsize2; ++x2) { + int64_t x0 = x2 * 2 - k0; + if (x0 < 0) x0 = 0; + int64_t x1 = x2 * 2 + k1 + 1; + if (x1 > xsize) x1 = xsize; + int64_t y0 = y2 * 2 - k0; + if (y0 < 0) y0 = 0; + int64_t y1 = y2 * 2 + k1 + 1; + if (y1 > ysize) y1 = ysize; + + float sum = 0; + for (int64_t y = y0; y < y1; ++y) { + const auto* row_in = input.Row(y); + for (int64_t x = x0; x < x1; ++x) { + double deriv = UpsamplerDeriv(x2, y2, x, y); + sum += deriv * row_in[x]; + } + } + row[x2] = sum; + } + } +} + +// Element-wise multiplies two images. +template +void ElwiseMul(const Plane& image1, const Plane& image2, Plane* out) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + JXL_CHECK(xsize == out->xsize()); + JXL_CHECK(ysize == out->ysize()); + for (size_t y = 0; y < ysize; ++y) { + const T* const JXL_RESTRICT row1 = image1.Row(y); + const T* const JXL_RESTRICT row2 = image2.Row(y); + T* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] * row2[x]; + } + } +} + +// Element-wise divides two images. +template +void ElwiseDiv(const Plane& image1, const Plane& image2, Plane* out) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + JXL_CHECK(xsize == out->xsize()); + JXL_CHECK(ysize == out->ysize()); + for (size_t y = 0; y < ysize; ++y) { + const T* const JXL_RESTRICT row1 = image1.Row(y); + const T* const JXL_RESTRICT row2 = image2.Row(y); + T* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] / row2[x]; + } + } +} + +void ReduceRinging(const ImageF& initial, const ImageF& mask, ImageF& down) { + int64_t xsize2 = down.xsize(); + int64_t ysize2 = down.ysize(); + + for (size_t y = 0; y < down.ysize(); y++) { + const float* row_mask = mask.Row(y); + float* row_out = down.Row(y); + for (size_t x = 0; x < down.xsize(); x++) { + float v = down.Row(y)[x]; + float min = initial.Row(y)[x]; + float max = initial.Row(y)[x]; + for (int64_t yi = -1; yi < 2; yi++) { + for (int64_t xi = -1; xi < 2; xi++) { + int64_t x2 = (int64_t)x + xi; + int64_t y2 = (int64_t)y + yi; + if (x2 < 0 || y2 < 0 || x2 >= (int64_t)xsize2 || + y2 >= (int64_t)ysize2) + continue; + min = std::min(min, initial.Row(y2)[x2]); + max = std::max(max, initial.Row(y2)[x2]); + } + } + + row_out[x] = v; + + // Clamp the pixel within the value of a small area to prevent ringning. + // The mask determines how much to clamp, clamp more to reduce more + // ringing in smooth areas, clamp less in noisy areas to get more + // sharpness. Higher mask_multiplier gives less clamping, so less + // ringing reduction. + const constexpr float mask_multiplier = 2; + float a = row_mask[x] * mask_multiplier; + float clip_min = min - a; + float clip_max = max + a; + if (row_out[x] < clip_min) row_out[x] = clip_min; + if (row_out[x] > clip_max) row_out[x] = clip_max; + } + } +} + +// TODO(lode): move this to a separate file enc_downsample.cc +void DownsampleImage2_Iterative(const ImageF& orig, ImageF* output) { + int64_t xsize = orig.xsize(); + int64_t ysize = orig.ysize(); + int64_t xsize2 = DivCeil(orig.xsize(), 2); + int64_t ysize2 = DivCeil(orig.ysize(), 2); + + ImageF box_downsample = CopyImage(orig); + DownsampleImage(&box_downsample, 2); + ImageF mask(box_downsample.xsize(), box_downsample.ysize()); + CreateMask(box_downsample, mask); + + output->ShrinkTo(xsize2, ysize2); + + // Initial result image using the sharper downsampling. + // Allocate extra space to avoid a reallocation when padding. + ImageF initial(DivCeil(orig.xsize(), 2) + kBlockDim, + DivCeil(orig.ysize(), 2) + kBlockDim); + initial.ShrinkTo(initial.xsize() - kBlockDim, initial.ysize() - kBlockDim); + DownsampleImage2_Sharper(orig, &initial); + + ImageF down = CopyImage(initial); + ImageF up(xsize, ysize); + ImageF corr(xsize, ysize); + ImageF corr2(xsize2, ysize2); + + // In the weights map, relatively higher values will allow less ringing but + // also less sharpness. With all constant values, it optimizes equally + // everywhere. Even in this case, the weights2 computed from + // this is still used and differs at the borders of the image. + // TODO(lode): Make use of the weights field for anti-ringing and clamping, + // the values are all set to 1 for now, but it is intended to be used for + // reducing ringing based on the mask, and taking clamping into account. + ImageF weights(xsize, ysize); + for (size_t y = 0; y < weights.ysize(); y++) { + auto* row = weights.Row(y); + for (size_t x = 0; x < weights.xsize(); x++) { + row[x] = 1; + } + } + ImageF weights2(xsize2, ysize2); + AntiUpsample(weights, &weights2); + + const size_t num_it = 3; + for (size_t it = 0; it < num_it; ++it) { + UpsampleImage(down, &up); + corr = LinComb(1, orig, -1, up); + ElwiseMul(corr, weights, &corr); + AntiUpsample(corr, &corr2); + ElwiseDiv(corr2, weights2, &corr2); + + down = LinComb(1, down, 1, corr2); + } + + ReduceRinging(initial, mask, down); + + // can't just use CopyImage, because the output image was prepared with + // padding. + for (size_t y = 0; y < down.ysize(); y++) { + for (size_t x = 0; x < down.xsize(); x++) { + float v = down.Row(y)[x]; + output->Row(y)[x] = v; + } + } +} + +void DownsampleImage2_Iterative(Image3F* opsin) { + // Allocate extra space to avoid a reallocation when padding. + Image3F downsampled(DivCeil(opsin->xsize(), 2) + kBlockDim, + DivCeil(opsin->ysize(), 2) + kBlockDim); + downsampled.ShrinkTo(downsampled.xsize() - kBlockDim, + downsampled.ysize() - kBlockDim); + + Image3F rgb(opsin->xsize(), opsin->ysize()); + OpsinParams opsin_params; // TODO: use the ones that are actually used + opsin_params.Init(kDefaultIntensityTarget); + OpsinToLinear(*opsin, Rect(rgb), nullptr, &rgb, opsin_params); + + ImageF mask(opsin->xsize(), opsin->ysize()); + ButteraugliParams butter_params; + ButteraugliComparator butter(rgb, butter_params); + butter.Mask(&mask); + ImageF mask_fuzzy(opsin->xsize(), opsin->ysize()); + + for (size_t c = 0; c < 3; c++) { + DownsampleImage2_Iterative(opsin->Plane(c), &downsampled.Plane(c)); + } + *opsin = std::move(downsampled); +} +} // namespace + +Status DefaultEncoderHeuristics::LossyFrameHeuristics( + PassesEncoderState* enc_state, ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* original_pixels, Image3F* opsin, ThreadPool* pool, + AuxOut* aux_out) { + PROFILER_ZONE("JxlLossyFrameHeuristics uninstrumented"); + + CompressParams& cparams = enc_state->cparams; + PassesSharedState& shared = enc_state->shared; + + // Compute parameters for noise synthesis. + if (shared.frame_header.flags & FrameHeader::kNoise) { + PROFILER_ZONE("enc GetNoiseParam"); + if (cparams.photon_noise_iso > 0) { + shared.image_features.noise_params = SimulatePhotonNoise( + opsin->xsize(), opsin->ysize(), cparams.photon_noise_iso); + } else { + // Don't start at zero amplitude since adding noise is expensive -- it + // significantly slows down decoding, and this is unlikely to + // completely go away even with advanced optimizations. After the + // kNoiseModelingRampUpDistanceRange we have reached the full level, + // i.e. noise is no longer represented by the compressed image, so we + // can add full noise by the noise modeling itself. + static const float kNoiseModelingRampUpDistanceRange = 0.6; + static const float kNoiseLevelAtStartOfRampUp = 0.25; + static const float kNoiseRampupStart = 1.0; + // TODO(user) test and properly select quality_coef with smooth + // filter + float quality_coef = 1.0f; + const float rampup = (cparams.butteraugli_distance - kNoiseRampupStart) / + kNoiseModelingRampUpDistanceRange; + if (rampup < 1.0f) { + quality_coef = kNoiseLevelAtStartOfRampUp + + (1.0f - kNoiseLevelAtStartOfRampUp) * rampup; + } + if (rampup < 0.0f) { + quality_coef = kNoiseRampupStart; + } + if (!GetNoiseParameter(*opsin, &shared.image_features.noise_params, + quality_coef)) { + shared.frame_header.flags &= ~FrameHeader::kNoise; + } + } + } + if (enc_state->shared.frame_header.upsampling != 1 && + !cparams.already_downsampled) { + // In VarDCT mode, LossyFrameHeuristics takes care of running downsampling + // after noise, if necessary. + if (cparams.resampling == 2) { + // TODO(lode): use the regular DownsampleImage, or adapt to the custom + // coefficients, if there is are custom upscaling coefficients in + // CustomTransformData + if (cparams.speed_tier <= SpeedTier::kSquirrel) { + // TODO(lode): DownsampleImage2_Iterative is currently too slow to + // be used for squirrel, make it faster, and / or enable it only for + // kitten. + DownsampleImage2_Iterative(opsin); + } else { + DownsampleImage2_Sharper(opsin); + } + } else { + DownsampleImage(opsin, cparams.resampling); + } + PadImageToBlockMultipleInPlace(opsin); + } + + const FrameDimensions& frame_dim = enc_state->shared.frame_dim; + size_t target_size = TargetSize(cparams, frame_dim); + size_t opsin_target_size = target_size; + if (cparams.target_size > 0 || cparams.target_bitrate > 0.0) { + cparams.target_size = opsin_target_size; + } else if (cparams.butteraugli_distance < 0) { + return JXL_FAILURE("Expected non-negative distance"); + } + + // Find and subtract splines. + if (cparams.speed_tier <= SpeedTier::kSquirrel) { + shared.image_features.splines = FindSplines(*opsin); + JXL_RETURN_IF_ERROR(shared.image_features.splines.InitializeDrawCache( + opsin->xsize(), opsin->ysize(), shared.cmap)); + shared.image_features.splines.SubtractFrom(opsin); + } + + // Find and subtract patches/dots. + if (ApplyOverride(cparams.patches, + cparams.speed_tier <= SpeedTier::kSquirrel)) { + FindBestPatchDictionary(*opsin, enc_state, pool, aux_out); + PatchDictionaryEncoder::SubtractFrom(shared.image_features.patches, opsin); + } + + static const float kAcQuant = 0.79f; + const float quant_dc = InitialQuantDC(cparams.butteraugli_distance); + Quantizer& quantizer = enc_state->shared.quantizer; + // We don't know the quant field yet, but for computing the global scale + // assuming that it will be the same as for Falcon mode is good enough. + quantizer.ComputeGlobalScaleAndQuant( + quant_dc, kAcQuant / cparams.butteraugli_distance, 0); + + // TODO(veluca): we can now run all the code from here to FindBestQuantizer + // (excluded) one rect at a time. Do that. + + // Dependency graph: + // + // input: either XYB or input image + // + // input image -> XYB [optional] + // XYB -> initial quant field + // XYB -> Gaborished XYB + // Gaborished XYB -> CfL1 + // initial quant field, Gaborished XYB, CfL1 -> ACS + // initial quant field, ACS, Gaborished XYB -> EPF control field + // initial quant field -> adjusted initial quant field + // adjusted initial quant field, ACS -> raw quant field + // raw quant field, ACS, Gaborished XYB -> CfL2 + // + // output: Gaborished XYB, CfL, ACS, raw quant field, EPF control field. + + ArControlFieldHeuristics ar_heuristics; + AcStrategyHeuristics acs_heuristics; + CfLHeuristics cfl_heuristics; + + if (!opsin->xsize()) { + JXL_ASSERT(HandlesColorConversion(cparams, *original_pixels)); + *opsin = Image3F(RoundUpToBlockDim(original_pixels->xsize()), + RoundUpToBlockDim(original_pixels->ysize())); + opsin->ShrinkTo(original_pixels->xsize(), original_pixels->ysize()); + ToXYB(*original_pixels, pool, opsin, /*linear=*/nullptr); + PadImageToBlockMultipleInPlace(opsin); + } + + // Compute an initial estimate of the quantization field. + // Call InitialQuantField only in Hare mode or slower. Otherwise, rely + // on simple heuristics in FindBestAcStrategy, or set a constant for Falcon + // mode. + if (cparams.speed_tier > SpeedTier::kHare || cparams.uniform_quant > 0) { + enc_state->initial_quant_field = + ImageF(shared.frame_dim.xsize_blocks, shared.frame_dim.ysize_blocks); + float q = cparams.uniform_quant > 0 + ? cparams.uniform_quant + : kAcQuant / cparams.butteraugli_distance; + FillImage(q, &enc_state->initial_quant_field); + } else { + // Call this here, as it relies on pre-gaborish values. + float butteraugli_distance_for_iqf = cparams.butteraugli_distance; + if (!shared.frame_header.loop_filter.gab) { + butteraugli_distance_for_iqf *= 0.73f; + } + enc_state->initial_quant_field = InitialQuantField( + butteraugli_distance_for_iqf, *opsin, shared.frame_dim, pool, 1.0f, + &enc_state->initial_quant_masking); + } + + // TODO(veluca): do something about animations. + + // Apply inverse-gaborish. + if (shared.frame_header.loop_filter.gab) { + GaborishInverse(opsin, 0.9908511000000001f, pool); + } + + cfl_heuristics.Init(*opsin); + acs_heuristics.Init(*opsin, enc_state); + + auto process_tile = [&](size_t tid, size_t thread) { + size_t n_enc_tiles = + DivCeil(enc_state->shared.frame_dim.xsize_blocks, kEncTileDimInBlocks); + size_t tx = tid % n_enc_tiles; + size_t ty = tid / n_enc_tiles; + size_t by0 = ty * kEncTileDimInBlocks; + size_t by1 = std::min((ty + 1) * kEncTileDimInBlocks, + enc_state->shared.frame_dim.ysize_blocks); + size_t bx0 = tx * kEncTileDimInBlocks; + size_t bx1 = std::min((tx + 1) * kEncTileDimInBlocks, + enc_state->shared.frame_dim.xsize_blocks); + Rect r(bx0, by0, bx1 - bx0, by1 - by0); + + // For speeds up to Wombat, we only compute the color correlation map + // once we know the transform type and the quantization map. + if (cparams.speed_tier <= SpeedTier::kSquirrel) { + cfl_heuristics.ComputeTile(r, *opsin, enc_state->shared.matrices, + /*ac_strategy=*/nullptr, + /*quantizer=*/nullptr, /*fast=*/false, thread, + &enc_state->shared.cmap); + } + + // Choose block sizes. + acs_heuristics.ProcessRect(r); + + // Choose amount of post-processing smoothing. + // TODO(veluca): should this go *after* AdjustQuantField? + ar_heuristics.RunRect(r, *opsin, enc_state, thread); + + // Always set the initial quant field, so we can compute the CfL map with + // more accuracy. The initial quant field might change in slower modes, but + // adjusting the quant field with butteraugli when all the other encoding + // parameters are fixed is likely a more reliable choice anyway. + AdjustQuantField(enc_state->shared.ac_strategy, r, + &enc_state->initial_quant_field); + quantizer.SetQuantFieldRect(enc_state->initial_quant_field, r, + &enc_state->shared.raw_quant_field); + + // Compute a non-default CfL map if we are at Hare speed, or slower. + if (cparams.speed_tier <= SpeedTier::kHare) { + cfl_heuristics.ComputeTile( + r, *opsin, enc_state->shared.matrices, &enc_state->shared.ac_strategy, + &enc_state->shared.quantizer, + /*fast=*/cparams.speed_tier >= SpeedTier::kWombat, thread, + &enc_state->shared.cmap); + } + }; + RunOnPool( + pool, 0, + DivCeil(enc_state->shared.frame_dim.xsize_blocks, kEncTileDimInBlocks) * + DivCeil(enc_state->shared.frame_dim.ysize_blocks, + kEncTileDimInBlocks), + [&](const size_t num_threads) { + ar_heuristics.PrepareForThreads(num_threads); + cfl_heuristics.PrepareForThreads(num_threads); + return true; + }, + process_tile, "Enc Heuristics"); + + acs_heuristics.Finalize(aux_out); + if (cparams.speed_tier <= SpeedTier::kHare) { + cfl_heuristics.ComputeDC(/*fast=*/cparams.speed_tier >= SpeedTier::kWombat, + &enc_state->shared.cmap); + } + + FindBestDequantMatrices(cparams, *opsin, modular_frame_encoder, + &enc_state->shared.matrices); + + // Refine quantization levels. + FindBestQuantizer(original_pixels, *opsin, enc_state, pool, aux_out); + + // Choose a context model that depends on the amount of quantization for AC. + if (cparams.speed_tier < SpeedTier::kFalcon) { + FindBestBlockEntropyModel(*enc_state); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_heuristics.h b/lib/jxl/enc_heuristics.h new file mode 100644 index 0000000..559603a --- /dev/null +++ b/lib/jxl/enc_heuristics.h @@ -0,0 +1,87 @@ +// 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_ENC_HEURISTICS_H_ +#define LIB_JXL_ENC_HEURISTICS_H_ + +// Hook for custom encoder heuristics (VarDCT only for now). + +#include +#include + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" +#include "lib/jxl/modular/encoding/enc_ma.h" + +namespace jxl { + +struct PassesEncoderState; +class ImageBundle; +class ModularFrameEncoder; + +class EncoderHeuristics { + public: + virtual ~EncoderHeuristics() = default; + // Initializes encoder structures in `enc_state` using the original image data + // in `original_pixels`, and the XYB image data in `opsin`. Also modifies the + // `opsin` image by applying Gaborish, and doing other modifications if + // necessary. `pool` is used for running the computations on multiple threads. + // `aux_out` collects statistics and can be used to print debug images. + virtual Status LossyFrameHeuristics( + PassesEncoderState* enc_state, ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* original_pixels, Image3F* opsin, ThreadPool* pool, + AuxOut* aux_out) = 0; + + // Custom fixed tree for lossless mode. Must set `tree` to a valid tree if + // the function returns true. + virtual bool CustomFixedTreeLossless(const FrameDimensions& frame_dim, + Tree* tree) { + return false; + } + + // If this method returns `true`, the `opsin` parameter to + // LossyFrameHeuristics will not be initialized, and should be initialized + // during the call. Moreover, `original_pixels` may not be in a linear + // colorspace (but will be the same as the `ib` value passed to this + // function). + virtual bool HandlesColorConversion(const CompressParams& cparams, + const ImageBundle& ib) { + return false; + } +}; + +class DefaultEncoderHeuristics : public EncoderHeuristics { + public: + Status LossyFrameHeuristics(PassesEncoderState* enc_state, + ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* original_pixels, + Image3F* opsin, ThreadPool* pool, + AuxOut* aux_out) override; + bool HandlesColorConversion(const CompressParams& cparams, + const ImageBundle& ib) override; +}; + +class FastEncoderHeuristics : public EncoderHeuristics { + public: + Status LossyFrameHeuristics(PassesEncoderState* enc_state, + ModularFrameEncoder* modular_frame_encoder, + const ImageBundle* linear, Image3F* opsin, + ThreadPool* pool, AuxOut* aux_out) override; +}; + +// Exposed here since it may be used by other EncoderHeuristics implementations +// outside this project. +void FindBestDequantMatrices(const CompressParams& cparams, + const Image3F& opsin, + ModularFrameEncoder* modular_frame_encoder, + DequantMatrices* dequant_matrices); + +} // namespace jxl + +#endif // LIB_JXL_ENC_HEURISTICS_H_ diff --git a/lib/jxl/enc_huffman.cc b/lib/jxl/enc_huffman.cc new file mode 100644 index 0000000..04b5669 --- /dev/null +++ b/lib/jxl/enc_huffman.cc @@ -0,0 +1,214 @@ +// 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/enc_huffman.h" + +#include +#include + +#include "lib/jxl/huffman_tree.h" + +namespace jxl { + +namespace { + +constexpr int kCodeLengthCodes = 18; + +void StoreHuffmanTreeOfHuffmanTreeToBitMask(const int num_codes, + const uint8_t* code_length_bitdepth, + BitWriter* writer) { + static const uint8_t kStorageOrder[kCodeLengthCodes] = { + 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + // The bit lengths of the Huffman code over the code length alphabet + // are compressed with the following static Huffman code: + // Symbol Code + // ------ ---- + // 0 00 + // 1 1110 + // 2 110 + // 3 01 + // 4 10 + // 5 1111 + static const uint8_t kHuffmanBitLengthHuffmanCodeSymbols[6] = {0, 7, 3, + 2, 1, 15}; + static const uint8_t kHuffmanBitLengthHuffmanCodeBitLengths[6] = {2, 4, 3, + 2, 2, 4}; + + // Throw away trailing zeros: + size_t codes_to_store = kCodeLengthCodes; + if (num_codes > 1) { + for (; codes_to_store > 0; --codes_to_store) { + if (code_length_bitdepth[kStorageOrder[codes_to_store - 1]] != 0) { + break; + } + } + } + size_t skip_some = 0; // skips none. + if (code_length_bitdepth[kStorageOrder[0]] == 0 && + code_length_bitdepth[kStorageOrder[1]] == 0) { + skip_some = 2; // skips two. + if (code_length_bitdepth[kStorageOrder[2]] == 0) { + skip_some = 3; // skips three. + } + } + writer->Write(2, skip_some); + for (size_t i = skip_some; i < codes_to_store; ++i) { + size_t l = code_length_bitdepth[kStorageOrder[i]]; + writer->Write(kHuffmanBitLengthHuffmanCodeBitLengths[l], + kHuffmanBitLengthHuffmanCodeSymbols[l]); + } +} + +void StoreHuffmanTreeToBitMask(const size_t huffman_tree_size, + const uint8_t* huffman_tree, + const uint8_t* huffman_tree_extra_bits, + const uint8_t* code_length_bitdepth, + const uint16_t* code_length_bitdepth_symbols, + BitWriter* writer) { + for (size_t i = 0; i < huffman_tree_size; ++i) { + size_t ix = huffman_tree[i]; + writer->Write(code_length_bitdepth[ix], code_length_bitdepth_symbols[ix]); + // Extra bits + switch (ix) { + case 16: + writer->Write(2, huffman_tree_extra_bits[i]); + break; + case 17: + writer->Write(3, huffman_tree_extra_bits[i]); + break; + } + } +} + +void StoreSimpleHuffmanTree(const uint8_t* depths, size_t symbols[4], + size_t num_symbols, size_t max_bits, + BitWriter* writer) { + // value of 1 indicates a simple Huffman code + writer->Write(2, 1); + writer->Write(2, num_symbols - 1); // NSYM - 1 + + // Sort + for (size_t i = 0; i < num_symbols; i++) { + for (size_t j = i + 1; j < num_symbols; j++) { + if (depths[symbols[j]] < depths[symbols[i]]) { + std::swap(symbols[j], symbols[i]); + } + } + } + + if (num_symbols == 2) { + writer->Write(max_bits, symbols[0]); + writer->Write(max_bits, symbols[1]); + } else if (num_symbols == 3) { + writer->Write(max_bits, symbols[0]); + writer->Write(max_bits, symbols[1]); + writer->Write(max_bits, symbols[2]); + } else { + writer->Write(max_bits, symbols[0]); + writer->Write(max_bits, symbols[1]); + writer->Write(max_bits, symbols[2]); + writer->Write(max_bits, symbols[3]); + // tree-select + writer->Write(1, depths[symbols[0]] == 1 ? 1 : 0); + } +} + +// num = alphabet size +// depths = symbol depths +void StoreHuffmanTree(const uint8_t* depths, size_t num, BitWriter* writer) { + // Write the Huffman tree into the compact representation. + std::unique_ptr arena(new uint8_t[2 * num]); + uint8_t* huffman_tree = arena.get(); + uint8_t* huffman_tree_extra_bits = arena.get() + num; + size_t huffman_tree_size = 0; + WriteHuffmanTree(depths, num, &huffman_tree_size, huffman_tree, + huffman_tree_extra_bits); + + // Calculate the statistics of the Huffman tree in the compact representation. + uint32_t huffman_tree_histogram[kCodeLengthCodes] = {0}; + for (size_t i = 0; i < huffman_tree_size; ++i) { + ++huffman_tree_histogram[huffman_tree[i]]; + } + + int num_codes = 0; + int code = 0; + for (int i = 0; i < kCodeLengthCodes; ++i) { + if (huffman_tree_histogram[i]) { + if (num_codes == 0) { + code = i; + num_codes = 1; + } else if (num_codes == 1) { + num_codes = 2; + break; + } + } + } + + // Calculate another Huffman tree to use for compressing both the + // earlier Huffman tree with. + uint8_t code_length_bitdepth[kCodeLengthCodes] = {0}; + uint16_t code_length_bitdepth_symbols[kCodeLengthCodes] = {0}; + CreateHuffmanTree(&huffman_tree_histogram[0], kCodeLengthCodes, 5, + &code_length_bitdepth[0]); + ConvertBitDepthsToSymbols(code_length_bitdepth, kCodeLengthCodes, + &code_length_bitdepth_symbols[0]); + + // Now, we have all the data, let's start storing it + StoreHuffmanTreeOfHuffmanTreeToBitMask(num_codes, code_length_bitdepth, + writer); + + if (num_codes == 1) { + code_length_bitdepth[code] = 0; + } + + // Store the real huffman tree now. + StoreHuffmanTreeToBitMask(huffman_tree_size, huffman_tree, + huffman_tree_extra_bits, &code_length_bitdepth[0], + code_length_bitdepth_symbols, writer); +} + +} // namespace + +void BuildAndStoreHuffmanTree(const uint32_t* histogram, const size_t length, + uint8_t* depth, uint16_t* bits, + BitWriter* writer) { + size_t count = 0; + size_t s4[4] = {0}; + for (size_t i = 0; i < length; i++) { + if (histogram[i]) { + if (count < 4) { + s4[count] = i; + } else if (count > 4) { + break; + } + count++; + } + } + + size_t max_bits_counter = length - 1; + size_t max_bits = 0; + while (max_bits_counter) { + max_bits_counter >>= 1; + ++max_bits; + } + + if (count <= 1) { + // Output symbol bits and depths are initialized with 0, nothing to do. + writer->Write(4, 1); + writer->Write(max_bits, s4[0]); + return; + } + + CreateHuffmanTree(histogram, length, 15, depth); + ConvertBitDepthsToSymbols(depth, length, bits); + + if (count <= 4) { + StoreSimpleHuffmanTree(depth, s4, count, max_bits, writer); + } else { + StoreHuffmanTree(depth, length, writer); + } +} + +} // namespace jxl diff --git a/lib/jxl/enc_huffman.h b/lib/jxl/enc_huffman.h new file mode 100644 index 0000000..d7a6658 --- /dev/null +++ b/lib/jxl/enc_huffman.h @@ -0,0 +1,22 @@ +// 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_ENC_HUFFMAN_H_ +#define LIB_JXL_ENC_HUFFMAN_H_ + +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +// Builds a Huffman tree for the given histogram, and encodes it into writer +// in a format that can be read by HuffmanDecodingData::ReadFromBitstream. +// An allotment for `writer` must already have been created by the caller. +void BuildAndStoreHuffmanTree(const uint32_t* histogram, size_t length, + uint8_t* depth, uint16_t* bits, + BitWriter* writer); + +} // namespace jxl + +#endif // LIB_JXL_ENC_HUFFMAN_H_ diff --git a/lib/jxl/enc_icc_codec.cc b/lib/jxl/enc_icc_codec.cc new file mode 100644 index 0000000..31d58ca --- /dev/null +++ b/lib/jxl/enc_icc_codec.cc @@ -0,0 +1,430 @@ +// 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/enc_icc_codec.h" + +#include + +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/icc_codec_common.h" + +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. +// Transposes a matrix of width columns and ceil(size / width) rows. There are +// size elements, size may be < width * height, if so the +// last elements of the bottom row are missing, the missing spots are +// transposed along with the filled spots, and the result has the missing +// elements at the bottom of the rightmost column. The input is the input matrix +// in scanline order, the output is the result matrix in scanline order, with +// missing elements skipped over (this may occur at multiple positions). +void Unshuffle(uint8_t* data, size_t size, size_t width) { + size_t height = (size + width - 1) / width; // amount of rows of input + PaddedBytes result(size); + // i = input index, j output index + size_t s = 0, j = 0; + for (size_t i = 0; i < size; i++) { + result[j] = data[i]; + j += height; + if (j >= size) j = ++s; + } + + for (size_t i = 0; i < size; i++) { + data[i] = result[i]; + } +} + +// This is performed by the encoder, the encoder must be able to encode any +// random byte stream (not just byte streams that are a valid ICC profile), so +// an error returned by this function is an implementation error. +Status PredictAndShuffle(size_t stride, size_t width, int order, size_t num, + const uint8_t* data, size_t size, size_t* pos, + PaddedBytes* result) { + JXL_RETURN_IF_ERROR(CheckOutOfBounds(*pos, num, size)); + // Required by the specification, see decoder. stride * 4 must be < *pos. + if (!*pos || ((*pos - 1u) >> 2u) < stride) { + return JXL_FAILURE("Invalid stride"); + } + if (*pos < stride * 4) return JXL_FAILURE("Too large stride"); + size_t start = result->size(); + for (size_t i = 0; i < num; i++) { + uint8_t predicted = + LinearPredictICCValue(data, *pos, i, stride, width, order); + result->push_back(data[*pos + i] - predicted); + } + *pos += num; + if (width > 1) Unshuffle(result->data() + start, num, width); + return true; +} +} // namespace + +// Outputs a transformed form of the given icc profile. The result itself is +// not particularly smaller than the input data in bytes, but it will be in a +// form that is easier to compress (more zeroes, ...) and will compress better +// with brotli. +Status PredictICC(const uint8_t* icc, size_t size, PaddedBytes* result) { + PaddedBytes commands; + PaddedBytes data; + + EncodeVarInt(size, result); + + // Header + PaddedBytes header = ICCInitialHeaderPrediction(); + EncodeUint32(0, size, &header); + for (size_t i = 0; i < kICCHeaderSize && i < size; i++) { + ICCPredictHeader(icc, size, header.data(), i); + data.push_back(icc[i] - header[i]); + } + if (size <= kICCHeaderSize) { + EncodeVarInt(0, result); // 0 commands + for (size_t i = 0; i < data.size(); i++) { + result->push_back(data[i]); + } + return true; + } + + std::vector tags; + std::vector tagstarts; + std::vector tagsizes; + std::map tagmap; + + // Tag list + size_t pos = kICCHeaderSize; + if (pos + 4 <= size) { + uint64_t numtags = DecodeUint32(icc, size, pos); + pos += 4; + EncodeVarInt(numtags + 1, &commands); + uint64_t prevtagstart = kICCHeaderSize + numtags * 12; + uint32_t prevtagsize = 0; + for (size_t i = 0; i < numtags; i++) { + if (pos + 12 > size) break; + + Tag tag = DecodeKeyword(icc, size, pos + 0); + uint32_t tagstart = DecodeUint32(icc, size, pos + 4); + uint32_t tagsize = DecodeUint32(icc, size, pos + 8); + pos += 12; + + tags.push_back(tag); + tagstarts.push_back(tagstart); + tagsizes.push_back(tagsize); + tagmap[tagstart] = tags.size() - 1; + + uint8_t tagcode = kCommandTagUnknown; + for (size_t j = 0; j < kNumTagStrings; j++) { + if (tag == *kTagStrings[j]) { + tagcode = j + kCommandTagStringFirst; + break; + } + } + + if (tag == kRtrcTag && pos + 24 < size) { + bool ok = true; + 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; + } + } + if (ok) { + tagcode = kCommandTagTRC; + pos += 24; + i += 2; + } + } + + if (tag == kRxyzTag && pos + 24 < size) { + bool ok = true; + ok &= DecodeKeyword(icc, size, pos + 0) == kGxyzTag; + ok &= DecodeKeyword(icc, size, pos + 12) == kBxyzTag; + uint32_t offsetr = tagstart; + uint32_t offsetg = DecodeUint32(icc, size, pos + 4); + uint32_t offsetb = DecodeUint32(icc, size, pos + 16); + uint32_t sizer = tagsize; + uint32_t sizeg = DecodeUint32(icc, size, pos + 8); + uint32_t sizeb = DecodeUint32(icc, size, pos + 20); + ok &= sizer == 20; + ok &= sizeg == 20; + ok &= sizeb == 20; + ok &= (offsetg == offsetr + 20); + ok &= (offsetb == offsetr + 40); + if (ok) { + tagcode = kCommandTagXYZ; + pos += 24; + i += 2; + } + } + + uint8_t command = tagcode; + uint64_t predicted_tagstart = prevtagstart + prevtagsize; + if (predicted_tagstart != tagstart) command |= kFlagBitOffset; + size_t predicted_tagsize = prevtagsize; + if (tag == kRxyzTag || tag == kGxyzTag || tag == kBxyzTag || + tag == kKxyzTag || tag == kWtptTag || tag == kBkptTag || + tag == kLumiTag) { + predicted_tagsize = 20; + } + if (predicted_tagsize != tagsize) command |= kFlagBitSize; + commands.push_back(command); + if (tagcode == 1) { + AppendKeyword(tag, &data); + } + if (command & kFlagBitOffset) EncodeVarInt(tagstart, &commands); + if (command & kFlagBitSize) EncodeVarInt(tagsize, &commands); + + prevtagstart = tagstart; + prevtagsize = tagsize; + } + } + // Indicate end of tag list or varint indicating there's none + commands.push_back(0); + + // Main content + // The main content in a valid ICC profile contains tagged elements, with the + // tag types (4 letter names) given by the tag list above, and the tag list + // pointing to the start and indicating the size of each tagged element. It is + // allowed for tagged elements to overlap, e.g. the curve for R, G and B could + // all point to the same one. + Tag tag; + size_t tagstart = 0, tagsize = 0, clutstart = 0; + + size_t last0 = pos; + // This loop appends commands to the output, processing some sub-section of a + // current tagged element each time. We need to keep track of the tagtype of + // the current element, and update it when we encounter the boundary of a + // next one. + // It is not required that the input data is a valid ICC profile, if the + // encoder does not recognize the data it will still be able to output bytes + // but will not predict as well. + while (pos <= size) { + size_t last1 = pos; + PaddedBytes commands_add; + PaddedBytes data_add; + + // This means the loop brought the position beyond the tag end. + if (pos > tagstart + tagsize) { + tag = {{0, 0, 0, 0}}; // nonsensical value + } + + if (commands_add.empty() && data_add.empty() && tagmap.count(pos) && + pos + 4 <= size) { + size_t index = tagmap[pos]; + tag = DecodeKeyword(icc, size, pos); + tagstart = tagstarts[index]; + tagsize = tagsizes[index]; + + if (tag == kMlucTag && pos + tagsize <= size && tagsize > 8 && + icc[pos + 4] == 0 && icc[pos + 5] == 0 && icc[pos + 6] == 0 && + icc[pos + 7] == 0) { + size_t num = tagsize - 8; + commands_add.push_back(kCommandTypeStartFirst + 3); + pos += 8; + commands_add.push_back(kCommandShuffle2); + EncodeVarInt(num, &commands_add); + size_t start = data_add.size(); + for (size_t i = 0; i < num; i++) { + data_add.push_back(icc[pos]); + pos++; + } + Unshuffle(data_add.data() + start, num, 2); + } + + if (tag == kCurvTag && pos + tagsize <= size && tagsize > 8 && + icc[pos + 4] == 0 && icc[pos + 5] == 0 && icc[pos + 6] == 0 && + icc[pos + 7] == 0) { + size_t num = tagsize - 8; + if (num > 16 && num < (1 << 28) && pos + num <= size && pos > 0) { + commands_add.push_back(kCommandTypeStartFirst + 5); + pos += 8; + commands_add.push_back(kCommandPredict); + int order = 1, width = 2, stride = width; + commands_add.push_back((order << 2) | (width - 1)); + EncodeVarInt(num, &commands_add); + JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc, + size, &pos, &data_add)); + } + } + } + + if (tag == kMab_Tag || tag == kMba_Tag) { + Tag subTag = DecodeKeyword(icc, size, pos); + if (pos + 12 < size && (subTag == kCurvTag || subTag == kVcgtTag) && + DecodeUint32(icc, size, pos + 4) == 0) { + uint32_t num = DecodeUint32(icc, size, pos + 8) * 2; + if (num > 16 && num < (1 << 28) && pos + 12 + num <= size) { + pos += 12; + last1 = pos; + commands_add.push_back(kCommandPredict); + int order = 1, width = 2, stride = width; + commands_add.push_back((order << 2) | (width - 1)); + EncodeVarInt(num, &commands_add); + JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc, + size, &pos, &data_add)); + } + } + + if (pos == tagstart + 24 && pos + 4 < size) { + // Note that this value can be remembered for next iterations of the + // loop, so the "pos == clutstart" if below can trigger during a later + // iteration. + clutstart = tagstart + DecodeUint32(icc, size, pos); + } + + if (pos == clutstart && clutstart + 16 < size) { + size_t numi = icc[tagstart + 8]; + size_t numo = icc[tagstart + 9]; + size_t width = icc[clutstart + 16]; + size_t stride = width * numo; + size_t num = width * numo; + for (size_t i = 0; i < numi && clutstart + i < size; i++) { + num *= icc[clutstart + i]; + } + if ((width == 1 || width == 2) && num > 64 && num < (1 << 28) && + pos + num <= size && pos > stride * 4) { + commands_add.push_back(kCommandPredict); + int order = 1; + uint8_t flags = + (order << 2) | (width - 1) | (stride == width ? 0 : 16); + commands_add.push_back(flags); + if (flags & 16) EncodeVarInt(stride, &commands_add); + EncodeVarInt(num, &commands_add); + JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc, + size, &pos, &data_add)); + } + } + } + + if (commands_add.empty() && data_add.empty() && tag == kGbd_Tag && + pos == tagstart + 8 && pos + tagsize - 8 <= size && pos > 16 && + tagsize > 8) { + size_t width = 4, order = 0, stride = width; + size_t num = tagsize - 8; + uint8_t flags = (order << 2) | (width - 1) | (stride == width ? 0 : 16); + commands_add.push_back(kCommandPredict); + commands_add.push_back(flags); + if (flags & 16) EncodeVarInt(stride, &commands_add); + EncodeVarInt(num, &commands_add); + JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc, + size, &pos, &data_add)); + } + + if (commands_add.empty() && data_add.empty() && pos + 20 <= size) { + Tag subTag = DecodeKeyword(icc, size, pos); + if (subTag == kXyz_Tag && DecodeUint32(icc, size, pos + 4) == 0) { + commands_add.push_back(kCommandXYZ); + pos += 8; + for (size_t j = 0; j < 12; j++) data_add.push_back(icc[pos++]); + } + } + + if (commands_add.empty() && data_add.empty() && pos + 8 <= size) { + if (DecodeUint32(icc, size, pos + 4) == 0) { + Tag subTag = DecodeKeyword(icc, size, pos); + for (size_t i = 0; i < kNumTypeStrings; i++) { + if (subTag == *kTypeStrings[i]) { + commands_add.push_back(kCommandTypeStartFirst + i); + pos += 8; + break; + } + } + } + } + + if (!(commands_add.empty() && data_add.empty()) || pos == size) { + if (last0 < last1) { + commands.push_back(kCommandInsert); + EncodeVarInt(last1 - last0, &commands); + while (last0 < last1) { + data.push_back(icc[last0++]); + } + } + for (size_t i = 0; i < commands_add.size(); i++) { + commands.push_back(commands_add[i]); + } + for (size_t i = 0; i < data_add.size(); i++) { + data.push_back(data_add[i]); + } + last0 = pos; + } + if (commands_add.empty() && data_add.empty()) { + pos++; + } + } + + EncodeVarInt(commands.size(), result); + for (size_t i = 0; i < commands.size(); i++) { + result->push_back(commands[i]); + } + for (size_t i = 0; i < data.size(); i++) { + result->push_back(data[i]); + } + + return true; +} + +Status WriteICC(const PaddedBytes& icc, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* JXL_RESTRICT aux_out) { + if (icc.empty()) return JXL_FAILURE("ICC must be non-empty"); + PaddedBytes enc; + JXL_RETURN_IF_ERROR(PredictICC(icc.data(), icc.size(), &enc)); + std::vector> tokens(1); + BitWriter::Allotment allotment(writer, 128); + JXL_RETURN_IF_ERROR(U64Coder::Write(enc.size(), writer)); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + + for (size_t i = 0; i < enc.size(); i++) { + tokens[0].emplace_back( + ICCANSContext(i, i > 0 ? enc[i - 1] : 0, i > 1 ? enc[i - 2] : 0), + enc[i]); + } + HistogramParams params; + params.lz77_method = enc.size() < 4096 ? HistogramParams::LZ77Method::kOptimal + : HistogramParams::LZ77Method::kLZ77; + EntropyEncodingData code; + std::vector context_map; + params.force_huffman = true; + BuildAndEncodeHistograms(params, kNumICCContexts, tokens, &code, &context_map, + writer, layer, aux_out); + WriteTokens(tokens[0], code, context_map, writer, layer, aux_out); + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_icc_codec.h b/lib/jxl/enc_icc_codec.h new file mode 100644 index 0000000..2480e3a --- /dev/null +++ b/lib/jxl/enc_icc_codec.h @@ -0,0 +1,33 @@ +// 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_ENC_ICC_CODEC_H_ +#define LIB_JXL_ENC_ICC_CODEC_H_ + +// Compressed representation of ICC profiles. + +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +// Should still be called if `icc.empty()` - if so, writes only 1 bit. +Status WriteICC(const PaddedBytes& icc, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* JXL_RESTRICT aux_out); + +// Exposed only for testing +Status PredictICC(const uint8_t* icc, size_t size, PaddedBytes* result); + +} // namespace jxl + +#endif // LIB_JXL_ENC_ICC_CODEC_H_ diff --git a/lib/jxl/enc_image_bundle.cc b/lib/jxl/enc_image_bundle.cc new file mode 100644 index 0000000..5aac244 --- /dev/null +++ b/lib/jxl/enc_image_bundle.cc @@ -0,0 +1,170 @@ +// 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/enc_image_bundle.h" + +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_color_management.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/luminance.h" + +namespace jxl { + +namespace { + +// Copies ib:rect, converts, and copies into out. +template +Status CopyToT(const ImageMetadata* metadata, const ImageBundle* ib, + const Rect& rect, const ColorEncoding& c_desired, + ThreadPool* pool, Image3* out) { + PROFILER_FUNC; + static_assert( + std::is_same::value || std::numeric_limits::min() == 0, + "CopyToT implemented only for float and unsigned types"); + ColorSpaceTransform c_transform; + // Changing IsGray is probably a bug. + JXL_CHECK(ib->IsGray() == c_desired.IsGray()); +#if JPEGXL_ENABLE_SKCMS + bool is_gray = false; +#else + bool is_gray = ib->IsGray(); +#endif + if (out->xsize() < rect.xsize() || out->ysize() < rect.ysize()) { + *out = Image3(rect.xsize(), rect.ysize()); + } else { + out->ShrinkTo(rect.xsize(), rect.ysize()); + } + RunOnPool( + pool, 0, rect.ysize(), + [&](size_t num_threads) { + return c_transform.Init(ib->c_current(), c_desired, + metadata->IntensityTarget(), rect.xsize(), + num_threads); + }, + [&](const int y, const int thread) { + float* mutable_src_buf = c_transform.BufSrc(thread); + const float* src_buf = mutable_src_buf; + // Interleave input. + if (is_gray) { + src_buf = rect.ConstPlaneRow(ib->color(), 0, y); + } else { + const float* JXL_RESTRICT row_in0 = + rect.ConstPlaneRow(ib->color(), 0, y); + const float* JXL_RESTRICT row_in1 = + rect.ConstPlaneRow(ib->color(), 1, y); + const float* JXL_RESTRICT row_in2 = + rect.ConstPlaneRow(ib->color(), 2, y); + for (size_t x = 0; x < rect.xsize(); x++) { + mutable_src_buf[3 * x + 0] = row_in0[x]; + mutable_src_buf[3 * x + 1] = row_in1[x]; + mutable_src_buf[3 * x + 2] = row_in2[x]; + } + } + float* JXL_RESTRICT dst_buf = c_transform.BufDst(thread); + DoColorSpaceTransform(&c_transform, thread, src_buf, dst_buf); + T* JXL_RESTRICT row_out0 = out->PlaneRow(0, y); + T* JXL_RESTRICT row_out1 = out->PlaneRow(1, y); + T* JXL_RESTRICT row_out2 = out->PlaneRow(2, y); + // De-interleave output and convert type. + if (std::is_same::value) { // deinterleave to float. + if (is_gray) { + for (size_t x = 0; x < rect.xsize(); x++) { + row_out0[x] = dst_buf[x]; + row_out1[x] = dst_buf[x]; + row_out2[x] = dst_buf[x]; + } + } else { + for (size_t x = 0; x < rect.xsize(); x++) { + row_out0[x] = dst_buf[3 * x + 0]; + row_out1[x] = dst_buf[3 * x + 1]; + row_out2[x] = dst_buf[3 * x + 2]; + } + } + } else { + // Convert to T, doing clamping. + float max = std::numeric_limits::max(); + auto cvt = [max](float in) { + float v = std::max(0.0f, std::min(max, in * max)); + return static_cast(v < 0 ? v - 0.5f : v + 0.5f); + }; + if (is_gray) { + for (size_t x = 0; x < rect.xsize(); x++) { + row_out0[x] = cvt(dst_buf[x]); + row_out1[x] = cvt(dst_buf[x]); + row_out2[x] = cvt(dst_buf[x]); + } + } else { + for (size_t x = 0; x < rect.xsize(); x++) { + row_out0[x] = cvt(dst_buf[3 * x + 0]); + row_out1[x] = cvt(dst_buf[3 * x + 1]); + row_out2[x] = cvt(dst_buf[3 * x + 2]); + } + } + } + }, + "Colorspace transform"); + return true; +} + +} // namespace + +Status ImageBundle::TransformTo(const ColorEncoding& c_desired, + ThreadPool* pool) { + PROFILER_FUNC; + JXL_RETURN_IF_ERROR(CopyTo(Rect(color_), c_desired, &color_, pool)); + c_current_ = c_desired; + return true; +} + +Status ImageBundle::CopyTo(const Rect& rect, const ColorEncoding& c_desired, + Image3B* out, ThreadPool* pool) const { + return CopyToT(metadata_, this, rect, c_desired, pool, out); +} +Status ImageBundle::CopyTo(const Rect& rect, const ColorEncoding& c_desired, + Image3F* out, ThreadPool* pool) const { + return CopyToT(metadata_, this, rect, c_desired, pool, out); +} + +Status ImageBundle::CopyToSRGB(const Rect& rect, Image3B* out, + ThreadPool* pool) const { + return CopyTo(rect, ColorEncoding::SRGB(IsGray()), out, pool); +} + +Status TransformIfNeeded(const ImageBundle& in, const ColorEncoding& c_desired, + ThreadPool* pool, ImageBundle* store, + const ImageBundle** out) { + if (in.c_current().SameColorEncoding(c_desired)) { + *out = ∈ + return true; + } + // TODO(janwas): avoid copying via createExternal+copyBackToIO + // instead of copy+createExternal+copyBackToIO + store->SetFromImage(CopyImage(in.color()), in.c_current()); + + // Must at least copy the alpha channel for use by external_image. + if (in.HasExtraChannels()) { + std::vector extra_channels; + for (const ImageF& extra_channel : in.extra_channels()) { + extra_channels.emplace_back(CopyImage(extra_channel)); + } + store->SetExtraChannels(std::move(extra_channels)); + } + + if (!store->TransformTo(c_desired, pool)) { + return false; + } + *out = store; + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_image_bundle.h b/lib/jxl/enc_image_bundle.h new file mode 100644 index 0000000..f5cd007 --- /dev/null +++ b/lib/jxl/enc_image_bundle.h @@ -0,0 +1,25 @@ +// 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_ENC_IMAGE_BUNDLE_H_ +#define LIB_JXL_ENC_IMAGE_BUNDLE_H_ + +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Does color transformation from in.c_current() to c_desired if the color +// encodings are different, or nothing if they are already the same. +// If color transformation is done, stores the transformed values into store and +// sets the out pointer to store, else leaves store untouched and sets the out +// pointer to &in. +// Returns false if color transform fails. +Status TransformIfNeeded(const ImageBundle& in, const ColorEncoding& c_desired, + ThreadPool* pool, ImageBundle* store, + const ImageBundle** out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_IMAGE_BUNDLE_H_ diff --git a/lib/jxl/enc_jxl_skcms.h b/lib/jxl/enc_jxl_skcms.h new file mode 100644 index 0000000..4be5420 --- /dev/null +++ b/lib/jxl/enc_jxl_skcms.h @@ -0,0 +1,54 @@ +// 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_ENC_JXL_SKCMS_H_ +#define LIB_JXL_ENC_JXL_SKCMS_H_ + +// skcms wrapper to rename the skcms symbols to avoid conflicting names with +// other projects using skcms as well. When using JPEGXL_BUNDLE_SKCMS the +// bundled functions will be renamed from skcms_ to jxl_skcms_ + +#ifdef SKCMS_API +#error "Must include jxl_skcms.h and not skcms.h directly" +#endif // SKCMS_API + +#if JPEGXL_BUNDLE_SKCMS + +#define skcms_252_random_bytes jxl_skcms_252_random_bytes +#define skcms_AdaptToXYZD50 jxl_skcms_AdaptToXYZD50 +#define skcms_ApproximateCurve jxl_skcms_ApproximateCurve +#define skcms_ApproximatelyEqualProfiles jxl_skcms_ApproximatelyEqualProfiles +#define skcms_AreApproximateInverses jxl_skcms_AreApproximateInverses +#define skcms_GetCHAD jxl_skcms_GetCHAD +#define skcms_GetTagByIndex jxl_skcms_GetTagByIndex +#define skcms_GetTagBySignature jxl_skcms_GetTagBySignature +#define skcms_GetWTPT jxl_skcms_GetWTPT +#define skcms_Identity_TransferFunction jxl_skcms_Identity_TransferFunction +#define skcms_MakeUsableAsDestination jxl_skcms_MakeUsableAsDestination +#define skcms_MakeUsableAsDestinationWithSingleCurve \ + jxl_skcms_MakeUsableAsDestinationWithSingleCurve +#define skcms_Matrix3x3_concat jxl_skcms_Matrix3x3_concat +#define skcms_Matrix3x3_invert jxl_skcms_Matrix3x3_invert +#define skcms_MaxRoundtripError jxl_skcms_MaxRoundtripError +#define skcms_Parse jxl_skcms_Parse +#define skcms_PrimariesToXYZD50 jxl_skcms_PrimariesToXYZD50 +#define skcms_sRGB_Inverse_TransferFunction \ + jxl_skcms_sRGB_Inverse_TransferFunction +#define skcms_sRGB_profile jxl_skcms_sRGB_profile +#define skcms_sRGB_TransferFunction jxl_skcms_sRGB_TransferFunction +#define skcms_TransferFunction_eval jxl_skcms_TransferFunction_eval +#define skcms_TransferFunction_invert jxl_skcms_TransferFunction_invert +#define skcms_TransferFunction_makeHLGish jxl_skcms_TransferFunction_makeHLGish +#define skcms_TransferFunction_makePQish jxl_skcms_TransferFunction_makePQish +#define skcms_Transform jxl_skcms_Transform +#define skcms_TransformWithPalette jxl_skcms_TransformWithPalette +#define skcms_TRCs_AreApproximateInverse jxl_skcms_TRCs_AreApproximateInverse +#define skcms_XYZD50_profile jxl_skcms_XYZD50_profile + +#endif // JPEGXL_BUNDLE_SKCMS + +#include "skcms.h" + +#endif // LIB_JXL_ENC_JXL_SKCMS_H_ diff --git a/lib/jxl/enc_modular.cc b/lib/jxl/enc_modular.cc new file mode 100644 index 0000000..ad60da0 --- /dev/null +++ b/lib/jxl/enc_modular.cc @@ -0,0 +1,1633 @@ +// 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/enc_modular.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/compressed_dc.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cluster.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/enc_patch_dictionary.h" +#include "lib/jxl/enc_quant_weights.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/gaborish.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/enc_encoding.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/encoding/ma_common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/options.h" +#include "lib/jxl/modular/transform/enc_transform.h" +#include "lib/jxl/toc.h" + +namespace jxl { + +namespace { +// Squeeze default quantization factors +// these quantization factors are for -Q 50 (other qualities simply scale the +// factors; things are rounded down and obviously cannot get below 1) +static const float squeeze_quality_factor = + 0.35; // for easy tweaking of the quality range (decrease this number for + // higher quality) +static const float squeeze_luma_factor = + 1.1; // for easy tweaking of the balance between luma (or anything + // non-chroma) and chroma (decrease this number for higher quality + // luma) +static const float squeeze_quality_factor_xyb = 2.4f; +static const float squeeze_xyb_qtable[3][16] = { + {163.84, 81.92, 40.96, 20.48, 10.24, 5.12, 2.56, 1.28, 0.64, 0.32, 0.16, + 0.08, 0.04, 0.02, 0.01, 0.005}, // Y + {1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.5, 0.5, 0.5, + 0.5}, // X + {2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.5, 0.5, + 0.5}, // B-Y +}; + +static const float squeeze_luma_qtable[16] = { + 163.84, 81.92, 40.96, 20.48, 10.24, 5.12, 2.56, 1.28, + 0.64, 0.32, 0.16, 0.08, 0.04, 0.02, 0.01, 0.005}; +// for 8-bit input, the range of YCoCg chroma is -255..255 so basically this +// does 4:2:0 subsampling (two most fine grained layers get quantized away) +static const float squeeze_chroma_qtable[16] = { + 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.5, 0.5, 0.5, 0.5}; + +// `cutoffs` must be sorted. +Tree MakeFixedTree(int property, const std::vector& cutoffs, + Predictor pred, size_t num_pixels) { + size_t log_px = CeilLog2Nonzero(num_pixels); + size_t min_gap = 0; + // Reduce fixed tree height when encoding small images. + if (log_px < 14) { + min_gap = 8 * (14 - log_px); + } + Tree tree; + struct NodeInfo { + size_t begin, end, pos; + }; + std::queue q; + // Leaf IDs will be set by roundtrip decoding the tree. + tree.push_back(PropertyDecisionNode::Leaf(pred)); + q.push(NodeInfo{0, cutoffs.size(), 0}); + while (!q.empty()) { + NodeInfo info = q.front(); + q.pop(); + if (info.begin + min_gap >= info.end) continue; + uint32_t split = (info.begin + info.end) / 2; + tree[info.pos] = + PropertyDecisionNode::Split(property, cutoffs[split], tree.size()); + q.push(NodeInfo{split + 1, info.end, tree.size()}); + tree.push_back(PropertyDecisionNode::Leaf(pred)); + q.push(NodeInfo{info.begin, split, tree.size()}); + tree.push_back(PropertyDecisionNode::Leaf(pred)); + } + return tree; +} + +Tree PredefinedTree(ModularOptions::TreeKind tree_kind, size_t total_pixels) { + if (tree_kind == ModularOptions::TreeKind::kJpegTranscodeACMeta) { + // All the data is 0, so no need for a fancy tree. + return {PropertyDecisionNode::Leaf(Predictor::Zero)}; + } + if (tree_kind == ModularOptions::TreeKind::kFalconACMeta) { + // All the data is 0 except the quant field. TODO(veluca): make that 0 too. + return {PropertyDecisionNode::Leaf(Predictor::Left)}; + } + if (tree_kind == ModularOptions::TreeKind::kACMeta) { + // Small image. + if (total_pixels < 1024) { + return {PropertyDecisionNode::Leaf(Predictor::Left)}; + } + Tree tree; + // 0: c > 1 + tree.push_back(PropertyDecisionNode::Split(0, 1, 1)); + // 1: c > 2 + tree.push_back(PropertyDecisionNode::Split(0, 2, 3)); + // 2: c > 0 + tree.push_back(PropertyDecisionNode::Split(0, 0, 5)); + // 3: EPF control field (all 0 or 4), top > 0 + tree.push_back(PropertyDecisionNode::Split(6, 0, 21)); + // 4: ACS+QF, y > 0 + tree.push_back(PropertyDecisionNode::Split(2, 0, 7)); + // 5: CfL x + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Gradient)); + // 6: CfL b + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Gradient)); + // 7: QF: split according to the left quant value. + tree.push_back(PropertyDecisionNode::Split(7, 5, 9)); + // 8: ACS: split in 4 segments (8x8 from 0 to 3, large square 4-5, large + // rectangular 6-11, 8x8 12+), according to previous ACS value. + tree.push_back(PropertyDecisionNode::Split(7, 5, 15)); + // QF + tree.push_back(PropertyDecisionNode::Split(7, 11, 11)); + tree.push_back(PropertyDecisionNode::Split(7, 3, 13)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Left)); + // ACS + tree.push_back(PropertyDecisionNode::Split(7, 11, 17)); + tree.push_back(PropertyDecisionNode::Split(7, 3, 19)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + // EPF, left > 0 + tree.push_back(PropertyDecisionNode::Split(7, 0, 23)); + tree.push_back(PropertyDecisionNode::Split(7, 0, 25)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + tree.push_back(PropertyDecisionNode::Leaf(Predictor::Zero)); + return tree; + } + if (tree_kind == ModularOptions::TreeKind::kWPFixedDC) { + std::vector cutoffs = { + -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, + -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, + 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500}; + return MakeFixedTree(kNumNonrefProperties - weighted::kNumProperties, + cutoffs, Predictor::Weighted, total_pixels); + } + if (tree_kind == ModularOptions::TreeKind::kGradientFixedDC) { + std::vector cutoffs = { + -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, + -11, -7, -4, -3, -1, 0, 1, 3, 5, 7, 11, + 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500}; + return MakeFixedTree(kGradientProp, cutoffs, Predictor::Gradient, + total_pixels); + } + JXL_ABORT("Unreachable"); + return {}; +} + +// Merges the trees in `trees` using nodes that decide on stream_id, as defined +// by `tree_splits`. +void MergeTrees(const std::vector& trees, + const std::vector& tree_splits, size_t begin, + size_t end, Tree* tree) { + JXL_ASSERT(trees.size() + 1 == tree_splits.size()); + JXL_ASSERT(end > begin); + JXL_ASSERT(end <= trees.size()); + if (end == begin + 1) { + // Insert the tree, adding the opportune offset to all child nodes. + // This will make the leaf IDs wrong, but subsequent roundtripping will fix + // them. + size_t sz = tree->size(); + tree->insert(tree->end(), trees[begin].begin(), trees[begin].end()); + for (size_t i = sz; i < tree->size(); i++) { + (*tree)[i].lchild += sz; + (*tree)[i].rchild += sz; + } + return; + } + size_t mid = (begin + end) / 2; + size_t splitval = tree_splits[mid] - 1; + size_t cur = tree->size(); + tree->emplace_back(1 /*stream_id*/, splitval, 0, 0, Predictor::Zero, 0, 1); + (*tree)[cur].lchild = tree->size(); + MergeTrees(trees, tree_splits, mid, end, tree); + (*tree)[cur].rchild = tree->size(); + MergeTrees(trees, tree_splits, begin, mid, tree); +} + +void QuantizeChannel(Channel& ch, const int q) { + if (q == 1) return; + for (size_t y = 0; y < ch.plane.ysize(); y++) { + pixel_type* row = ch.plane.Row(y); + for (size_t x = 0; x < ch.plane.xsize(); x++) { + if (row[x] < 0) { + row[x] = -((-row[x] + q / 2) / q) * q; + } else { + row[x] = ((row[x] + q / 2) / q) * q; + } + } + } +} + +// convert binary32 float that corresponds to custom [bits]-bit float (with +// [exp_bits] exponent bits) to a [bits]-bit integer representation that should +// 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) { + JXL_ASSERT(sizeof(pixel_type) * 8 >= bits); + if (!fp) { + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row_in[x] * factor + 0.5f; + } + return true; + } + if (bits == 32 && fp) { + JXL_ASSERT(exp_bits == 8); + memcpy((void*)row_out, (const void*)row_in, 4 * xsize); + return true; + } + + int exp_bias = (1 << (exp_bits - 1)) - 1; + int max_exp = (1 << exp_bits) - 1; + uint32_t sign = (1u << (bits - 1)); + int mant_bits = bits - exp_bits - 1; + int mant_shift = 23 - mant_bits; + for (size_t x = 0; x < xsize; ++x) { + uint32_t f; + memcpy(&f, &row_in[x], 4); + int signbit = (f >> 31); + f &= 0x7fffffff; + if (f == 0) { + row_out[x] = (signbit ? sign : 0); + continue; + } + int exp = (f >> 23) - 127; + if (exp == 128) return JXL_FAILURE("Inf/NaN not allowed"); + int mantissa = (f & 0x007fffff); + // broke up the binary32 into its parts, now reassemble into + // arbitrary float + exp += exp_bias; + if (exp < 0) { // will become a subnormal number + // add implicit leading 1 to mantissa + mantissa |= 0x00800000; + if (exp < -mant_bits) { + return JXL_FAILURE( + "Invalid float number: %g cannot be represented with %i " + "exp_bits and %i mant_bits (exp %i)", + row_in[x], exp_bits, mant_bits, exp); + } + mantissa >>= 1 - exp; + exp = 0; + } + // exp should be representable in exp_bits, otherwise input was + // invalid + if (exp > max_exp) return JXL_FAILURE("Invalid float exponent"); + if (mantissa & ((1 << mant_shift) - 1)) { + return JXL_FAILURE("%g is losing precision (mant: %x)", row_in[x], + mantissa); + } + mantissa >>= mant_shift; + f = (signbit ? sign : 0); + f |= (exp << mant_bits); + f |= mantissa; + row_out[x] = (pixel_type)f; + } + return true; +} +} // namespace + +ModularFrameEncoder::ModularFrameEncoder(const FrameHeader& frame_header, + const CompressParams& cparams_orig) + : 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{100.0, 100.0}) { + switch (cparams.decoding_speed_tier) { + case 0: + break; + case 1: + cparams.options.wp_tree_mode = ModularOptions::TreeMode::kWPOnly; + break; + case 2: { + cparams.options.wp_tree_mode = ModularOptions::TreeMode::kGradientOnly; + cparams.options.predictor = Predictor::Gradient; + break; + } + case 3: { // LZ77, no Gradient. + cparams.options.nb_repeats = 0; + cparams.options.predictor = Predictor::Gradient; + break; + } + default: { // LZ77, no predictor. + cparams.options.nb_repeats = 0; + cparams.options.predictor = Predictor::Zero; + break; + } + } + } + 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) { + cparams.responsive = 0; + } else { + cparams.responsive = 1; + } + } + + if (cparams.speed_tier > SpeedTier::kWombat) { + cparams.options.splitting_heuristics_node_threshold = 192; + } else { + cparams.options.splitting_heuristics_node_threshold = 96; + } + { + // Set properties. + std::vector prop_order; + if (cparams.responsive) { + // Properties in order of their likelihood of being useful for Squeeze + // residuals. + prop_order = {0, 1, 4, 5, 6, 7, 8, 15, 9, 10, 11, 12, 13, 14, 2, 3}; + } else { + // Same, but for the non-Squeeze case. + prop_order = {0, 1, 15, 9, 10, 11, 12, 13, 14, 2, 3, 4, 5, 6, 7, 8}; + } + switch (cparams.speed_tier) { + case SpeedTier::kSquirrel: + cparams.options.splitting_heuristics_properties.assign( + prop_order.begin(), prop_order.begin() + 8); + cparams.options.max_property_values = 32; + break; + case SpeedTier::kKitten: + cparams.options.splitting_heuristics_properties.assign( + prop_order.begin(), prop_order.begin() + 10); + cparams.options.max_property_values = 64; + break; + case SpeedTier::kTortoise: + cparams.options.splitting_heuristics_properties = prop_order; + cparams.options.max_property_values = 256; + break; + default: + cparams.options.splitting_heuristics_properties.assign( + prop_order.begin(), prop_order.begin() + 6); + cparams.options.max_property_values = 16; + break; + } + if (cparams.speed_tier > SpeedTier::kTortoise) { + // Gradient in previous channels. + for (int i = 0; i < cparams.options.max_properties; i++) { + cparams.options.splitting_heuristics_properties.push_back( + kNumNonrefProperties + i * 4 + 3); + } + } else { + // All the extra properties in Tortoise mode. + for (int i = 0; i < cparams.options.max_properties * 4; i++) { + cparams.options.splitting_heuristics_properties.push_back( + kNumNonrefProperties + i); + } + } + } + + if (cparams.options.predictor == static_cast(-1)) { + // no explicit predictor(s) given, set a good default + if ((cparams.speed_tier <= SpeedTier::kTortoise || + cparams.modular_mode == false) && + quality == 100 && 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) { + // zero predictor for Squeeze residues + cparams.options.predictor = Predictor::Zero; + } else if (quality < 100) { + // If not responsive and lossy. TODO(veluca): use near_lossless instead? + cparams.options.predictor = Predictor::Gradient; + } else if (cparams.speed_tier < SpeedTier::kFalcon) { + // try median and weighted predictor for anything else + cparams.options.predictor = Predictor::Best; + } else if (cparams.speed_tier == SpeedTier::kFalcon) { + // just weighted predictor in falcon mode + cparams.options.predictor = Predictor::Weighted; + } else if (cparams.speed_tier > SpeedTier::kFalcon) { + // just gradient predictor in thunder mode + cparams.options.predictor = Predictor::Gradient; + } + } + tree_splits.push_back(0); + if (cparams.modular_mode == false) { + cparams.options.fast_decode_multiplier = 1.0f; + tree_splits.push_back(ModularStreamId::VarDCTDC(0).ID(frame_dim)); + tree_splits.push_back(ModularStreamId::ModularDC(0).ID(frame_dim)); + tree_splits.push_back(ModularStreamId::ACMetadata(0).ID(frame_dim)); + tree_splits.push_back(ModularStreamId::QuantTable(0).ID(frame_dim)); + tree_splits.push_back(ModularStreamId::ModularAC(0, 0).ID(frame_dim)); + ac_metadata_size.resize(frame_dim.num_dc_groups); + extra_dc_precision.resize(frame_dim.num_dc_groups); + } + tree_splits.push_back(num_streams); + cparams.options.max_chan_size = frame_dim.group_dim; + cparams.options.group_dim = frame_dim.group_dim; + + // TODO(veluca): figure out how to use different predictor sets per channel. + stream_options.resize(num_streams, cparams.options); +} + +bool do_transform(Image& image, const Transform& tr, + const weighted::Header& wp_header, + jxl::ThreadPool* pool = nullptr) { + Transform t = tr; + bool did_it = TransformForward(t, image, wp_header, pool); + if (did_it) image.transform.push_back(t); + return did_it; +} + +Status ModularFrameEncoder::ComputeEncodingData( + const FrameHeader& frame_header, const ImageMetadata& metadata, + Image3F* JXL_RESTRICT color, const std::vector& extra_channels, + PassesEncoderState* JXL_RESTRICT enc_state, ThreadPool* pool, + AuxOut* aux_out, bool do_color) { + const FrameDimensions& frame_dim = enc_state->shared.frame_dim; + + if (do_color && frame_header.loop_filter.gab) { + GaborishInverse(color, 0.9908511000000001f, pool); + } + + if (do_color && metadata.bit_depth.bits_per_sample <= 16 && + cparams.speed_tier < SpeedTier::kCheetah) { + FindBestPatchDictionary(*color, enc_state, nullptr, aux_out, + cparams.color_transform == ColorTransform::kXYB); + PatchDictionaryEncoder::SubtractFrom( + enc_state->shared.image_features.patches, color); + } + + // Convert ImageBundle to modular Image object + const size_t xsize = frame_dim.xsize; + const size_t ysize = frame_dim.ysize; + + int nb_chans = 3; + if (metadata.color_encoding.IsGray() && + cparams.color_transform == ColorTransform::kNone) { + nb_chans = 1; + } + if (!do_color) nb_chans = 0; + + nb_chans += extra_channels.size(); + + bool fp = metadata.bit_depth.floating_point_sample && + cparams.color_transform != ColorTransform::kXYB; + + // bits_per_sample is just metadata for XYB images. + if (metadata.bit_depth.bits_per_sample >= 32 && do_color && + cparams.color_transform != ColorTransform::kXYB) { + if (metadata.bit_depth.bits_per_sample == 32 && fp == false) { + return JXL_FAILURE("uint32_t not supported in enc_modular"); + } else if (metadata.bit_depth.bits_per_sample > 32) { + return JXL_FAILURE("bits_per_sample > 32 not supported"); + } + } + + 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}; + DequantMatricesSetCustomDC(&enc_state->shared.matrices, enc_factors); + } + pixel_type maxval = gi.bitdepth < 32 ? (1u << gi.bitdepth) - 1 : 0; + if (do_color) { + for (; c < 3; c++) { + if (metadata.color_encoding.IsGray() && + cparams.color_transform == ColorTransform::kNone && + c != (cparams.color_transform == ColorTransform::kXYB ? 1 : 0)) + continue; + int c_out = c; + // XYB is encoded as YX(B-Y) + if (cparams.color_transform == ColorTransform::kXYB && c < 2) + c_out = 1 - c_out; + float factor = maxval; + if (cparams.color_transform == ColorTransform::kXYB) + factor = enc_state->shared.matrices.InvDCQuant(c); + if (c == 2 && cparams.color_transform == ColorTransform::kXYB) { + JXL_ASSERT(!fp); + for (size_t y = 0; y < ysize; ++y) { + const float* const JXL_RESTRICT row_in = color->PlaneRow(c, y); + pixel_type* const JXL_RESTRICT row_out = gi.channel[c_out].Row(y); + pixel_type* const JXL_RESTRICT row_Y = gi.channel[0].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row_in[x] * factor + 0.5f; + row_out[x] -= row_Y[x]; + } + } + } else { + int bits = metadata.bit_depth.bits_per_sample; + int exp_bits = metadata.bit_depth.exponent_bits_per_sample; + gi.channel[c_out].hshift = + enc_state->shared.frame_header.chroma_subsampling.HShift(c); + gi.channel[c_out].vshift = + enc_state->shared.frame_header.chroma_subsampling.VShift(c); + size_t xsize_shifted = DivCeil(xsize, 1 << gi.channel[c_out].hshift); + size_t ysize_shifted = DivCeil(ysize, 1 << gi.channel[c_out].vshift); + gi.channel[c_out].shrink(xsize_shifted, ysize_shifted); + for (size_t y = 0; y < ysize_shifted; ++y) { + const float* const JXL_RESTRICT row_in = color->PlaneRow(c, y); + pixel_type* const JXL_RESTRICT row_out = gi.channel[c_out].Row(y); + JXL_RETURN_IF_ERROR(float_to_int(row_in, row_out, xsize_shifted, bits, + exp_bits, fp, factor)); + } + } + } + if (metadata.color_encoding.IsGray() && + cparams.color_transform == ColorTransform::kNone) + c = 1; + } + + for (size_t ec = 0; ec < extra_channels.size(); ec++, c++) { + const ExtraChannelInfo& eci = metadata.extra_channel_info[ec]; + size_t ecups = frame_header.extra_channel_upsampling[ec]; + gi.channel[c].shrink(DivCeil(frame_dim.xsize_upsampled, ecups), + DivCeil(frame_dim.ysize_upsampled, ecups)); + gi.channel[c].hshift = gi.channel[c].vshift = + CeilLog2Nonzero(ecups) - CeilLog2Nonzero(frame_header.upsampling); + + 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)); + for (size_t y = 0; y < gi.channel[c].plane.ysize(); ++y) { + const float* const JXL_RESTRICT row_in = extra_channels[ec].Row(y); + pixel_type* const JXL_RESTRICT row_out = gi.channel[c].Row(y); + JXL_RETURN_IF_ERROR(float_to_int(row_in, row_out, + gi.channel[c].plane.xsize(), bits, + exp_bits, fp, factor)); + } + } + JXL_ASSERT(c == nb_chans); + + // Set options and apply transformations + + if (quality < 100) { + if (cparams.palette_colors != 0) { + JXL_DEBUG_V(3, "Lossy encode, not doing palette transforms"); + } + if (cparams.color_transform == ColorTransform::kXYB) { + cparams.channel_colors_pre_transform_percent = 0; + } + cparams.channel_colors_percent = 0; + cparams.palette_colors = 0; + cparams.lossy_palette = false; + } + + // if few colors, do all-channel palette before trying channel palette + // Logic is as follows: + // - if you can make a palette with few colors (arbitrary threshold: 200), + // then you can also make channel palettes, but they will just be extra + // signaling cost for almost no benefit + // - if the palette needs more colors, then channel palette might help to + // reduce palette signaling cost + if (cparams.palette_colors != 0 && cparams.speed_tier < SpeedTier::kFalcon) { + // all-channel palette (e.g. RGBA) + if (gi.channel.size() > 1) { + Transform maybe_palette(TransformId::kPalette); + maybe_palette.begin_c = gi.nb_meta_channels; + maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; + maybe_palette.nb_colors = + std::min(std::min(200, (int)(xsize * ysize / 8)), + std::abs(cparams.palette_colors) / 16); + maybe_palette.ordered_palette = cparams.palette_colors >= 0; + maybe_palette.lossy_palette = false; + do_transform(gi, maybe_palette, weighted::Header(), pool); + } + } + + // Global channel palette + if (cparams.channel_colors_pre_transform_percent > 0 && + !cparams.lossy_palette && + (cparams.speed_tier <= SpeedTier::kThunder || + (do_color && metadata.bit_depth.bits_per_sample > 8))) { + // single channel palette (like FLIF's ChannelCompact) + size_t nb_channels = gi.channel.size() - gi.nb_meta_channels; + for (size_t i = 0; i < nb_channels; i++) { + int min, max; + compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); + int64_t colors = max - min + 1; + JXL_DEBUG_V(10, "Channel %zu: range=%i..%i", i, min, max); + Transform maybe_palette_1(TransformId::kPalette); + maybe_palette_1.begin_c = i + gi.nb_meta_channels; + maybe_palette_1.num_c = 1; + // simple heuristic: if less than X percent of the values in the range + // actually occur, it is probably worth it to do a compaction + // (but only if the channel palette is less than 6% the size of the + // image itself) + maybe_palette_1.nb_colors = std::min( + (int)(xsize * ysize / 16), + (int)(cparams.channel_colors_pre_transform_percent / 100. * colors)); + if (do_transform(gi, maybe_palette_1, weighted::Header(), pool)) { + // effective bit depth is lower, adjust quantization accordingly + compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); + if (max < maxval) maxval = max; + } + } + } + + // Global palette + if ((cparams.palette_colors != 0 || cparams.lossy_palette) && + cparams.speed_tier < SpeedTier::kFalcon) { + // all-channel palette (e.g. RGBA) + if (gi.channel.size() - gi.nb_meta_channels > 1) { + Transform maybe_palette(TransformId::kPalette); + maybe_palette.begin_c = gi.nb_meta_channels; + maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; + maybe_palette.nb_colors = + std::min((int)(xsize * ysize / 8), std::abs(cparams.palette_colors)); + maybe_palette.ordered_palette = cparams.palette_colors >= 0; + maybe_palette.lossy_palette = + (cparams.lossy_palette && maybe_palette.num_c == 3); + if (maybe_palette.lossy_palette) { + maybe_palette.predictor = Predictor::Average4; + } + // TODO(veluca): use a custom weighted header if using the weighted + // predictor. + do_transform(gi, maybe_palette, weighted::Header(), pool); + } + // all-minus-one-channel palette (RGB with separate alpha, or CMY with + // separate K) + if (gi.channel.size() - gi.nb_meta_channels > 3) { + Transform maybe_palette_3(TransformId::kPalette); + maybe_palette_3.begin_c = gi.nb_meta_channels; + maybe_palette_3.num_c = gi.channel.size() - gi.nb_meta_channels - 1; + maybe_palette_3.nb_colors = + std::min((int)(xsize * ysize / 8), std::abs(cparams.palette_colors)); + maybe_palette_3.ordered_palette = cparams.palette_colors >= 0; + maybe_palette_3.lossy_palette = cparams.lossy_palette; + if (maybe_palette_3.lossy_palette) { + maybe_palette_3.predictor = Predictor::Average4; + } + do_transform(gi, maybe_palette_3, weighted::Header(), pool); + } + } + + if (cparams.color_transform == ColorTransform::kNone && do_color && !fp && + gi.channel.size() - gi.nb_meta_channels >= 3) { + if (cparams.colorspace == 1 || + (cparams.colorspace < 0 && + (quality < 100 || 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); + } 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); + } + } + + if (cparams.responsive && !gi.channel.empty()) { + do_transform(gi, Transform(TransformId::kSqueeze), weighted::Header(), + pool); // use default squeezing + } + + std::vector quants; + + if (quality < 100 || cquality < 100) { + quants.resize(gi.channel.size(), 1); + JXL_DEBUG_V( + 2, + "Adding quantization constants corresponding to luma quality %.2f " + "and chroma quality %.2f", + quality, cquality); + 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; + } + 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; + } + + if (cparams.options.nb_repeats == 0) { + return JXL_FAILURE("nb_repeats = 0 not supported with modular lossy!"); + } + for (uint32_t i = gi.nb_meta_channels; i < gi.channel.size(); i++) { + Channel& ch = gi.channel[i]; + int shift = ch.hshift + ch.vshift; // number of pixel halvings + if (shift > 16) shift = 16; + if (shift > 0) shift--; + int q; + // assuming default Squeeze here + int component = ((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 * + squeeze_xyb_qtable[component][shift]; + } else { + if (cparams.colorspace != 0 && component > 0 && component < 3) { + q = cquality * squeeze_quality_factor * squeeze_chroma_qtable[shift]; + } else { + q = quality * squeeze_quality_factor * squeeze_luma_factor * + squeeze_luma_qtable[shift]; + } + } + if (q < 1) q = 1; + QuantizeChannel(gi.channel[i], q); + quants[i] = q; + } + } + + // Fill other groups. + struct GroupParams { + Rect rect; + int minShift; + int maxShift; + ModularStreamId id; + }; + std::vector stream_params; + + stream_options[0] = cparams.options; + + // DC + for (size_t group_id = 0; group_id < frame_dim.num_dc_groups; group_id++) { + const size_t gx = group_id % frame_dim.xsize_dc_groups; + const size_t gy = group_id / frame_dim.xsize_dc_groups; + const Rect rect(gx * frame_dim.dc_group_dim, gy * frame_dim.dc_group_dim, + frame_dim.dc_group_dim, frame_dim.dc_group_dim); + // minShift==3 because (frame_dim.dc_group_dim >> 3) == frame_dim.group_dim + // maxShift==1000 is infinity + stream_params.push_back( + GroupParams{rect, 3, 1000, ModularStreamId::ModularDC(group_id)}); + } + // AC global -> nothing. + // AC + for (size_t group_id = 0; group_id < frame_dim.num_groups; group_id++) { + const size_t gx = group_id % frame_dim.xsize_groups; + const size_t gy = group_id / frame_dim.xsize_groups; + const Rect mrect(gx * frame_dim.group_dim, gy * frame_dim.group_dim, + frame_dim.group_dim, frame_dim.group_dim); + for (size_t i = 0; i < enc_state->progressive_splitter.GetNumPasses(); + i++) { + int maxShift, minShift; + frame_header.passes.GetDownsamplingBracket(i, minShift, maxShift); + stream_params.push_back(GroupParams{ + mrect, minShift, maxShift, ModularStreamId::ModularAC(group_id, i)}); + } + } + gi_channel.resize(stream_images.size()); + + RunOnPool( + pool, 0, stream_params.size(), ThreadPool::SkipInit(), + [&](size_t i, size_t _) { + stream_options[stream_params[i].id.ID(frame_dim)] = cparams.options; + JXL_CHECK(PrepareStreamParams( + stream_params[i].rect, cparams, stream_params[i].minShift, + stream_params[i].maxShift, stream_params[i].id, do_color)); + }, + "ChooseParams"); + { + // Clear out channels that have been copied to groups. + Image& full_image = stream_images[0]; + size_t c = full_image.nb_meta_channels; + for (; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + if (fc.w > frame_dim.group_dim || fc.h > frame_dim.group_dim) break; + } + for (; c < full_image.channel.size(); c++) { + full_image.channel[c].plane = ImageI(); + } + } + + if (!quants.empty()) { + for (uint32_t stream_id = 0; stream_id < stream_images.size(); + stream_id++) { + // skip non-modular stream_ids + if (stream_id > 0 && gi_channel[stream_id].empty()) continue; + Image& image = stream_images[stream_id]; + const ModularOptions& options = stream_options[stream_id]; + for (uint32_t i = image.nb_meta_channels; i < image.channel.size(); i++) { + if (i >= image.nb_meta_channels && + (image.channel[i].w > options.max_chan_size || + image.channel[i].h > options.max_chan_size)) { + continue; + } + if (stream_id > 0 && gi_channel[stream_id].empty()) continue; + size_t ch_id = stream_id == 0 + ? i + : gi_channel[stream_id][i - image.nb_meta_channels]; + uint32_t q = quants[ch_id]; + // Inform the tree splitting heuristics that each channel in each group + // used this quantization factor. This will produce a tree with the + // given multipliers. + if (multiplier_info.empty() || + multiplier_info.back().range[1][0] != stream_id || + multiplier_info.back().multiplier != q) { + StaticPropRange range; + range[0] = {{i, i + 1}}; + range[1] = {{stream_id, stream_id + 1}}; + multiplier_info.push_back({range, (uint32_t)q}); + } else { + // Previous channel in the same group had the same quantization + // factor. Don't provide two different ranges, as that creates + // unnecessary nodes. + multiplier_info.back().range[0][1] = i + 1; + } + } + } + // Merge group+channel settings that have the same channels and quantization + // factors, to avoid unnecessary nodes. + std::sort(multiplier_info.begin(), multiplier_info.end(), + [](ModularMultiplierInfo a, ModularMultiplierInfo b) { + return std::make_tuple(a.range, a.multiplier) < + std::make_tuple(b.range, b.multiplier); + }); + size_t new_num = 1; + for (size_t i = 1; i < multiplier_info.size(); i++) { + ModularMultiplierInfo& prev = multiplier_info[new_num - 1]; + ModularMultiplierInfo& cur = multiplier_info[i]; + if (prev.range[0] == cur.range[0] && prev.multiplier == cur.multiplier && + prev.range[1][1] == cur.range[1][0]) { + prev.range[1][1] = cur.range[1][1]; + } else { + multiplier_info[new_num++] = multiplier_info[i]; + } + } + multiplier_info.resize(new_num); + } + + JXL_RETURN_IF_ERROR(ValidateChannelDimensions(gi, stream_options[0])); + + return PrepareEncoding(pool, enc_state->shared.frame_dim, + enc_state->heuristics.get(), aux_out); +} + +Status ModularFrameEncoder::PrepareEncoding(ThreadPool* pool, + const FrameDimensions& frame_dim, + EncoderHeuristics* heuristics, + AuxOut* aux_out) { + if (!tree.empty()) return true; + + // Compute tree. + size_t num_streams = stream_images.size(); + stream_headers.resize(num_streams); + tokens.resize(num_streams); + + if (heuristics->CustomFixedTreeLossless(frame_dim, &tree)) { + // Using a fixed tree. + } else if (cparams.speed_tier < SpeedTier::kFalcon || quality != 100 || + !cparams.modular_mode) { + // Avoid creating a tree with leaves that don't correspond to any pixels. + std::vector useful_splits; + useful_splits.reserve(tree_splits.size()); + for (size_t chunk = 0; chunk < tree_splits.size() - 1; chunk++) { + bool has_pixels = false; + size_t start = tree_splits[chunk]; + size_t stop = tree_splits[chunk + 1]; + for (size_t i = start; i < stop; i++) { + for (const Channel& c : stream_images[i].channel) { + if (c.w && c.h) has_pixels = true; + } + } + if (has_pixels) { + useful_splits.push_back(tree_splits[chunk]); + } + } + // Don't do anything if modular mode does not have any pixels in this image + if (useful_splits.empty()) return true; + useful_splits.push_back(tree_splits.back()); + + std::atomic_flag invalid_force_wp = ATOMIC_FLAG_INIT; + + std::vector trees(useful_splits.size() - 1); + RunOnPool( + pool, 0, useful_splits.size() - 1, ThreadPool::SkipInit(), + [&](size_t chunk, size_t _) { + // TODO(veluca): parallelize more. + size_t total_pixels = 0; + uint32_t start = useful_splits[chunk]; + uint32_t stop = useful_splits[chunk + 1]; + uint32_t max_c = 0; + if (stream_options[start].tree_kind != + ModularOptions::TreeKind::kLearn) { + for (size_t i = start; i < stop; i++) { + for (const Channel& ch : stream_images[i].channel) { + total_pixels += ch.w * ch.h; + } + } + trees[chunk] = + PredefinedTree(stream_options[start].tree_kind, total_pixels); + return; + } + TreeSamples tree_samples; + if (!tree_samples.SetPredictor(stream_options[start].predictor, + stream_options[start].wp_tree_mode)) { + invalid_force_wp.test_and_set(std::memory_order_acq_rel); + return; + } + if (!tree_samples.SetProperties( + stream_options[start].splitting_heuristics_properties, + stream_options[start].wp_tree_mode)) { + invalid_force_wp.test_and_set(std::memory_order_acq_rel); + return; + } + std::vector pixel_samples; + std::vector diff_samples; + std::vector group_pixel_count; + std::vector channel_pixel_count; + for (size_t i = start; i < stop; i++) { + max_c = std::max(stream_images[i].channel.size(), max_c); + CollectPixelSamples(stream_images[i], stream_options[i], i, + group_pixel_count, channel_pixel_count, + pixel_samples, diff_samples); + } + StaticPropRange range; + range[0] = {{0, max_c}}; + range[1] = {{start, stop}}; + auto local_multiplier_info = multiplier_info; + + tree_samples.PreQuantizeProperties( + range, local_multiplier_info, group_pixel_count, + channel_pixel_count, pixel_samples, diff_samples, + stream_options[start].max_property_values); + for (size_t i = start; i < stop; i++) { + JXL_CHECK(ModularGenericCompress( + stream_images[i], stream_options[i], /*writer=*/nullptr, + /*aux_out=*/nullptr, 0, i, &tree_samples, &total_pixels)); + } + + // TODO(veluca): parallelize more. + trees[chunk] = + LearnTree(std::move(tree_samples), total_pixels, + stream_options[start], local_multiplier_info, range); + }, + "LearnTrees"); + if (invalid_force_wp.test_and_set(std::memory_order_acq_rel)) { + return JXL_FAILURE("PrepareEncoding: force_no_wp with {Weighted}"); + } + tree.clear(); + MergeTrees(trees, useful_splits, 0, useful_splits.size() - 1, &tree); + } else { + // Fixed tree. + size_t total_pixels = 0; + for (const Image& img : stream_images) { + for (const Channel& ch : img.channel) { + total_pixels += ch.w * ch.h; + } + } + if (cparams.speed_tier <= SpeedTier::kFalcon) { + tree = PredefinedTree(ModularOptions::TreeKind::kWPFixedDC, total_pixels); + } else if (cparams.speed_tier <= SpeedTier::kThunder) { + tree = PredefinedTree(ModularOptions::TreeKind::kGradientFixedDC, + total_pixels); + } else { + tree = {PropertyDecisionNode::Leaf(Predictor::Gradient)}; + } + } + tree_tokens.resize(1); + tree_tokens[0].clear(); + Tree decoded_tree; + TokenizeTree(tree, &tree_tokens[0], &decoded_tree); + JXL_ASSERT(tree.size() == decoded_tree.size()); + tree = std::move(decoded_tree); + + if (WantDebugOutput(aux_out)) { + PrintTree(tree, aux_out->debug_prefix + "/global_tree"); + } + + image_widths.resize(num_streams); + RunOnPool( + pool, 0, num_streams, ThreadPool::SkipInit(), + [&](size_t stream_id, size_t _) { + AuxOut my_aux_out; + if (aux_out) { + my_aux_out.dump_image = aux_out->dump_image; + my_aux_out.debug_prefix = aux_out->debug_prefix; + } + tokens[stream_id].clear(); + JXL_CHECK(ModularGenericCompress( + stream_images[stream_id], stream_options[stream_id], + /*writer=*/nullptr, &my_aux_out, 0, stream_id, + /*tree_samples=*/nullptr, + /*total_pixels=*/nullptr, + /*tree=*/&tree, /*header=*/&stream_headers[stream_id], + /*tokens=*/&tokens[stream_id], + /*widths=*/&image_widths[stream_id])); + }, + "ComputeTokens"); + return true; +} + +Status ModularFrameEncoder::EncodeGlobalInfo(BitWriter* writer, + AuxOut* aux_out) { + BitWriter::Allotment allotment(writer, 1); + // If we are using brotli, or not using modular mode. + if (tree_tokens.empty() || tree_tokens[0].empty()) { + writer->Write(1, 0); + ReclaimAndCharge(writer, &allotment, kLayerModularTree, aux_out); + return true; + } + writer->Write(1, 1); + ReclaimAndCharge(writer, &allotment, kLayerModularTree, aux_out); + + // Write tree + HistogramParams params; + if (cparams.speed_tier > SpeedTier::kKitten) { + params.clustering = HistogramParams::ClusteringType::kFast; + params.ans_histogram_strategy = + cparams.speed_tier > SpeedTier::kThunder + ? HistogramParams::ANSHistogramStrategy::kFast + : HistogramParams::ANSHistogramStrategy::kApproximate; + params.lz77_method = + cparams.decoding_speed_tier >= 3 && cparams.modular_mode + ? (cparams.speed_tier >= SpeedTier::kFalcon + ? HistogramParams::LZ77Method::kRLE + : HistogramParams::LZ77Method::kLZ77) + : HistogramParams::LZ77Method::kNone; + // Near-lossless DC, as well as modular mode, require choosing hybrid uint + // more carefully. + if ((!extra_dc_precision.empty() && extra_dc_precision[0] != 0) || + (cparams.modular_mode && cparams.speed_tier < SpeedTier::kCheetah)) { + params.uint_method = HistogramParams::HybridUintMethod::kFast; + } else { + params.uint_method = HistogramParams::HybridUintMethod::kNone; + } + } else if (cparams.speed_tier <= SpeedTier::kTortoise) { + params.lz77_method = HistogramParams::LZ77Method::kOptimal; + } else { + params.lz77_method = HistogramParams::LZ77Method::kLZ77; + } + if (cparams.decoding_speed_tier >= 1) { + params.max_histograms = 12; + } + BuildAndEncodeHistograms(params, kNumTreeContexts, tree_tokens, &code, + &context_map, writer, kLayerModularTree, aux_out); + WriteTokens(tree_tokens[0], code, context_map, writer, kLayerModularTree, + aux_out); + params.image_widths = image_widths; + // Write histograms. + BuildAndEncodeHistograms(params, (tree.size() + 1) / 2, tokens, &code, + &context_map, writer, kLayerModularGlobal, aux_out); + return true; +} + +Status ModularFrameEncoder::EncodeStream(BitWriter* writer, AuxOut* aux_out, + size_t layer, + const ModularStreamId& stream) { + size_t stream_id = stream.ID(frame_dim); + if (stream_images[stream_id].channel.empty()) { + return true; // Image with no channels, header never gets decoded. + } + JXL_RETURN_IF_ERROR( + Bundle::Write(stream_headers[stream_id], writer, layer, aux_out)); + WriteTokens(tokens[stream_id], code, context_map, writer, layer, aux_out); + return true; +} + +namespace { +float EstimateWPCost(const Image& img, size_t i) { + size_t extra_bits = 0; + float histo_cost = 0; + HybridUintConfig config; + int32_t cutoffs[] = {-500, -392, -255, -191, -127, -95, -63, -47, -31, + -23, -15, -11, -7, -4, -3, -1, 0, 1, + 3, 5, 7, 11, 15, 23, 31, 47, 63, + 95, 127, 191, 255, 392, 500}; + constexpr size_t nc = sizeof(cutoffs) / sizeof(*cutoffs) + 1; + Histogram histo[nc] = {}; + weighted::Header wp_header; + PredictorMode(i, &wp_header); + for (const Channel& ch : img.channel) { + const intptr_t onerow = ch.plane.PixelsPerRow(); + weighted::State wp_state(wp_header, ch.w, ch.h); + Properties properties(1); + for (size_t y = 0; y < ch.h; y++) { + const pixel_type* JXL_RESTRICT r = ch.Row(y); + for (size_t x = 0; x < ch.w; x++) { + size_t offset = 0; + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + pixel_type_w topright = + (x + 1 < ch.w && y ? *(r + x + 1 - onerow) : top); + pixel_type_w toptop = (y > 1 ? *(r + x - onerow - onerow) : top); + pixel_type guess = wp_state.Predict( + x, y, ch.w, top, left, topright, topleft, toptop, &properties, + offset); + size_t ctx = 0; + for (int c : cutoffs) { + ctx += c >= properties[0]; + } + pixel_type res = r[x] - guess; + uint32_t token, nbits, bits; + config.Encode(PackSigned(res), &token, &nbits, &bits); + histo[ctx].Add(token); + extra_bits += nbits; + wp_state.UpdateErrors(r[x], x, y, ch.w); + } + } + for (size_t h = 0; h < nc; h++) { + histo_cost += histo[h].ShannonEntropy(); + histo[h].Clear(); + } + } + return histo_cost + extra_bits; +} + +float EstimateCost(const Image& img) { + // TODO(veluca): consider SIMDfication of this code. + size_t extra_bits = 0; + float histo_cost = 0; + HybridUintConfig config; + uint32_t cutoffs[] = {0, 1, 3, 5, 7, 11, 15, 23, 31, + 47, 63, 95, 127, 191, 255, 392, 500}; + constexpr size_t nc = sizeof(cutoffs) / sizeof(*cutoffs) + 1; + Histogram histo[nc] = {}; + for (const Channel& ch : img.channel) { + const intptr_t onerow = ch.plane.PixelsPerRow(); + for (size_t y = 0; y < ch.h; y++) { + const pixel_type* JXL_RESTRICT r = ch.Row(y); + for (size_t x = 0; x < ch.w; x++) { + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + size_t maxdiff = std::max(std::max(left, top), topleft) - + std::min(std::min(left, top), topleft); + size_t ctx = 0; + for (uint32_t c : cutoffs) { + ctx += c > maxdiff; + } + pixel_type res = r[x] - ClampedGradient(top, left, topleft); + uint32_t token, nbits, bits; + config.Encode(PackSigned(res), &token, &nbits, &bits); + histo[ctx].Add(token); + extra_bits += nbits; + } + } + for (size_t h = 0; h < nc; h++) { + histo_cost += histo[h].ShannonEntropy(); + histo[h].Clear(); + } + } + return histo_cost + extra_bits; +} + +} // namespace + +Status ModularFrameEncoder::PrepareStreamParams(const Rect& rect, + const CompressParams& cparams, + int minShift, int maxShift, + const ModularStreamId& stream, + bool do_color) { + size_t stream_id = stream.ID(frame_dim); + JXL_ASSERT(stream_id != 0); + Image& full_image = stream_images[0]; + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + Image& gi = stream_images[stream_id]; + gi = Image(xsize, ysize, full_image.bitdepth, 0); + // start at the first bigger-than-frame_dim.group_dim non-metachannel + size_t c = full_image.nb_meta_channels; + for (; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + if (fc.w > frame_dim.group_dim || fc.h > frame_dim.group_dim) break; + } + for (; c < full_image.channel.size(); c++) { + Channel& fc = full_image.channel[c]; + int shift = std::min(fc.hshift, fc.vshift); + if (shift > maxShift) continue; + if (shift < minShift) continue; + Rect r(rect.x0() >> fc.hshift, rect.y0() >> fc.vshift, + rect.xsize() >> fc.hshift, rect.ysize() >> fc.vshift, fc.w, fc.h); + if (r.xsize() == 0 || r.ysize() == 0) continue; + gi_channel[stream_id].push_back(c); + Channel gc(r.xsize(), r.ysize()); + gc.hshift = fc.hshift; + gc.vshift = fc.vshift; + for (size_t y = 0; y < r.ysize(); ++y) { + const pixel_type* const JXL_RESTRICT row_in = r.ConstRow(fc.plane, y); + pixel_type* const JXL_RESTRICT row_out = gc.Row(y); + for (size_t x = 0; x < r.xsize(); ++x) { + row_out[x] = row_in[x]; + } + } + gi.channel.emplace_back(std::move(gc)); + } + + // 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 && + cparams.speed_tier < SpeedTier::kCheetah) { + // all-channel palette (e.g. RGBA) + if (gi.channel.size() - gi.nb_meta_channels > 1) { + Transform maybe_palette(TransformId::kPalette); + maybe_palette.begin_c = gi.nb_meta_channels; + maybe_palette.num_c = gi.channel.size() - gi.nb_meta_channels; + maybe_palette.nb_colors = std::abs(cparams.palette_colors); + maybe_palette.ordered_palette = cparams.palette_colors >= 0; + do_transform(gi, maybe_palette, weighted::Header()); + } + // all-minus-one-channel palette (RGB with separate alpha, or CMY with + // separate K) + if (gi.channel.size() - gi.nb_meta_channels > 3) { + Transform maybe_palette_3(TransformId::kPalette); + maybe_palette_3.begin_c = gi.nb_meta_channels; + maybe_palette_3.num_c = gi.channel.size() - gi.nb_meta_channels - 1; + maybe_palette_3.nb_colors = std::abs(cparams.palette_colors); + maybe_palette_3.ordered_palette = cparams.palette_colors >= 0; + maybe_palette_3.lossy_palette = cparams.lossy_palette; + if (maybe_palette_3.lossy_palette) { + maybe_palette_3.predictor = Predictor::Weighted; + } + do_transform(gi, maybe_palette_3, weighted::Header()); + } + } + + // Local channel palette + if (cparams.channel_colors_percent > 0 && quality == 100 && + !cparams.lossy_palette && cparams.speed_tier < SpeedTier::kCheetah) { + // single channel palette (like FLIF's ChannelCompact) + size_t nb_channels = gi.channel.size() - gi.nb_meta_channels; + for (size_t i = 0; i < nb_channels; i++) { + int min, max; + compute_minmax(gi.channel[gi.nb_meta_channels + i], &min, &max); + int colors = max - min + 1; + JXL_DEBUG_V(10, "Channel %zu: range=%i..%i", i, min, max); + Transform maybe_palette_1(TransformId::kPalette); + maybe_palette_1.begin_c = i + gi.nb_meta_channels; + maybe_palette_1.num_c = 1; + // simple heuristic: if less than X percent of the values in the range + // actually occur, it is probably worth it to do a compaction + // (but only if the channel palette is less than 80% the size of the + // image itself) + maybe_palette_1.nb_colors = + std::min((int)(xsize * ysize * 0.8), + (int)(cparams.channel_colors_percent / 100. * colors)); + do_transform(gi, maybe_palette_1, weighted::Header()); + } + } + + // 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 && + cparams.responsive == false && do_color && + cparams.speed_tier <= SpeedTier::kHare) { + Transform sg(TransformId::kRCT); + sg.begin_c = gi.nb_meta_channels; + size_t nb_rcts_to_try = 0; + switch (cparams.speed_tier) { + case SpeedTier::kLightning: + case SpeedTier::kThunder: + case SpeedTier::kFalcon: + case SpeedTier::kCheetah: + nb_rcts_to_try = 0; // Just do global YCoCg + break; + case SpeedTier::kHare: + nb_rcts_to_try = 4; + break; + case SpeedTier::kWombat: + nb_rcts_to_try = 5; + break; + case SpeedTier::kSquirrel: + nb_rcts_to_try = 7; + break; + case SpeedTier::kKitten: + nb_rcts_to_try = 9; + break; + case SpeedTier::kTortoise: + nb_rcts_to_try = 19; + break; + } + float best_cost = std::numeric_limits::max(); + size_t best_rct = 0; + // These should be 19 actually different transforms; the remaining ones + // are equivalent to one of these (note that the first two are do-nothing + // and YCoCg) modulo channel reordering (which only matters in the case of + // MA-with-prev-channels-properties) and/or sign (e.g. RmG vs GmR) + for (int i : {0 * 7 + 0, 0 * 7 + 6, 0 * 7 + 5, 1 * 7 + 3, 3 * 7 + 5, + 5 * 7 + 5, 1 * 7 + 5, 2 * 7 + 5, 1 * 7 + 1, 0 * 7 + 4, + 1 * 7 + 2, 2 * 7 + 1, 2 * 7 + 2, 2 * 7 + 3, 4 * 7 + 4, + 4 * 7 + 5, 0 * 7 + 2, 0 * 7 + 1, 0 * 7 + 3}) { + if (nb_rcts_to_try == 0) break; + int num_transforms_to_keep = gi.transform.size(); + sg.rct_type = i; + do_transform(gi, sg, weighted::Header()); + float cost = EstimateCost(gi); + if (cost < best_cost) { + best_rct = i; + best_cost = cost; + } + nb_rcts_to_try--; + // Ensure we do not clamp channels to their supposed range, as this + // otherwise breaks in the presence of patches. + gi.undo_transforms(weighted::Header(), num_transforms_to_keep == 0 + ? -1 + : num_transforms_to_keep); + } + // Apply the best RCT to the image for future encoding. + sg.rct_type = best_rct; + do_transform(gi, sg, weighted::Header()); + } else { + // No need to try anything, just use the default options. + } + size_t nb_wp_modes = 1; + if (cparams.speed_tier <= SpeedTier::kTortoise) { + nb_wp_modes = 5; + } else if (cparams.speed_tier <= SpeedTier::kKitten) { + nb_wp_modes = 2; + } + if (nb_wp_modes > 1 && + (stream_options[stream_id].predictor == Predictor::Weighted || + stream_options[stream_id].predictor == Predictor::Best || + stream_options[stream_id].predictor == Predictor::Variable)) { + float best_cost = std::numeric_limits::max(); + stream_options[stream_id].wp_mode = 0; + for (size_t i = 0; i < nb_wp_modes; i++) { + float cost = EstimateWPCost(gi, i); + if (cost < best_cost) { + best_cost = cost; + stream_options[stream_id].wp_mode = i; + } + } + } + return true; +} + +int QuantizeWP(const int32_t* qrow, size_t onerow, size_t c, size_t x, size_t y, + size_t w, weighted::State* wp_state, float value, + float inv_factor) { + float svalue = value * inv_factor; + PredictionResult pred = + PredictNoTreeWP(w, qrow + x, onerow, x, y, Predictor::Weighted, wp_state); + svalue -= pred.guess; + int residual = roundf(svalue); + if (residual > 2 || residual < -2) residual = roundf(svalue * 0.5) * 2; + return residual + pred.guess; +} + +int QuantizeGradient(const int32_t* qrow, size_t onerow, size_t c, size_t x, + size_t y, size_t w, float value, float inv_factor) { + float svalue = value * inv_factor; + PredictionResult pred = + PredictNoTreeNoWP(w, qrow + x, onerow, x, y, Predictor::Gradient); + svalue -= pred.guess; + int residual = roundf(svalue); + if (residual > 2 || residual < -2) residual = roundf(svalue * 0.5) * 2; + return residual + pred.guess; +} + +void ModularFrameEncoder::AddVarDCTDC(const Image3F& dc, size_t group_index, + bool nl_dc, + PassesEncoderState* enc_state) { + const Rect r = enc_state->shared.DCGroupRect(group_index); + extra_dc_precision[group_index] = nl_dc ? 1 : 0; + float mul = 1 << extra_dc_precision[group_index]; + + size_t stream_id = ModularStreamId::VarDCTDC(group_index).ID(frame_dim); + stream_options[stream_id].max_chan_size = 0xFFFFFF; + stream_options[stream_id].predictor = Predictor::Weighted; + stream_options[stream_id].wp_tree_mode = ModularOptions::TreeMode::kWPOnly; + if (cparams.speed_tier >= SpeedTier::kSquirrel) { + stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kWPFixedDC; + } + if (cparams.decoding_speed_tier >= 1) { + stream_options[stream_id].tree_kind = + ModularOptions::TreeKind::kGradientFixedDC; + } + + stream_images[stream_id] = Image(r.xsize(), r.ysize(), 8, 3); + if (nl_dc && stream_options[stream_id].tree_kind == + ModularOptions::TreeKind::kGradientFixedDC) { + JXL_ASSERT(enc_state->shared.frame_header.chroma_subsampling.Is444()); + for (size_t c : {1, 0, 2}) { + float inv_factor = enc_state->shared.quantizer.GetInvDcStep(c) * mul; + float y_factor = enc_state->shared.quantizer.GetDcStep(1) / mul; + float cfl_factor = enc_state->shared.cmap.DCFactors()[c]; + for (size_t y = 0; y < r.ysize(); y++) { + int32_t* quant_row = + stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + size_t stride = stream_images[stream_id] + .channel[c < 2 ? c ^ 1 : c] + .plane.PixelsPerRow(); + const float* row = r.ConstPlaneRow(dc, c, y); + if (c == 1) { + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = QuantizeGradient(quant_row, stride, c, x, y, + r.xsize(), row[x], inv_factor); + } + } else { + int32_t* quant_row_y = + stream_images[stream_id].channel[0].plane.Row(y); + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = QuantizeGradient( + quant_row, stride, c, x, y, r.xsize(), + row[x] - quant_row_y[x] * (y_factor * cfl_factor), inv_factor); + } + } + } + } + } else if (nl_dc) { + JXL_ASSERT(enc_state->shared.frame_header.chroma_subsampling.Is444()); + for (size_t c : {1, 0, 2}) { + float inv_factor = enc_state->shared.quantizer.GetInvDcStep(c) * mul; + float y_factor = enc_state->shared.quantizer.GetDcStep(1) / mul; + float cfl_factor = enc_state->shared.cmap.DCFactors()[c]; + weighted::Header header; + weighted::State wp_state(header, r.xsize(), r.ysize()); + for (size_t y = 0; y < r.ysize(); y++) { + int32_t* quant_row = + stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + size_t stride = stream_images[stream_id] + .channel[c < 2 ? c ^ 1 : c] + .plane.PixelsPerRow(); + const float* row = r.ConstPlaneRow(dc, c, y); + if (c == 1) { + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = QuantizeWP(quant_row, stride, c, x, y, r.xsize(), + &wp_state, row[x], inv_factor); + wp_state.UpdateErrors(quant_row[x], x, y, r.xsize()); + } + } else { + int32_t* quant_row_y = + stream_images[stream_id].channel[0].plane.Row(y); + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = QuantizeWP( + quant_row, stride, c, x, y, r.xsize(), &wp_state, + row[x] - quant_row_y[x] * (y_factor * cfl_factor), inv_factor); + wp_state.UpdateErrors(quant_row[x], x, y, r.xsize()); + } + } + } + } + } else if (enc_state->shared.frame_header.chroma_subsampling.Is444()) { + for (size_t c : {1, 0, 2}) { + float inv_factor = enc_state->shared.quantizer.GetInvDcStep(c) * mul; + float y_factor = enc_state->shared.quantizer.GetDcStep(1) / mul; + float cfl_factor = enc_state->shared.cmap.DCFactors()[c]; + for (size_t y = 0; y < r.ysize(); y++) { + int32_t* quant_row = + stream_images[stream_id].channel[c < 2 ? c ^ 1 : c].plane.Row(y); + const float* row = r.ConstPlaneRow(dc, c, y); + if (c == 1) { + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = roundf(row[x] * inv_factor); + } + } else { + int32_t* quant_row_y = + stream_images[stream_id].channel[0].plane.Row(y); + for (size_t x = 0; x < r.xsize(); x++) { + quant_row[x] = + roundf((row[x] - quant_row_y[x] * (y_factor * cfl_factor)) * + inv_factor); + } + } + } + } + } else { + for (size_t c : {1, 0, 2}) { + Rect rect( + r.x0() >> enc_state->shared.frame_header.chroma_subsampling.HShift(c), + r.y0() >> enc_state->shared.frame_header.chroma_subsampling.VShift(c), + r.xsize() >> + enc_state->shared.frame_header.chroma_subsampling.HShift(c), + r.ysize() >> + enc_state->shared.frame_header.chroma_subsampling.VShift(c)); + float inv_factor = enc_state->shared.quantizer.GetInvDcStep(c) * mul; + size_t ys = rect.ysize(); + size_t xs = rect.xsize(); + Channel& ch = stream_images[stream_id].channel[c < 2 ? c ^ 1 : c]; + ch.w = xs; + ch.h = ys; + ch.shrink(); + for (size_t y = 0; y < ys; y++) { + int32_t* quant_row = ch.plane.Row(y); + const float* row = rect.ConstPlaneRow(dc, c, y); + for (size_t x = 0; x < xs; x++) { + quant_row[x] = roundf(row[x] * inv_factor); + } + } + } + } + + DequantDC(r, &enc_state->shared.dc_storage, &enc_state->shared.quant_dc, + stream_images[stream_id], enc_state->shared.quantizer.MulDC(), + 1.0 / mul, enc_state->shared.cmap.DCFactors(), + enc_state->shared.frame_header.chroma_subsampling, + enc_state->shared.block_ctx_map); +} + +void ModularFrameEncoder::AddACMetadata(size_t group_index, bool jpeg_transcode, + PassesEncoderState* enc_state) { + const Rect r = enc_state->shared.DCGroupRect(group_index); + size_t stream_id = ModularStreamId::ACMetadata(group_index).ID(frame_dim); + stream_options[stream_id].max_chan_size = 0xFFFFFF; + stream_options[stream_id].wp_tree_mode = ModularOptions::TreeMode::kNoWP; + if (jpeg_transcode) { + stream_options[stream_id].tree_kind = + ModularOptions::TreeKind::kJpegTranscodeACMeta; + } else if (cparams.speed_tier >= SpeedTier::kFalcon) { + stream_options[stream_id].tree_kind = + ModularOptions::TreeKind::kFalconACMeta; + } else if (cparams.speed_tier > SpeedTier::kKitten) { + stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kACMeta; + } + // If we are using a non-constant CfL field, and are in a slow enough mode, + // re-enable tree computation for it. + if (cparams.speed_tier < SpeedTier::kSquirrel && + cparams.force_cfl_jpeg_recompression) { + stream_options[stream_id].tree_kind = ModularOptions::TreeKind::kLearn; + } + // YToX, YToB, ACS + QF, EPF + Image& image = stream_images[stream_id]; + image = Image(r.xsize(), r.ysize(), 8, 4); + static_assert(kColorTileDimInBlocks == 8, "Color tile size changed"); + Rect cr(r.x0() >> 3, r.y0() >> 3, (r.xsize() + 7) >> 3, (r.ysize() + 7) >> 3); + image.channel[0] = Channel(cr.xsize(), cr.ysize(), 3, 3); + image.channel[1] = Channel(cr.xsize(), cr.ysize(), 3, 3); + image.channel[2] = Channel(r.xsize() * r.ysize(), 2, 0, 0); + ConvertPlaneAndClamp(cr, enc_state->shared.cmap.ytox_map, + Rect(image.channel[0].plane), &image.channel[0].plane); + ConvertPlaneAndClamp(cr, enc_state->shared.cmap.ytob_map, + Rect(image.channel[1].plane), &image.channel[1].plane); + size_t num = 0; + for (size_t y = 0; y < r.ysize(); y++) { + AcStrategyRow row_acs = enc_state->shared.ac_strategy.ConstRow(r, y); + const int* row_qf = r.ConstRow(enc_state->shared.raw_quant_field, y); + const uint8_t* row_epf = r.ConstRow(enc_state->shared.epf_sharpness, y); + int* out_acs = image.channel[2].plane.Row(0); + int* out_qf = image.channel[2].plane.Row(1); + int* row_out_epf = image.channel[3].plane.Row(y); + for (size_t x = 0; x < r.xsize(); x++) { + row_out_epf[x] = row_epf[x]; + if (!row_acs[x].IsFirstBlock()) continue; + out_acs[num] = row_acs[x].RawStrategy(); + out_qf[num] = row_qf[x] - 1; + num++; + } + } + image.channel[2].w = num; + ac_metadata_size[group_index] = num; +} + +void ModularFrameEncoder::EncodeQuantTable( + size_t size_x, size_t size_y, BitWriter* writer, + const QuantEncoding& encoding, size_t idx, + ModularFrameEncoder* modular_frame_encoder) { + JXL_ASSERT(encoding.qraw.qtable != nullptr); + JXL_ASSERT(size_x * size_y * 3 == encoding.qraw.qtable->size()); + JXL_CHECK(F16Coder::Write(encoding.qraw.qtable_den, writer)); + if (modular_frame_encoder) { + JXL_CHECK(modular_frame_encoder->EncodeStream( + writer, nullptr, 0, ModularStreamId::QuantTable(idx))); + return; + } + Image image(size_x, size_y, 8, 3); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < size_y; y++) { + int* JXL_RESTRICT row = image.channel[c].Row(y); + for (size_t x = 0; x < size_x; x++) { + row[x] = (*encoding.qraw.qtable)[c * size_x * size_y + y * size_x + x]; + } + } + } + ModularOptions cfopts; + JXL_CHECK(ModularGenericCompress(image, cfopts, writer)); +} + +void ModularFrameEncoder::AddQuantTable(size_t size_x, size_t size_y, + const QuantEncoding& encoding, + size_t idx) { + size_t stream_id = ModularStreamId::QuantTable(idx).ID(frame_dim); + JXL_ASSERT(encoding.qraw.qtable != nullptr); + JXL_ASSERT(size_x * size_y * 3 == encoding.qraw.qtable->size()); + Image& image = stream_images[stream_id]; + image = Image(size_x, size_y, 8, 3); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < size_y; y++) { + int* JXL_RESTRICT row = image.channel[c].Row(y); + for (size_t x = 0; x < size_x; x++) { + row[x] = (*encoding.qraw.qtable)[c * size_x * size_y + y * size_x + x]; + } + } + } +} +} // namespace jxl diff --git a/lib/jxl/enc_modular.h b/lib/jxl/enc_modular.h new file mode 100644 index 0000000..e728273 --- /dev/null +++ b/lib/jxl/enc_modular.h @@ -0,0 +1,92 @@ +// 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_ENC_MODULAR_H_ +#define LIB_JXL_ENC_MODULAR_H_ + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_modular.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +class ModularFrameEncoder { + public: + ModularFrameEncoder(const FrameHeader& frame_header, + const CompressParams& cparams_orig); + Status ComputeEncodingData(const FrameHeader& frame_header, + const ImageMetadata& metadata, + Image3F* JXL_RESTRICT color, + const std::vector& extra_channels, + PassesEncoderState* JXL_RESTRICT enc_state, + ThreadPool* pool, AuxOut* aux_out, bool do_color); + // Encodes global info (tree + histograms) in the `writer`. + Status EncodeGlobalInfo(BitWriter* writer, AuxOut* aux_out); + // Encodes a specific modular image (identified by `stream`) in the `writer`, + // assigning bits to the provided `layer`. + Status EncodeStream(BitWriter* writer, AuxOut* aux_out, size_t layer, + const ModularStreamId& stream); + // Creates a modular image for a given DC group of VarDCT mode. `dc` is the + // input DC image, not quantized; the group is specified by `group_index`, and + // `nl_dc` decides whether to apply a near-lossless processing to the DC or + // not. + void AddVarDCTDC(const Image3F& dc, size_t group_index, bool nl_dc, + PassesEncoderState* enc_state); + // Creates a modular image for the AC metadata of the given group + // (`group_index`). + void AddACMetadata(size_t group_index, bool jpeg_transcode, + PassesEncoderState* enc_state); + // Encodes a RAW quantization table in `writer`. If `modular_frame_encoder` is + // null, the quantization table in `encoding` is used, with dimensions `size_x + // x size_y`. Otherwise, the table with ID `idx` is encoded from the given + // `modular_frame_encoder`. + static void EncodeQuantTable(size_t size_x, size_t size_y, BitWriter* writer, + const QuantEncoding& encoding, size_t idx, + ModularFrameEncoder* modular_frame_encoder); + // Stores a quantization table for future usage with `EncodeQuantTable`. + void AddQuantTable(size_t size_x, size_t size_y, + const QuantEncoding& encoding, size_t idx); + + std::vector ac_metadata_size; + std::vector extra_dc_precision; + + private: + Status PrepareEncoding(ThreadPool* pool, const FrameDimensions& frame_dim, + EncoderHeuristics* heuristics, + AuxOut* aux_out = nullptr); + Status PrepareStreamParams(const Rect& rect, const CompressParams& cparams, + int minShift, int maxShift, + const ModularStreamId& stream, bool do_color); + std::vector stream_images; + std::vector stream_options; + + Tree tree; + std::vector> tree_tokens; + std::vector stream_headers; + std::vector> tokens; + EntropyEncodingData code; + std::vector context_map; + FrameDimensions frame_dim; + CompressParams cparams; + float quality = cparams.quality_pair.first; + float cquality = cparams.quality_pair.second; + std::vector tree_splits; + std::vector multiplier_info; + std::vector> gi_channel; + std::vector image_widths; +}; + +} // namespace jxl + +#endif // LIB_JXL_ENC_MODULAR_H_ diff --git a/lib/jxl/enc_noise.cc b/lib/jxl/enc_noise.cc new file mode 100644 index 0000000..383b792 --- /dev/null +++ b/lib/jxl/enc_noise.cc @@ -0,0 +1,378 @@ +// 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/enc_noise.h" + +#include +#include +#include + +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/robust_statistics.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/optimize.h" + +namespace jxl { +namespace { + +using OptimizeArray = optimize::Array; + +float GetScoreSumsOfAbsoluteDifferences(const Image3F& opsin, const int x, + const int y, const int block_size) { + const int small_bl_size_x = 3; + const int small_bl_size_y = 4; + const int kNumSAD = + (block_size - small_bl_size_x) * (block_size - small_bl_size_y); + // block_size x block_size reference pixels + int counter = 0; + const int offset = 2; + + std::vector sad(kNumSAD, 0); + for (int y_bl = 0; y_bl + small_bl_size_y < block_size; ++y_bl) { + for (int x_bl = 0; x_bl + small_bl_size_x < block_size; ++x_bl) { + float sad_sum = 0; + // size of the center patch, we compare all the patches inside window with + // the center one + for (int cy = 0; cy < small_bl_size_y; ++cy) { + for (int cx = 0; cx < small_bl_size_x; ++cx) { + float wnd = 0.5f * (opsin.PlaneRow(1, y + y_bl + cy)[x + x_bl + cx] + + opsin.PlaneRow(0, y + y_bl + cy)[x + x_bl + cx]); + float center = + 0.5f * (opsin.PlaneRow(1, y + offset + cy)[x + offset + cx] + + opsin.PlaneRow(0, y + offset + cy)[x + offset + cx]); + sad_sum += std::abs(center - wnd); + } + } + sad[counter++] = sad_sum; + } + } + const int kSamples = (kNumSAD) / 2; + // As with ROAD (rank order absolute distance), we keep the smallest half of + // the values in SAD (we use here the more robust patch SAD instead of + // absolute single-pixel differences). + std::sort(sad.begin(), sad.end()); + const float total_sad_sum = + std::accumulate(sad.begin(), sad.begin() + kSamples, 0.0f); + return total_sad_sum / kSamples; +} + +class NoiseHistogram { + public: + static constexpr int kBins = 256; + + NoiseHistogram() { std::fill(bins, bins + kBins, 0); } + + void Increment(const float x) { bins[Index(x)] += 1; } + int Get(const float x) const { return bins[Index(x)]; } + int Bin(const size_t bin) const { return bins[bin]; } + + void Print() const { + for (unsigned int bin : bins) { + printf("%d\n", bin); + } + } + + int Mode() const { + uint32_t cdf[kBins]; + std::partial_sum(bins, bins + kBins, cdf); + return HalfRangeMode()(cdf, kBins); + } + + double Quantile(double q01) const { + const int64_t total = std::accumulate(bins, bins + kBins, int64_t{1}); + const int64_t target = static_cast(q01 * total); + // Until sum >= target: + int64_t sum = 0; + size_t i = 0; + for (; i < kBins; ++i) { + sum += bins[i]; + // Exact match: assume middle of bin i + if (sum == target) { + return i + 0.5; + } + if (sum > target) break; + } + + // Next non-empty bin (in case histogram is sparsely filled) + size_t next = i + 1; + while (next < kBins && bins[next] == 0) { + ++next; + } + + // Linear interpolation according to how far into next we went + const double excess = target - sum; + const double weight_next = bins[Index(next)] / excess; + return ClampX(next * weight_next + i * (1.0 - weight_next)); + } + + // Inter-quartile range + double IQR() const { return Quantile(0.75) - Quantile(0.25); } + + private: + template + T ClampX(const T x) const { + return std::min(std::max(T(0), x), T(kBins - 1)); + } + size_t Index(const float x) const { return ClampX(static_cast(x)); } + + uint32_t bins[kBins]; +}; + +std::vector GetSADScoresForPatches(const Image3F& opsin, + const size_t block_s, + const size_t num_bin, + NoiseHistogram* sad_histogram) { + std::vector sad_scores( + (opsin.ysize() / block_s) * (opsin.xsize() / block_s), 0.0f); + + int block_index = 0; + + for (size_t y = 0; y + block_s <= opsin.ysize(); y += block_s) { + for (size_t x = 0; x + block_s <= opsin.xsize(); x += block_s) { + float sad_sc = GetScoreSumsOfAbsoluteDifferences(opsin, x, y, block_s); + sad_scores[block_index++] = sad_sc; + sad_histogram->Increment(sad_sc * num_bin); + } + } + return sad_scores; +} + +float GetSADThreshold(const NoiseHistogram& histogram, const int num_bin) { + // Here we assume that the most patches with similar SAD value is a "flat" + // patches. However, some images might contain regular texture part and + // generate second strong peak at the histogram + // TODO(user) handle bimodal and heavy-tailed case + const int mode = histogram.Mode(); + return static_cast(mode) / NoiseHistogram::kBins; +} + +// loss = sum asym * (F(x) - nl)^2 + kReg * num_points * sum (w[i] - w[i+1])^2 +// where asym = 1 if F(x) < nl, kAsym if F(x) > nl. +struct LossFunction { + explicit LossFunction(std::vector nl0) : nl(std::move(nl0)) {} + + double Compute(const OptimizeArray& w, OptimizeArray* df, + bool skip_regularization = false) const { + constexpr double kReg = 0.005; + constexpr double kAsym = 1.1; + double loss_function = 0; + for (size_t i = 0; i < w.size(); i++) { + (*df)[i] = 0; + } + for (auto ind : nl) { + std::pair pos = IndexAndFrac(ind.intensity); + JXL_DASSERT(pos.first >= 0 && static_cast(pos.first) < + NoiseParams::kNumNoisePoints - 1); + double low = w[pos.first]; + double hi = w[pos.first + 1]; + double val = low * (1.0f - pos.second) + hi * pos.second; + double dist = val - ind.noise_level; + if (dist > 0) { + loss_function += kAsym * dist * dist; + (*df)[pos.first] -= kAsym * (1.0f - pos.second) * dist; + (*df)[pos.first + 1] -= kAsym * pos.second * dist; + } else { + loss_function += dist * dist; + (*df)[pos.first] -= (1.0f - pos.second) * dist; + (*df)[pos.first + 1] -= pos.second * dist; + } + } + if (skip_regularization) return loss_function; + for (size_t i = 0; i + 1 < w.size(); i++) { + double diff = w[i] - w[i + 1]; + loss_function += kReg * nl.size() * diff * diff; + (*df)[i] -= kReg * diff * nl.size(); + (*df)[i + 1] += kReg * diff * nl.size(); + } + return loss_function; + } + + std::vector nl; +}; + +void OptimizeNoiseParameters(const std::vector& noise_level, + NoiseParams* noise_params) { + constexpr double kMaxError = 1e-3; + static const double kPrecision = 1e-8; + static const int kMaxIter = 40; + + float avg = 0; + for (const NoiseLevel& nl : noise_level) { + avg += nl.noise_level; + } + avg /= noise_level.size(); + + LossFunction loss_function(noise_level); + OptimizeArray parameter_vector; + for (size_t i = 0; i < parameter_vector.size(); i++) { + parameter_vector[i] = avg; + } + + parameter_vector = optimize::OptimizeWithScaledConjugateGradientMethod( + loss_function, parameter_vector, kPrecision, kMaxIter); + + OptimizeArray df = parameter_vector; + float loss = loss_function.Compute(parameter_vector, &df, + /*skip_regularization=*/true) / + noise_level.size(); + + // Approximation went too badly: escape with no noise at all. + if (loss > kMaxError) { + noise_params->Clear(); + return; + } + + for (size_t i = 0; i < parameter_vector.size(); i++) { + noise_params->lut[i] = std::max(parameter_vector[i], 0.0); + } +} + +std::vector GetNoiseLevel( + const Image3F& opsin, const std::vector& texture_strength, + const float threshold, const size_t block_s) { + std::vector noise_level_per_intensity; + + const int filt_size = 1; + static const float kLaplFilter[filt_size * 2 + 1][filt_size * 2 + 1] = { + {-0.25f, -1.0f, -0.25f}, + {-1.0f, 5.0f, -1.0f}, + {-0.25f, -1.0f, -0.25f}, + }; + + // The noise model is built based on channel 0.5 * (X+Y) as we notice that it + // is similar to the model 0.5 * (Y-X) + size_t patch_index = 0; + + for (size_t y = 0; y + block_s <= opsin.ysize(); y += block_s) { + for (size_t x = 0; x + block_s <= opsin.xsize(); x += block_s) { + if (texture_strength[patch_index] <= threshold) { + // Calculate mean value + float mean_int = 0; + for (size_t y_bl = 0; y_bl < block_s; ++y_bl) { + for (size_t x_bl = 0; x_bl < block_s; ++x_bl) { + mean_int += 0.5f * (opsin.PlaneRow(1, y + y_bl)[x + x_bl] + + opsin.PlaneRow(0, y + y_bl)[x + x_bl]); + } + } + mean_int /= block_s * block_s; + + // Calculate Noise level + float noise_level = 0; + size_t count = 0; + for (size_t y_bl = 0; y_bl < block_s; ++y_bl) { + for (size_t x_bl = 0; x_bl < block_s; ++x_bl) { + float filtered_value = 0; + for (int y_f = -1 * filt_size; y_f <= filt_size; ++y_f) { + if ((static_cast(y_bl) + y_f) >= 0 && + (y_bl + y_f) < block_s) { + for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) { + if ((static_cast(x_bl) + x_f) >= 0 && + (x_bl + x_f) < block_s) { + filtered_value += + 0.5f * + (opsin.PlaneRow(1, y + y_bl + y_f)[x + x_bl + x_f] + + opsin.PlaneRow(0, y + y_bl + y_f)[x + x_bl + x_f]) * + kLaplFilter[y_f + filt_size][x_f + filt_size]; + } else { + filtered_value += + 0.5f * + (opsin.PlaneRow(1, y + y_bl + y_f)[x + x_bl - x_f] + + opsin.PlaneRow(0, y + y_bl + y_f)[x + x_bl - x_f]) * + kLaplFilter[y_f + filt_size][x_f + filt_size]; + } + } + } else { + for (int x_f = -1 * filt_size; x_f <= filt_size; ++x_f) { + if ((static_cast(x_bl) + x_f) >= 0 && + (x_bl + x_f) < block_s) { + filtered_value += + 0.5f * + (opsin.PlaneRow(1, y + y_bl - y_f)[x + x_bl + x_f] + + opsin.PlaneRow(0, y + y_bl - y_f)[x + x_bl + x_f]) * + kLaplFilter[y_f + filt_size][x_f + filt_size]; + } else { + filtered_value += + 0.5f * + (opsin.PlaneRow(1, y + y_bl - y_f)[x + x_bl - x_f] + + opsin.PlaneRow(0, y + y_bl - y_f)[x + x_bl - x_f]) * + kLaplFilter[y_f + filt_size][x_f + filt_size]; + } + } + } + } + noise_level += std::abs(filtered_value); + ++count; + } + } + noise_level /= count; + NoiseLevel nl; + nl.intensity = mean_int; + nl.noise_level = noise_level; + noise_level_per_intensity.push_back(nl); + } + ++patch_index; + } + } + return noise_level_per_intensity; +} + +void EncodeFloatParam(float val, float precision, BitWriter* writer) { + JXL_ASSERT(val >= 0); + const int absval_quant = static_cast(val * precision + 0.5f); + JXL_ASSERT(absval_quant < (1 << 10)); + writer->Write(10, absval_quant); +} + +} // namespace + +Status GetNoiseParameter(const Image3F& opsin, NoiseParams* noise_params, + float quality_coef) { + // The size of a patch in decoder might be different from encoder's patch + // size. + // For encoder: the patch size should be big enough to estimate + // noise level, but, at the same time, it should be not too big + // to be able to estimate intensity value of the patch + const size_t block_s = 8; + const size_t kNumBin = 256; + NoiseHistogram sad_histogram; + std::vector sad_scores = + GetSADScoresForPatches(opsin, block_s, kNumBin, &sad_histogram); + float sad_threshold = GetSADThreshold(sad_histogram, kNumBin); + // If threshold is too large, the image has a strong pattern. This pattern + // fools our model and it will add too much noise. Therefore, we do not add + // noise for such images + if (sad_threshold > 0.15f || sad_threshold <= 0.0f) { + noise_params->Clear(); + return false; + } + std::vector nl = + GetNoiseLevel(opsin, sad_scores, sad_threshold, block_s); + + OptimizeNoiseParameters(nl, noise_params); + for (float& i : noise_params->lut) { + i *= quality_coef * 1.4; + } + return noise_params->HasAny(); +} + +void EncodeNoise(const NoiseParams& noise_params, BitWriter* writer, + size_t layer, AuxOut* aux_out) { + JXL_ASSERT(noise_params.HasAny()); + + BitWriter::Allotment allotment(writer, NoiseParams::kNumNoisePoints * 16); + for (float i : noise_params.lut) { + EncodeFloatParam(i, kNoisePrecision, writer); + } + ReclaimAndCharge(writer, &allotment, layer, aux_out); +} + +} // namespace jxl diff --git a/lib/jxl/enc_noise.h b/lib/jxl/enc_noise.h new file mode 100644 index 0000000..15fb07a --- /dev/null +++ b/lib/jxl/enc_noise.h @@ -0,0 +1,33 @@ +// 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_ENC_NOISE_H_ +#define LIB_JXL_ENC_NOISE_H_ + +// Noise parameter estimation. + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/image.h" +#include "lib/jxl/noise.h" + +namespace jxl { + +// Get parameters of the noise for NoiseParams model +// Returns whether a valid noise model (with HasAny()) is set. +Status GetNoiseParameter(const Image3F& opsin, NoiseParams* noise_params, + float quality_coef); + +// Does not write anything if `noise_params` are empty. Otherwise, caller must +// set FrameHeader.flags.kNoise. +void EncodeNoise(const NoiseParams& noise_params, BitWriter* writer, + size_t layer, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_NOISE_H_ diff --git a/lib/jxl/enc_params.h b/lib/jxl/enc_params.h new file mode 100644 index 0000000..8dc6a20 --- /dev/null +++ b/lib/jxl/enc_params.h @@ -0,0 +1,274 @@ +// 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_ENC_PARAMS_H_ +#define LIB_JXL_ENC_PARAMS_H_ + +// Parameters and flags that govern JXL compression. + +#include +#include + +#include + +#include "lib/jxl/base/override.h" +#include "lib/jxl/butteraugli/butteraugli.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +enum class SpeedTier { + // Turns on FindBestQuantizationHQ loop. Equivalent to "guetzli" mode. + kTortoise = 1, + // Turns on FindBestQuantization butteraugli loop. + kKitten = 2, + // Turns on dots, patches, and spline detection by default, as well as full + // context clustering. Default. + kSquirrel = 3, + // Turns on error diffusion and full AC strategy heuristics. Equivalent to + // "fast" mode. + kWombat = 4, + // Turns on gaborish by default, non-default cmap, initial quant field. + kHare = 5, + // Turns on simple heuristics for AC strategy, quant field, and clustering; + // also enables coefficient reordering. + kCheetah = 6, + // Turns off most encoder features. Does context clustering. + // Modular: uses fixed tree with Weighted predictor. + kFalcon = 7, + // Currently fastest possible setting for VarDCT. + // Modular: uses fixed tree with Gradient predictor. + kThunder = 8, + // VarDCT: same as kThunder. + // Modular: no tree, Gradient predictor, fast histograms + kLightning = 9 +}; + +inline bool ParseSpeedTier(const std::string& s, SpeedTier* out) { + if (s == "lightning") { + *out = SpeedTier::kLightning; + return true; + } else if (s == "thunder") { + *out = SpeedTier::kThunder; + return true; + } else if (s == "falcon") { + *out = SpeedTier::kFalcon; + return true; + } else if (s == "cheetah") { + *out = SpeedTier::kCheetah; + return true; + } else if (s == "hare") { + *out = SpeedTier::kHare; + return true; + } else if (s == "fast" || s == "wombat") { + *out = SpeedTier::kWombat; + return true; + } else if (s == "squirrel") { + *out = SpeedTier::kSquirrel; + return true; + } else if (s == "kitten") { + *out = SpeedTier::kKitten; + return true; + } else if (s == "guetzli" || s == "tortoise") { + *out = SpeedTier::kTortoise; + return true; + } + size_t st = 10 - static_cast(strtoull(s.c_str(), nullptr, 0)); + if (st <= static_cast(SpeedTier::kLightning) && + st >= static_cast(SpeedTier::kTortoise)) { + *out = SpeedTier(st); + return true; + } + return false; +} + +inline const char* SpeedTierName(SpeedTier speed_tier) { + switch (speed_tier) { + case SpeedTier::kLightning: + return "lightning"; + case SpeedTier::kThunder: + return "thunder"; + case SpeedTier::kFalcon: + return "falcon"; + case SpeedTier::kCheetah: + return "cheetah"; + case SpeedTier::kHare: + return "hare"; + case SpeedTier::kWombat: + return "wombat"; + case SpeedTier::kSquirrel: + return "squirrel"; + case SpeedTier::kKitten: + return "kitten"; + case SpeedTier::kTortoise: + return "tortoise"; + } + return "INVALID"; +} + +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +struct CompressParams { + float butteraugli_distance = 1.0f; + size_t target_size = 0; + float target_bitrate = 0.0f; + + // 0.0 means search for the adaptive quantization map that matches the + // butteraugli distance, positive values mean quantize everywhere with that + // value. + float uniform_quant = 0.0f; + float quant_border_bias = 0.0f; + + // Try to achieve a maximum pixel-by-pixel error on each channel. + bool max_error_mode = false; + float max_error[3] = {0.0, 0.0, 0.0}; + + SpeedTier speed_tier = SpeedTier::kSquirrel; + + // 0 = default. + // 1 = slightly worse quality. + // 4 = fastest speed, lowest quality + // TODO(veluca): hook this up to the C API. + size_t decoding_speed_tier = 0; + + int max_butteraugli_iters = 4; + + int max_butteraugli_iters_guetzli_mode = 100; + + ColorTransform color_transform = ColorTransform::kXYB; + YCbCrChromaSubsampling chroma_subsampling; + + // If true, the "modular mode options" members below are used. + bool modular_mode = false; + + // Change group size in modular mode (0=128, 1=256, 2=512, 3=1024). + size_t modular_group_size_shift = 1; + + Override preview = Override::kDefault; + Override noise = Override::kDefault; + Override dots = Override::kDefault; + Override patches = Override::kDefault; + Override gaborish = Override::kDefault; + int epf = -1; + + // Progressive mode. + bool progressive_mode = false; + + // Quantized-progressive mode. + bool qprogressive_mode = false; + + // Put center groups first in the bitstream. + bool centerfirst = false; + + // Pixel coordinates of the center. First group will contain that center. + size_t center_x = static_cast(-1); + size_t center_y = static_cast(-1); + + int progressive_dc = -1; + + // If on: preserve color of invisible pixels (if off: don't care) + // Default: on for lossless, off for lossy + Override keep_invisible = Override::kDefault; + + // Progressive-mode saliency. + // + // How many progressive saliency-encoding steps to perform. + // - 1: Encode only DC and lowest-frequency AC. Does not need a saliency-map. + // - 2: Encode only DC+LF, dropping all HF AC data. + // Does not need a saliency-map. + // - 3: Encode DC+LF+{salient HF}, dropping all non-salient HF data. + // - 4: Encode DC+LF+{salient HF}+{other HF}. + // - 5: Encode DC+LF+{quantized HF}+{low HF bits}. + size_t saliency_num_progressive_steps = 3; + // Every saliency-heatmap cell with saliency >= threshold will be considered + // as 'salient'. The default value of 0.0 will consider every AC-block + // as salient, hence not require a saliency-map, and not actually generate + // a 4th progressive step. + float saliency_threshold = 0.0f; + // Saliency-map (owned by caller). + ImageF* saliency_map = nullptr; + + // Input and output file name. Will be used to provide pluggable saliency + // extractor with paths. + const char* file_in = nullptr; + const char* file_out = nullptr; + + // Currently unused as of 2020-01. + bool clear_metadata = false; + + // Prints extra information during/after encoding. + bool verbose = false; + + ButteraugliParams ba_params; + + // Force usage of CfL when doing JPEG recompression. This can have unexpected + // effects on the decoded pixels, while still being JPEG-compliant and + // allowing reconstruction of the original JPEG. + bool force_cfl_jpeg_recompression = true; + + // Set the noise to what it would approximately be if shooting at the nominal + // exposure for a given ISO setting on a 35mm camera. + float photon_noise_iso = 0; + + // modular mode options below + ModularOptions options; + int responsive = -1; + // A pair of . + std::pair 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; + // Use Local channel palette if #colors < this percentage of range + float channel_colors_percent = 80.f; + int palette_colors = 1 << 10; // up to 10-bit palette is probably worthwhile + bool lossy_palette = false; + + // 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; + } + + // Sets the parameters required to make the codec lossless. + void SetLossless() { + modular_mode = true; + quality_pair.first = 100; + quality_pair.second = 100; + color_transform = jxl::ColorTransform::kNone; + } + + bool use_new_heuristics = false; + + // Down/upsample the image before encoding / after decoding by this factor. + // The resampling value can also be set to 0 to automatically choose based + // on distance, however EncodeFrame doesn't support this, so it is + // required to process the CompressParams to set a valid positive resampling + // value and altered butteraugli score if this is used. + size_t resampling = 1; + size_t ec_resampling = 1; + // Skip the downsampling before encoding if this is true. + bool already_downsampled = false; +}; + +static constexpr float kMinButteraugliForDynamicAR = 0.5f; +static constexpr float kMinButteraugliForDots = 3.0f; +static constexpr float kMinButteraugliToSubtractOriginalPatches = 3.0f; +static constexpr float kMinButteraugliDistanceForProgressiveDc = 4.5f; + +// Always off +static constexpr float kMinButteraugliForNoise = 99.0f; + +// Minimum butteraugli distance the encoder accepts. +static constexpr float kMinButteraugliDistance = 0.01f; + +// Tile size for encoder-side processing. Must be equal to color tile dim in the +// current implementation. +static constexpr size_t kEncTileDim = 64; +static constexpr size_t kEncTileDimInBlocks = kEncTileDim / kBlockDim; + +} // namespace jxl + +#endif // LIB_JXL_ENC_PARAMS_H_ diff --git a/lib/jxl/enc_patch_dictionary.cc b/lib/jxl/enc_patch_dictionary.cc new file mode 100644 index 0000000..dcdbdf7 --- /dev/null +++ b/lib/jxl/enc_patch_dictionary.cc @@ -0,0 +1,842 @@ +// 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/enc_patch_dictionary.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/dec_frame.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_dot_dictionary.h" +#include "lib/jxl/enc_frame.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/patch_dictionary_internal.h" + +namespace jxl { + +// static +void PatchDictionaryEncoder::Encode(const PatchDictionary& pdic, + BitWriter* writer, size_t layer, + AuxOut* aux_out) { + JXL_ASSERT(pdic.HasAny()); + std::vector> tokens(1); + + auto add_num = [&](int context, size_t num) { + tokens[0].emplace_back(context, num); + }; + size_t num_ref_patch = 0; + for (size_t i = 0; i < pdic.positions_.size();) { + size_t i_start = i; + while (i < pdic.positions_.size() && + pdic.positions_[i].ref_pos == pdic.positions_[i_start].ref_pos) { + i++; + } + num_ref_patch++; + } + add_num(kNumRefPatchContext, num_ref_patch); + for (size_t i = 0; i < pdic.positions_.size();) { + size_t i_start = i; + while (i < pdic.positions_.size() && + pdic.positions_[i].ref_pos == pdic.positions_[i_start].ref_pos) { + i++; + } + size_t num = i - i_start; + JXL_ASSERT(num > 0); + add_num(kReferenceFrameContext, pdic.positions_[i_start].ref_pos.ref); + add_num(kPatchReferencePositionContext, + pdic.positions_[i_start].ref_pos.x0); + add_num(kPatchReferencePositionContext, + pdic.positions_[i_start].ref_pos.y0); + add_num(kPatchSizeContext, pdic.positions_[i_start].ref_pos.xsize - 1); + add_num(kPatchSizeContext, pdic.positions_[i_start].ref_pos.ysize - 1); + add_num(kPatchCountContext, num - 1); + for (size_t j = i_start; j < i; j++) { + const PatchPosition& pos = pdic.positions_[j]; + if (j == i_start) { + add_num(kPatchPositionContext, pos.x); + add_num(kPatchPositionContext, pos.y); + } else { + add_num(kPatchOffsetContext, + PackSigned(pos.x - pdic.positions_[j - 1].x)); + add_num(kPatchOffsetContext, + PackSigned(pos.y - pdic.positions_[j - 1].y)); + } + JXL_ASSERT(pdic.shared_->metadata->m.extra_channel_info.size() + 1 == + pos.blending.size()); + for (size_t i = 0; + i < pdic.shared_->metadata->m.extra_channel_info.size() + 1; i++) { + const PatchBlending& info = pos.blending[i]; + add_num(kPatchBlendModeContext, static_cast(info.mode)); + if (UsesAlpha(info.mode) && + pdic.shared_->metadata->m.extra_channel_info.size() > 1) { + add_num(kPatchAlphaChannelContext, info.alpha_channel); + } + if (UsesClamp(info.mode)) { + add_num(kPatchClampContext, info.clamp); + } + } + } + } + + EntropyEncodingData codes; + std::vector context_map; + BuildAndEncodeHistograms(HistogramParams(), kNumPatchDictionaryContexts, + tokens, &codes, &context_map, writer, layer, + aux_out); + WriteTokens(tokens[0], codes, context_map, writer, layer, aux_out); +} + +// static +void PatchDictionaryEncoder::SubtractFrom(const PatchDictionary& pdic, + Image3F* opsin) { + // TODO(veluca): this can likely be optimized knowing it runs on full images. + for (size_t y = 0; y < opsin->ysize(); y++) { + if (y + 1 >= pdic.patch_starts_.size()) continue; + float* JXL_RESTRICT rows[3] = { + opsin->PlaneRow(0, y), + opsin->PlaneRow(1, y), + opsin->PlaneRow(2, y), + }; + for (size_t id = pdic.patch_starts_[y]; id < pdic.patch_starts_[y + 1]; + id++) { + const PatchPosition& pos = pdic.positions_[pdic.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; + const float* JXL_RESTRICT ref_rows[3] = { + pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( + 0, pos.ref_pos.y0 + iy) + + pos.ref_pos.x0, + pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( + 1, pos.ref_pos.y0 + iy) + + pos.ref_pos.x0, + pdic.shared_->reference_frames[ref].frame->color()->ConstPlaneRow( + 2, pos.ref_pos.y0 + iy) + + pos.ref_pos.x0, + }; + for (size_t ix = 0; ix < xsize; ix++) { + for (size_t c = 0; c < 3; c++) { + if (pos.blending[0].mode == PatchBlendMode::kAdd) { + rows[c][bx + ix] -= ref_rows[c][ix]; + } else if (pos.blending[0].mode == PatchBlendMode::kReplace) { + rows[c][bx + ix] = 0; + } else if (pos.blending[0].mode == PatchBlendMode::kNone) { + // Nothing to do. + } else { + JXL_ABORT("Blending mode %u not yet implemented", + (uint32_t)pos.blending[0].mode); + } + } + } + } + } +} + +namespace { + +struct PatchColorspaceInfo { + float kChannelDequant[3]; + float kChannelWeights[3]; + + explicit PatchColorspaceInfo(bool is_xyb) { + if (is_xyb) { + kChannelDequant[0] = 0.01615; + kChannelDequant[1] = 0.08875; + kChannelDequant[2] = 0.1922; + kChannelWeights[0] = 30.0; + kChannelWeights[1] = 3.0; + kChannelWeights[2] = 1.0; + } else { + kChannelDequant[0] = 20.0f / 255; + kChannelDequant[1] = 22.0f / 255; + kChannelDequant[2] = 20.0f / 255; + kChannelWeights[0] = 0.017 * 255; + kChannelWeights[1] = 0.02 * 255; + kChannelWeights[2] = 0.017 * 255; + } + } + + float ScaleForQuantization(float val, size_t c) { + return val / kChannelDequant[c]; + } + + int Quantize(float val, size_t c) { + return truncf(ScaleForQuantization(val, c)); + } + + bool is_similar_v(const float v1[3], const float v2[3], float threshold) { + float distance = 0; + for (size_t c = 0; c < 3; c++) { + distance += std::fabs(v1[c] - v2[c]) * kChannelWeights[c]; + } + return distance <= threshold; + } +}; + +std::vector FindTextLikePatches( + const Image3F& opsin, const PassesEncoderState* JXL_RESTRICT state, + ThreadPool* pool, AuxOut* aux_out, bool is_xyb) { + if (state->cparams.patches == Override::kOff) return {}; + + PatchColorspaceInfo pci(is_xyb); + float kSimilarThreshold = 0.8f; + + auto is_similar_impl = [&pci](std::pair p1, + std::pair p2, + const float* JXL_RESTRICT rows[3], + size_t stride, float threshold) { + float v1[3], v2[3]; + for (size_t c = 0; c < 3; c++) { + v1[c] = rows[c][p1.second * stride + p1.first]; + v2[c] = rows[c][p2.second * stride + p2.first]; + } + return pci.is_similar_v(v1, v2, threshold); + }; + + std::atomic has_screenshot_areas{false}; + const size_t opsin_stride = opsin.PixelsPerRow(); + const float* JXL_RESTRICT opsin_rows[3] = {opsin.ConstPlaneRow(0, 0), + opsin.ConstPlaneRow(1, 0), + opsin.ConstPlaneRow(2, 0)}; + + auto is_same = [&opsin_rows, opsin_stride](std::pair p1, + std::pair p2) { + for (size_t c = 0; c < 3; c++) { + float v1 = opsin_rows[c][p1.second * opsin_stride + p1.first]; + float v2 = opsin_rows[c][p2.second * opsin_stride + p2.first]; + if (std::fabs(v1 - v2) > 1e-4) { + return false; + } + } + return true; + }; + + auto is_similar = [&](std::pair p1, + std::pair p2) { + return is_similar_impl(p1, p2, opsin_rows, opsin_stride, kSimilarThreshold); + }; + + constexpr int64_t kPatchSide = 4; + constexpr int64_t kExtraSide = 4; + + // Look for kPatchSide size squares, naturally aligned, that all have the same + // pixel values. + ImageB is_screenshot_like(DivCeil(opsin.xsize(), kPatchSide), + DivCeil(opsin.ysize(), kPatchSide)); + ZeroFillImage(&is_screenshot_like); + uint8_t* JXL_RESTRICT screenshot_row = is_screenshot_like.Row(0); + const size_t screenshot_stride = is_screenshot_like.PixelsPerRow(); + const auto process_row = [&](uint64_t y, int _) { + for (uint64_t x = 0; x < opsin.xsize() / kPatchSide; x++) { + bool all_same = true; + for (size_t iy = 0; iy < static_cast(kPatchSide); iy++) { + for (size_t ix = 0; ix < static_cast(kPatchSide); ix++) { + size_t cx = x * kPatchSide + ix; + size_t cy = y * kPatchSide + iy; + if (!is_same({cx, cy}, {x * kPatchSide, y * kPatchSide})) { + all_same = false; + break; + } + } + } + if (!all_same) continue; + size_t num = 0; + size_t num_same = 0; + for (int64_t iy = -kExtraSide; iy < kExtraSide + kPatchSide; iy++) { + for (int64_t ix = -kExtraSide; ix < kExtraSide + kPatchSide; ix++) { + int64_t cx = x * kPatchSide + ix; + int64_t cy = y * kPatchSide + iy; + if (cx < 0 || static_cast(cx) >= opsin.xsize() || // + cy < 0 || static_cast(cy) >= opsin.ysize()) { + continue; + } + num++; + if (is_same({cx, cy}, {x * kPatchSide, y * kPatchSide})) num_same++; + } + } + // Too few equal pixels nearby. + if (num_same * 8 < num * 7) continue; + screenshot_row[y * screenshot_stride + x] = 1; + has_screenshot_areas = true; + } + }; + RunOnPool(pool, 0, opsin.ysize() / kPatchSide, ThreadPool::SkipInit(), + process_row, "IsScreenshotLike"); + + // TODO(veluca): also parallelize the rest of this function. + if (WantDebugOutput(aux_out)) { + aux_out->DumpPlaneNormalized("screenshot_like", is_screenshot_like); + } + + constexpr int kSearchRadius = 1; + + if (!ApplyOverride(state->cparams.patches, has_screenshot_areas)) { + return {}; + } + + // Search for "similar enough" pixels near the screenshot-like areas. + ImageB is_background(opsin.xsize(), opsin.ysize()); + ZeroFillImage(&is_background); + Image3F background(opsin.xsize(), opsin.ysize()); + ZeroFillImage(&background); + constexpr size_t kDistanceLimit = 50; + float* JXL_RESTRICT background_rows[3] = { + background.PlaneRow(0, 0), + background.PlaneRow(1, 0), + background.PlaneRow(2, 0), + }; + const size_t background_stride = background.PixelsPerRow(); + uint8_t* JXL_RESTRICT is_background_row = is_background.Row(0); + const size_t is_background_stride = is_background.PixelsPerRow(); + std::vector< + std::pair, std::pair>> + queue; + size_t queue_front = 0; + for (size_t y = 0; y < opsin.ysize(); y++) { + for (size_t x = 0; x < opsin.xsize(); x++) { + if (!screenshot_row[screenshot_stride * (y / kPatchSide) + + (x / kPatchSide)]) + continue; + queue.push_back({{x, y}, {x, y}}); + } + } + while (queue.size() != queue_front) { + std::pair cur = queue[queue_front].first; + std::pair src = queue[queue_front].second; + queue_front++; + if (is_background_row[cur.second * is_background_stride + cur.first]) + continue; + is_background_row[cur.second * is_background_stride + cur.first] = 1; + for (size_t c = 0; c < 3; c++) { + background_rows[c][cur.second * background_stride + cur.first] = + opsin_rows[c][src.second * opsin_stride + src.first]; + } + for (int dx = -kSearchRadius; dx <= kSearchRadius; dx++) { + for (int dy = -kSearchRadius; dy <= kSearchRadius; dy++) { + if (dx == 0 && dy == 0) continue; + int next_first = cur.first + dx; + int next_second = cur.second + dy; + if (next_first < 0 || next_second < 0 || + static_cast(next_first) >= opsin.xsize() || + static_cast(next_second) >= opsin.ysize()) { + continue; + } + if (static_cast( + std::abs(next_first - static_cast(src.first)) + + std::abs(next_second - static_cast(src.second))) > + kDistanceLimit) { + continue; + } + std::pair next{next_first, next_second}; + if (is_similar(src, next)) { + if (!screenshot_row[next.second / kPatchSide * screenshot_stride + + next.first / kPatchSide] || + is_same(src, next)) { + if (!is_background_row[next.second * is_background_stride + + next.first]) + queue.emplace_back(next, src); + } + } + } + } + } + queue.clear(); + + ImageF ccs; + std::mt19937 rng; + std::uniform_real_distribution dist(0.5, 1.0); + bool paint_ccs = false; + if (WantDebugOutput(aux_out)) { + aux_out->DumpPlaneNormalized("is_background", is_background); + if (is_xyb) { + aux_out->DumpXybImage("background", background); + } else { + aux_out->DumpImage("background", background); + } + ccs = ImageF(opsin.xsize(), opsin.ysize()); + ZeroFillImage(&ccs); + paint_ccs = true; + } + + constexpr float kVerySimilarThreshold = 0.03f; + constexpr float kHasSimilarThreshold = 0.03f; + + const float* JXL_RESTRICT const_background_rows[3] = { + background_rows[0], background_rows[1], background_rows[2]}; + auto is_similar_b = [&](std::pair p1, std::pair p2) { + return is_similar_impl(p1, p2, const_background_rows, background_stride, + kVerySimilarThreshold); + }; + + constexpr int kMinPeak = 2; + constexpr int kHasSimilarRadius = 2; + + std::vector info; + + // Find small CC outside the "similar enough" areas, compute bounding boxes, + // and run heuristics to exclude some patches. + ImageB visited(opsin.xsize(), opsin.ysize()); + ZeroFillImage(&visited); + uint8_t* JXL_RESTRICT visited_row = visited.Row(0); + const size_t visited_stride = visited.PixelsPerRow(); + std::vector> cc; + std::vector> stack; + for (size_t y = 0; y < opsin.ysize(); y++) { + for (size_t x = 0; x < opsin.xsize(); x++) { + if (is_background_row[y * is_background_stride + x]) continue; + cc.clear(); + stack.clear(); + stack.emplace_back(x, y); + size_t min_x = x; + size_t max_x = x; + size_t min_y = y; + size_t max_y = y; + std::pair reference; + bool found_border = false; + bool all_similar = true; + while (!stack.empty()) { + std::pair cur = stack.back(); + stack.pop_back(); + if (visited_row[cur.second * visited_stride + cur.first]) continue; + visited_row[cur.second * visited_stride + cur.first] = 1; + if (cur.first < min_x) min_x = cur.first; + if (cur.first > max_x) max_x = cur.first; + if (cur.second < min_y) min_y = cur.second; + if (cur.second > max_y) max_y = cur.second; + if (paint_ccs) { + cc.push_back(cur); + } + for (int dx = -kSearchRadius; dx <= kSearchRadius; dx++) { + for (int dy = -kSearchRadius; dy <= kSearchRadius; dy++) { + if (dx == 0 && dy == 0) continue; + int next_first = static_cast(cur.first) + dx; + int next_second = static_cast(cur.second) + dy; + if (next_first < 0 || next_second < 0 || + static_cast(next_first) >= opsin.xsize() || + static_cast(next_second) >= opsin.ysize()) { + continue; + } + std::pair next{next_first, next_second}; + if (!is_background_row[next.second * is_background_stride + + next.first]) { + stack.push_back(next); + } else { + if (!found_border) { + reference = next; + found_border = true; + } else { + if (!is_similar_b(next, reference)) all_similar = false; + } + } + } + } + } + if (!found_border || !all_similar || max_x - min_x >= kMaxPatchSize || + max_y - min_y >= kMaxPatchSize) { + continue; + } + size_t bpos = background_stride * reference.second + reference.first; + float ref[3] = {background_rows[0][bpos], background_rows[1][bpos], + background_rows[2][bpos]}; + bool has_similar = false; + for (size_t iy = std::max( + static_cast(min_y) - kHasSimilarRadius, 0); + iy < std::min(max_y + kHasSimilarRadius + 1, opsin.ysize()); iy++) { + for (size_t ix = std::max( + static_cast(min_x) - kHasSimilarRadius, 0); + ix < std::min(max_x + kHasSimilarRadius + 1, opsin.xsize()); + ix++) { + size_t opos = opsin_stride * iy + ix; + float px[3] = {opsin_rows[0][opos], opsin_rows[1][opos], + opsin_rows[2][opos]}; + if (pci.is_similar_v(ref, px, kHasSimilarThreshold)) { + has_similar = true; + } + } + } + if (!has_similar) continue; + info.emplace_back(); + info.back().second.emplace_back(min_x, min_y); + QuantizedPatch& patch = info.back().first; + patch.xsize = max_x - min_x + 1; + patch.ysize = max_y - min_y + 1; + int max_value = 0; + for (size_t c : {1, 0, 2}) { + for (size_t iy = min_y; iy <= max_y; iy++) { + for (size_t ix = min_x; ix <= max_x; ix++) { + size_t offset = (iy - min_y) * patch.xsize + ix - min_x; + patch.fpixels[c][offset] = + opsin_rows[c][iy * opsin_stride + ix] - ref[c]; + int val = pci.Quantize(patch.fpixels[c][offset], c); + patch.pixels[c][offset] = val; + if (std::abs(val) > max_value) max_value = std::abs(val); + } + } + } + if (max_value < kMinPeak) { + info.pop_back(); + continue; + } + if (paint_ccs) { + float cc_color = dist(rng); + for (std::pair p : cc) { + ccs.Row(p.second)[p.first] = cc_color; + } + } + } + } + + if (paint_ccs) { + JXL_ASSERT(WantDebugOutput(aux_out)); + aux_out->DumpPlaneNormalized("ccs", ccs); + } + if (info.empty()) { + return {}; + } + + // Remove duplicates. + constexpr size_t kMinPatchOccurences = 2; + std::sort(info.begin(), info.end()); + size_t unique = 0; + for (size_t i = 1; i < info.size(); i++) { + if (info[i].first == info[unique].first) { + info[unique].second.insert(info[unique].second.end(), + info[i].second.begin(), info[i].second.end()); + } else { + if (info[unique].second.size() >= kMinPatchOccurences) { + unique++; + } + info[unique] = info[i]; + } + } + if (info[unique].second.size() >= kMinPatchOccurences) { + unique++; + } + info.resize(unique); + + size_t max_patch_size = 0; + + for (size_t i = 0; i < info.size(); i++) { + size_t pixels = info[i].first.xsize * info[i].first.ysize; + if (pixels > max_patch_size) max_patch_size = pixels; + } + + // don't use patches if all patches are smaller than this + constexpr size_t kMinMaxPatchSize = 20; + if (max_patch_size < kMinMaxPatchSize) return {}; + + // Ensure that the specified set of patches doesn't produce out-of-bounds + // pixels. + // TODO(veluca): figure out why this is still necessary even with RCTs that + // don't depend on bit depth. + if (state->cparams.modular_mode && state->cparams.quality_pair.first >= 100) { + constexpr size_t kMaxPatchArea = kMaxPatchSize * kMaxPatchSize; + std::vector min_then_max_px(2 * kMaxPatchArea); + for (size_t i = 0; i < info.size(); i++) { + for (size_t c = 0; c < 3; c++) { + float* JXL_RESTRICT min_px = min_then_max_px.data(); + float* JXL_RESTRICT max_px = min_px + kMaxPatchArea; + std::fill(min_px, min_px + kMaxPatchArea, 1); + std::fill(max_px, max_px + kMaxPatchArea, 0); + size_t xsize = info[i].first.xsize; + for (size_t j = 0; j < info[i].second.size(); j++) { + size_t bx = info[i].second[j].first; + size_t by = info[i].second[j].second; + for (size_t iy = 0; iy < info[i].first.ysize; iy++) { + for (size_t ix = 0; ix < xsize; ix++) { + float v = opsin_rows[c][(by + iy) * opsin_stride + bx + ix]; + if (v < min_px[iy * xsize + ix]) min_px[iy * xsize + ix] = v; + if (v > max_px[iy * xsize + ix]) max_px[iy * xsize + ix] = v; + } + } + } + for (size_t iy = 0; iy < info[i].first.ysize; iy++) { + for (size_t ix = 0; ix < xsize; ix++) { + float smallest = min_px[iy * xsize + ix]; + float biggest = max_px[iy * xsize + ix]; + JXL_ASSERT(smallest <= biggest); + float& out = info[i].first.fpixels[c][iy * xsize + ix]; + // Clamp fpixels so that subtracting the patch never creates a + // negative value, or a value above 1. + JXL_ASSERT(biggest - 1 <= smallest); + out = std::max(smallest, out); + out = std::min(biggest - 1.f, out); + } + } + } + } + } + return info; +} + +} // namespace + +void FindBestPatchDictionary(const Image3F& opsin, + PassesEncoderState* JXL_RESTRICT state, + ThreadPool* pool, AuxOut* aux_out, bool is_xyb) { + std::vector info = + FindTextLikePatches(opsin, state, pool, aux_out, is_xyb); + + // TODO(veluca): this doesn't work if both dots and patches are enabled. + // For now, since dots and patches are not likely to occur in the same kind of + // images, disable dots if some patches were found. + if (info.empty() && + ApplyOverride( + state->cparams.dots, + state->cparams.speed_tier <= SpeedTier::kSquirrel && + state->cparams.butteraugli_distance >= kMinButteraugliForDots)) { + info = FindDotDictionary(state->cparams, opsin, state->shared.cmap, pool); + } + + if (info.empty()) return; + + std::sort( + info.begin(), info.end(), [&](const PatchInfo& a, const PatchInfo& b) { + return a.first.xsize * a.first.ysize > b.first.xsize * b.first.ysize; + }); + + size_t max_x_size = 0; + size_t max_y_size = 0; + size_t total_pixels = 0; + + for (size_t i = 0; i < info.size(); i++) { + size_t pixels = info[i].first.xsize * info[i].first.ysize; + if (max_x_size < info[i].first.xsize) max_x_size = info[i].first.xsize; + if (max_y_size < info[i].first.ysize) max_y_size = info[i].first.ysize; + total_pixels += pixels; + } + + // Bin-packing & conversion of patches. + constexpr float kBinPackingSlackness = 1.05f; + size_t ref_xsize = std::max(max_x_size, std::sqrt(total_pixels)); + size_t ref_ysize = std::max(max_y_size, std::sqrt(total_pixels)); + std::vector> ref_positions(info.size()); + // TODO(veluca): allow partial overlaps of patches that have the same pixels. + size_t max_y = 0; + do { + max_y = 0; + // Increase packed image size. + ref_xsize = ref_xsize * kBinPackingSlackness + 1; + ref_ysize = ref_ysize * kBinPackingSlackness + 1; + + ImageB occupied(ref_xsize, ref_ysize); + ZeroFillImage(&occupied); + uint8_t* JXL_RESTRICT occupied_rows = occupied.Row(0); + size_t occupied_stride = occupied.PixelsPerRow(); + + bool success = true; + // For every patch... + for (size_t patch = 0; patch < info.size(); patch++) { + size_t x0 = 0; + size_t y0 = 0; + size_t xsize = info[patch].first.xsize; + size_t ysize = info[patch].first.ysize; + bool found = false; + // For every possible start position ... + for (; y0 + ysize <= ref_ysize; y0++) { + x0 = 0; + for (; x0 + xsize <= ref_xsize; x0++) { + bool has_occupied_pixel = false; + size_t x = x0; + // Check if it is possible to place the patch in this position in the + // reference frame. + for (size_t y = y0; y < y0 + ysize; y++) { + x = x0; + for (; x < x0 + xsize; x++) { + if (occupied_rows[y * occupied_stride + x]) { + has_occupied_pixel = true; + break; + } + } + } // end of positioning check + if (!has_occupied_pixel) { + found = true; + break; + } + x0 = x; // Jump to next pixel after the occupied one. + } + if (found) break; + } // end of start position checking + + // We didn't find a possible position: repeat from the beginning with a + // larger reference frame size. + if (!found) { + success = false; + break; + } + + // We found a position: mark the corresponding positions in the reference + // image as used. + ref_positions[patch] = {x0, y0}; + for (size_t y = y0; y < y0 + ysize; y++) { + for (size_t x = x0; x < x0 + xsize; x++) { + occupied_rows[y * occupied_stride + x] = true; + } + } + max_y = std::max(max_y, y0 + ysize); + } + + if (success) break; + } while (true); + + JXL_ASSERT(ref_ysize >= max_y); + + ref_ysize = max_y; + + Image3F reference_frame(ref_xsize, ref_ysize); + // TODO(veluca): figure out a better way to fill the image. + ZeroFillImage(&reference_frame); + std::vector positions; + float* JXL_RESTRICT ref_rows[3] = { + reference_frame.PlaneRow(0, 0), + reference_frame.PlaneRow(1, 0), + reference_frame.PlaneRow(2, 0), + }; + size_t ref_stride = reference_frame.PixelsPerRow(); + + for (size_t i = 0; i < info.size(); i++) { + PatchReferencePosition ref_pos; + ref_pos.xsize = info[i].first.xsize; + ref_pos.ysize = info[i].first.ysize; + ref_pos.x0 = ref_positions[i].first; + ref_pos.y0 = ref_positions[i].second; + ref_pos.ref = 0; + for (size_t y = 0; y < ref_pos.ysize; y++) { + for (size_t x = 0; x < ref_pos.xsize; x++) { + for (size_t c = 0; c < 3; c++) { + ref_rows[c][(y + ref_pos.y0) * ref_stride + x + ref_pos.x0] = + info[i].first.fpixels[c][y * ref_pos.xsize + x]; + } + } + } + // Add color channels, ignore other channels. + std::vector blending_info( + state->shared.metadata->m.extra_channel_info.size() + 1, + PatchBlending{PatchBlendMode::kNone, 0, false}); + blending_info[0].mode = PatchBlendMode::kAdd; + for (const auto& pos : info[i].second) { + positions.emplace_back( + PatchPosition{pos.first, pos.second, blending_info, ref_pos}); + } + } + + 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, pool, true); + + // TODO(veluca): this assumes that applying patches is commutative, which is + // not true for all blending modes. This code only produces kAdd patches, so + // this works out. + std::sort(positions.begin(), positions.end()); + PatchDictionaryEncoder::SetPositions(&state->shared.image_features.patches, + std::move(positions)); +} + +void RoundtripPatchFrame(Image3F* reference_frame, + PassesEncoderState* JXL_RESTRICT state, int idx, + CompressParams& cparams, ThreadPool* pool, + bool subtract) { + FrameInfo patch_frame_info; + cparams.resampling = 1; + cparams.ec_resampling = 1; + cparams.dots = Override::kOff; + cparams.noise = Override::kOff; + cparams.modular_mode = true; + cparams.responsive = 0; + cparams.progressive_dc = 0; + cparams.progressive_mode = false; + cparams.qprogressive_mode = false; + // Use gradient predictor and not Predictor::Best. + cparams.options.predictor = Predictor::Gradient; + patch_frame_info.save_as_reference = idx; // always saved. + patch_frame_info.frame_type = FrameType::kReferenceOnly; + patch_frame_info.save_before_color_transform = true; + ImageBundle ib(&state->shared.metadata->m); + // TODO(veluca): metadata.color_encoding is a lie: ib is in XYB, but there is + // no simple way to express that yet. + patch_frame_info.ib_needs_color_transform = false; + ib.SetFromImage(std::move(*reference_frame), + state->shared.metadata->m.color_encoding); + if (!ib.metadata()->extra_channel_info.empty()) { + // Add dummy extra channels to the patch image: patch encoding does not yet + // support extra channels, but the codec expects that the amount of extra + // channels in frames matches that in the metadata of the codestream. + std::vector extra_channels; + extra_channels.reserve(ib.metadata()->extra_channel_info.size()); + for (size_t i = 0; i < ib.metadata()->extra_channel_info.size(); i++) { + extra_channels.emplace_back(ib.xsize(), ib.ysize()); + // Must initialize the image with data to not affect blending with + // uninitialized memory. + // TODO(lode): patches must copy and use the real extra channels instead. + ZeroFillImage(&extra_channels.back()); + } + ib.SetExtraChannels(std::move(extra_channels)); + } + PassesEncoderState roundtrip_state; + auto special_frame = std::unique_ptr(new BitWriter()); + JXL_CHECK(EncodeFrame(cparams, patch_frame_info, state->shared.metadata, ib, + &roundtrip_state, pool, special_frame.get(), nullptr)); + const Span encoded = special_frame->GetSpan(); + state->special_frames.emplace_back(std::move(special_frame)); + if (subtract) { + BitReader br(encoded); + ImageBundle decoded(&state->shared.metadata->m); + PassesDecoderState dec_state; + JXL_CHECK(dec_state.output_encoding_info.Set( + *state->shared.metadata, + ColorEncoding::LinearSRGB( + state->shared.metadata->m.color_encoding.IsGray()))); + JXL_CHECK(DecodeFrame({}, &dec_state, pool, &br, &decoded, + *state->shared.metadata, /*constraints=*/nullptr)); + // if the frame itself uses patches, we need to decode another frame + if (!dec_state.shared_storage.reference_frames[idx] + .storage.color() + ->xsize()) + JXL_CHECK(DecodeFrame({}, &dec_state, pool, &br, &decoded, + *state->shared.metadata, /*constraints=*/nullptr)); + JXL_CHECK(br.Close()); + state->shared.reference_frames[idx] = + std::move(dec_state.shared_storage.reference_frames[idx]); + } else { + state->shared.reference_frames[idx].storage = std::move(ib); + } + state->shared.reference_frames[idx].frame = + &state->shared.reference_frames[idx].storage; +} + +} // namespace jxl diff --git a/lib/jxl/enc_patch_dictionary.h b/lib/jxl/enc_patch_dictionary.h new file mode 100644 index 0000000..1a51d4e --- /dev/null +++ b/lib/jxl/enc_patch_dictionary.h @@ -0,0 +1,66 @@ +// 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_ENC_PATCH_DICTIONARY_H_ +#define LIB_JXL_ENC_PATCH_DICTIONARY_H_ + +// Chooses reference patches, and avoids encoding them once per occurrence. + +#include +#include +#include + +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/opsin_params.h" + +namespace jxl { + +// Friend class of PatchDictionary. +class PatchDictionaryEncoder { + public: + // Only call if HasAny(). + static void Encode(const PatchDictionary& pdic, BitWriter* writer, + size_t layer, AuxOut* aux_out); + + static void SetPositions(PatchDictionary* pdic, + std::vector positions) { + if (pdic->positions_.empty()) { + pdic->positions_ = std::move(positions); + } else { + pdic->positions_.insert(pdic->positions_.end(), positions.begin(), + positions.end()); + } + pdic->ComputePatchCache(); + } + + static void SubtractFrom(const PatchDictionary& pdic, Image3F* opsin); +}; + +void FindBestPatchDictionary(const Image3F& opsin, + PassesEncoderState* JXL_RESTRICT state, + ThreadPool* pool, AuxOut* aux_out, + bool is_xyb = true); + +void RoundtripPatchFrame(Image3F* reference_frame, + PassesEncoderState* JXL_RESTRICT state, int idx, + CompressParams& cparams, ThreadPool* pool, + bool subtract); + +} // namespace jxl + +#endif // LIB_JXL_ENC_PATCH_DICTIONARY_H_ diff --git a/lib/jxl/enc_photon_noise.cc b/lib/jxl/enc_photon_noise.cc new file mode 100644 index 0000000..3786ef5 --- /dev/null +++ b/lib/jxl/enc_photon_noise.cc @@ -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/enc_photon_noise.h" + +namespace jxl { + +namespace { + +// Assumes a daylight-like spectrum. +// https://www.strollswithmydog.com/effective-quantum-efficiency-of-sensor/#:~:text=11%2C260%20photons/um%5E2/lx-s +constexpr float kPhotonsPerLxSPerUm2 = 11260; + +// Order of magnitude for cameras in the 2010-2020 decade, taking the CFA into +// account. +constexpr float kEffectiveQuantumEfficiency = 0.20; + +// TODO(sboukortt): reevaluate whether these are good defaults, notably whether +// it would be worth making read noise higher at lower ISO settings. +constexpr float kPhotoResponseNonUniformity = 0.005; +constexpr float kInputReferredReadNoise = 3; + +// Assumes a 35mm sensor. +constexpr float kSensorAreaUm2 = 36000.f * 24000; + +template +inline constexpr T Square(const T x) { + return x * x; +} +template +inline constexpr T Cube(const T x) { + return x * x * x; +} + +} // namespace + +NoiseParams SimulatePhotonNoise(const size_t xsize, const size_t ysize, + const float iso) { + const float kOpsinAbsorbanceBiasCbrt = std::cbrt(kOpsinAbsorbanceBias[1]); + + // Focal plane exposure for 18% of kDefaultIntensityTarget, in lx·s. + // (ISO = 10 lx·s ÷ H) + const float h_18 = 10 / iso; + + const float pixel_area_um2 = kSensorAreaUm2 / (xsize * ysize); + + const float electrons_per_pixel_18 = kEffectiveQuantumEfficiency * + kPhotonsPerLxSPerUm2 * h_18 * + pixel_area_um2; + + NoiseParams params; + + for (size_t i = 0; i < NoiseParams::kNumNoisePoints; ++i) { + const float scaled_index = i / (NoiseParams::kNumNoisePoints - 2.f); + // scaled_index is used for XYB = (0, 2·scaled_index, 2·scaled_index) + const float y = 2 * scaled_index; + // 1 = default intensity target + const float linear = std::max( + 0.f, Cube(y - kOpsinAbsorbanceBiasCbrt) + kOpsinAbsorbanceBias[1]); + const float electrons_per_pixel = electrons_per_pixel_18 * (linear / 0.18f); + // Quadrature sum of read noise, photon shot noise (sqrt(S) so simply not + // squared here) and photo response non-uniformity. + // https://doi.org/10.1117/3.725073 + // Units are electrons rms. + const float noise = + std::sqrt(Square(kInputReferredReadNoise) + electrons_per_pixel + + Square(kPhotoResponseNonUniformity * electrons_per_pixel)); + const float linear_noise = noise * (0.18f / electrons_per_pixel_18); + const float opsin_derivative = + (1.f / 3) / Square(std::cbrt(linear - kOpsinAbsorbanceBias[1])); + const float opsin_noise = linear_noise * opsin_derivative; + + // TODO(sboukortt): verify more thoroughly whether the denominator is + // correct. + params.lut[i] = + Clamp1(opsin_noise / + (0.22f // norm_const + * std::sqrt(2.f) // red_noise + green_noise + * 1.13f // standard deviation of a plane of generated noise + ), + 0.f, 1.f); + } + + return params; +} + +} // namespace jxl diff --git a/lib/jxl/enc_photon_noise.h b/lib/jxl/enc_photon_noise.h new file mode 100644 index 0000000..f43e14d --- /dev/null +++ b/lib/jxl/enc_photon_noise.h @@ -0,0 +1,22 @@ +// 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_ENC_PHOTON_NOISE_H_ +#define LIB_JXL_ENC_PHOTON_NOISE_H_ + +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/noise.h" + +namespace jxl { + +// Constructs a NoiseParams representing the noise that would be seen at the +// selected nominal exposure on a last-decade (as of 2021) color camera with a +// 36×24mm sensor (“35mm format”). +NoiseParams SimulatePhotonNoise(size_t xsize, size_t ysize, float iso); + +} // namespace jxl + +#endif // LIB_JXL_ENC_PHOTON_NOISE_H_ diff --git a/lib/jxl/enc_photon_noise_test.cc b/lib/jxl/enc_photon_noise_test.cc new file mode 100644 index 0000000..3790fde --- /dev/null +++ b/lib/jxl/enc_photon_noise_test.cc @@ -0,0 +1,50 @@ +// 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/enc_photon_noise.h" + +#include "gmock/gmock.h" + +namespace jxl { +namespace { + +using ::testing::FloatNear; +using ::testing::Pointwise; + +MATCHER(AreApproximatelyEqual, "") { + constexpr float kTolerance = 1e-6; + const float actual = std::get<0>(arg); + const float expected = std::get<1>(arg); + return testing::ExplainMatchResult(FloatNear(expected, kTolerance), actual, + result_listener); +} + +TEST(EncPhotonNoiseTest, LUTs) { + EXPECT_THAT( + SimulatePhotonNoise(/*xsize=*/6000, /*ysize=*/4000, /*iso=*/100).lut, + Pointwise(AreApproximatelyEqual(), + {0.00259652, 0.0139648, 0.00681551, 0.00632582, 0.00694917, + 0.00803922, 0.00934574, 0.0107607})); + EXPECT_THAT( + SimulatePhotonNoise(/*xsize=*/6000, /*ysize=*/4000, /*iso=*/800).lut, + Pointwise(AreApproximatelyEqual(), + {0.02077220, 0.0420923, 0.01820690, 0.01439020, 0.01293670, + 0.01254030, 0.01277390, 0.0134161})); + EXPECT_THAT( + SimulatePhotonNoise(/*xsize=*/6000, /*ysize=*/4000, /*iso=*/6400).lut, + Pointwise(AreApproximatelyEqual(), + {0.1661770, 0.1691120, 0.05309080, 0.03963960, 0.03357410, + 0.03001650, 0.02776740, 0.0263478})); + + // Lower when measured on a per-pixel basis as there are fewer of them. + EXPECT_THAT( + SimulatePhotonNoise(/*xsize=*/4000, /*ysize=*/3000, /*iso=*/6400).lut, + Pointwise(AreApproximatelyEqual(), + {0.0830886, 0.1008720, 0.0367748, 0.0280305, 0.0240236, + 0.0218040, 0.0205771, 0.0200058})); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/enc_quant_weights.cc b/lib/jxl/enc_quant_weights.cc new file mode 100644 index 0000000..33d0e47 --- /dev/null +++ b/lib/jxl/enc_quant_weights.cc @@ -0,0 +1,203 @@ +// 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/enc_quant_weights.h" + +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +namespace { + +Status EncodeDctParams(const DctQuantWeightParams& params, BitWriter* writer) { + JXL_ASSERT(params.num_distance_bands >= 1); + writer->Write(DctQuantWeightParams::kLog2MaxDistanceBands, + params.num_distance_bands - 1); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < params.num_distance_bands; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Write( + params.distance_bands[c][i] * (i == 0 ? (1 / 64.0f) : 1.0f), writer)); + } + } + return true; +} + +Status EncodeQuant(const QuantEncoding& encoding, size_t idx, size_t size_x, + size_t size_y, BitWriter* writer, + ModularFrameEncoder* modular_frame_encoder) { + writer->Write(kLog2NumQuantModes, encoding.mode); + size_x *= kBlockDim; + size_y *= kBlockDim; + switch (encoding.mode) { + case QuantEncoding::kQuantModeLibrary: { + writer->Write(kCeilLog2NumPredefinedTables, encoding.predefined); + break; + } + case QuantEncoding::kQuantModeID: { + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 3; i++) { + JXL_RETURN_IF_ERROR( + F16Coder::Write(encoding.idweights[c][i] * (1.0f / 64), writer)); + } + } + break; + } + case QuantEncoding::kQuantModeDCT2: { + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 6; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Write( + encoding.dct2weights[c][i] * (1.0f / 64), writer)); + } + } + break; + } + case QuantEncoding::kQuantModeDCT4X8: { + for (size_t c = 0; c < 3; c++) { + JXL_RETURN_IF_ERROR( + F16Coder::Write(encoding.dct4x8multipliers[c], writer)); + } + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); + break; + } + case QuantEncoding::kQuantModeDCT4: { + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 2; i++) { + JXL_RETURN_IF_ERROR( + F16Coder::Write(encoding.dct4multipliers[c][i], writer)); + } + } + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); + break; + } + case QuantEncoding::kQuantModeDCT: { + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); + break; + } + case QuantEncoding::kQuantModeRAW: { + ModularFrameEncoder::EncodeQuantTable(size_x, size_y, writer, encoding, + idx, modular_frame_encoder); + break; + } + case QuantEncoding::kQuantModeAFV: { + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 9; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Write( + encoding.afv_weights[c][i] * (i < 6 ? 1.0f / 64 : 1.0f), writer)); + } + JXL_RETURN_IF_ERROR(EncodeDctParams(encoding.dct_params, writer)); + JXL_RETURN_IF_ERROR( + EncodeDctParams(encoding.dct_params_afv_4x4, writer)); + } + break; + } + } + return true; +} + +} // namespace + +Status DequantMatricesEncode(const DequantMatrices* matrices, BitWriter* writer, + size_t layer, AuxOut* aux_out, + ModularFrameEncoder* modular_frame_encoder) { + bool all_default = true; + const std::vector& encodings = matrices->encodings(); + + for (size_t i = 0; i < encodings.size(); i++) { + if (encodings[i].mode != QuantEncoding::kQuantModeLibrary || + encodings[i].predefined != 0) { + all_default = false; + } + } + // TODO(janwas): better bound + BitWriter::Allotment allotment(writer, 512 * 1024); + writer->Write(1, all_default); + if (!all_default) { + for (size_t i = 0; i < encodings.size(); i++) { + JXL_RETURN_IF_ERROR(EncodeQuant( + encodings[i], i, DequantMatrices::required_size_x[i], + DequantMatrices::required_size_y[i], writer, modular_frame_encoder)); + } + } + ReclaimAndCharge(writer, &allotment, layer, aux_out); + return true; +} + +Status DequantMatricesEncodeDC(const DequantMatrices* matrices, + BitWriter* writer, size_t layer, + AuxOut* aux_out) { + bool all_default = true; + const float* dc_quant = matrices->DCQuants(); + for (size_t c = 0; c < 3; c++) { + if (dc_quant[c] != kDCQuant[c]) { + all_default = false; + } + } + BitWriter::Allotment allotment(writer, 1 + sizeof(float) * kBitsPerByte * 3); + writer->Write(1, all_default); + if (!all_default) { + for (size_t c = 0; c < 3; c++) { + JXL_RETURN_IF_ERROR(F16Coder::Write(dc_quant[c] * 128.0f, writer)); + } + } + ReclaimAndCharge(writer, &allotment, layer, aux_out); + return true; +} + +void DequantMatricesSetCustomDC(DequantMatrices* matrices, const float* dc) { + matrices->SetDCQuant(dc); + // Roundtrip encode/decode DC to ensure same values as decoder. + BitWriter writer; + JXL_CHECK(DequantMatricesEncodeDC(matrices, &writer, 0, nullptr)); + writer.ZeroPadToByte(); + BitReader br(writer.GetSpan()); + // Called only in the encoder: should fail only for programmer errors. + JXL_CHECK(matrices->DecodeDC(&br)); + JXL_CHECK(br.Close()); +} + +void DequantMatricesSetCustom(DequantMatrices* matrices, + const std::vector& encodings, + ModularFrameEncoder* encoder) { + JXL_ASSERT(encodings.size() == DequantMatrices::kNum); + matrices->SetEncodings(encodings); + for (size_t i = 0; i < encodings.size(); i++) { + if (encodings[i].mode == QuantEncodingInternal::kQuantModeRAW) { + encoder->AddQuantTable(DequantMatrices::required_size_x[i] * kBlockDim, + DequantMatrices::required_size_y[i] * kBlockDim, + encodings[i], i); + } + } + // Roundtrip encode/decode the matrices to ensure same values as decoder. + // Do not pass modular en/decoder, as they only change entropy and not + // values. + BitWriter writer; + JXL_CHECK(DequantMatricesEncode(matrices, &writer, 0, nullptr)); + writer.ZeroPadToByte(); + BitReader br(writer.GetSpan()); + // Called only in the encoder: should fail only for programmer errors. + JXL_CHECK(matrices->Decode(&br)); + JXL_CHECK(br.Close()); +} + +} // namespace jxl diff --git a/lib/jxl/enc_quant_weights.h b/lib/jxl/enc_quant_weights.h new file mode 100644 index 0000000..89033d8 --- /dev/null +++ b/lib/jxl/enc_quant_weights.h @@ -0,0 +1,29 @@ +// 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_ENC_QUANT_WEIGHTS_H_ +#define LIB_JXL_ENC_QUANT_WEIGHTS_H_ + +#include "lib/jxl/quant_weights.h" + +namespace jxl { + +Status DequantMatricesEncode( + const DequantMatrices* matrices, BitWriter* writer, size_t layer, + AuxOut* aux_out, ModularFrameEncoder* modular_frame_encoder = nullptr); +Status DequantMatricesEncodeDC(const DequantMatrices* matrices, + BitWriter* writer, size_t layer, + AuxOut* aux_out); +// For consistency with QuantEncoding, higher values correspond to more +// precision. +void DequantMatricesSetCustomDC(DequantMatrices* matrices, const float* dc); + +void DequantMatricesSetCustom(DequantMatrices* matrices, + const std::vector& encodings, + ModularFrameEncoder* encoder); + +} // namespace jxl + +#endif // LIB_JXL_ENC_QUANT_WEIGHTS_H_ diff --git a/lib/jxl/enc_splines.cc b/lib/jxl/enc_splines.cc new file mode 100644 index 0000000..cdb797d --- /dev/null +++ b/lib/jxl/enc_splines.cc @@ -0,0 +1,96 @@ +// 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 + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/splines.h" + +namespace jxl { + +class QuantizedSplineEncoder { + public: + // Only call if HasAny(). + static void Tokenize(const QuantizedSpline& spline, + std::vector* const tokens) { + tokens->emplace_back(kNumControlPointsContext, + spline.control_points_.size()); + for (const auto& point : spline.control_points_) { + tokens->emplace_back(kControlPointsContext, PackSigned(point.first)); + tokens->emplace_back(kControlPointsContext, PackSigned(point.second)); + } + const auto encode_dct = [tokens](const int dct[32]) { + for (int i = 0; i < 32; ++i) { + tokens->emplace_back(kDCTContext, PackSigned(dct[i])); + } + }; + for (int c = 0; c < 3; ++c) { + encode_dct(spline.color_dct_[c]); + } + encode_dct(spline.sigma_dct_); + } +}; + +namespace { + +void EncodeAllStartingPoints(const std::vector& points, + std::vector* tokens) { + int64_t last_x = 0; + int64_t last_y = 0; + for (size_t i = 0; i < points.size(); i++) { + const int64_t x = lroundf(points[i].x); + const int64_t y = lroundf(points[i].y); + if (i == 0) { + tokens->emplace_back(kStartingPositionContext, x); + tokens->emplace_back(kStartingPositionContext, y); + } else { + tokens->emplace_back(kStartingPositionContext, PackSigned(x - last_x)); + tokens->emplace_back(kStartingPositionContext, PackSigned(y - last_y)); + } + last_x = x; + last_y = y; + } +} + +} // namespace + +void EncodeSplines(const Splines& splines, BitWriter* writer, + const size_t layer, const HistogramParams& histogram_params, + AuxOut* aux_out) { + JXL_ASSERT(splines.HasAny()); + + const std::vector& quantized_splines = + splines.QuantizedSplines(); + std::vector> tokens(1); + tokens[0].emplace_back(kNumSplinesContext, quantized_splines.size() - 1); + EncodeAllStartingPoints(splines.StartingPoints(), &tokens[0]); + + tokens[0].emplace_back(kQuantizationAdjustmentContext, + PackSigned(splines.GetQuantizationAdjustment())); + + for (const QuantizedSpline& spline : quantized_splines) { + QuantizedSplineEncoder::Tokenize(spline, &tokens[0]); + } + + EntropyEncodingData codes; + std::vector context_map; + BuildAndEncodeHistograms(histogram_params, kNumSplineContexts, tokens, &codes, + &context_map, writer, layer, aux_out); + WriteTokens(tokens[0], codes, context_map, writer, layer, aux_out); +} + +Splines FindSplines(const Image3F& opsin) { + // TODO: implement spline detection. + return {}; +} + +} // namespace jxl diff --git a/lib/jxl/enc_splines.h b/lib/jxl/enc_splines.h new file mode 100644 index 0000000..732d77a --- /dev/null +++ b/lib/jxl/enc_splines.h @@ -0,0 +1,39 @@ +// 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_ENC_SPLINES_H_ +#define LIB_JXL_ENC_SPLINES_H_ + +#include +#include + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/image.h" +#include "lib/jxl/splines.h" + +namespace jxl { + +// Only call if splines.HasAny(). +void EncodeSplines(const Splines& splines, BitWriter* writer, + const size_t layer, const HistogramParams& histogram_params, + AuxOut* aux_out); + +Splines FindSplines(const Image3F& opsin); + +} // namespace jxl + +#endif // LIB_JXL_ENC_SPLINES_H_ diff --git a/lib/jxl/enc_toc.cc b/lib/jxl/enc_toc.cc new file mode 100644 index 0000000..c877b0c --- /dev/null +++ b/lib/jxl/enc_toc.cc @@ -0,0 +1,46 @@ +// 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/enc_toc.h" + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_coeff_order.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/toc.h" + +namespace jxl { +Status WriteGroupOffsets(const std::vector& group_codes, + const std::vector* permutation, + BitWriter* JXL_RESTRICT writer, AuxOut* aux_out) { + BitWriter::Allotment allotment(writer, MaxBits(group_codes.size())); + if (permutation && !group_codes.empty()) { + // Don't write a permutation at all for an empty group_codes. + writer->Write(1, 1); // permutation + JXL_DASSERT(permutation->size() == group_codes.size()); + EncodePermutation(permutation->data(), /*skip=*/0, permutation->size(), + writer, /* layer= */ 0, aux_out); + + } else { + writer->Write(1, 0); // no permutation + } + writer->ZeroPadToByte(); // before TOC entries + + for (size_t i = 0; i < group_codes.size(); i++) { + JXL_ASSERT(group_codes[i].BitsWritten() % kBitsPerByte == 0); + const size_t group_size = group_codes[i].BitsWritten() / kBitsPerByte; + JXL_RETURN_IF_ERROR(U32Coder::Write(kTocDist, group_size, writer)); + } + writer->ZeroPadToByte(); // before first group + ReclaimAndCharge(writer, &allotment, kLayerTOC, aux_out); + return true; +} + +} // namespace jxl diff --git a/lib/jxl/enc_toc.h b/lib/jxl/enc_toc.h new file mode 100644 index 0000000..dc81a5d --- /dev/null +++ b/lib/jxl/enc_toc.h @@ -0,0 +1,29 @@ +// 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_ENC_TOC_H_ +#define LIB_JXL_ENC_TOC_H_ + +#include +#include + +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +// Writes the group offsets. If the permutation vector is nullptr, the identity +// permutation will be used. +Status WriteGroupOffsets(const std::vector& group_codes, + const std::vector* permutation, + BitWriter* JXL_RESTRICT writer, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_ENC_TOC_H_ diff --git a/lib/jxl/enc_transforms-inl.h b/lib/jxl/enc_transforms-inl.h new file mode 100644 index 0000000..c2f8e61 --- /dev/null +++ b/lib/jxl/enc_transforms-inl.h @@ -0,0 +1,844 @@ +// 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. + +#if defined(LIB_JXL_ENC_TRANSFORMS_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_ENC_TRANSFORMS_INL_H_ +#undef LIB_JXL_ENC_TRANSFORMS_INL_H_ +#else +#define LIB_JXL_ENC_TRANSFORMS_INL_H_ +#endif + +#include + +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/dct-inl.h" +#include "lib/jxl/dct_scales.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +template +struct DoIDCT { + template + void operator()(float* JXL_RESTRICT from, const To& to, + float* JXL_RESTRICT scratch_space) { + ComputeScaledIDCT()(from, to, scratch_space); + } +}; + +template +struct DoIDCT { + template + void operator()(float* JXL_RESTRICT from, const To& to, + float* JXL_RESTRICT scratch_space) const { + ComputeTransposedScaledIDCT()(from, to, scratch_space); + } +}; + +// Inverse of ReinterpretingDCT. +template +HWY_INLINE void ReinterpretingIDCT(const float* input, + const size_t input_stride, float* output, + const size_t output_stride) { + HWY_ALIGN float block[ROWS * COLS] = {}; + if (ROWS < COLS) { + for (size_t y = 0; y < LF_ROWS; y++) { + for (size_t x = 0; x < LF_COLS; x++) { + block[y * COLS + x] = input[y * input_stride + x] * + DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } else { + for (size_t y = 0; y < LF_COLS; y++) { + for (size_t x = 0; x < LF_ROWS; x++) { + block[y * ROWS + x] = input[y * input_stride + x] * + DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } + + // ROWS, COLS <= 8, so we can put scratch space on the stack. + HWY_ALIGN float scratch_space[ROWS * COLS]; + DoIDCT()(block, DCTTo(output, output_stride), scratch_space); +} + +template +void DCT2TopBlock(const float* block, size_t stride, float* out) { + static_assert(kBlockDim % S == 0, "S should be a divisor of kBlockDim"); + static_assert(S % 2 == 0, "S should be even"); + float temp[kDCTBlockSize]; + constexpr size_t num_2x2 = S / 2; + for (size_t y = 0; y < num_2x2; y++) { + for (size_t x = 0; x < num_2x2; x++) { + float c00 = block[y * 2 * stride + x * 2]; + float c01 = block[y * 2 * stride + x * 2 + 1]; + float c10 = block[(y * 2 + 1) * stride + x * 2]; + float c11 = block[(y * 2 + 1) * stride + x * 2 + 1]; + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + r00 *= 0.25f; + r01 *= 0.25f; + r10 *= 0.25f; + r11 *= 0.25f; + temp[y * kBlockDim + x] = r00; + temp[y * kBlockDim + num_2x2 + x] = r01; + temp[(y + num_2x2) * kBlockDim + x] = r10; + temp[(y + num_2x2) * kBlockDim + num_2x2 + x] = r11; + } + } + for (size_t y = 0; y < S; y++) { + for (size_t x = 0; x < S; x++) { + out[y * kBlockDim + x] = temp[y * kBlockDim + x]; + } + } +} + +void AFVDCT4x4(const float* JXL_RESTRICT pixels, float* JXL_RESTRICT coeffs) { + HWY_ALIGN static constexpr float k4x4AFVBasisTranspose[16][16] = { + { + 0.2500000000000000, + 0.8769029297991420f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + -0.4105377591765233f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + 0.2206518106944235f, + 0.0000000000000000, + 0.0000000000000000, + -0.7071067811865474f, + 0.6235485373547691f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + -0.1014005039375376f, + 0.4067007583026075f, + -0.2125574805828875f, + 0.0000000000000000, + -0.0643507165794627f, + -0.4517556589999482f, + -0.3046847507248690f, + 0.3017929516615495f, + 0.4082482904638627f, + 0.1747866975480809f, + -0.2110560104933578f, + -0.1426608480880726f, + -0.1381354035075859f, + -0.1743760259965107f, + 0.1135498731499434f, + }, + { + 0.2500000000000000, + -0.1014005039375375f, + 0.4444481661973445f, + 0.3085497062849767f, + 0.0000000000000000f, + -0.0643507165794627f, + 0.1585450355184006f, + 0.5112616136591823f, + 0.2579236279634118f, + 0.0000000000000000, + 0.0812611176717539f, + 0.1856718091610980f, + -0.3416446842253372f, + 0.3302282550303788f, + 0.0702790691196284f, + -0.0741750459581035f, + }, + { + 0.2500000000000000, + 0.2206518106944236f, + 0.0000000000000000, + 0.0000000000000000, + 0.7071067811865476f, + 0.6235485373547694f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + -0.1014005039375378f, + 0.0000000000000000, + 0.4706702258572536f, + 0.0000000000000000, + -0.0643507165794628f, + -0.0403851516082220f, + 0.0000000000000000, + 0.1627234014286620f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.7367497537172237f, + 0.0875511500058708f, + -0.2921026642334881f, + 0.1940289303259434f, + }, + { + 0.2500000000000000, + -0.1014005039375377f, + 0.1957439937204294f, + -0.1621205195722993f, + 0.0000000000000000, + -0.0643507165794628f, + 0.0074182263792424f, + -0.2904801297289980f, + 0.0952002265347504f, + 0.0000000000000000, + -0.3675398009862027f, + 0.4921585901373873f, + 0.2462710772207515f, + -0.0794670660590957f, + 0.3623817333531167f, + -0.4351904965232280f, + }, + { + 0.2500000000000000, + -0.1014005039375376f, + 0.2929100136981264f, + 0.0000000000000000, + 0.0000000000000000, + -0.0643507165794627f, + 0.3935103426921017f, + -0.0657870154914280f, + 0.0000000000000000, + -0.4082482904638628f, + -0.3078822139579090f, + -0.3852501370925192f, + -0.0857401903551931f, + -0.4613374887461511f, + 0.0000000000000000, + 0.2191868483885747f, + }, + { + 0.2500000000000000, + -0.1014005039375376f, + -0.4067007583026072f, + -0.2125574805828705f, + 0.0000000000000000, + -0.0643507165794627f, + -0.4517556589999464f, + 0.3046847507248840f, + 0.3017929516615503f, + -0.4082482904638635f, + -0.1747866975480813f, + 0.2110560104933581f, + -0.1426608480880734f, + -0.1381354035075829f, + -0.1743760259965108f, + 0.1135498731499426f, + }, + { + 0.2500000000000000, + -0.1014005039375377f, + -0.1957439937204287f, + -0.1621205195722833f, + 0.0000000000000000, + -0.0643507165794628f, + 0.0074182263792444f, + 0.2904801297290076f, + 0.0952002265347505f, + 0.0000000000000000, + 0.3675398009862011f, + -0.4921585901373891f, + 0.2462710772207514f, + -0.0794670660591026f, + 0.3623817333531165f, + -0.4351904965232251f, + }, + { + 0.2500000000000000, + -0.1014005039375375f, + 0.0000000000000000, + -0.4706702258572528f, + 0.0000000000000000, + -0.0643507165794627f, + 0.1107416575309343f, + 0.0000000000000000, + -0.1627234014286617f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.1488339922711357f, + 0.4972464710953509f, + 0.2921026642334879f, + 0.5550443808910661f, + }, + { + 0.2500000000000000, + -0.1014005039375377f, + 0.1137907446044809f, + -0.1464291867126764f, + 0.0000000000000000, + -0.0643507165794628f, + 0.0829816309488205f, + -0.2388977352334460f, + -0.3531238544981630f, + -0.4082482904638630f, + 0.4826689115059883f, + 0.1741941265991622f, + -0.0476868035022925f, + 0.1253805944856366f, + -0.4326608024727445f, + -0.2546827712406646f, + }, + { + 0.2500000000000000, + -0.1014005039375377f, + -0.4444481661973438f, + 0.3085497062849487f, + 0.0000000000000000, + -0.0643507165794628f, + 0.1585450355183970f, + -0.5112616136592012f, + 0.2579236279634129f, + 0.0000000000000000, + -0.0812611176717504f, + -0.1856718091610990f, + -0.3416446842253373f, + 0.3302282550303805f, + 0.0702790691196282f, + -0.0741750459581023f, + }, + { + 0.2500000000000000, + -0.1014005039375376f, + -0.2929100136981264f, + 0.0000000000000000, + 0.0000000000000000, + -0.0643507165794627f, + 0.3935103426921022f, + 0.0657870154914254f, + 0.0000000000000000, + 0.4082482904638634f, + 0.3078822139579031f, + 0.3852501370925211f, + -0.0857401903551927f, + -0.4613374887461554f, + 0.0000000000000000, + 0.2191868483885728f, + }, + { + 0.2500000000000000, + -0.1014005039375376f, + -0.1137907446044814f, + -0.1464291867126654f, + 0.0000000000000000, + -0.0643507165794627f, + 0.0829816309488214f, + 0.2388977352334547f, + -0.3531238544981624f, + 0.4082482904638630f, + -0.4826689115059858f, + -0.1741941265991621f, + -0.0476868035022928f, + 0.1253805944856431f, + -0.4326608024727457f, + -0.2546827712406641f, + }, + { + 0.2500000000000000, + -0.1014005039375374f, + 0.0000000000000000, + 0.4251149611657548f, + 0.0000000000000000, + -0.0643507165794626f, + -0.4517556589999480f, + 0.0000000000000000, + -0.6035859033230976f, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + -0.1426608480880724f, + -0.1381354035075845f, + 0.3487520519930227f, + 0.1135498731499429f, + }, + }; + + const HWY_CAPPED(float, 16) d; + for (size_t i = 0; i < 16; i += Lanes(d)) { + auto scalar = Zero(d); + for (size_t j = 0; j < 16; j++) { + auto px = Set(d, pixels[j]); + auto basis = Load(d, k4x4AFVBasisTranspose[j] + i); + scalar = MulAdd(px, basis, scalar); + } + Store(scalar, d, coeffs + i); + } +} + +// Coefficient layout: +// - (even, even) positions hold AFV coefficients +// - (odd, even) positions hold DCT4x4 coefficients +// - (any, odd) positions hold DCT4x8 coefficients +template +void AFVTransformFromPixels(const float* JXL_RESTRICT pixels, + size_t pixels_stride, + float* JXL_RESTRICT coefficients) { + HWY_ALIGN float scratch_space[4 * 8 * 2]; + size_t afv_x = afv_kind & 1; + size_t afv_y = afv_kind / 2; + HWY_ALIGN float block[4 * 8]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + block[(afv_y == 1 ? 3 - iy : iy) * 4 + (afv_x == 1 ? 3 - ix : ix)] = + pixels[(iy + 4 * afv_y) * pixels_stride + ix + 4 * afv_x]; + } + } + // AFV coefficients in (even, even) positions. + HWY_ALIGN float coeff[4 * 4]; + AFVDCT4x4(block, coeff); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + coefficients[iy * 2 * 8 + ix * 2] = coeff[iy * 4 + ix]; + } + } + // 4x4 DCT of the block with same y and different x. + ComputeTransposedScaledDCT<4>()( + DCTFrom(pixels + afv_y * 4 * pixels_stride + (afv_x == 1 ? 0 : 4), + pixels_stride), + block, scratch_space); + // ... in (odd, even) positions. + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[iy * 2 * 8 + ix * 2 + 1] = block[iy * 4 + ix]; + } + } + // 4x8 DCT of the other half of the block. + ComputeScaledDCT<4, 8>()( + DCTFrom(pixels + (afv_y == 1 ? 0 : 4) * pixels_stride, pixels_stride), + block, scratch_space); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[(1 + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + float block00 = coefficients[0] * 0.25f; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + coefficients[0] = (block00 + block01 + 2 * block10) * 0.25f; + coefficients[1] = (block00 - block01) * 0.5f; + coefficients[8] = (block00 + block01 - 2 * block10) * 0.25f; +} + +HWY_MAYBE_UNUSED void TransformFromPixels(const AcStrategy::Type strategy, + const float* JXL_RESTRICT pixels, + size_t pixels_stride, + float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT scratch_space) { + using Type = AcStrategy::Type; + switch (strategy) { + case Type::IDENTITY: { + PROFILER_ZONE("DCT Identity"); + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + float block_dc = 0; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + block_dc += pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix]; + } + } + block_dc *= 1.0f / 16; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 1 && iy == 1) continue; + coefficients[(y + iy * 2) * 8 + x + ix * 2] = + pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix] - + pixels[(y * 4 + 1) * pixels_stride + x * 4 + 1]; + } + } + coefficients[(y + 2) * 8 + x + 2] = coefficients[y * 8 + x]; + coefficients[y * 8 + x] = block_dc; + } + } + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + break; + } + case Type::DCT8X4: { + PROFILER_ZONE("DCT 8x4"); + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 8]; + ComputeScaledDCT<8, 4>()(DCTFrom(pixels + x * 4, pixels_stride), block, + scratch_space); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + // Store transposed. + coefficients[(x + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + } + float block0 = coefficients[0]; + float block1 = coefficients[8]; + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + break; + } + case Type::DCT4X8: { + PROFILER_ZONE("DCT 4x8"); + for (size_t y = 0; y < 2; y++) { + HWY_ALIGN float block[4 * 8]; + ComputeScaledDCT<4, 8>()( + DCTFrom(pixels + y * 4 * pixels_stride, pixels_stride), block, + scratch_space); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[(y + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + } + float block0 = coefficients[0]; + float block1 = coefficients[8]; + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + break; + } + case Type::DCT4X4: { + PROFILER_ZONE("DCT 4"); + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 4]; + ComputeTransposedScaledDCT<4>()( + DCTFrom(pixels + y * 4 * pixels_stride + x * 4, pixels_stride), + block, scratch_space); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + coefficients[(y + iy * 2) * 8 + x + ix * 2] = block[iy * 4 + ix]; + } + } + } + } + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + break; + } + case Type::DCT2X2: { + PROFILER_ZONE("DCT 2"); + DCT2TopBlock<8>(pixels, pixels_stride, coefficients); + DCT2TopBlock<4>(coefficients, kBlockDim, coefficients); + DCT2TopBlock<2>(coefficients, kBlockDim, coefficients); + break; + } + case Type::DCT16X16: { + PROFILER_ZONE("DCT 16"); + ComputeTransposedScaledDCT<16>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::DCT16X8: { + PROFILER_ZONE("DCT 16x8"); + ComputeScaledDCT<16, 8>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT8X16: { + PROFILER_ZONE("DCT 8x16"); + ComputeScaledDCT<8, 16>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT32X8: { + PROFILER_ZONE("DCT 32x8"); + ComputeScaledDCT<32, 8>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT8X32: { + PROFILER_ZONE("DCT 8x32"); + ComputeScaledDCT<8, 32>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT32X16: { + PROFILER_ZONE("DCT 32x16"); + ComputeScaledDCT<32, 16>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT16X32: { + PROFILER_ZONE("DCT 16x32"); + ComputeScaledDCT<16, 32>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT32X32: { + PROFILER_ZONE("DCT 32"); + ComputeTransposedScaledDCT<32>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::DCT: { + PROFILER_ZONE("DCT 8"); + ComputeTransposedScaledDCT<8>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::AFV0: { + PROFILER_ZONE("AFV0"); + AFVTransformFromPixels<0>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV1: { + PROFILER_ZONE("AFV1"); + AFVTransformFromPixels<1>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV2: { + PROFILER_ZONE("AFV2"); + AFVTransformFromPixels<2>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV3: { + PROFILER_ZONE("AFV3"); + AFVTransformFromPixels<3>(pixels, pixels_stride, coefficients); + break; + } + case Type::DCT64X64: { + PROFILER_ZONE("DCT 64x64"); + ComputeTransposedScaledDCT<64>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::DCT64X32: { + PROFILER_ZONE("DCT 64x32"); + ComputeScaledDCT<64, 32>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT32X64: { + PROFILER_ZONE("DCT 32x64"); + ComputeScaledDCT<32, 64>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT128X128: { + PROFILER_ZONE("DCT 128x128"); + ComputeTransposedScaledDCT<128>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::DCT128X64: { + PROFILER_ZONE("DCT 128x64"); + ComputeScaledDCT<128, 64>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT64X128: { + PROFILER_ZONE("DCT 64x128"); + ComputeScaledDCT<64, 128>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT256X256: { + PROFILER_ZONE("DCT 256x256"); + ComputeTransposedScaledDCT<256>()(DCTFrom(pixels, pixels_stride), + coefficients, scratch_space); + break; + } + case Type::DCT256X128: { + PROFILER_ZONE("DCT 256x128"); + ComputeScaledDCT<256, 128>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::DCT128X256: { + PROFILER_ZONE("DCT 128x256"); + ComputeScaledDCT<128, 256>()(DCTFrom(pixels, pixels_stride), coefficients, + scratch_space); + break; + } + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +HWY_MAYBE_UNUSED void DCFromLowestFrequencies(const AcStrategy::Type strategy, + const float* block, float* dc, + size_t dc_stride) { + using Type = AcStrategy::Type; + switch (strategy) { + case Type::DCT16X8: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT8X16: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT16X16: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X8: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT8X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X16: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT16X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT64X32: { + ReinterpretingIDCT( + block, 8 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X64: { + ReinterpretingIDCT( + block, 8 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT64X64: { + ReinterpretingIDCT( + block, 8 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT128X64: { + ReinterpretingIDCT< + /*DCT_ROWS=*/16 * kBlockDim, /*DCT_COLS=*/8 * kBlockDim, + /*LF_ROWS=*/16, /*LF_COLS=*/8, /*ROWS=*/16, /*COLS=*/8>( + block, 16 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT64X128: { + ReinterpretingIDCT< + /*DCT_ROWS=*/8 * kBlockDim, /*DCT_COLS=*/16 * kBlockDim, + /*LF_ROWS=*/8, /*LF_COLS=*/16, /*ROWS=*/8, /*COLS=*/16>( + block, 16 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT128X128: { + ReinterpretingIDCT< + /*DCT_ROWS=*/16 * kBlockDim, /*DCT_COLS=*/16 * kBlockDim, + /*LF_ROWS=*/16, /*LF_COLS=*/16, /*ROWS=*/16, /*COLS=*/16>( + block, 16 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT256X128: { + ReinterpretingIDCT< + /*DCT_ROWS=*/32 * kBlockDim, /*DCT_COLS=*/16 * kBlockDim, + /*LF_ROWS=*/32, /*LF_COLS=*/16, /*ROWS=*/32, /*COLS=*/16>( + block, 32 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT128X256: { + ReinterpretingIDCT< + /*DCT_ROWS=*/16 * kBlockDim, /*DCT_COLS=*/32 * kBlockDim, + /*LF_ROWS=*/16, /*LF_COLS=*/32, /*ROWS=*/16, /*COLS=*/32>( + block, 32 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT256X256: { + ReinterpretingIDCT< + /*DCT_ROWS=*/32 * kBlockDim, /*DCT_COLS=*/32 * kBlockDim, + /*LF_ROWS=*/32, /*LF_COLS=*/32, /*ROWS=*/32, /*COLS=*/32>( + block, 32 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT: + case Type::DCT2X2: + case Type::DCT4X4: + case Type::DCT4X8: + case Type::DCT8X4: + case Type::AFV0: + case Type::AFV1: + case Type::AFV2: + case Type::AFV3: + case Type::IDENTITY: + dc[0] = block[0]; + break; + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_ENC_TRANSFORMS_INL_H_ diff --git a/lib/jxl/enc_transforms.cc b/lib/jxl/enc_transforms.cc new file mode 100644 index 0000000..8978ba1 --- /dev/null +++ b/lib/jxl/enc_transforms.cc @@ -0,0 +1,41 @@ +// 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/enc_transforms.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_transforms.cc" +#include +#include + +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/enc_transforms-inl.h" + +namespace jxl { + +#if HWY_ONCE +HWY_EXPORT(TransformFromPixels); +void TransformFromPixels(const AcStrategy::Type strategy, + const float* JXL_RESTRICT pixels, size_t pixels_stride, + float* JXL_RESTRICT coefficients, + float* scratch_space) { + return HWY_DYNAMIC_DISPATCH(TransformFromPixels)( + strategy, pixels, pixels_stride, coefficients, scratch_space); +} + +HWY_EXPORT(DCFromLowestFrequencies); +void DCFromLowestFrequencies(AcStrategy::Type strategy, const float* block, + float* dc, size_t dc_stride) { + return HWY_DYNAMIC_DISPATCH(DCFromLowestFrequencies)(strategy, block, dc, + dc_stride); +} + +HWY_EXPORT(AFVDCT4x4); +void AFVDCT4x4(const float* JXL_RESTRICT pixels, float* JXL_RESTRICT coeffs) { + return HWY_DYNAMIC_DISPATCH(AFVDCT4x4)(pixels, coeffs); +} +#endif // HWY_ONCE + +} // namespace jxl diff --git a/lib/jxl/enc_transforms.h b/lib/jxl/enc_transforms.h new file mode 100644 index 0000000..039ccc3 --- /dev/null +++ b/lib/jxl/enc_transforms.h @@ -0,0 +1,32 @@ +// 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_ENC_TRANSFORMS_H_ +#define LIB_JXL_ENC_TRANSFORMS_H_ + +// Facade for (non-inlined) integral transforms. + +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +void TransformFromPixels(const AcStrategy::Type strategy, + const float* JXL_RESTRICT pixels, size_t pixels_stride, + float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT scratch_space); + +// Equivalent of the above for DC image. +void DCFromLowestFrequencies(AcStrategy::Type strategy, const float* block, + float* dc, size_t dc_stride); + +void AFVDCT4x4(const float* JXL_RESTRICT pixels, float* JXL_RESTRICT coeffs); + +} // namespace jxl + +#endif // LIB_JXL_ENC_TRANSFORMS_H_ diff --git a/lib/jxl/enc_xyb.cc b/lib/jxl/enc_xyb.cc new file mode 100644 index 0000000..57383b1 --- /dev/null +++ b/lib/jxl/enc_xyb.cc @@ -0,0 +1,437 @@ +// 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/enc_xyb.h" + +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/enc_xyb.cc" +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/transfer_functions-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::ShiftRight; + +// Returns cbrt(x) + add with 6 ulp max error. +// Modified from vectormath_exp.h, Apache 2 license. +// https://www.agner.org/optimize/vectorclass.zip +template +V CubeRootAndAdd(const V x, const V add) { + const HWY_FULL(float) df; + const HWY_FULL(int32_t) di; + + const auto kExpBias = Set(di, 0x54800000); // cast(1.) + cast(1.) / 3 + const auto kExpMul = Set(di, 0x002AAAAA); // shifted 1/3 + const auto k1_3 = Set(df, 1.0f / 3); + const auto k4_3 = Set(df, 4.0f / 3); + + const auto xa = x; // assume inputs never negative + const auto xa_3 = k1_3 * xa; + + // Multiply exponent by -1/3 + const auto m1 = BitCast(di, xa); + // Special case for 0. 0 is represented with an exponent of 0, so the + // "kExpBias - 1/3 * exp" below gives the wrong result. The IfThenZeroElse() + // sets those values as 0, which prevents having NaNs in the computations + // below. + const auto m2 = + IfThenZeroElse(m1 == Zero(di), kExpBias - (ShiftRight<23>(m1)) * kExpMul); + auto r = BitCast(df, m2); + + // Newton-Raphson iterations + for (int i = 0; i < 3; i++) { + const auto r2 = r * r; + r = NegMulAdd(xa_3, r2 * r2, k4_3 * r); + } + // Final iteration + auto r2 = r * r; + r = MulAdd(k1_3, NegMulAdd(xa, r2 * r2, r), r); + r2 = r * r; + r = MulAdd(r2, x, add); + + return r; +} + +// Ensures infinity norm is bounded. +void TestCubeRoot() { + const HWY_FULL(float) d; + float max_err = 0.0f; + for (uint64_t x5 = 0; x5 < 2000000; x5++) { + const float x = x5 * 1E-5f; + const float expected = cbrtf(x); + HWY_ALIGN float approx[MaxLanes(d)]; + Store(CubeRootAndAdd(Set(d, x), Zero(d)), d, approx); + + // All lanes are same + for (size_t i = 1; i < Lanes(d); ++i) { + JXL_ASSERT(std::abs(approx[0] - approx[i]) <= 1.2E-7f); + } + + const float err = std::abs(approx[0] - expected); + max_err = std::max(max_err, err); + } + // printf("max err %e\n", max_err); + JXL_ASSERT(max_err < 8E-7f); +} + +// 4x3 matrix * 3x1 SIMD vectors +template +JXL_INLINE void OpsinAbsorbance(const V r, const V g, const V b, + const float* JXL_RESTRICT premul_absorb, + V* JXL_RESTRICT mixed0, V* JXL_RESTRICT mixed1, + V* JXL_RESTRICT mixed2) { + const float* bias = &kOpsinAbsorbanceBias[0]; + const HWY_FULL(float) d; + const size_t N = Lanes(d); + const auto m0 = Load(d, premul_absorb + 0 * N); + const auto m1 = Load(d, premul_absorb + 1 * N); + const auto m2 = Load(d, premul_absorb + 2 * N); + const auto m3 = Load(d, premul_absorb + 3 * N); + const auto m4 = Load(d, premul_absorb + 4 * N); + const auto m5 = Load(d, premul_absorb + 5 * N); + const auto m6 = Load(d, premul_absorb + 6 * N); + const auto m7 = Load(d, premul_absorb + 7 * N); + const auto m8 = Load(d, premul_absorb + 8 * N); + *mixed0 = MulAdd(m0, r, MulAdd(m1, g, MulAdd(m2, b, Set(d, bias[0])))); + *mixed1 = MulAdd(m3, r, MulAdd(m4, g, MulAdd(m5, b, Set(d, bias[1])))); + *mixed2 = MulAdd(m6, r, MulAdd(m7, g, MulAdd(m8, b, Set(d, bias[2])))); +} + +template +void StoreXYB(const V r, V g, const V b, float* JXL_RESTRICT valx, + float* JXL_RESTRICT valy, float* JXL_RESTRICT valz) { + const HWY_FULL(float) d; + const V half = Set(d, 0.5f); + Store(half * (r - g), d, valx); + Store(half * (r + g), d, valy); + Store(b, d, valz); +} + +// Converts one RGB vector to XYB. +template +void LinearRGBToXYB(const V r, const V g, const V b, + const float* JXL_RESTRICT premul_absorb, + float* JXL_RESTRICT valx, float* JXL_RESTRICT valy, + float* JXL_RESTRICT valz) { + V mixed0, mixed1, mixed2; + OpsinAbsorbance(r, g, b, premul_absorb, &mixed0, &mixed1, &mixed2); + + // mixed* should be non-negative even for wide-gamut, so clamp to zero. + mixed0 = ZeroIfNegative(mixed0); + mixed1 = ZeroIfNegative(mixed1); + mixed2 = ZeroIfNegative(mixed2); + + const HWY_FULL(float) d; + const size_t N = Lanes(d); + mixed0 = CubeRootAndAdd(mixed0, Load(d, premul_absorb + 9 * N)); + mixed1 = CubeRootAndAdd(mixed1, Load(d, premul_absorb + 10 * N)); + mixed2 = CubeRootAndAdd(mixed2, Load(d, premul_absorb + 11 * N)); + StoreXYB(mixed0, mixed1, mixed2, valx, valy, valz); + + // For wide-gamut inputs, r/g/b and valx (but not y/z) are often negative. +} + +// Input/output uses the codec.h scaling: nominally 0-1 if in-gamut. +template +V LinearFromSRGB(V encoded) { + return TF_SRGB().DisplayFromEncoded(encoded); +} + +void LinearSRGBToXYB(const Image3F& linear, + const float* JXL_RESTRICT premul_absorb, ThreadPool* pool, + Image3F* JXL_RESTRICT xyb) { + const size_t xsize = linear.xsize(); + + const HWY_FULL(float) d; + RunOnPool( + pool, 0, static_cast(linear.ysize()), ThreadPool::SkipInit(), + [&](const int task, const int /*thread*/) { + const size_t y = static_cast(task); + const float* JXL_RESTRICT row_in0 = linear.ConstPlaneRow(0, y); + const float* JXL_RESTRICT row_in1 = linear.ConstPlaneRow(1, y); + const float* JXL_RESTRICT row_in2 = linear.ConstPlaneRow(2, y); + float* JXL_RESTRICT row_xyb0 = xyb->PlaneRow(0, y); + float* JXL_RESTRICT row_xyb1 = xyb->PlaneRow(1, y); + float* JXL_RESTRICT row_xyb2 = xyb->PlaneRow(2, y); + + for (size_t x = 0; x < xsize; x += Lanes(d)) { + const auto in_r = Load(d, row_in0 + x); + const auto in_g = Load(d, row_in1 + x); + const auto in_b = Load(d, row_in2 + x); + LinearRGBToXYB(in_r, in_g, in_b, premul_absorb, row_xyb0 + x, + row_xyb1 + x, row_xyb2 + x); + } + }, + "LinearToXYB"); +} + +void SRGBToXYB(const Image3F& srgb, const float* JXL_RESTRICT premul_absorb, + ThreadPool* pool, Image3F* JXL_RESTRICT xyb) { + const size_t xsize = srgb.xsize(); + + const HWY_FULL(float) d; + RunOnPool( + pool, 0, static_cast(srgb.ysize()), ThreadPool::SkipInit(), + [&](const int task, const int /*thread*/) { + const size_t y = static_cast(task); + const float* JXL_RESTRICT row_srgb0 = srgb.ConstPlaneRow(0, y); + const float* JXL_RESTRICT row_srgb1 = srgb.ConstPlaneRow(1, y); + const float* JXL_RESTRICT row_srgb2 = srgb.ConstPlaneRow(2, y); + float* JXL_RESTRICT row_xyb0 = xyb->PlaneRow(0, y); + float* JXL_RESTRICT row_xyb1 = xyb->PlaneRow(1, y); + float* JXL_RESTRICT row_xyb2 = xyb->PlaneRow(2, y); + + for (size_t x = 0; x < xsize; x += Lanes(d)) { + const auto in_r = LinearFromSRGB(Load(d, row_srgb0 + x)); + const auto in_g = LinearFromSRGB(Load(d, row_srgb1 + x)); + const auto in_b = LinearFromSRGB(Load(d, row_srgb2 + x)); + LinearRGBToXYB(in_r, in_g, in_b, premul_absorb, row_xyb0 + x, + row_xyb1 + x, row_xyb2 + x); + } + }, + "SRGBToXYB"); +} + +void SRGBToXYBAndLinear(const Image3F& srgb, + const float* JXL_RESTRICT premul_absorb, + ThreadPool* pool, Image3F* JXL_RESTRICT xyb, + Image3F* JXL_RESTRICT linear) { + const size_t xsize = srgb.xsize(); + + const HWY_FULL(float) d; + RunOnPool( + pool, 0, static_cast(srgb.ysize()), ThreadPool::SkipInit(), + [&](const int task, const int /*thread*/) { + const size_t y = static_cast(task); + const float* JXL_RESTRICT row_srgb0 = srgb.ConstPlaneRow(0, y); + const float* JXL_RESTRICT row_srgb1 = srgb.ConstPlaneRow(1, y); + const float* JXL_RESTRICT row_srgb2 = srgb.ConstPlaneRow(2, y); + + float* JXL_RESTRICT row_linear0 = linear->PlaneRow(0, y); + float* JXL_RESTRICT row_linear1 = linear->PlaneRow(1, y); + float* JXL_RESTRICT row_linear2 = linear->PlaneRow(2, y); + + float* JXL_RESTRICT row_xyb0 = xyb->PlaneRow(0, y); + float* JXL_RESTRICT row_xyb1 = xyb->PlaneRow(1, y); + float* JXL_RESTRICT row_xyb2 = xyb->PlaneRow(2, y); + + for (size_t x = 0; x < xsize; x += Lanes(d)) { + const auto in_r = LinearFromSRGB(Load(d, row_srgb0 + x)); + const auto in_g = LinearFromSRGB(Load(d, row_srgb1 + x)); + const auto in_b = LinearFromSRGB(Load(d, row_srgb2 + x)); + + Store(in_r, d, row_linear0 + x); + Store(in_g, d, row_linear1 + x); + Store(in_b, d, row_linear2 + x); + + LinearRGBToXYB(in_r, in_g, in_b, premul_absorb, row_xyb0 + x, + row_xyb1 + x, row_xyb2 + x); + } + }, + "SRGBToXYBAndLinear"); +} + +// This is different from Butteraugli's OpsinDynamicsImage() in the sense that +// it does not contain a sensitivity multiplier based on the blurred image. +const ImageBundle* ToXYB(const ImageBundle& in, ThreadPool* pool, + Image3F* JXL_RESTRICT xyb, + ImageBundle* const JXL_RESTRICT linear) { + PROFILER_FUNC; + + const size_t xsize = in.xsize(); + const size_t ysize = in.ysize(); + JXL_ASSERT(SameSize(in, *xyb)); + + const HWY_FULL(float) d; + // Pre-broadcasted constants + HWY_ALIGN float premul_absorb[MaxLanes(d) * 12]; + const size_t N = Lanes(d); + for (size_t i = 0; i < 9; ++i) { + const auto absorb = Set(d, kOpsinAbsorbanceMatrix[i] * + (in.metadata()->IntensityTarget() / 255.0f)); + Store(absorb, d, premul_absorb + i * N); + } + for (size_t i = 0; i < 3; ++i) { + const auto neg_bias_cbrt = Set(d, -cbrtf(kOpsinAbsorbanceBias[i])); + Store(neg_bias_cbrt, d, premul_absorb + (9 + i) * N); + } + + const bool want_linear = linear != nullptr; + + const ColorEncoding& c_linear_srgb = ColorEncoding::LinearSRGB(in.IsGray()); + // Linear sRGB inputs are rare but can be useful for the fastest encoders, for + // which undoing the sRGB transfer function would be a large part of the cost. + if (c_linear_srgb.SameColorEncoding(in.c_current())) { + LinearSRGBToXYB(in.color(), premul_absorb, pool, xyb); + // This only happens if kitten or slower, moving ImageBundle might be + // possible but the encoder is much slower than this copy. + if (want_linear) { + *linear = in.Copy(); + return linear; + } + return ∈ + } + + // Common case: already sRGB, can avoid the color transform + if (in.IsSRGB()) { + // Common case: can avoid allocating/copying + if (!want_linear) { + SRGBToXYB(in.color(), premul_absorb, pool, xyb); + return ∈ + } + + // Slow encoder also wants linear sRGB. + linear->SetFromImage(Image3F(xsize, ysize), c_linear_srgb); + SRGBToXYBAndLinear(in.color(), premul_absorb, pool, xyb, linear->color()); + return linear; + } + + // General case: not sRGB, need color transform. + ImageBundle linear_storage; // Local storage only used if !want_linear. + + ImageBundle* linear_storage_ptr; + if (want_linear) { + // Caller asked for linear, use that storage directly. + linear_storage_ptr = linear; + } else { + // Caller didn't ask for linear, create our own local storage + // OK to reuse metadata, it will not be changed. + linear_storage = ImageBundle(const_cast(in.metadata())); + linear_storage_ptr = &linear_storage; + } + + const ImageBundle* ptr; + JXL_CHECK( + TransformIfNeeded(in, c_linear_srgb, pool, linear_storage_ptr, &ptr)); + // If no transform was necessary, should have taken the above codepath. + JXL_ASSERT(ptr == linear_storage_ptr); + + LinearSRGBToXYB(*linear_storage_ptr->color(), premul_absorb, pool, xyb); + return want_linear ? linear : ∈ +} + +// Transform RGB to YCbCr. +// Could be performed in-place (i.e. Y, Cb and Cr could alias R, B and B). +void RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, + const ImageF& b_plane, ImageF* y_plane, ImageF* cb_plane, + ImageF* cr_plane, ThreadPool* pool) { + const HWY_FULL(float) df; + const size_t S = Lanes(df); // Step. + + const size_t xsize = r_plane.xsize(); + const size_t ysize = r_plane.ysize(); + if ((xsize == 0) || (ysize == 0)) return; + + // Full-range BT.601 as defined by JFIF Clause 7: + // https://www.itu.int/rec/T-REC-T.871-201105-I/en + const auto k128 = Set(df, 128.0f / 255); + const auto kR = Set(df, 0.299f); // NTSC luma + const auto kG = Set(df, 0.587f); + const auto kB = Set(df, 0.114f); + const auto kAmpR = Set(df, 0.701f); + const auto kAmpB = Set(df, 0.886f); + const auto kDiffR = kAmpR + kR; + const auto kDiffB = kAmpB + kB; + const auto kNormR = Set(df, 1.0f) / (kAmpR + kG + kB); + const auto kNormB = Set(df, 1.0f) / (kR + kG + kAmpB); + + constexpr size_t kGroupArea = kGroupDim * kGroupDim; + const size_t lines_per_group = DivCeil(kGroupArea, xsize); + const size_t num_stripes = DivCeil(ysize, lines_per_group); + const auto transform = [&](int idx, int /* thread*/) { + const size_t y0 = idx * lines_per_group; + const size_t y1 = std::min(y0 + lines_per_group, ysize); + for (size_t y = y0; y < y1; ++y) { + const float* r_row = r_plane.ConstRow(y); + const float* g_row = g_plane.ConstRow(y); + const float* b_row = b_plane.ConstRow(y); + float* y_row = y_plane->Row(y); + float* cb_row = cb_plane->Row(y); + float* cr_row = cr_plane->Row(y); + for (size_t x = 0; x < xsize; x += S) { + const auto r = Load(df, r_row + x); + const auto g = Load(df, g_row + x); + const auto b = Load(df, b_row + x); + const auto r_base = r * kR; + const auto r_diff = r * kDiffR; + const auto g_base = g * kG; + const auto b_base = b * kB; + const auto b_diff = b * kDiffB; + const auto y_base = r_base + g_base + b_base; + const auto y_vec = y_base - k128; + const auto cb_vec = (b_diff - y_base) * kNormB; + const auto cr_vec = (r_diff - y_base) * kNormR; + Store(y_vec, df, y_row + x); + Store(cb_vec, df, cb_row + x); + Store(cr_vec, df, cr_row + x); + } + } + }; + RunOnPool(pool, 0, static_cast(num_stripes), ThreadPool::SkipInit(), + transform, "RgbToYcbCr"); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(ToXYB); +const ImageBundle* ToXYB(const ImageBundle& in, ThreadPool* pool, + Image3F* JXL_RESTRICT xyb, + ImageBundle* JXL_RESTRICT linear_storage) { + return HWY_DYNAMIC_DISPATCH(ToXYB)(in, pool, xyb, linear_storage); +} + +HWY_EXPORT(RgbToYcbcr); +void RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, + const ImageF& b_plane, ImageF* y_plane, ImageF* cb_plane, + ImageF* cr_plane, ThreadPool* pool) { + return HWY_DYNAMIC_DISPATCH(RgbToYcbcr)(r_plane, g_plane, b_plane, y_plane, + cb_plane, cr_plane, pool); +} + +HWY_EXPORT(TestCubeRoot); +void TestCubeRoot() { return HWY_DYNAMIC_DISPATCH(TestCubeRoot)(); } + +// DEPRECATED +Image3F OpsinDynamicsImage(const Image3B& srgb8) { + ImageMetadata metadata; + metadata.SetUintSamples(8); + metadata.color_encoding = ColorEncoding::SRGB(); + ImageBundle ib(&metadata); + ib.SetFromImage(ConvertToFloat(srgb8), metadata.color_encoding); + JXL_CHECK(ib.TransformTo(ColorEncoding::LinearSRGB(ib.IsGray()))); + ThreadPool* null_pool = nullptr; + Image3F xyb(srgb8.xsize(), srgb8.ysize()); + + ImageBundle linear_storage(&metadata); + (void)ToXYB(ib, null_pool, &xyb, &linear_storage); + return xyb; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/enc_xyb.h b/lib/jxl/enc_xyb.h new file mode 100644 index 0000000..f30ae2f --- /dev/null +++ b/lib/jxl/enc_xyb.h @@ -0,0 +1,45 @@ +// 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_ENC_XYB_H_ +#define LIB_JXL_ENC_XYB_H_ + +// Converts to XYB color space. + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" + +namespace jxl { + +// Converts any color space to XYB. If `linear` is not null, returns `linear` +// after filling it with a linear sRGB copy of `in`. Otherwise, returns `&in`. +// +// NOTE this return value can avoid an extra color conversion if `in` would +// later be passed to JxlButteraugliComparator. +const ImageBundle* ToXYB(const ImageBundle& in, ThreadPool* pool, + Image3F* JXL_RESTRICT xyb, + ImageBundle* JXL_RESTRICT linear = nullptr); + +// Bt.601 to match JPEG/JFIF. Outputs _signed_ YCbCr values suitable for DCT, +// see F.1.1.3 of T.81 (because our data type is float, there is no need to add +// a bias to make the values unsigned). +void RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, + const ImageF& b_plane, ImageF* y_plane, ImageF* cb_plane, + ImageF* cr_plane, ThreadPool* pool); + +// DEPRECATED, used by opsin_image_wrapper. +Image3F OpsinDynamicsImage(const Image3B& srgb8); + +// For opsin_image_test. +void TestCubeRoot(); + +} // namespace jxl + +#endif // LIB_JXL_ENC_XYB_H_ diff --git a/lib/jxl/encode.cc b/lib/jxl/encode.cc new file mode 100644 index 0000000..b73854d --- /dev/null +++ b/lib/jxl/encode.cc @@ -0,0 +1,489 @@ +// 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 "jxl/encode.h" + +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_icc_codec.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" + +// Debug-printing failure macro similar to JXL_FAILURE, but for the status code +// JXL_ENC_ERROR +#ifdef JXL_CRASH_ON_ERROR +#define JXL_API_ERROR(format, ...) \ + (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ + ::jxl::Abort(), JXL_ENC_ERROR) +#else // JXL_CRASH_ON_ERROR +#define JXL_API_ERROR(format, ...) \ + (((JXL_DEBUG_ON_ERROR) && \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__)), \ + JXL_ENC_ERROR) +#endif // JXL_CRASH_ON_ERROR + +namespace jxl {} // namespace jxl + +uint32_t JxlEncoderVersion(void) { + return JPEGXL_MAJOR_VERSION * 1000000 + JPEGXL_MINOR_VERSION * 1000 + + JPEGXL_PATCH_VERSION; +} + +JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { + jxl::MemoryManagerUniquePtr input_frame = + std::move(input_frame_queue[0]); + input_frame_queue.erase(input_frame_queue.begin()); + + // TODO(zond): If the frame queue is empty and the input_closed is true, + // then mark this frame as the last. + + jxl::BitWriter writer; + + if (!wrote_bytes) { + if (use_container) { + output_byte_queue.insert( + output_byte_queue.end(), jxl::kContainerHeader, + jxl::kContainerHeader + sizeof(jxl::kContainerHeader)); + if (store_jpeg_metadata && jpeg_metadata.size() > 0) { + jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_metadata.size(), + false, &output_byte_queue); + output_byte_queue.insert(output_byte_queue.end(), jpeg_metadata.begin(), + jpeg_metadata.end()); + } + } + if (!WriteHeaders(&metadata, &writer, nullptr)) { + return JXL_ENC_ERROR; + } + // Only send ICC (at least several hundred bytes) if fields aren't enough. + if (metadata.m.color_encoding.WantICC()) { + if (!jxl::WriteICC(metadata.m.color_encoding.ICC(), &writer, + jxl::kLayerHeader, nullptr)) { + return JXL_ENC_ERROR; + } + } + + // TODO(lode): preview should be added here if a preview image is added + + // Each frame should start on byte boundaries. + writer.ZeroPadToByte(); + } + + // TODO(zond): Handle progressive mode like EncodeFile does it. + // TODO(zond): Handle animation like EncodeFile does it, by checking if + // JxlEncoderCloseInput has been called and if the frame queue is + // empty (to see if it's the last animation frame). + + if (metadata.m.xyb_encoded) { + input_frame->option_values.cparams.color_transform = + jxl::ColorTransform::kXYB; + } else { + // TODO(zond): Figure out when to use kYCbCr instead. + input_frame->option_values.cparams.color_transform = + jxl::ColorTransform::kNone; + } + + jxl::PassesEncoderState enc_state; + if (!jxl::EncodeFrame(input_frame->option_values.cparams, jxl::FrameInfo{}, + &metadata, input_frame->frame, &enc_state, + thread_pool.get(), &writer, + /*aux_out=*/nullptr)) { + return JXL_ENC_ERROR; + } + + jxl::PaddedBytes bytes = std::move(writer).TakeBytes(); + + if (use_container && !wrote_bytes) { + if (input_closed && input_frame_queue.empty()) { + jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), bytes.size(), + /*unbounded=*/false, &output_byte_queue); + } else { + jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), 0, /*unbounded=*/true, + &output_byte_queue); + } + } + + output_byte_queue.insert(output_byte_queue.end(), bytes.data(), + bytes.data() + bytes.size()); + wrote_bytes = true; + + last_used_cparams = input_frame->option_values.cparams; + + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderSetColorEncoding(JxlEncoder* enc, + const JxlColorEncoding* color) { + if (enc->color_encoding_set) { + // Already set + return JXL_ENC_ERROR; + } + if (!jxl::ConvertExternalToInternalColorEncoding( + *color, &enc->metadata.m.color_encoding)) { + return JXL_ENC_ERROR; + } + enc->color_encoding_set = true; + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderSetICCProfile(JxlEncoder* enc, + const uint8_t* icc_profile, + size_t size) { + if (enc->color_encoding_set) { + // Already set + return JXL_ENC_ERROR; + } + jxl::PaddedBytes icc; + icc.assign(icc_profile, icc_profile + size); + if (!enc->metadata.m.color_encoding.SetICC(std::move(icc))) { + return JXL_ENC_ERROR; + } + enc->color_encoding_set = true; + return JXL_ENC_SUCCESS; +} + +void JxlEncoderInitBasicInfo(JxlBasicInfo* info) { + info->have_container = JXL_FALSE; + info->xsize = 0; + info->ysize = 0; + info->bits_per_sample = 8; + info->exponent_bits_per_sample = 0; + info->intensity_target = 255.f; + info->min_nits = 0.f; + info->relative_to_max_display = JXL_FALSE; + info->linear_below = 0.f; + info->uses_original_profile = JXL_FALSE; + info->have_preview = JXL_FALSE; + info->have_animation = JXL_FALSE; + info->orientation = JXL_ORIENT_IDENTITY; + info->num_color_channels = 3; + info->num_extra_channels = 0; + info->alpha_bits = 0; + info->alpha_exponent_bits = 0; + info->alpha_premultiplied = JXL_FALSE; + info->preview.xsize = 0; + info->preview.ysize = 0; + info->animation.tps_numerator = 10; + info->animation.tps_denominator = 1; + info->animation.num_loops = 0; + info->animation.have_timecodes = JXL_FALSE; +} + +JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, + const JxlBasicInfo* info) { + if (!enc->metadata.size.Set(info->xsize, info->ysize)) { + return JXL_ENC_ERROR; + } + if (!info->exponent_bits_per_sample) { + if (info->bits_per_sample > 0 && info->bits_per_sample <= 24) { + enc->metadata.m.SetUintSamples(info->bits_per_sample); + } else { + return JXL_ENC_ERROR; + } + } else if (info->bits_per_sample == 32 && + info->exponent_bits_per_sample == 8) { + enc->metadata.m.SetFloat32Samples(); + } else if (info->bits_per_sample == 16 && + info->exponent_bits_per_sample == 5) { + enc->metadata.m.SetFloat16Samples(); + } else { + return JXL_ENC_NOT_SUPPORTED; + } + if (info->alpha_bits > 0 && info->alpha_exponent_bits > 0) { + return JXL_ENC_NOT_SUPPORTED; + } + switch (info->alpha_bits) { + case 0: + break; + case 32: + case 16: + enc->metadata.m.SetAlphaBits(16); + break; + case 8: + enc->metadata.m.SetAlphaBits(info->alpha_bits); + break; + default: + return JXL_ENC_ERROR; + break; + } + enc->metadata.m.xyb_encoded = !info->uses_original_profile; + if (info->orientation > 0 && info->orientation <= 8) { + enc->metadata.m.orientation = info->orientation; + } else { + return JXL_API_ERROR("Invalid value for orientation field"); + } + enc->basic_info_set = true; + return JXL_ENC_SUCCESS; +} + +JxlEncoderOptions* JxlEncoderOptionsCreate(JxlEncoder* enc, + const JxlEncoderOptions* source) { + auto opts = + jxl::MemoryManagerMakeUnique(&enc->memory_manager); + if (!opts) return nullptr; + opts->enc = enc; + if (source != nullptr) { + opts->values = source->values; + } else { + opts->values.lossless = false; + } + JxlEncoderOptions* ret = opts.get(); + enc->encoder_options.emplace_back(std::move(opts)); + return ret; +} + +JxlEncoderStatus JxlEncoderOptionsSetLossless(JxlEncoderOptions* options, + const JXL_BOOL lossless) { + options->values.lossless = lossless; + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderOptionsSetEffort(JxlEncoderOptions* options, + const int effort) { + if (effort < 1 || effort > 9) { + return JXL_ENC_ERROR; + } + options->values.cparams.speed_tier = static_cast(10 - effort); + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderOptionsSetDistance(JxlEncoderOptions* options, + float distance) { + if (distance < 0 || distance > 15) { + return JXL_ENC_ERROR; + } + options->values.cparams.butteraugli_distance = distance; + return JXL_ENC_SUCCESS; +} + +JxlEncoder* JxlEncoderCreate(const JxlMemoryManager* memory_manager) { + JxlMemoryManager local_memory_manager; + if (!jxl::MemoryManagerInit(&local_memory_manager, memory_manager)) { + return nullptr; + } + + void* alloc = + jxl::MemoryManagerAlloc(&local_memory_manager, sizeof(JxlEncoder)); + if (!alloc) return nullptr; + JxlEncoder* enc = new (alloc) JxlEncoder(); + enc->memory_manager = local_memory_manager; + + return enc; +} + +void JxlEncoderReset(JxlEncoder* enc) { + enc->thread_pool.reset(); + enc->input_frame_queue.clear(); + enc->encoder_options.clear(); + enc->output_byte_queue.clear(); + enc->wrote_bytes = false; + enc->metadata = jxl::CodecMetadata(); + enc->last_used_cparams = jxl::CompressParams(); + enc->input_closed = false; + enc->basic_info_set = false; + enc->color_encoding_set = false; +} + +void JxlEncoderDestroy(JxlEncoder* enc) { + if (enc) { + // Call destructor directly since custom free function is used. + enc->~JxlEncoder(); + jxl::MemoryManagerFree(&enc->memory_manager, enc); + } +} + +JxlEncoderStatus JxlEncoderUseContainer(JxlEncoder* enc, + JXL_BOOL use_container) { + enc->use_container = static_cast(use_container); + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, + JXL_BOOL store_jpeg_metadata) { + enc->store_jpeg_metadata = static_cast(store_jpeg_metadata); + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderSetParallelRunner(JxlEncoder* enc, + JxlParallelRunner parallel_runner, + void* parallel_runner_opaque) { + if (enc->thread_pool) return JXL_API_ERROR("parallel runner already set"); + enc->thread_pool = jxl::MemoryManagerMakeUnique( + &enc->memory_manager, parallel_runner, parallel_runner_opaque); + if (!enc->thread_pool) { + return JXL_ENC_ERROR; + } + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderAddJPEGFrame(const JxlEncoderOptions* options, + const uint8_t* buffer, size_t size) { + if (options->enc->input_closed) { + return JXL_ENC_ERROR; + } + + jxl::CodecInOut io; + if (!jxl::jpeg::DecodeImageJPG(jxl::Span(buffer, size), &io)) { + return JXL_ENC_ERROR; + } + + if (!options->enc->color_encoding_set) { + if (!SetColorEncodingFromJpegData( + *io.Main().jpeg_data, &options->enc->metadata.m.color_encoding)) { + return JXL_ENC_ERROR; + } + } + + if (!options->enc->basic_info_set) { + JxlBasicInfo basic_info; + JxlEncoderInitBasicInfo(&basic_info); + basic_info.xsize = io.Main().jpeg_data->width; + basic_info.ysize = io.Main().jpeg_data->height; + basic_info.uses_original_profile = true; + if (JxlEncoderSetBasicInfo(options->enc, &basic_info) != JXL_ENC_SUCCESS) { + return JXL_ENC_ERROR; + } + } + + if (options->enc->metadata.m.xyb_encoded) { + // Can't XYB encode a lossless JPEG. + return JXL_ENC_ERROR; + } + + if (options->enc->store_jpeg_metadata) { + jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data; + jxl::PaddedBytes jpeg_data; + if (!EncodeJPEGData(data_in, &jpeg_data)) { + return JXL_ENC_ERROR; + } + options->enc->jpeg_metadata = std::vector( + jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); + } + + auto queued_frame = jxl::MemoryManagerMakeUnique( + &options->enc->memory_manager, + // JxlEncoderQueuedFrame is a struct with no constructors, so we use the + // default move constructor there. + jxl::JxlEncoderQueuedFrame{options->values, + jxl::ImageBundle(&options->enc->metadata.m)}); + if (!queued_frame) { + return JXL_ENC_ERROR; + } + queued_frame->frame.SetFromImage(std::move(*io.Main().color()), + io.Main().c_current()); + queued_frame->frame.jpeg_data = std::move(io.Main().jpeg_data); + queued_frame->frame.color_transform = io.Main().color_transform; + queued_frame->frame.chroma_subsampling = io.Main().chroma_subsampling; + + if (options->values.lossless) { + queued_frame->option_values.cparams.SetLossless(); + } + + options->enc->input_frame_queue.emplace_back(std::move(queued_frame)); + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderAddImageFrame(const JxlEncoderOptions* options, + const JxlPixelFormat* pixel_format, + const void* buffer, size_t size) { + if (!options->enc->basic_info_set || !options->enc->color_encoding_set) { + return JXL_ENC_ERROR; + } + + if (options->enc->input_closed) { + return JXL_ENC_ERROR; + } + + auto queued_frame = jxl::MemoryManagerMakeUnique( + &options->enc->memory_manager, + // JxlEncoderQueuedFrame is a struct with no constructors, so we use the + // default move constructor there. + jxl::JxlEncoderQueuedFrame{options->values, + jxl::ImageBundle(&options->enc->metadata.m)}); + + if (!queued_frame) { + return JXL_ENC_ERROR; + } + + jxl::ColorEncoding c_current; + if (options->enc->metadata.m.xyb_encoded) { + if ((pixel_format->data_type == JXL_TYPE_FLOAT) || + (pixel_format->data_type == JXL_TYPE_FLOAT16)) { + c_current = + jxl::ColorEncoding::LinearSRGB(pixel_format->num_channels < 3); + } else { + c_current = jxl::ColorEncoding::SRGB(pixel_format->num_channels < 3); + } + } else { + c_current = options->enc->metadata.m.color_encoding; + } + + if (!jxl::BufferToImageBundle(*pixel_format, options->enc->metadata.xsize(), + options->enc->metadata.ysize(), buffer, size, + options->enc->thread_pool.get(), c_current, + &(queued_frame->frame))) { + return JXL_ENC_ERROR; + } + + if (options->values.lossless) { + queued_frame->option_values.cparams.SetLossless(); + } + + options->enc->input_frame_queue.emplace_back(std::move(queued_frame)); + return JXL_ENC_SUCCESS; +} + +void JxlEncoderCloseInput(JxlEncoder* enc) { enc->input_closed = true; } + +JxlEncoderStatus JxlEncoderProcessOutput(JxlEncoder* enc, uint8_t** next_out, + size_t* avail_out) { + while (*avail_out > 0 && + (!enc->output_byte_queue.empty() || !enc->input_frame_queue.empty())) { + if (!enc->output_byte_queue.empty()) { + size_t to_copy = std::min(*avail_out, enc->output_byte_queue.size()); + memcpy(static_cast(*next_out), enc->output_byte_queue.data(), + to_copy); + *next_out += to_copy; + *avail_out -= to_copy; + enc->output_byte_queue.erase(enc->output_byte_queue.begin(), + enc->output_byte_queue.begin() + to_copy); + } else if (!enc->input_frame_queue.empty()) { + if (enc->RefillOutputByteQueue() != JXL_ENC_SUCCESS) { + return JXL_ENC_ERROR; + } + } + } + + if (!enc->output_byte_queue.empty() || !enc->input_frame_queue.empty()) { + return JXL_ENC_NEED_MORE_OUTPUT; + } + return JXL_ENC_SUCCESS; +} + +JxlEncoderStatus JxlEncoderOptionsSetDecodingSpeed(JxlEncoderOptions* options, + int tier) { + if (tier < 0 || tier > 4) { + return JXL_ENC_ERROR; + } + options->values.cparams.decoding_speed_tier = tier; + return JXL_ENC_SUCCESS; +} + +void JxlColorEncodingSetToSRGB(JxlColorEncoding* color_encoding, + JXL_BOOL is_gray) { + ConvertInternalToExternalColorEncoding(jxl::ColorEncoding::SRGB(is_gray), + color_encoding); +} + +void JxlColorEncodingSetToLinearSRGB(JxlColorEncoding* color_encoding, + JXL_BOOL is_gray) { + ConvertInternalToExternalColorEncoding( + jxl::ColorEncoding::LinearSRGB(is_gray), color_encoding); +} diff --git a/lib/jxl/encode_internal.h b/lib/jxl/encode_internal.h new file mode 100644 index 0000000..4c144bf --- /dev/null +++ b/lib/jxl/encode_internal.h @@ -0,0 +1,120 @@ +/* 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_ENCODE_INTERNAL_H_ +#define LIB_JXL_ENCODE_INTERNAL_H_ + +#include + +#include "jxl/encode.h" +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" +#include "jxl/types.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/enc_frame.h" +#include "lib/jxl/memory_manager_internal.h" + +namespace jxl { + +typedef struct JxlEncoderOptionsValuesStruct { + // lossless is a separate setting from cparams because it is a combination + // setting that overrides multiple settings inside of cparams. + bool lossless; + jxl::CompressParams cparams; +} JxlEncoderOptionsValues; + +typedef struct JxlEncoderQueuedFrame { + JxlEncoderOptionsValues option_values; + jxl::ImageBundle frame; +} JxlEncoderQueuedFrame; + +typedef std::array BoxType; + +// Utility function that makes a BoxType from a null terminated string literal. +constexpr BoxType MakeBoxType(const char (&type)[5]) { + return BoxType( + {{static_cast(type[0]), static_cast(type[1]), + static_cast(type[2]), static_cast(type[3])}}); +} + +constexpr unsigned char kContainerHeader[] = { + 0, 0, 0, 0xc, 'J', 'X', 'L', ' ', 0xd, 0xa, 0x87, + 0xa, 0, 0, 0, 0x14, 'f', 't', 'y', 'p', 'j', 'x', + 'l', ' ', 0, 0, 0, 0, 'j', 'x', 'l', ' '}; + +namespace { +template +uint8_t* Extend(T* vec, size_t size) { + vec->resize(vec->size() + size, 0); + return vec->data() + vec->size() - size; +} +} // namespace + +// Appends a JXL container box header with given type, size, and unbounded +// properties to output. +template +void AppendBoxHeader(const jxl::BoxType& type, size_t size, bool unbounded, + T* output) { + uint64_t box_size = 0; + bool large_size = false; + if (!unbounded) { + box_size = size + 8; + if (box_size >= 0x100000000ull) { + large_size = true; + } + } + + StoreBE32(large_size ? 1 : box_size, Extend(output, 4)); + + for (size_t i = 0; i < 4; i++) { + output->push_back(*(type.data() + i)); + } + + if (large_size) { + StoreBE64(box_size, Extend(output, 8)); + } +} + +} // namespace jxl + +struct JxlEncoderStruct { + JxlMemoryManager memory_manager; + jxl::MemoryManagerUniquePtr thread_pool{ + nullptr, jxl::MemoryManagerDeleteHelper(&memory_manager)}; + std::vector> encoder_options; + + std::vector> + input_frame_queue; + std::vector output_byte_queue; + + bool use_container = false; + bool store_jpeg_metadata = false; + jxl::CodecMetadata metadata; + std::vector jpeg_metadata; + + bool wrote_bytes = false; + jxl::CompressParams last_used_cparams; + + bool input_closed = false; + bool basic_info_set = false; + bool color_encoding_set = false; + + // Takes the first frame in the input_frame_queue, encodes it, and appends the + // bytes to the output_byte_queue. + JxlEncoderStatus RefillOutputByteQueue(); + + // Appends the bytes of a JXL box header with the provided type and size to + // the end of the output_byte_queue. If unbounded is true, the size won't be + // added to the header and the box will be assumed to continue until EOF. + void AppendBoxHeader(const jxl::BoxType& type, size_t size, bool unbounded); +}; + +struct JxlEncoderOptionsStruct { + JxlEncoder* enc; + jxl::JxlEncoderOptionsValues values; +}; + +#endif // LIB_JXL_ENCODE_INTERNAL_H_ diff --git a/lib/jxl/encode_test.cc b/lib/jxl/encode_test.cc new file mode 100644 index 0000000..f12c211 --- /dev/null +++ b/lib/jxl/encode_test.cc @@ -0,0 +1,594 @@ +// 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 "jxl/encode.h" + +#include "gtest/gtest.h" +#include "jxl/encode_cxx.h" +#include "lib/extras/codec.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/jpeg/dec_jpeg_data.h" +#include "lib/jxl/jpeg/dec_jpeg_data_writer.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +TEST(EncodeTest, AddFrameAfterCloseInputTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + + JxlEncoderCloseInput(enc.get()); + + size_t xsize = 64; + size_t ysize = 64; + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + + jxl::CodecInOut input_io = + jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); + + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_ERROR, + JxlEncoderAddImageFrame(options, &pixel_format, pixels.data(), + pixels.size())); +} + +TEST(EncodeTest, AddJPEGAfterCloseTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + + JxlEncoderCloseInput(enc.get()); + + const std::string jpeg_path = + "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); + jxl::CodecInOut orig_io; + ASSERT_TRUE( + SetFromBytes(jxl::Span(orig), &orig_io, /*pool=*/nullptr)); + + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + + EXPECT_EQ(JXL_ENC_ERROR, + JxlEncoderAddJPEGFrame(options, orig.data(), orig.size())); +} + +TEST(EncodeTest, AddFrameBeforeColorEncodingTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + + size_t xsize = 64; + size_t ysize = 64; + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + + jxl::CodecInOut input_io = + jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); + + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_ERROR, + JxlEncoderAddImageFrame(options, &pixel_format, pixels.data(), + pixels.size())); +} + +TEST(EncodeTest, AddFrameBeforeBasicInfoTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + + size_t xsize = 64; + size_t ysize = 64; + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + + jxl::CodecInOut input_io = + jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); + + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_ERROR, + JxlEncoderAddImageFrame(options, &pixel_format, pixels.data(), + pixels.size())); +} + +TEST(EncodeTest, DefaultAllocTest) { + JxlEncoder* enc = JxlEncoderCreate(nullptr); + EXPECT_NE(nullptr, enc); + JxlEncoderDestroy(enc); +} + +TEST(EncodeTest, CustomAllocTest) { + struct CalledCounters { + int allocs = 0; + int frees = 0; + } counters; + + JxlMemoryManager mm; + mm.opaque = &counters; + mm.alloc = [](void* opaque, size_t size) { + reinterpret_cast(opaque)->allocs++; + return malloc(size); + }; + mm.free = [](void* opaque, void* address) { + reinterpret_cast(opaque)->frees++; + free(address); + }; + + { + JxlEncoderPtr enc = JxlEncoderMake(&mm); + EXPECT_NE(nullptr, enc.get()); + EXPECT_LE(1, counters.allocs); + EXPECT_EQ(0, counters.frees); + } + EXPECT_LE(1, counters.frees); +} + +TEST(EncodeTest, DefaultParallelRunnerTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetParallelRunner(enc.get(), nullptr, nullptr)); +} + +void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, + const JxlEncoderOptions* options) { + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + + jxl::CodecInOut input_io = + jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); + + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + if (options->values.lossless) { + basic_info.uses_original_profile = true; + } else { + basic_info.uses_original_profile = false; + } + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddImageFrame(options, &pixel_format, pixels.data(), + pixels.size())); + JxlEncoderCloseInput(enc); + + std::vector compressed = std::vector(64); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + EXPECT_EQ(JXL_ENC_SUCCESS, process_result); + + jxl::DecompressParams dparams; + jxl::CodecInOut decoded_io; + EXPECT_TRUE(jxl::DecodeFile( + dparams, jxl::Span(compressed.data(), compressed.size()), + &decoded_io, /*pool=*/nullptr)); + + jxl::ButteraugliParams ba; + EXPECT_LE(ButteraugliDistance(input_io, decoded_io, ba, + /*distmap=*/nullptr, nullptr), + 3.0f); +} + +void VerifyFrameEncoding(JxlEncoder* enc, const JxlEncoderOptions* options) { + VerifyFrameEncoding(63, 129, enc, options); +} + +TEST(EncodeTest, FrameEncodingTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + VerifyFrameEncoding(enc.get(), JxlEncoderOptionsCreate(enc.get(), nullptr)); +} + +TEST(EncodeTest, EncoderResetTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + VerifyFrameEncoding(50, 200, enc.get(), + JxlEncoderOptionsCreate(enc.get(), nullptr)); + // Encoder should become reusable for a new image from scratch after using + // reset. + JxlEncoderReset(enc.get()); + VerifyFrameEncoding(157, 77, enc.get(), + JxlEncoderOptionsCreate(enc.get(), nullptr)); +} + +TEST(EncodeTest, OptionsTest) { + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderOptionsSetEffort(options, 5)); + VerifyFrameEncoding(enc.get(), options); + EXPECT_EQ(jxl::SpeedTier::kHare, enc->last_used_cparams.speed_tier); + } + + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + // Lower than currently supported values + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderOptionsSetEffort(options, 0)); + // Higher than currently supported values + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderOptionsSetEffort(options, 10)); + } + + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderOptionsSetLossless(options, JXL_TRUE)); + VerifyFrameEncoding(enc.get(), options); + EXPECT_EQ(true, enc->last_used_cparams.IsLossless()); + } + + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderOptionsSetDistance(options, 0.5)); + VerifyFrameEncoding(enc.get(), options); + EXPECT_EQ(0.5, enc->last_used_cparams.butteraugli_distance); + } + + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + // Disallowed negative distance + EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderOptionsSetDistance(options, -1)); + } + + { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderOptionsSetDecodingSpeed(options, 2)); + VerifyFrameEncoding(enc.get(), options); + EXPECT_EQ(2u, enc->last_used_cparams.decoding_speed_tier); + } +} + +namespace { +// Returns a copy of buf from offset to offset+size, or a new zeroed vector if +// the result would have been out of bounds taking integer overflow into +// account. +const std::vector SliceSpan(const jxl::Span& buf, + size_t offset, size_t size) { + if (offset + size >= buf.size()) { + return std::vector(size, 0); + } + if (offset + size < offset) { + return std::vector(size, 0); + } + return std::vector(buf.data() + offset, buf.data() + offset + size); +} + +struct Box { + // The type of the box. + // If "uuid", use extended_type instead + char type[4] = {0, 0, 0, 0}; + + // The extended_type is only used when type == "uuid". + // Extended types are not used in JXL. However, the box format itself + // supports this so they are handled correctly. + char extended_type[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + // Box data. + jxl::Span data = jxl::Span(nullptr, 0); + + // If the size is not given, the datasize extends to the end of the file. + // If this field is false, the size field is not encoded when the box is + // serialized. + bool data_size_given = true; + + // If successful, returns true and sets `in` to be the rest data (if any). + // If `in` contains a box with a size larger than `in.size()`, will not + // modify `in`, and will return true but the data `Span` will + // remain set to nullptr. + // If unsuccessful, returns error and doesn't modify `in`. + jxl::Status Decode(jxl::Span* in) { + // Total box_size including this header itself. + uint64_t box_size = LoadBE32(SliceSpan(*in, 0, 4).data()); + size_t pos = 4; + + memcpy(type, SliceSpan(*in, pos, 4).data(), 4); + pos += 4; + + if (box_size == 1) { + // If the size is 1, it indicates extended size read from 64-bit integer. + box_size = LoadBE64(SliceSpan(*in, pos, 8).data()); + pos += 8; + } + + if (!memcmp("uuid", type, 4)) { + memcpy(extended_type, SliceSpan(*in, pos, 16).data(), 16); + pos += 16; + } + + // This is the end of the box header, the box data begins here. Handle + // the data size now. + const size_t header_size = pos; + + if (box_size != 0) { + if (box_size < header_size) { + return JXL_FAILURE("Invalid box size"); + } + if (box_size > in->size()) { + // The box is fine, but the input is too short. + return true; + } + data_size_given = true; + data = jxl::Span(in->data() + header_size, + box_size - header_size); + } else { + data_size_given = false; + data = jxl::Span(in->data() + header_size, + in->size() - header_size); + } + + *in = jxl::Span(in->data() + header_size + data.size(), + in->size() - header_size - data.size()); + return true; + } +}; + +struct Container { + std::vector boxes; + + // If successful, returns true and sets `in` to be the rest data (if any). + // If unsuccessful, returns error and doesn't modify `in`. + jxl::Status Decode(jxl::Span* in) { + boxes.clear(); + + Box signature_box; + JXL_RETURN_IF_ERROR(signature_box.Decode(in)); + if (memcmp("JXL ", signature_box.type, 4) != 0) { + return JXL_FAILURE("Invalid magic signature"); + } + if (signature_box.data.size() != 4) + return JXL_FAILURE("Invalid magic signature"); + if (signature_box.data[0] != 0xd || signature_box.data[1] != 0xa || + signature_box.data[2] != 0x87 || signature_box.data[3] != 0xa) { + return JXL_FAILURE("Invalid magic signature"); + } + + Box ftyp_box; + JXL_RETURN_IF_ERROR(ftyp_box.Decode(in)); + if (memcmp("ftyp", ftyp_box.type, 4) != 0) { + return JXL_FAILURE("Invalid ftyp"); + } + if (ftyp_box.data.size() != 12) return JXL_FAILURE("Invalid ftyp"); + const char* expected = "jxl \0\0\0\0jxl "; + if (memcmp(expected, ftyp_box.data.data(), 12) != 0) + return JXL_FAILURE("Invalid ftyp"); + + while (in->size() > 0) { + Box box = {}; + JXL_RETURN_IF_ERROR(box.Decode(in)); + if (box.data.data() == nullptr) { + // The decoding encountered a box, but not enough data yet. + return true; + } + boxes.emplace_back(box); + } + + return true; + } +}; + +} // namespace + +TEST(EncodeTest, SingleFrameBoundedJXLCTest) { + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + EXPECT_NE(nullptr, enc.get()); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc.get(), + true)); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + + size_t xsize = 71; + size_t ysize = 23; + JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; + std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); + + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/false); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddImageFrame(options, &pixel_format, pixels.data(), + pixels.size())); + JxlEncoderCloseInput(enc.get()); + + std::vector compressed = std::vector(64); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + EXPECT_EQ(JXL_ENC_SUCCESS, process_result); + + Container container = {}; + jxl::Span encoded_span = + jxl::Span(compressed.data(), compressed.size()); + EXPECT_TRUE(container.Decode(&encoded_span)); + EXPECT_EQ(0u, encoded_span.size()); + EXPECT_EQ(0, memcmp("jxlc", container.boxes[0].type, 4)); + EXPECT_EQ(true, container.boxes[0].data_size_given); +} + +TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { + const std::string jpeg_path = + "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); + jxl::CodecInOut orig_io; + ASSERT_TRUE( + SetFromBytes(jxl::Span(orig), &orig_io, /*pool=*/nullptr)); + + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc.get(), JXL_TRUE)); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderStoreJPEGMetadata(enc.get(), JXL_TRUE)); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddJPEGFrame(options, orig.data(), orig.size())); + JxlEncoderCloseInput(enc.get()); + + std::vector compressed = std::vector(64); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + EXPECT_EQ(JXL_ENC_SUCCESS, process_result); + + Container container = {}; + jxl::Span encoded_span = + jxl::Span(compressed.data(), compressed.size()); + EXPECT_TRUE(container.Decode(&encoded_span)); + EXPECT_EQ(0u, encoded_span.size()); + EXPECT_EQ(0, memcmp("jbrd", container.boxes[0].type, 4)); + EXPECT_EQ(0, memcmp("jxlc", container.boxes[1].type, 4)); + + jxl::CodecInOut decoded_io; + decoded_io.Main().jpeg_data = jxl::make_unique(); + EXPECT_TRUE(jxl::jpeg::DecodeJPEGData(container.boxes[0].data, + decoded_io.Main().jpeg_data.get())); + + jxl::DecompressParams dparams; + dparams.keep_dct = true; + EXPECT_TRUE( + jxl::DecodeFile(dparams, container.boxes[1].data, &decoded_io, nullptr)); + + std::vector decoded_jpeg_bytes; + auto write = [&decoded_jpeg_bytes](const uint8_t* buf, size_t len) { + decoded_jpeg_bytes.insert(decoded_jpeg_bytes.end(), buf, buf + len); + return len; + }; + EXPECT_TRUE(jxl::jpeg::WriteJpeg(*decoded_io.Main().jpeg_data, write)); + + EXPECT_EQ(decoded_jpeg_bytes.size(), orig.size()); + EXPECT_EQ(0, memcmp(decoded_jpeg_bytes.data(), orig.data(), orig.size())); +} + +TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGFrameTest)) { + for (int skip_basic_info = 0; skip_basic_info < 2; skip_basic_info++) { + for (int skip_color_encoding = 0; skip_color_encoding < 2; + skip_color_encoding++) { + const std::string jpeg_path = + "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); + jxl::CodecInOut orig_io; + ASSERT_TRUE(SetFromBytes(jxl::Span(orig), &orig_io, + /*pool=*/nullptr)); + + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + + if (!skip_basic_info) { + JxlBasicInfo basic_info; + JxlEncoderInitBasicInfo(&basic_info); + basic_info.xsize = orig_io.xsize(); + basic_info.ysize = orig_io.ysize(); + basic_info.uses_original_profile = true; + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetBasicInfo(enc.get(), &basic_info)); + } + if (!skip_color_encoding) { + JxlColorEncoding color_encoding; + JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); + } + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddJPEGFrame(options, orig.data(), orig.size())); + JxlEncoderCloseInput(enc.get()); + + std::vector compressed = std::vector(64); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size() - (next_out - compressed.data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = + JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() * 2); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + compressed.resize(next_out - compressed.data()); + EXPECT_EQ(JXL_ENC_SUCCESS, process_result); + + jxl::DecompressParams dparams; + jxl::CodecInOut decoded_io; + EXPECT_TRUE(jxl::DecodeFile( + dparams, + jxl::Span(compressed.data(), compressed.size()), + &decoded_io, /*pool=*/nullptr)); + + jxl::ButteraugliParams ba; + EXPECT_LE(ButteraugliDistance(orig_io, decoded_io, ba, + /*distmap=*/nullptr, nullptr), + 2.5f); + } + } +} diff --git a/lib/jxl/entropy_coder.cc b/lib/jxl/entropy_coder.cc new file mode 100644 index 0000000..0043c2d --- /dev/null +++ b/lib/jxl/entropy_coder.cc @@ -0,0 +1,70 @@ +// 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/entropy_coder.h" + +#include +#include + +#include +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_context_map.h" +#include "lib/jxl/epf.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +Status DecodeBlockCtxMap(BitReader* br, BlockCtxMap* block_ctx_map) { + auto& dct = block_ctx_map->dc_thresholds; + auto& qft = block_ctx_map->qf_thresholds; + auto& ctx_map = block_ctx_map->ctx_map; + bool is_default = br->ReadFixedBits<1>(); + if (is_default) { + *block_ctx_map = BlockCtxMap(); + return true; + } + block_ctx_map->num_dc_ctxs = 1; + for (int j : {0, 1, 2}) { + dct[j].resize(br->ReadFixedBits<4>()); + block_ctx_map->num_dc_ctxs *= dct[j].size() + 1; + for (int& i : dct[j]) { + i = UnpackSigned(U32Coder::Read(kDCThresholdDist, br)); + } + } + qft.resize(br->ReadFixedBits<4>()); + for (uint32_t& i : qft) { + i = U32Coder::Read(kQFThresholdDist, br) + 1; + } + + if (block_ctx_map->num_dc_ctxs * (qft.size() + 1) > 64) { + return JXL_FAILURE("Invalid block context map: too big"); + } + + ctx_map.resize(3 * kNumOrders * block_ctx_map->num_dc_ctxs * + (qft.size() + 1)); + JXL_RETURN_IF_ERROR(DecodeContextMap(&ctx_map, &block_ctx_map->num_ctxs, br)); + if (block_ctx_map->num_ctxs > 16) { + return JXL_FAILURE("Invalid block context map: too many distinct contexts"); + } + return true; +} + +constexpr uint8_t BlockCtxMap::kDefaultCtxMap[]; // from ac_context.h + +} // namespace jxl diff --git a/lib/jxl/entropy_coder.h b/lib/jxl/entropy_coder.h new file mode 100644 index 0000000..e4afa7a --- /dev/null +++ b/lib/jxl/entropy_coder.h @@ -0,0 +1,45 @@ +// 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_ENTROPY_CODER_H_ +#define LIB_JXL_ENTROPY_CODER_H_ + +#include +#include + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/field_encodings.h" + +// Entropy coding and context modeling of DC and AC coefficients, as well as AC +// strategy and quantization field. + +namespace jxl { + +static JXL_INLINE int32_t PredictFromTopAndLeft( + const int32_t* const JXL_RESTRICT row_top, + const int32_t* const JXL_RESTRICT row, size_t x, int32_t default_val) { + if (x == 0) { + return row_top == nullptr ? default_val : row_top[x]; + } + if (row_top == nullptr) { + return row[x - 1]; + } + return (row_top[x] + row[x - 1] + 1) / 2; +} + +static constexpr U32Enc kDCThresholdDist(Bits(4), BitsOffset(8, 16), + BitsOffset(16, 272), + BitsOffset(32, 65808)); + +static constexpr U32Enc kQFThresholdDist(Bits(2), BitsOffset(3, 4), + BitsOffset(5, 12), BitsOffset(8, 44)); + +Status DecodeBlockCtxMap(BitReader* br, BlockCtxMap* block_ctx_map); + +} // namespace jxl + +#endif // LIB_JXL_ENTROPY_CODER_H_ diff --git a/lib/jxl/entropy_coder_test.cc b/lib/jxl/entropy_coder_test.cc new file mode 100644 index 0000000..b613106 --- /dev/null +++ b/lib/jxl/entropy_coder_test.cc @@ -0,0 +1,70 @@ +// 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. + +// TODO(deymo): Move these tests to dec_ans.h and common.h + +#include + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" + +namespace jxl { +namespace { + +TEST(EntropyCoderTest, PackUnpack) { + for (int32_t i = -31; i < 32; ++i) { + uint32_t packed = PackSigned(i); + EXPECT_LT(packed, 63u); + int32_t unpacked = UnpackSigned(packed); + EXPECT_EQ(i, unpacked); + } +} + +struct DummyBitReader { + uint32_t nbits, bits; + void Consume(uint32_t nbits) {} + uint32_t PeekBits(uint32_t n) { + EXPECT_EQ(n, nbits); + return bits; + } +}; + +void HybridUintRoundtrip(HybridUintConfig config, size_t limit = 1 << 24) { + std::mt19937 rng(0); + std::uniform_int_distribution dist(0, limit); + constexpr size_t kNumIntegers = 1 << 20; + std::vector integers(kNumIntegers); + std::vector token(kNumIntegers); + std::vector nbits(kNumIntegers); + std::vector bits(kNumIntegers); + for (size_t i = 0; i < kNumIntegers; i++) { + integers[i] = dist(rng); + config.Encode(integers[i], &token[i], &nbits[i], &bits[i]); + } + for (size_t i = 0; i < kNumIntegers; i++) { + DummyBitReader br{nbits[i], bits[i]}; + EXPECT_EQ(integers[i], + ANSSymbolReader::ReadHybridUintConfig(config, token[i], &br)); + } +} + +TEST(HybridUintTest, Test000) { + HybridUintRoundtrip(HybridUintConfig{0, 0, 0}); +} +TEST(HybridUintTest, Test411) { + HybridUintRoundtrip(HybridUintConfig{4, 1, 1}); +} +TEST(HybridUintTest, Test420) { + HybridUintRoundtrip(HybridUintConfig{4, 2, 0}); +} +TEST(HybridUintTest, Test421) { + HybridUintRoundtrip(HybridUintConfig{4, 2, 1}, 256); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/epf.cc b/lib/jxl/epf.cc new file mode 100644 index 0000000..b40a9a2 --- /dev/null +++ b/lib/jxl/epf.cc @@ -0,0 +1,684 @@ +// 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. + +// Edge-preserving smoothing: weighted average based on L1 patch similarity. + +#include "lib/jxl/epf.h" + +#include +#include +#include +#include +#include + +#include +#include +#include // std::accumulate +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/epf.cc" +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/filters.h" +#include "lib/jxl/filters_internal.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Vec; + +// The EPF logic treats 8x8 blocks as one unit, each with their own sigma. +// It should be possible to do two blocks at a time in AVX3 vectors, at some +// increase in complexity (broadcasting sigma0/1 to lanes 0..7 and 8..15). +using DF = HWY_CAPPED(float, GroupBorderAssigner::kPaddingXRound); +using DU = HWY_CAPPED(uint32_t, GroupBorderAssigner::kPaddingXRound); + +// kInvSigmaNum / 0.3 +constexpr float kMinSigma = -3.90524291751269967465540850526868f; + +DF df; + +JXL_INLINE Vec Weight(Vec sad, Vec inv_sigma, Vec thres) { + auto v = MulAdd(sad, inv_sigma, Set(DF(), 1.0f)); + auto v2 = v * v; + return IfThenZeroElse(v <= thres, v2); +} + +template +JXL_INLINE void AddPixelStep1(int row, const FilterRows& rows, size_t x, + Vec sad, Vec inv_sigma, + const LoopFilter& lf, Vec* JXL_RESTRICT X, + Vec* JXL_RESTRICT Y, Vec* JXL_RESTRICT B, + Vec* JXL_RESTRICT w) { + auto cx = aligned ? Load(DF(), rows.GetInputRow(row, 0) + x) + : LoadU(DF(), rows.GetInputRow(row, 0) + x); + auto cy = aligned ? Load(DF(), rows.GetInputRow(row, 1) + x) + : LoadU(DF(), rows.GetInputRow(row, 1) + x); + auto cb = aligned ? Load(DF(), rows.GetInputRow(row, 2) + x) + : LoadU(DF(), rows.GetInputRow(row, 2) + x); + + auto weight = Weight(sad, inv_sigma, Set(df, lf.epf_pass1_zeroflush)); + *w += weight; + *X = MulAdd(weight, cx, *X); + *Y = MulAdd(weight, cy, *Y); + *B = MulAdd(weight, cb, *B); +} + +template +JXL_INLINE void AddPixelStep2(int row, const FilterRows& rows, size_t x, + Vec rx, Vec ry, Vec rb, + Vec inv_sigma, const LoopFilter& lf, + Vec* JXL_RESTRICT X, Vec* JXL_RESTRICT Y, + Vec* JXL_RESTRICT B, + Vec* JXL_RESTRICT w) { + auto cx = aligned ? Load(DF(), rows.GetInputRow(row, 0) + x) + : LoadU(DF(), rows.GetInputRow(row, 0) + x); + auto cy = aligned ? Load(DF(), rows.GetInputRow(row, 1) + x) + : LoadU(DF(), rows.GetInputRow(row, 1) + x); + auto cb = aligned ? Load(DF(), rows.GetInputRow(row, 2) + x) + : LoadU(DF(), rows.GetInputRow(row, 2) + x); + + auto sad = AbsDiff(cx, rx) * Set(df, lf.epf_channel_scale[0]); + sad = MulAdd(AbsDiff(cy, ry), Set(df, lf.epf_channel_scale[1]), sad); + sad = MulAdd(AbsDiff(cb, rb), Set(df, lf.epf_channel_scale[2]), sad); + + auto weight = Weight(sad, inv_sigma, Set(df, lf.epf_pass2_zeroflush)); + + *w += weight; + *X = MulAdd(weight, cx, *X); + *Y = MulAdd(weight, cy, *Y); + *B = MulAdd(weight, cb, *B); +} + +template +void GaborishVector(const D df, const float* JXL_RESTRICT row_t, + const float* JXL_RESTRICT row_m, + const float* JXL_RESTRICT row_b, const V w0, const V w1, + const V w2, float* JXL_RESTRICT row_out) { +// Filter x0 is only aligned to blocks (8 floats = 32 bytes). For larger +// vectors, treat loads as unaligned (we manually align the Store). +#undef LoadMaybeU +#if HWY_CAP_GE512 +#define LoadMaybeU LoadU +#else +#define LoadMaybeU Load +#endif + + const auto t = LoadMaybeU(df, row_t); + const auto tl = LoadU(df, row_t - 1); + const auto tr = LoadU(df, row_t + 1); + const auto m = LoadMaybeU(df, row_m); + const auto l = LoadU(df, row_m - 1); + const auto r = LoadU(df, row_m + 1); + const auto b = LoadMaybeU(df, row_b); + const auto bl = LoadU(df, row_b - 1); + const auto br = LoadU(df, row_b + 1); + const auto sum0 = m; + const auto sum1 = (l + r) + (t + b); + const auto sum2 = (tl + tr) + (bl + br); + auto pixels = MulAdd(sum2, w2, MulAdd(sum1, w1, sum0 * w0)); + Store(pixels, df, row_out); +} + +void GaborishRow(const FilterRows& rows, const LoopFilter& /* lf */, + const FilterWeights& filter_weights, size_t x0, size_t x1, + size_t /*sigma_x_offset*/, size_t /* image_y_mod_8 */) { + JXL_DASSERT(x0 % Lanes(df) == 0); + + const float* JXL_RESTRICT gab_weights = filter_weights.gab_weights; + for (size_t c = 0; c < 3; c++) { + const float* JXL_RESTRICT row_t = rows.GetInputRow(-1, c); + const float* JXL_RESTRICT row_m = rows.GetInputRow(0, c); + const float* JXL_RESTRICT row_b = rows.GetInputRow(1, c); + float* JXL_RESTRICT row_out = rows.GetOutputRow(c); + + size_t ix = x0; + +#if HWY_CAP_GE512 + const HWY_FULL(float) dfull; // Gaborish is not block-dependent. + + // For AVX3, x0 might only be aligned to 8, not 16; if so, do a capped + // vector first to ensure full (Store-only!) alignment, then full vectors. + const uintptr_t addr = reinterpret_cast(row_out + ix); + if ((addr % 64) != 0 && ix < x1) { + const auto w0 = Set(df, gab_weights[3 * c + 0]); + const auto w1 = Set(df, gab_weights[3 * c + 1]); + const auto w2 = Set(df, gab_weights[3 * c + 2]); + GaborishVector(df, row_t + ix, row_m + ix, row_b + ix, w0, w1, w2, + row_out + ix); + ix += Lanes(df); + } + + const auto wfull0 = Set(dfull, gab_weights[3 * c + 0]); + const auto wfull1 = Set(dfull, gab_weights[3 * c + 1]); + const auto wfull2 = Set(dfull, gab_weights[3 * c + 2]); + for (; ix + Lanes(dfull) <= x1; ix += Lanes(dfull)) { + GaborishVector(dfull, row_t + ix, row_m + ix, row_b + ix, wfull0, wfull1, + wfull2, row_out + ix); + } +#endif + + // Non-AVX3 loop, or last capped vector for AVX3, if necessary + const auto w0 = Set(df, gab_weights[3 * c + 0]); + const auto w1 = Set(df, gab_weights[3 * c + 1]); + const auto w2 = Set(df, gab_weights[3 * c + 2]); + for (; ix < x1; ix += Lanes(df)) { + GaborishVector(df, row_t + ix, row_m + ix, row_b + ix, w0, w1, w2, + row_out + ix); + } + } +} + +// Step 0: 5x5 plus-shaped kernel with 5 SADs per pixel (3x3 +// plus-shaped). So this makes this filter a 7x7 filter. +void Epf0Row(const FilterRows& rows, const LoopFilter& lf, + const FilterWeights& filter_weights, size_t x0, size_t x1, + size_t sigma_x_offset, size_t image_y_mod_8) { + JXL_DASSERT(x0 % Lanes(df) == 0); + const float* JXL_RESTRICT row_sigma = rows.GetSigmaRow(); + + float sm = lf.epf_pass0_sigma_scale; + float bsm = sm * lf.epf_border_sad_mul; + + HWY_ALIGN float sad_mul[kBlockDim] = {bsm, sm, sm, sm, sm, sm, sm, bsm}; + + if (image_y_mod_8 == 0 || image_y_mod_8 == kBlockDim - 1) { + for (size_t i = 0; i < kBlockDim; i += Lanes(df)) { + Store(Set(df, bsm), df, sad_mul + i); + } + } + + for (size_t x = x0; x < x1; x += Lanes(df)) { + size_t bx = (x + sigma_x_offset) / kBlockDim; + size_t ix = (x + sigma_x_offset) % kBlockDim; + if (row_sigma[bx] < kMinSigma) { + for (size_t c = 0; c < 3; c++) { + auto px = Load(df, rows.GetInputRow(0, c) + x); + Store(px, df, rows.GetOutputRow(c) + x); + } + continue; + } + + const auto sm = Load(df, sad_mul + ix); + const auto inv_sigma = Set(DF(), row_sigma[bx]) * sm; + + decltype(Zero(df)) sads[12]; + for (size_t i = 0; i < 12; i++) sads[i] = Zero(df); + constexpr std::array sads_off[12] = { + {{-2, 0}}, {{-1, -1}}, {{-1, 0}}, {{-1, 1}}, {{0, -2}}, {{0, -1}}, + {{0, 1}}, {{0, 2}}, {{1, -1}}, {{1, 0}}, {{1, 1}}, {{2, 0}}, + }; + + // compute sads + // TODO(veluca): consider unrolling and optimizing this. + for (size_t c = 0; c < 3; c++) { + auto scale = Set(df, lf.epf_channel_scale[c]); + for (size_t i = 0; i < 12; i++) { + auto sad = Zero(df); + constexpr std::array plus_off[] = { + {{0, 0}}, {{-1, 0}}, {{0, -1}}, {{1, 0}}, {{0, 1}}}; + for (size_t j = 0; j < 5; j++) { + const auto r11 = LoadU( + df, rows.GetInputRow(plus_off[j][0], c) + x + plus_off[j][1]); + const auto c11 = + LoadU(df, rows.GetInputRow(sads_off[i][0] + plus_off[j][0], c) + + x + sads_off[i][1] + plus_off[j][1]); + sad += AbsDiff(r11, c11); + } + sads[i] = MulAdd(sad, scale, sads[i]); + } + } + const auto x_cc = LoadU(df, rows.GetInputRow(0, 0) + x); + const auto y_cc = LoadU(df, rows.GetInputRow(0, 1) + x); + const auto b_cc = LoadU(df, rows.GetInputRow(0, 2) + x); + + auto w = Set(df, 1); + auto X = x_cc; + auto Y = y_cc; + auto B = b_cc; + + for (size_t i = 0; i < 12; i++) { + AddPixelStep1(/*row=*/sads_off[i][0], rows, + x + sads_off[i][1], sads[i], inv_sigma, + lf, &X, &Y, &B, &w); + } + +#if JXL_HIGH_PRECISION + auto inv_w = Set(df, 1.0f) / w; +#else + auto inv_w = ApproximateReciprocal(w); +#endif + Store(X * inv_w, df, rows.GetOutputRow(0) + x); + Store(Y * inv_w, df, rows.GetOutputRow(1) + x); + Store(B * inv_w, df, rows.GetOutputRow(2) + x); + } +} + +// Step 1: 3x3 plus-shaped kernel with 5 SADs per pixel (also 3x3 +// plus-shaped). So this makes this filter a 5x5 filter. +void Epf1Row(const FilterRows& rows, const LoopFilter& lf, + const FilterWeights& filter_weights, size_t x0, size_t x1, + size_t sigma_x_offset, size_t image_y_mod_8) { + JXL_DASSERT(x0 % Lanes(df) == 0); + const float* JXL_RESTRICT row_sigma = rows.GetSigmaRow(); + + float sm = 1.0f; + float bsm = sm * lf.epf_border_sad_mul; + + HWY_ALIGN float sad_mul[kBlockDim] = {bsm, sm, sm, sm, sm, sm, sm, bsm}; + + if (image_y_mod_8 == 0 || image_y_mod_8 == kBlockDim - 1) { + for (size_t i = 0; i < kBlockDim; i += Lanes(df)) { + Store(Set(df, bsm), df, sad_mul + i); + } + } + + for (size_t x = x0; x < x1; x += Lanes(df)) { + size_t bx = (x + sigma_x_offset) / kBlockDim; + size_t ix = (x + sigma_x_offset) % kBlockDim; + if (row_sigma[bx] < kMinSigma) { + for (size_t c = 0; c < 3; c++) { + auto px = Load(df, rows.GetInputRow(0, c) + x); + Store(px, df, rows.GetOutputRow(c) + x); + } + continue; + } + + const auto sm = Load(df, sad_mul + ix); + const auto inv_sigma = Set(DF(), row_sigma[bx]) * sm; + auto sad0 = Zero(df); + auto sad1 = Zero(df); + auto sad2 = Zero(df); + auto sad3 = Zero(df); + + // compute sads + for (size_t c = 0; c < 3; c++) { + // center px = 22, px above = 21 + auto t = Undefined(df); + + const auto p20 = Load(df, rows.GetInputRow(-2, c) + x); + const auto p21 = Load(df, rows.GetInputRow(-1, c) + x); + auto sad0c = AbsDiff(p20, p21); // SAD 2, 1 + + const auto p11 = LoadU(df, rows.GetInputRow(-1, c) + x - 1); + auto sad1c = AbsDiff(p11, p21); // SAD 1, 2 + + const auto p31 = LoadU(df, rows.GetInputRow(-1, c) + x + 1); + auto sad2c = AbsDiff(p31, p21); // SAD 3, 2 + + const auto p02 = LoadU(df, rows.GetInputRow(0, c) + x - 2); + const auto p12 = LoadU(df, rows.GetInputRow(0, c) + x - 1); + sad1c += AbsDiff(p02, p12); // SAD 1, 2 + sad0c += AbsDiff(p11, p12); // SAD 2, 1 + + const auto p22 = LoadU(df, rows.GetInputRow(0, c) + x); + t = AbsDiff(p12, p22); + sad1c += t; // SAD 1, 2 + sad2c += t; // SAD 3, 2 + t = AbsDiff(p22, p21); + auto sad3c = t; // SAD 2, 3 + sad0c += t; // SAD 2, 1 + + const auto p32 = LoadU(df, rows.GetInputRow(0, c) + x + 1); + sad0c += AbsDiff(p31, p32); // SAD 2, 1 + t = AbsDiff(p22, p32); + sad1c += t; // SAD 1, 2 + sad2c += t; // SAD 3, 2 + + const auto p42 = LoadU(df, rows.GetInputRow(0, c) + x + 2); + sad2c += AbsDiff(p42, p32); // SAD 3, 2 + + const auto p13 = LoadU(df, rows.GetInputRow(1, c) + x - 1); + sad3c += AbsDiff(p13, p12); // SAD 2, 3 + + const auto p23 = Load(df, rows.GetInputRow(1, c) + x); + t = AbsDiff(p22, p23); + sad0c += t; // SAD 2, 1 + sad3c += t; // SAD 2, 3 + sad1c += AbsDiff(p13, p23); // SAD 1, 2 + + const auto p33 = LoadU(df, rows.GetInputRow(1, c) + x + 1); + sad2c += AbsDiff(p33, p23); // SAD 3, 2 + sad3c += AbsDiff(p33, p32); // SAD 2, 3 + + const auto p24 = Load(df, rows.GetInputRow(2, c) + x); + sad3c += AbsDiff(p24, p23); // SAD 2, 3 + + auto scale = Set(df, lf.epf_channel_scale[c]); + sad0 = MulAdd(sad0c, scale, sad0); + sad1 = MulAdd(sad1c, scale, sad1); + sad2 = MulAdd(sad2c, scale, sad2); + sad3 = MulAdd(sad3c, scale, sad3); + } + const auto x_cc = Load(df, rows.GetInputRow(0, 0) + x); + const auto y_cc = Load(df, rows.GetInputRow(0, 1) + x); + const auto b_cc = Load(df, rows.GetInputRow(0, 2) + x); + + auto w = Set(df, 1); + auto X = x_cc; + auto Y = y_cc; + auto B = b_cc; + + // Top row + AddPixelStep1(/*row=*/-1, rows, x, sad0, inv_sigma, lf, + &X, &Y, &B, &w); + // Center + AddPixelStep1(/*row=*/0, rows, x - 1, sad1, inv_sigma, + lf, &X, &Y, &B, &w); + AddPixelStep1(/*row=*/0, rows, x + 1, sad2, inv_sigma, + lf, &X, &Y, &B, &w); + // Bottom + AddPixelStep1(/*row=*/1, rows, x, sad3, inv_sigma, lf, &X, + &Y, &B, &w); +#if JXL_HIGH_PRECISION + auto inv_w = Set(df, 1.0f) / w; +#else + auto inv_w = ApproximateReciprocal(w); +#endif + Store(X * inv_w, df, rows.GetOutputRow(0) + x); + Store(Y * inv_w, df, rows.GetOutputRow(1) + x); + Store(B * inv_w, df, rows.GetOutputRow(2) + x); + } +} + +// Step 2: 3x3 plus-shaped kernel with a single reference pixel, ran on +// the output of the previous step. +void Epf2Row(const FilterRows& rows, const LoopFilter& lf, + const FilterWeights& filter_weights, size_t x0, size_t x1, + size_t sigma_x_offset, size_t image_y_mod_8) { + JXL_DASSERT(x0 % Lanes(df) == 0); + const float* JXL_RESTRICT row_sigma = rows.GetSigmaRow(); + + float sm = lf.epf_pass2_sigma_scale; + float bsm = sm * lf.epf_border_sad_mul; + + HWY_ALIGN float sad_mul[kBlockDim] = {bsm, sm, sm, sm, sm, sm, sm, bsm}; + + if (image_y_mod_8 == 0 || image_y_mod_8 == kBlockDim - 1) { + for (size_t i = 0; i < kBlockDim; i += Lanes(df)) { + Store(Set(df, bsm), df, sad_mul + i); + } + } + + for (size_t x = x0; x < x1; x += Lanes(df)) { + size_t bx = (x + sigma_x_offset) / kBlockDim; + size_t ix = (x + sigma_x_offset) % kBlockDim; + + if (row_sigma[bx] < kMinSigma) { + for (size_t c = 0; c < 3; c++) { + auto px = Load(df, rows.GetInputRow(0, c) + x); + Store(px, df, rows.GetOutputRow(c) + x); + } + continue; + } + + const auto sm = Load(df, sad_mul + ix); + const auto inv_sigma = Set(DF(), row_sigma[bx]) * sm; + + const auto x_cc = Load(df, rows.GetInputRow(0, 0) + x); + const auto y_cc = Load(df, rows.GetInputRow(0, 1) + x); + const auto b_cc = Load(df, rows.GetInputRow(0, 2) + x); + + auto w = Set(df, 1); + auto X = x_cc; + auto Y = y_cc; + auto B = b_cc; + + // Top row + AddPixelStep2(/*row=*/-1, rows, x, x_cc, y_cc, b_cc, + inv_sigma, lf, &X, &Y, &B, &w); + // Center + AddPixelStep2(/*row=*/0, rows, x - 1, x_cc, y_cc, b_cc, + inv_sigma, lf, &X, &Y, &B, &w); + AddPixelStep2(/*row=*/0, rows, x + 1, x_cc, y_cc, b_cc, + inv_sigma, lf, &X, &Y, &B, &w); + // Bottom + AddPixelStep2(/*row=*/1, rows, x, x_cc, y_cc, b_cc, + inv_sigma, lf, &X, &Y, &B, &w); + +#if JXL_HIGH_PRECISION + auto inv_w = Set(df, 1.0f) / w; +#else + auto inv_w = ApproximateReciprocal(w); +#endif + Store(X * inv_w, df, rows.GetOutputRow(0) + x); + Store(Y * inv_w, df, rows.GetOutputRow(1) + x); + Store(B * inv_w, df, rows.GetOutputRow(2) + x); + } +} + +constexpr FilterDefinition kGaborishFilter{&GaborishRow, 1}; +constexpr FilterDefinition kEpf0Filter{&Epf0Row, 3}; +constexpr FilterDefinition kEpf1Filter{&Epf1Row, 2}; +constexpr FilterDefinition kEpf2Filter{&Epf2Row, 1}; + +void FilterPipelineInit(FilterPipeline* fp, const LoopFilter& lf, + const Image3F& in, const Rect& in_rect, + const Rect& image_rect, size_t image_ysize, + Image3F* out, const Rect& out_rect) { + JXL_DASSERT(lf.gab || lf.epf_iters > 0); + // All EPF filters use sigma so we need to compute it. + fp->compute_sigma = lf.epf_iters > 0; + + fp->num_filters = 0; + fp->storage_rows_used = 0; + // First filter always uses the input image. + fp->filters[0].SetInput(&in, in_rect, image_rect, image_ysize); + + if (lf.gab) { + fp->AddStep(kGaborishFilter); + } + + if (lf.epf_iters == 1) { + fp->AddStep(kEpf1Filter); + } else if (lf.epf_iters == 2) { + fp->AddStep(kEpf1Filter); + fp->AddStep(kEpf2Filter); + } else if (lf.epf_iters == 3) { + fp->AddStep(kEpf0Filter); + fp->AddStep(kEpf1Filter); + fp->AddStep(kEpf2Filter); + } + + // At least one of the filters was enabled so "num_filters" must be non-zero. + JXL_DASSERT(fp->num_filters > 0); + + // Set the output of the last filter as the output image. + fp->filters[fp->num_filters - 1].SetOutput(out, out_rect); + + // Walk the list of filters backwards to compute how many rows are needed. + size_t col_border = 0; + for (int i = fp->num_filters - 1; i >= 0; i--) { + // Compute the region where we need to apply this filter. Depending on the + // step we might need to compute a larger portion than the original rect + // because of the border needed by other stages. This is the range of valid + // output values we produce, however we run the filter over a larger region + // to make those values multiple of Lanes(df). + const size_t x0 = + FilterPipeline::FilterStep::MaxLeftPadding(image_rect.x0()) - + col_border; + const size_t x1 = + FilterPipeline::FilterStep::MaxLeftPadding(image_rect.x0()) + + image_rect.xsize() + col_border; + + fp->filters[i].filter_x0 = x0 - x0 % Lanes(df); + fp->filters[i].filter_x1 = RoundUpTo(x1, Lanes(df)); + + // The extra border needed for future filtering. + fp->filters[i].output_col_border = col_border; + col_border += fp->filters[i].filter_def.border; + } + fp->total_border = col_border; + JXL_ASSERT(fp->total_border == lf.Padding()); + JXL_ASSERT(fp->total_border <= kMaxFilterBorder); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(FilterPipelineInit); // Local function + +// Mirror n floats starting at *p and store them before p. +JXL_INLINE void LeftMirror(float* p, size_t n) { + for (size_t i = 0; i < n; i++) { + *(p - 1 - i) = p[i]; + } +} + +// Mirror n floats starting at *(p - n) and store them at *p. +JXL_INLINE void RightMirror(float* p, size_t n) { + for (size_t i = 0; i < n; i++) { + p[i] = *(p - 1 - i); + } +} + +void ComputeSigma(const Rect& block_rect, PassesDecoderState* state) { + const LoopFilter& lf = state->shared->frame_header.loop_filter; + JXL_CHECK(lf.epf_iters > 0); + const AcStrategyImage& ac_strategy = state->shared->ac_strategy; + const float quant_scale = state->shared->quantizer.Scale(); + + const size_t sigma_stride = state->filter_weights.sigma.PixelsPerRow(); + const size_t sharpness_stride = state->shared->epf_sharpness.PixelsPerRow(); + + for (size_t by = 0; by < block_rect.ysize(); ++by) { + float* JXL_RESTRICT sigma_row = + block_rect.Row(&state->filter_weights.sigma, by); + const uint8_t* JXL_RESTRICT sharpness_row = + block_rect.ConstRow(state->shared->epf_sharpness, by); + AcStrategyRow acs_row = ac_strategy.ConstRow(block_rect, by); + const int* const JXL_RESTRICT row_quant = + block_rect.ConstRow(state->shared->raw_quant_field, by); + + for (size_t bx = 0; bx < block_rect.xsize(); bx++) { + AcStrategy acs = acs_row[bx]; + size_t llf_x = acs.covered_blocks_x(); + if (!acs.IsFirstBlock()) continue; + // quant_scale is smaller for low quality. + // quant_scale is roughly 0.08 / butteraugli score. + // + // row_quant is smaller for low quality. + // row_quant is a quantization multiplier of form 1.0 / + // row_quant[bx] + // + // lf.epf_quant_mul is a parameter in the format + // kInvSigmaNum is a constant + float sigma_quant = + lf.epf_quant_mul / (quant_scale * row_quant[bx] * kInvSigmaNum); + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + float sigma = + sigma_quant * + lf.epf_sharp_lut[sharpness_row[bx + ix + iy * sharpness_stride]]; + // Avoid infinities. + sigma = std::min(-1e-4f, sigma); // TODO(veluca): remove this. + sigma_row[bx + ix + kSigmaPadding + + (iy + kSigmaPadding) * sigma_stride] = 1.0f / sigma; + } + } + // TODO(veluca): remove this padding. + // Left padding with mirroring. + if (bx + block_rect.x0() == 0) { + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + LeftMirror( + sigma_row + kSigmaPadding + (iy + kSigmaPadding) * sigma_stride, + kSigmaBorder); + } + } + // Right padding with mirroring. + if (bx + block_rect.x0() + llf_x == + state->shared->frame_dim.xsize_blocks) { + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + RightMirror(sigma_row + kSigmaPadding + bx + llf_x + + (iy + kSigmaPadding) * sigma_stride, + kSigmaBorder); + } + } + // Offsets for row copying, in blocks. + size_t offset_before = bx + block_rect.x0() == 0 ? 1 : bx + kSigmaPadding; + size_t offset_after = + bx + block_rect.x0() + llf_x == state->shared->frame_dim.xsize_blocks + ? kSigmaPadding + llf_x + bx + kSigmaBorder + : kSigmaPadding + llf_x + bx; + size_t num = offset_after - offset_before; + // Above + if (by + block_rect.y0() == 0) { + for (size_t iy = 0; iy < kSigmaBorder; iy++) { + memcpy( + sigma_row + offset_before + + (kSigmaPadding - 1 - iy) * sigma_stride, + sigma_row + offset_before + (kSigmaPadding + iy) * sigma_stride, + num * sizeof(*sigma_row)); + } + } + // Below + if (by + block_rect.y0() + acs.covered_blocks_y() == + state->shared->frame_dim.ysize_blocks) { + for (size_t iy = 0; iy < kSigmaBorder; iy++) { + memcpy( + sigma_row + offset_before + + sigma_stride * (acs.covered_blocks_y() + kSigmaPadding + iy), + sigma_row + offset_before + + sigma_stride * + (acs.covered_blocks_y() + kSigmaPadding - 1 - iy), + num * sizeof(*sigma_row)); + } + } + } + } +} + +FilterPipeline* PrepareFilterPipeline( + PassesDecoderState* dec_state, const Rect& image_rect, const Image3F& input, + const Rect& input_rect, size_t image_ysize, size_t thread, + Image3F* JXL_RESTRICT out, const Rect& output_rect) { + const LoopFilter& lf = dec_state->shared->frame_header.loop_filter; + // image_rect, input and output must all have the same kPaddingXRound + // alignment for SIMD, but it doesn't need to be 0. + JXL_DASSERT(image_rect.x0() % GroupBorderAssigner::kPaddingXRound == + input_rect.x0() % GroupBorderAssigner::kPaddingXRound); + JXL_DASSERT(image_rect.x0() % GroupBorderAssigner::kPaddingXRound == + output_rect.x0() % GroupBorderAssigner::kPaddingXRound); + + // We need enough pixels to access the padding and the rounding to + // GroupBorderAssigner::kPaddingXRound to the left of the image. + JXL_DASSERT(input_rect.x0() >= + input_rect.x0() % GroupBorderAssigner::kPaddingXRound + + lf.Padding()); + + JXL_DASSERT(image_rect.xsize() == input_rect.xsize()); + JXL_DASSERT(image_rect.xsize() == output_rect.xsize()); + FilterPipeline* fp = &(dec_state->filter_pipelines[thread]); + fp->image_rect = image_rect; + + HWY_DYNAMIC_DISPATCH(FilterPipelineInit) + (fp, lf, input, input_rect, image_rect, image_ysize, out, output_rect); + return fp; +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/epf.h b/lib/jxl/epf.h new file mode 100644 index 0000000..a2fd9d1 --- /dev/null +++ b/lib/jxl/epf.h @@ -0,0 +1,55 @@ +// 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_EPF_H_ +#define LIB_JXL_EPF_H_ + +// Fast SIMD "in-loop" edge preserving filter (adaptive, nonlinear). + +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_cache.h" +#include "lib/jxl/filters.h" +#include "lib/jxl/passes_state.h" + +namespace jxl { + +// 4 * (sqrt(0.5)-1), so that Weight(sigma) = 0.5. +static constexpr float kInvSigmaNum = -1.1715728752538099024f; + +// Fills the `state->filter_weights.sigma` image with the precomputed sigma +// values in the area inside `block_rect`. Accesses the AC strategy, quant field +// and epf_sharpness fields in the corresponding positions. +void ComputeSigma(const Rect& block_rect, PassesDecoderState* state); + +// Applies Gaborish + EPF to the given `image_rect` part of the image (used to +// select the sigma values). Input pixels are taken from `input:input_rect`, and +// the filtering result is written to `out:output_rect`. `dec_state->sigma` must +// be padded with `kMaxFilterPadding/kBlockDim` values along the x axis. +// All rects must have the same alignment module +// GroupBorderAssigner::kPaddingXRound pixels. +// `input_rect`, `output_rect` and `image_rect` must all have the same size. +// At least `lf.Padding()` pixels must be accessible and contain valid values +// outside of `image_rect` in `input`. Also, depending on the implementation, +// more pixels in the input up to a vector size boundary should be accessible +// but may contain uninitialized data. +// +// This function only prepares and returns the pipeline, to perform the +// filtering process it must be called on all row from -lf.Padding() to +// image_rect.ysize() + lf.Padding() . +// +// Note: if the output_rect x0 or x1 are not a multiple of kPaddingXRound more +// pixels with potentially uninitialized data will be written to the output left +// and right of the requested rect up to a multiple of kPaddingXRound pixels. +FilterPipeline* PrepareFilterPipeline( + PassesDecoderState* dec_state, const Rect& image_rect, const Image3F& input, + const Rect& input_rect, size_t image_ysize, size_t thread, + Image3F* JXL_RESTRICT out, const Rect& output_rect); + +} // namespace jxl + +#endif // LIB_JXL_EPF_H_ diff --git a/lib/jxl/fake_parallel_runner_testonly.h b/lib/jxl/fake_parallel_runner_testonly.h new file mode 100644 index 0000000..79cc713 --- /dev/null +++ b/lib/jxl/fake_parallel_runner_testonly.h @@ -0,0 +1,76 @@ +// 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_FAKE_PARALLEL_RUNNER_TESTONLY_H_ +#define LIB_JXL_FAKE_PARALLEL_RUNNER_TESTONLY_H_ + +#include + +#include +#include + +namespace jxl { + +// A parallel runner implementation that runs all the jobs in a single thread +// (the caller thread) but runs them pretending to use multiple threads and +// potentially out of order. This is useful for testing conditions that only +// occur under heavy load where the order of operations is different. +class FakeParallelRunner { + public: + FakeParallelRunner(uint32_t order_seed, uint32_t num_threads) + : order_seed_(order_seed), rng_(order_seed), num_threads_(num_threads) { + if (num_threads_ < 1) num_threads_ = 1; + } + + JxlParallelRetCode Run(void* jxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start, + uint32_t end) { + JxlParallelRetCode ret = init(jxl_opaque, num_threads_); + if (ret != 0) return ret; + + if (order_seed_ == 0) { + for (uint32_t i = start; i < end; i++) { + func(jxl_opaque, i, i % num_threads_); + } + } else { + std::vector order(end - start); + for (uint32_t i = start; i < end; i++) { + order[i - start] = i; + } + std::shuffle(order.begin(), order.end(), rng_); + for (uint32_t i = start; i < end; i++) { + func(jxl_opaque, order[i - start], i % num_threads_); + } + } + return ret; + } + + private: + // Seed for the RNG for defining the execution order. A value of 0 means + // sequential order from start to end. + uint32_t order_seed_; + + // The PRNG object, initialized with the order_seed_. Only used if the seed is + // not 0. + std::mt19937 rng_; + + // Number of fake threads. All the tasks are run on the same thread, but using + // different thread_id values based on this num_threads. + uint32_t num_threads_; +}; + +} // namespace jxl + +extern "C" { +// Function to pass as the parallel runner. +JxlParallelRetCode JxlFakeParallelRunner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + return static_cast(runner_opaque) + ->Run(jpegxl_opaque, init, func, start_range, end_range); +} +} + +#endif // LIB_JXL_FAKE_PARALLEL_RUNNER_TESTONLY_H_ diff --git a/lib/jxl/fast_math-inl.h b/lib/jxl/fast_math-inl.h new file mode 100644 index 0000000..60be668 --- /dev/null +++ b/lib/jxl/fast_math-inl.h @@ -0,0 +1,175 @@ +// 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. + +// Fast SIMD math ops (log2, encoder only, cos, erf for splines) + +#if defined(LIB_JXL_FAST_MATH_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_FAST_MATH_INL_H_ +#undef LIB_JXL_FAST_MATH_INL_H_ +#else +#define LIB_JXL_FAST_MATH_INL_H_ +#endif + +#include + +#include "lib/jxl/common.h" +#include "lib/jxl/rational_polynomial-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::ShiftLeft; +using hwy::HWY_NAMESPACE::ShiftRight; + +// Computes base-2 logarithm like std::log2. Undefined if negative / NaN. +// L1 error ~3.9E-6 +template +V FastLog2f(const DF df, V x) { + // 2,2 rational polynomial approximation of std::log1p(x) / std::log(2). + HWY_ALIGN const float p[4 * (2 + 1)] = {HWY_REP4(-1.8503833400518310E-06f), + HWY_REP4(1.4287160470083755E+00f), + HWY_REP4(7.4245873327820566E-01f)}; + HWY_ALIGN const float q[4 * (2 + 1)] = {HWY_REP4(9.9032814277590719E-01f), + HWY_REP4(1.0096718572241148E+00f), + HWY_REP4(1.7409343003366853E-01f)}; + + const Rebind di; + const auto x_bits = BitCast(di, x); + + // Range reduction to [-1/3, 1/3] - 3 integer, 2 float ops + const auto exp_bits = x_bits - Set(di, 0x3f2aaaab); // = 2/3 + // Shifted exponent = log2; also used to clear mantissa. + const auto exp_shifted = ShiftRight<23>(exp_bits); + const auto mantissa = BitCast(df, x_bits - ShiftLeft<23>(exp_shifted)); + const auto exp_val = ConvertTo(df, exp_shifted); + return EvalRationalPolynomial(df, mantissa - Set(df, 1.0f), p, q) + exp_val; +} + +// max relative error ~3e-7 +template +V FastPow2f(const DF df, V x) { + const Rebind di; + auto floorx = Floor(x); + auto exp = BitCast(df, ShiftLeft<23>(ConvertTo(di, floorx) + Set(di, 127))); + auto frac = x - floorx; + auto num = frac + Set(df, 1.01749063e+01); + num = MulAdd(num, frac, Set(df, 4.88687798e+01)); + num = MulAdd(num, frac, Set(df, 9.85506591e+01)); + num = num * exp; + auto den = MulAdd(frac, Set(df, 2.10242958e-01), Set(df, -2.22328856e-02)); + den = MulAdd(den, frac, Set(df, -1.94414990e+01)); + den = MulAdd(den, frac, Set(df, 9.85506633e+01)); + return num / den; +} + +// max relative error ~3e-5 +template +V FastPowf(const DF df, V base, V exponent) { + return FastPow2f(df, FastLog2f(df, base) * exponent); +} + +// Computes cosine like std::cos. +// L1 error 7e-5. +template +V FastCosf(const DF df, V x) { + // Step 1: range reduction to [0, 2pi) + const auto pi2 = Set(df, kPi * 2.0f); + const auto pi2_inv = Set(df, 0.5f / kPi); + const auto npi2 = Floor(x * pi2_inv) * pi2; + const auto xmodpi2 = x - npi2; + // Step 2: range reduction to [0, pi] + const auto x_pi = Min(xmodpi2, pi2 - xmodpi2); + // Step 3: range reduction to [0, pi/2] + const auto above_pihalf = x_pi >= Set(df, kPi / 2.0f); + const auto x_pihalf = IfThenElse(above_pihalf, Set(df, kPi) - x_pi, x_pi); + // Step 4: Taylor-like approximation, scaled by 2**0.75 to make angle + // duplication steps faster, on x/4. + const auto xs = x_pihalf * Set(df, 0.25f); + const auto x2 = xs * xs; + const auto x4 = x2 * x2; + const auto cosx_prescaling = + MulAdd(x4, Set(df, 0.06960438), + MulAdd(x2, Set(df, -0.84087373), Set(df, 1.68179268))); + // Step 5: angle duplication. + const auto cosx_scale1 = + MulAdd(cosx_prescaling, cosx_prescaling, Set(df, -1.414213562)); + const auto cosx_scale2 = MulAdd(cosx_scale1, cosx_scale1, Set(df, -1)); + // Step 6: change sign if needed. + const Rebind du; + auto signbit = ShiftLeft<31>(BitCast(du, VecFromMask(df, above_pihalf))); + return BitCast(df, signbit ^ BitCast(du, cosx_scale2)); +} + +// Computes the error function like std::erf. +// L1 error 7e-4. +template +V FastErff(const DF df, V x) { + // Formula from + // https://en.wikipedia.org/wiki/Error_function#Numerical_approximations + // but constants have been recomputed. + const auto xle0 = x <= Zero(df); + const auto absx = Abs(x); + // Compute 1 - 1 / ((((x * a + b) * x + c) * x + d) * x + 1)**4 + const auto denom1 = + MulAdd(absx, Set(df, 7.77394369e-02), Set(df, 2.05260015e-04)); + const auto denom2 = MulAdd(denom1, absx, Set(df, 2.32120216e-01)); + const auto denom3 = MulAdd(denom2, absx, Set(df, 2.77820801e-01)); + const auto denom4 = MulAdd(denom3, absx, Set(df, 1.0f)); + const auto denom5 = denom4 * denom4; + const auto inv_denom5 = Set(df, 1.0f) / denom5; + const auto result = NegMulAdd(inv_denom5, inv_denom5, Set(df, 1.0f)); + // Change sign if needed. + const Rebind du; + auto signbit = ShiftLeft<31>(BitCast(du, VecFromMask(df, xle0))); + return BitCast(df, signbit ^ BitCast(du, result)); +} + +inline float FastLog2f(float f) { + HWY_CAPPED(float, 1) D; + return GetLane(FastLog2f(D, Set(D, f))); +} + +inline float FastPow2f(float f) { + HWY_CAPPED(float, 1) D; + return GetLane(FastPow2f(D, Set(D, f))); +} + +inline float FastPowf(float b, float e) { + HWY_CAPPED(float, 1) D; + return GetLane(FastPowf(D, Set(D, b), Set(D, e))); +} + +inline float FastCosf(float f) { + HWY_CAPPED(float, 1) D; + return GetLane(FastCosf(D, Set(D, f))); +} + +inline float FastErff(float f) { + HWY_CAPPED(float, 1) D; + return GetLane(FastErff(D, Set(D, f))); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_FAST_MATH_INL_H_ + +#if HWY_ONCE + +namespace jxl { +inline float FastLog2f(float f) { return HWY_STATIC_DISPATCH(FastLog2f)(f); } +inline float FastPow2f(float f) { return HWY_STATIC_DISPATCH(FastPow2f)(f); } +inline float FastPowf(float b, float e) { + return HWY_STATIC_DISPATCH(FastPowf)(b, e); +} +inline float FastCosf(float f) { return HWY_STATIC_DISPATCH(FastCosf)(f); } +inline float FastErff(float f) { return HWY_STATIC_DISPATCH(FastErff)(f); } +} // namespace jxl + +#endif // HWY_ONCE diff --git a/lib/jxl/fast_math_test.cc b/lib/jxl/fast_math_test.cc new file mode 100644 index 0000000..50c3bbb --- /dev/null +++ b/lib/jxl/fast_math_test.cc @@ -0,0 +1,280 @@ +// 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 + +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/fast_math_test.cc" +#include + +#include "lib/jxl/dec_xyb-inl.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/transfer_functions-inl.h" + +// Test utils +#include +#include +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +HWY_NOINLINE void TestFastLog2() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(1e-7f, 1e3f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const auto actual_v = FastLog2f(d, Set(d, f)); + const float actual = GetLane(actual_v); + const float abs_err = std::abs(std::log2(f) - actual); + EXPECT_LT(abs_err, 2.9E-6) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastPow2() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(-100, 100); + float max_rel_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const auto actual_v = FastPow2f(d, Set(d, f)); + const float actual = GetLane(actual_v); + const float expected = std::pow(2, f); + const float rel_err = std::abs(expected - actual) / expected; + EXPECT_LT(rel_err, 3.1E-7) << "f = " << f; + max_rel_err = std::max(max_rel_err, rel_err); + } + printf("max rel err %e\n", static_cast(max_rel_err)); +} + +HWY_NOINLINE void TestFastPow() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution distb(1e-3f, 1e3f); + std::uniform_real_distribution diste(-10, 10); + float max_rel_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float b = distb(rng); + const float e = diste(rng); + const auto actual_v = FastPowf(d, Set(d, b), Set(d, e)); + const float actual = GetLane(actual_v); + const float expected = std::pow(b, e); + const float rel_err = std::abs(expected - actual) / expected; + EXPECT_LT(rel_err, 3E-5) << "b = " << b << " e = " << e; + max_rel_err = std::max(max_rel_err, rel_err); + } + printf("max rel err %e\n", static_cast(max_rel_err)); +} + +HWY_NOINLINE void TestFastCos() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(-1e3f, 1e3f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const auto actual_v = FastCosf(d, Set(d, f)); + const float actual = GetLane(actual_v); + const float abs_err = std::abs(std::cos(f) - actual); + EXPECT_LT(abs_err, 7E-5) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastErf() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(-5.f, 5.f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const auto actual_v = FastErff(d, Set(d, f)); + const float actual = GetLane(actual_v); + const float abs_err = std::abs(std::erf(f) - actual); + EXPECT_LT(abs_err, 7E-4) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastSRGB() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(0.0f, 1.0f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const auto actual_v = FastLinearToSRGB(d, Set(d, f)); + const float actual = GetLane(actual_v); + const float expected = GetLane(TF_SRGB().EncodedFromDisplay(d, Set(d, f))); + const float abs_err = std::abs(expected - actual); + EXPECT_LT(abs_err, 1.2E-4) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastPQEFD() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(0.0f, 1.0f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const float actual = GetLane(TF_PQ().EncodedFromDisplay(d, Set(d, f))); + const float expected = TF_PQ().EncodedFromDisplay(f); + const float abs_err = std::abs(expected - actual); + EXPECT_LT(abs_err, 7e-7) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastHLGEFD() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(0.0f, 1.0f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const float actual = GetLane(TF_HLG().EncodedFromDisplay(d, Set(d, f))); + const float expected = TF_HLG().EncodedFromDisplay(f); + const float abs_err = std::abs(expected - actual); + EXPECT_LT(abs_err, 5e-7) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFast709EFD() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(0.0f, 1.0f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const float actual = GetLane(TF_709().EncodedFromDisplay(d, Set(d, f))); + const float expected = TF_709().EncodedFromDisplay(f); + const float abs_err = std::abs(expected - actual); + EXPECT_LT(abs_err, 2e-6) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastPQDFE() { + constexpr size_t kNumTrials = 1 << 23; + std::mt19937 rng(1); + std::uniform_real_distribution dist(0.0f, 1.0f); + float max_abs_err = 0; + HWY_FULL(float) d; + for (size_t i = 0; i < kNumTrials; i++) { + const float f = dist(rng); + const float actual = GetLane(TF_PQ().DisplayFromEncoded(d, Set(d, f))); + const float expected = TF_PQ().DisplayFromEncoded(f); + const float abs_err = std::abs(expected - actual); + EXPECT_LT(abs_err, 3E-6) << "f = " << f; + max_abs_err = std::max(max_abs_err, abs_err); + } + printf("max abs err %e\n", static_cast(max_abs_err)); +} + +HWY_NOINLINE void TestFastXYB() { + if (!HasFastXYBTosRGB8()) return; + ImageMetadata metadata; + ImageBundle ib(&metadata); + int scaling = 1; + int n = 256 * scaling; + float inv_scaling = 1.0f / scaling; + int kChunk = 32; + // The image is divided in chunks to reduce total memory usage. + for (int cr = 0; cr < n; cr += kChunk) { + for (int cg = 0; cg < n; cg += kChunk) { + for (int cb = 0; cb < n; cb += kChunk) { + Image3F chunk(kChunk * kChunk, kChunk); + for (int ir = 0; ir < kChunk; ir++) { + for (int ig = 0; ig < kChunk; ig++) { + for (int ib = 0; ib < kChunk; ib++) { + float r = (cr + ir) * inv_scaling; + float g = (cg + ig) * inv_scaling; + float b = (cb + ib) * inv_scaling; + chunk.PlaneRow(0, ir)[ig * kChunk + ib] = r * (1.0f / 255); + chunk.PlaneRow(1, ir)[ig * kChunk + ib] = g * (1.0f / 255); + chunk.PlaneRow(2, ir)[ig * kChunk + ib] = b * (1.0f / 255); + } + } + } + ib.SetFromImage(std::move(chunk), ColorEncoding::SRGB()); + Image3F xyb(kChunk * kChunk, kChunk); + std::vector roundtrip(kChunk * kChunk * kChunk * 3); + ToXYB(ib, nullptr, &xyb); + jxl::HWY_NAMESPACE::FastXYBTosRGB8( + xyb, Rect(xyb), Rect(xyb), nullptr, Rect(), /*is_rgba=*/false, + roundtrip.data(), xyb.xsize(), xyb.xsize() * 3); + for (int ir = 0; ir < kChunk; ir++) { + for (int ig = 0; ig < kChunk; ig++) { + for (int ib = 0; ib < kChunk; ib++) { + float r = (cr + ir) * inv_scaling; + float g = (cg + ig) * inv_scaling; + float b = (cb + ib) * inv_scaling; + size_t idx = ir * kChunk * kChunk + ig * kChunk + ib; + int rr = roundtrip[3 * idx]; + int rg = roundtrip[3 * idx + 1]; + int rb = roundtrip[3 * idx + 2]; + EXPECT_LT(abs(r - rr), 2) << "expected " << r << " got " << rr; + EXPECT_LT(abs(g - rg), 2) << "expected " << g << " got " << rg; + EXPECT_LT(abs(b - rb), 2) << "expected " << b << " got " << rb; + } + } + } + } + } + } +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +class FastMathTargetTest : public hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(FastMathTargetTest); + +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastLog2); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPow2); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPow); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastCos); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastErf); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastSRGB); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPQDFE); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastPQEFD); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastHLGEFD); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFast709EFD); +HWY_EXPORT_AND_TEST_P(FastMathTargetTest, TestFastXYB); + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/field_encodings.h b/lib/jxl/field_encodings.h new file mode 100644 index 0000000..00d0880 --- /dev/null +++ b/lib/jxl/field_encodings.h @@ -0,0 +1,123 @@ +// 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_FIELD_ENCODINGS_H_ +#define LIB_JXL_FIELD_ENCODINGS_H_ + +// Constants needed to encode/decode fields; avoids including the full fields.h. + +#include +#include + +#include + +#include "hwy/base.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +class Visitor; +class Fields { + public: + virtual ~Fields() = default; + virtual const char* Name() const = 0; + virtual Status VisitFields(Visitor* JXL_RESTRICT visitor) = 0; +}; + +// Distribution of U32 values for one particular selector. Represents either a +// power of two-sized range, or a single value. A separate type ensures this is +// only passed to the U32Enc ctor. +struct U32Distr { + // No need to validate - all `d` are legitimate. + constexpr explicit U32Distr(uint32_t d) : d(d) {} + + static constexpr uint32_t kDirect = 0x80000000u; + + constexpr bool IsDirect() const { return (d & kDirect) != 0; } + + // Only call if IsDirect(). + constexpr uint32_t Direct() const { return d & (kDirect - 1); } + + // Only call if !IsDirect(). + constexpr size_t ExtraBits() const { return (d & 0x1F) + 1; } + uint32_t Offset() const { return (d >> 5) & 0x3FFFFFF; } + + uint32_t d; +}; + +// A direct-coded 31-bit value occupying 2 bits in the bitstream. +constexpr U32Distr Val(uint32_t value) { + return U32Distr(value | U32Distr::kDirect); +} + +// Value - `offset` will be signaled in `bits` extra bits. +constexpr U32Distr BitsOffset(uint32_t bits, uint32_t offset) { + return U32Distr(((bits - 1) & 0x1F) + ((offset & 0x3FFFFFF) << 5)); +} + +// Value will be signaled in `bits` extra bits. +constexpr U32Distr Bits(uint32_t bits) { return BitsOffset(bits, 0); } + +// See U32Coder documentation in fields.h. +class U32Enc { + public: + constexpr U32Enc(const U32Distr d0, const U32Distr d1, const U32Distr d2, + const U32Distr d3) + : d_{d0, d1, d2, d3} {} + + // Returns the U32Distr at `selector` = 0..3, least-significant first. + U32Distr GetDistr(const uint32_t selector) const { + JXL_ASSERT(selector < 4); + return d_[selector]; + } + + private: + U32Distr d_[4]; +}; + +// Returns bit with the given `index` (0 = least significant). +template +static inline constexpr uint64_t MakeBit(T index) { + return 1ULL << static_cast(index); +} + +// Returns vector of all possible values of an Enum type. Relies on each Enum +// providing an overload of EnumBits() that returns a bit array of its values, +// which implies values must be in [0, 64). +template +std::vector Values() { + uint64_t bits = EnumBits(Enum()); + + std::vector values; + values.reserve(hwy::PopCount(bits)); + + // For each 1-bit in bits: add its index as value + while (bits != 0) { + const int index = Num0BitsBelowLS1Bit_Nonzero(bits); + values.push_back(static_cast(index)); + bits &= bits - 1; // clear least-significant bit + } + return values; +} + +// Returns true if value is one of Values(). +template +Status EnumValid(const Enum value) { + if (static_cast(value) >= 64) { + return JXL_FAILURE("Value %u too large for %s\n", + static_cast(value), EnumName(Enum())); + } + const uint64_t bit = MakeBit(value); + if ((EnumBits(Enum()) & bit) == 0) { + return JXL_FAILURE("Invalid value %u for %s\n", + static_cast(value), EnumName(Enum())); + } + return true; +} + +} // namespace jxl + +#endif // LIB_JXL_FIELD_ENCODINGS_H_ diff --git a/lib/jxl/fields.cc b/lib/jxl/fields.cc new file mode 100644 index 0000000..7f00c44 --- /dev/null +++ b/lib/jxl/fields.cc @@ -0,0 +1,985 @@ +// 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/fields.h" + +#include + +#include +#include + +#include "hwy/base.h" +#include "lib/jxl/base/bits.h" + +namespace jxl { + +namespace { + +// A bundle can be in one of three states concerning extensions: not-begun, +// active, ended. Bundles may be nested, so we need a stack of states. +class ExtensionStates { + public: + void Push() { + // Initial state = not-begun. + begun_ <<= 1; + ended_ <<= 1; + } + + // Clears current state; caller must check IsEnded beforehand. + void Pop() { + begun_ >>= 1; + ended_ >>= 1; + } + + // Returns true if state == active || state == ended. + Status IsBegun() const { return (begun_ & 1) != 0; } + // Returns true if state != not-begun && state != active. + Status IsEnded() const { return (ended_ & 1) != 0; } + + void Begin() { + JXL_ASSERT(!IsBegun()); + JXL_ASSERT(!IsEnded()); + begun_ += 1; + } + + void End() { + JXL_ASSERT(IsBegun()); + JXL_ASSERT(!IsEnded()); + ended_ += 1; + } + + private: + // Current state := least-significant bit of begun_ and ended_. + uint64_t begun_ = 0; + uint64_t ended_ = 0; +}; + +// Visitors generate Init/AllDefault/Read/Write logic for all fields. Each +// bundle's VisitFields member function calls visitor->U32 etc. We do not +// overload operator() because a function name is easier to search for. + +class VisitorBase : public Visitor { + public: + explicit VisitorBase(bool print_bundles = false) + : print_bundles_(print_bundles) {} + ~VisitorBase() override { JXL_ASSERT(depth_ == 0); } + + // This is the only call site of Fields::VisitFields. Adds tracing and + // ensures EndExtensions was called. + Status Visit(Fields* fields, const char* visitor_name) override { + fputs(visitor_name, stdout); // No newline; no effect if empty + if (print_bundles_) { + Trace("%s\n", print_bundles_ ? fields->Name() : ""); + } + + depth_ += 1; + JXL_ASSERT(depth_ <= Bundle::kMaxExtensions); + extension_states_.Push(); + + const Status ok = fields->VisitFields(this); + + if (ok) { + // If VisitFields called BeginExtensions, must also call + // EndExtensions. + JXL_ASSERT(!extension_states_.IsBegun() || extension_states_.IsEnded()); + } else { + // Failed, undefined state: don't care whether EndExtensions was + // called. + } + + extension_states_.Pop(); + JXL_ASSERT(depth_ != 0); + depth_ -= 1; + + return ok; + } + + // For visitors accepting a const Visitor, need to const-cast so we can call + // the non-const Visitor::VisitFields. NOTE: C is not modified except the + // `all_default` field by CanEncodeVisitor. + Status VisitConst(const Fields& t, const char* message) { + return Visit(const_cast(&t), message); + } + + // Derived types (overridden by InitVisitor because it is unsafe to read + // from *value there) + + Status Bool(bool default_value, bool* JXL_RESTRICT value) override { + uint32_t bits = *value ? 1 : 0; + JXL_RETURN_IF_ERROR(Bits(1, static_cast(default_value), &bits)); + JXL_DASSERT(bits <= 1); + *value = bits == 1; + return true; + } + + // Overridden by ReadVisitor and WriteVisitor. + // Called before any conditional visit based on "extensions". + // Overridden by ReadVisitor, CanEncodeVisitor and WriteVisitor. + Status BeginExtensions(uint64_t* JXL_RESTRICT extensions) override { + JXL_RETURN_IF_ERROR(U64(0, extensions)); + + extension_states_.Begin(); + return true; + } + + // Called after all extension fields (if any). Although non-extension + // fields could be visited afterward, we prefer the convention that + // extension fields are always the last to be visited. Overridden by + // ReadVisitor. + Status EndExtensions() override { + extension_states_.End(); + return true; + } + + protected: + // Prints indentation, . + JXL_FORMAT(2, 3) // 1-based plus one because member function + void Trace(const char* format, ...) const { + // Indentation. + printf("%*s", static_cast(2 * depth_), ""); + + va_list args; + va_start(args, format); + vfprintf(stdout, format, args); + va_end(args); + } + + private: + size_t depth_ = 0; // for indentation. + ExtensionStates extension_states_; + const bool print_bundles_; +}; + +struct InitVisitor : public VisitorBase { + Status Bits(const size_t /*unused*/, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status U32(const U32Enc /*unused*/, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status U64(const uint64_t default_value, + uint64_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status Bool(bool default_value, bool* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status F16(const float default_value, float* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + // Always visit conditional fields to ensure they are initialized. + Status Conditional(bool /*condition*/) override { return true; } + + Status AllDefault(const Fields& /*fields*/, + bool* JXL_RESTRICT all_default) override { + // Just initialize this field and don't skip initializing others. + JXL_RETURN_IF_ERROR(Bool(true, all_default)); + return false; + } + + Status VisitNested(Fields* /*fields*/) override { + // Avoid re-initializing nested bundles (their ctors already called + // Bundle::Init for their fields). + return true; + } + + const char* VisitorName() override { return "InitVisitor"; } +}; + +// Similar to InitVisitor, but also initializes nested fields. +struct SetDefaultVisitor : public VisitorBase { + Status Bits(const size_t /*unused*/, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status U32(const U32Enc /*unused*/, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status U64(const uint64_t default_value, + uint64_t* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status Bool(bool default_value, bool* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + Status F16(const float default_value, float* JXL_RESTRICT value) override { + *value = default_value; + return true; + } + + // Always visit conditional fields to ensure they are initialized. + Status Conditional(bool /*condition*/) override { return true; } + + Status AllDefault(const Fields& /*fields*/, + bool* JXL_RESTRICT all_default) override { + // Just initialize this field and don't skip initializing others. + JXL_RETURN_IF_ERROR(Bool(true, all_default)); + return false; + } + + const char* VisitorName() override { return "SetDefaultVisitor"; } +}; + +class AllDefaultVisitor : public VisitorBase { + public: + explicit AllDefaultVisitor(bool print_all_default) + : VisitorBase(print_all_default), print_all_default_(print_all_default) {} + + Status Bits(const size_t bits, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + if (print_all_default_) { + Trace(" u(%zu) = %u, default %u\n", bits, *value, default_value); + } + + all_default_ &= *value == default_value; + return true; + } + + Status U32(const U32Enc /*unused*/, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) override { + if (print_all_default_) { + Trace(" U32 = %u, default %u\n", *value, default_value); + } + + all_default_ &= *value == default_value; + return true; + } + + Status U64(const uint64_t default_value, + uint64_t* JXL_RESTRICT value) override { + if (print_all_default_) { + Trace(" U64 = %" PRIu64 ", default %" PRIu64 "\n", *value, + default_value); + } + + all_default_ &= *value == default_value; + return true; + } + + Status F16(const float default_value, float* JXL_RESTRICT value) override { + if (print_all_default_) { + Trace(" F16 = %.6f, default %.6f\n", static_cast(*value), + static_cast(default_value)); + } + all_default_ &= std::abs(*value - default_value) < 1E-6f; + return true; + } + + Status AllDefault(const Fields& /*fields*/, + bool* JXL_RESTRICT /*all_default*/) override { + // Visit all fields so we can compute the actual all_default_ value. + return false; + } + + bool AllDefault() const { return all_default_; } + + const char* VisitorName() override { return "AllDefaultVisitor"; } + + private: + const bool print_all_default_; + bool all_default_ = true; +}; + +class ReadVisitor : public VisitorBase { + public: + ReadVisitor(BitReader* reader, bool print_read) + : VisitorBase(print_read), print_read_(print_read), reader_(reader) {} + + Status Bits(const size_t bits, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + *value = BitsCoder::Read(bits, reader_); + if (!reader_->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for header"); + } + if (print_read_) Trace(" u(%zu) = %u\n", bits, *value); + return true; + } + + Status U32(const U32Enc dist, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + *value = U32Coder::Read(dist, reader_); + if (!reader_->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for header"); + } + if (print_read_) Trace(" U32 = %u\n", *value); + return true; + } + + Status U64(const uint64_t /*default_value*/, + uint64_t* JXL_RESTRICT value) override { + *value = U64Coder::Read(reader_); + if (!reader_->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for header"); + } + if (print_read_) Trace(" U64 = %" PRIu64 "\n", *value); + return true; + } + + Status F16(const float /*default_value*/, + float* JXL_RESTRICT value) override { + ok_ &= F16Coder::Read(reader_, value); + if (!reader_->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for header"); + } + if (print_read_) Trace(" F16 = %f\n", static_cast(*value)); + return true; + } + + void SetDefault(Fields* fields) override { Bundle::SetDefault(fields); } + + bool IsReading() const override { return true; } + + // This never fails because visitors are expected to keep reading until + // EndExtensions, see comment there. + Status BeginExtensions(uint64_t* JXL_RESTRICT extensions) override { + JXL_QUIET_RETURN_IF_ERROR(VisitorBase::BeginExtensions(extensions)); + if (*extensions == 0) return true; + + // For each nonzero bit, i.e. extension that is present: + for (uint64_t remaining_extensions = *extensions; remaining_extensions != 0; + remaining_extensions &= remaining_extensions - 1) { + const size_t idx_extension = + Num0BitsBelowLS1Bit_Nonzero(remaining_extensions); + // Read additional U64 (one per extension) indicating the number of bits + // (allows skipping individual extensions). + JXL_RETURN_IF_ERROR(U64(0, &extension_bits_[idx_extension])); + if (!SafeAdd(total_extension_bits_, extension_bits_[idx_extension], + total_extension_bits_)) { + return JXL_FAILURE("Extension bits overflowed, invalid codestream"); + } + } + // Used by EndExtensions to skip past any _remaining_ extensions. + pos_after_ext_size_ = reader_->TotalBitsConsumed(); + JXL_ASSERT(pos_after_ext_size_ != 0); + return true; + } + + Status EndExtensions() override { + JXL_QUIET_RETURN_IF_ERROR(VisitorBase::EndExtensions()); + // Happens if extensions == 0: don't read size, done. + if (pos_after_ext_size_ == 0) return true; + + // Not enough bytes as set by BeginExtensions or earlier. Do not return + // this as an JXL_FAILURE or false (which can also propagate to error + // through e.g. JXL_RETURN_IF_ERROR), since this may be used while + // silently checking whether there are enough bytes. If this case must be + // treated as an error, reader_>Close() will do this, just like is already + // done for non-extension fields. + if (!enough_bytes_) return true; + + // Skip new fields this (old?) decoder didn't know about, if any. + const size_t bits_read = reader_->TotalBitsConsumed(); + uint64_t end; + if (!SafeAdd(pos_after_ext_size_, total_extension_bits_, end)) { + return JXL_FAILURE("Invalid extension size, caused overflow"); + } + if (bits_read > end) { + return JXL_FAILURE("Read more extension bits than budgeted"); + } + const size_t remaining_bits = end - bits_read; + if (remaining_bits != 0) { + JXL_WARNING("Skipping %zu-bit extension(s)", remaining_bits); + reader_->SkipBits(remaining_bits); + if (!reader_->AllReadsWithinBounds()) { + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for header"); + } + } + return true; + } + + Status OK() const { return ok_; } + + const char* VisitorName() override { return "ReadVisitor"; } + + private: + const bool print_read_; + + // Whether any error other than not enough bytes occurred. + bool ok_ = true; + + // Whether there are enough input bytes to read from. + bool enough_bytes_ = true; + BitReader* const reader_; + // May be 0 even if the corresponding extension is present. + uint64_t extension_bits_[Bundle::kMaxExtensions] = {0}; + uint64_t total_extension_bits_ = 0; + size_t pos_after_ext_size_ = 0; // 0 iff extensions == 0. +}; + +class MaxBitsVisitor : public VisitorBase { + public: + Status Bits(const size_t bits, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT /*value*/) override { + max_bits_ += BitsCoder::MaxEncodedBits(bits); + return true; + } + + Status U32(const U32Enc enc, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT /*value*/) override { + max_bits_ += U32Coder::MaxEncodedBits(enc); + return true; + } + + Status U64(const uint64_t /*default_value*/, + uint64_t* JXL_RESTRICT /*value*/) override { + max_bits_ += U64Coder::MaxEncodedBits(); + return true; + } + + Status F16(const float /*default_value*/, + float* JXL_RESTRICT /*value*/) override { + max_bits_ += F16Coder::MaxEncodedBits(); + return true; + } + + Status AllDefault(const Fields& /*fields*/, + bool* JXL_RESTRICT all_default) override { + JXL_RETURN_IF_ERROR(Bool(true, all_default)); + return false; // For max bits, assume nothing is default + } + + // Always visit conditional fields to get a (loose) upper bound. + Status Conditional(bool /*condition*/) override { return true; } + + Status BeginExtensions(uint64_t* JXL_RESTRICT /*extensions*/) override { + // Skip - extensions are not included in "MaxBits" because their length + // is potentially unbounded. + return true; + } + + Status EndExtensions() override { return true; } + + size_t MaxBits() const { return max_bits_; } + + const char* VisitorName() override { return "MaxBitsVisitor"; } + + private: + size_t max_bits_ = 0; +}; + +class CanEncodeVisitor : public VisitorBase { + public: + explicit CanEncodeVisitor(bool print_sizes) + : VisitorBase(print_sizes), print_sizes_(print_sizes) {} + + Status Bits(const size_t bits, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + size_t encoded_bits = 0; + ok_ &= BitsCoder::CanEncode(bits, *value, &encoded_bits); + if (print_sizes_) Trace("u(%zu) = %u\n", bits, *value); + encoded_bits_ += encoded_bits; + return true; + } + + Status U32(const U32Enc enc, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + size_t encoded_bits = 0; + ok_ &= U32Coder::CanEncode(enc, *value, &encoded_bits); + if (print_sizes_) Trace("U32(%zu) = %u\n", encoded_bits, *value); + encoded_bits_ += encoded_bits; + return true; + } + + Status U64(const uint64_t /*default_value*/, + uint64_t* JXL_RESTRICT value) override { + size_t encoded_bits = 0; + ok_ &= U64Coder::CanEncode(*value, &encoded_bits); + if (print_sizes_) { + Trace("U64(%zu) = %" PRIu64 "\n", encoded_bits, *value); + } + encoded_bits_ += encoded_bits; + return true; + } + + Status F16(const float /*default_value*/, + float* JXL_RESTRICT value) override { + size_t encoded_bits = 0; + ok_ &= F16Coder::CanEncode(*value, &encoded_bits); + if (print_sizes_) { + Trace("F16(%zu) = %.6f\n", encoded_bits, static_cast(*value)); + } + encoded_bits_ += encoded_bits; + return true; + } + + Status AllDefault(const Fields& fields, + bool* JXL_RESTRICT all_default) override { + *all_default = Bundle::AllDefault(fields); + JXL_RETURN_IF_ERROR(Bool(true, all_default)); + return *all_default; + } + + Status BeginExtensions(uint64_t* JXL_RESTRICT extensions) override { + JXL_QUIET_RETURN_IF_ERROR(VisitorBase::BeginExtensions(extensions)); + extensions_ = *extensions; + if (*extensions != 0) { + JXL_ASSERT(pos_after_ext_ == 0); + pos_after_ext_ = encoded_bits_; + JXL_ASSERT(pos_after_ext_ != 0); // visited "extensions" + } + return true; + } + // EndExtensions = default. + + Status GetSizes(size_t* JXL_RESTRICT extension_bits, + size_t* JXL_RESTRICT total_bits) { + JXL_RETURN_IF_ERROR(ok_); + *extension_bits = 0; + *total_bits = encoded_bits_; + // Only if extension field was nonzero will we encode their sizes. + if (pos_after_ext_ != 0) { + JXL_ASSERT(encoded_bits_ >= pos_after_ext_); + *extension_bits = encoded_bits_ - pos_after_ext_; + // Also need to encode *extension_bits and bill it to *total_bits. + size_t encoded_bits = 0; + ok_ &= U64Coder::CanEncode(*extension_bits, &encoded_bits); + *total_bits += encoded_bits; + + // TODO(janwas): support encoding individual extension sizes. We + // currently ascribe all bits to the first and send zeros for the + // others. + for (size_t i = 1; i < hwy::PopCount(extensions_); ++i) { + encoded_bits = 0; + ok_ &= U64Coder::CanEncode(0, &encoded_bits); + *total_bits += encoded_bits; + } + } + return true; + } + + const char* VisitorName() override { return "CanEncodeVisitor"; } + + private: + const bool print_sizes_; + bool ok_ = true; + size_t encoded_bits_ = 0; + uint64_t extensions_ = 0; + // Snapshot of encoded_bits_ after visiting the extension field, but NOT + // including the hidden extension sizes. + uint64_t pos_after_ext_ = 0; +}; + +class WriteVisitor : public VisitorBase { + public: + WriteVisitor(const size_t extension_bits, BitWriter* JXL_RESTRICT writer) + : extension_bits_(extension_bits), writer_(writer) {} + + Status Bits(const size_t bits, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + ok_ &= BitsCoder::Write(bits, *value, writer_); + return true; + } + Status U32(const U32Enc enc, const uint32_t /*default_value*/, + uint32_t* JXL_RESTRICT value) override { + ok_ &= U32Coder::Write(enc, *value, writer_); + return true; + } + + Status U64(const uint64_t /*default_value*/, + uint64_t* JXL_RESTRICT value) override { + ok_ &= U64Coder::Write(*value, writer_); + return true; + } + + Status F16(const float /*default_value*/, + float* JXL_RESTRICT value) override { + ok_ &= F16Coder::Write(*value, writer_); + return true; + } + + Status BeginExtensions(uint64_t* JXL_RESTRICT extensions) override { + JXL_QUIET_RETURN_IF_ERROR(VisitorBase::BeginExtensions(extensions)); + if (*extensions == 0) { + JXL_ASSERT(extension_bits_ == 0); + return true; + } + // TODO(janwas): extend API to pass in array of extension_bits, one per + // extension. We currently ascribe all bits to the first extension, but + // this is only an encoder limitation. NOTE: extension_bits_ can be zero + // if an extension does not require any additional fields. + ok_ &= U64Coder::Write(extension_bits_, writer_); + // For each nonzero bit except the lowest/first (already written): + for (uint64_t remaining_extensions = *extensions & (*extensions - 1); + remaining_extensions != 0; + remaining_extensions &= remaining_extensions - 1) { + ok_ &= U64Coder::Write(0, writer_); + } + return true; + } + // EndExtensions = default. + + Status OK() const { return ok_; } + + const char* VisitorName() override { return "WriteVisitor"; } + + private: + const size_t extension_bits_; + BitWriter* JXL_RESTRICT writer_; + bool ok_ = true; +}; + +} // namespace + +void Bundle::Init(Fields* fields) { + InitVisitor visitor; + if (!visitor.Visit(fields, PrintVisitors() ? "-- Init\n" : "")) { + JXL_ABORT("Init should never fail"); + } +} +void Bundle::SetDefault(Fields* fields) { + SetDefaultVisitor visitor; + if (!visitor.Visit(fields, PrintVisitors() ? "-- SetDefault\n" : "")) { + JXL_ABORT("SetDefault should never fail"); + } +} +bool Bundle::AllDefault(const Fields& fields) { + AllDefaultVisitor visitor(/*print_all_default=*/PrintAllDefault()); + const char* name = + (PrintVisitors() || PrintAllDefault()) ? "[[AllDefault\n" : ""; + if (!visitor.VisitConst(fields, name)) { + JXL_ABORT("AllDefault should never fail"); + } + + if (PrintAllDefault()) printf(" %d]]\n", visitor.AllDefault()); + return visitor.AllDefault(); +} +size_t Bundle::MaxBits(const Fields& fields) { + MaxBitsVisitor visitor; +#if JXL_ENABLE_ASSERT + Status ret = +#else + (void) +#endif // JXL_ENABLE_ASSERT + visitor.VisitConst(fields, PrintVisitors() ? "-- MaxBits\n" : ""); + JXL_ASSERT(ret); + return visitor.MaxBits(); +} +Status Bundle::CanEncode(const Fields& fields, size_t* extension_bits, + size_t* total_bits) { + CanEncodeVisitor visitor(/*print_sizes=*/PrintSizes()); + const char* name = (PrintVisitors() || PrintSizes()) ? "[[CanEncode\n" : ""; + JXL_QUIET_RETURN_IF_ERROR(visitor.VisitConst(fields, name)); + JXL_QUIET_RETURN_IF_ERROR(visitor.GetSizes(extension_bits, total_bits)); + if (PrintSizes()) printf(" %zu]]\n", *total_bits); + return true; +} +Status Bundle::Read(BitReader* reader, Fields* fields) { + ReadVisitor visitor(reader, /*print_read=*/PrintRead()); + JXL_RETURN_IF_ERROR( + visitor.Visit(fields, PrintVisitors() ? "-- Read\n" : "")); + return visitor.OK(); +} +bool Bundle::CanRead(BitReader* reader, Fields* fields) { + ReadVisitor visitor(reader, /*print_read=*/PrintRead()); + Status status = visitor.Visit(fields, PrintVisitors() ? "-- Read\n" : ""); + // We are only checking here whether there are enough bytes. We still return + // true for other errors because it means there are enough bytes to determine + // there's an error. Use Read() to determine which error it is. + return status.code() != StatusCode::kNotEnoughBytes; +} +Status Bundle::Write(const Fields& fields, BitWriter* writer, size_t layer, + AuxOut* aux_out) { + size_t extension_bits, total_bits; + JXL_RETURN_IF_ERROR(CanEncode(fields, &extension_bits, &total_bits)); + + BitWriter::Allotment allotment(writer, total_bits); + WriteVisitor visitor(extension_bits, writer); + JXL_RETURN_IF_ERROR( + visitor.VisitConst(fields, PrintVisitors() ? "-- Write\n" : "")); + JXL_RETURN_IF_ERROR(visitor.OK()); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + return true; +} + +size_t U32Coder::MaxEncodedBits(const U32Enc enc) { + size_t extra_bits = 0; + for (uint32_t selector = 0; selector < 4; ++selector) { + const U32Distr d = enc.GetDistr(selector); + if (d.IsDirect()) { + continue; + } else { + extra_bits = std::max(extra_bits, d.ExtraBits()); + } + } + return 2 + extra_bits; +} + +Status U32Coder::CanEncode(const U32Enc enc, const uint32_t value, + size_t* JXL_RESTRICT encoded_bits) { + uint32_t selector; + size_t total_bits; + const Status ok = ChooseSelector(enc, value, &selector, &total_bits); + *encoded_bits = ok ? total_bits : 0; + return ok; +} + +uint32_t U32Coder::Read(const U32Enc enc, BitReader* JXL_RESTRICT reader) { + const uint32_t selector = reader->ReadFixedBits<2>(); + const U32Distr d = enc.GetDistr(selector); + if (d.IsDirect()) { + return d.Direct(); + } else { + return reader->ReadBits(d.ExtraBits()) + d.Offset(); + } +} + +// Returns false if the value is too large to encode. +Status U32Coder::Write(const U32Enc enc, const uint32_t value, + BitWriter* JXL_RESTRICT writer) { + uint32_t selector; + size_t total_bits; + JXL_RETURN_IF_ERROR(ChooseSelector(enc, value, &selector, &total_bits)); + + writer->Write(2, selector); + + const U32Distr d = enc.GetDistr(selector); + if (!d.IsDirect()) { // Nothing more to write for direct encoding + const uint32_t offset = d.Offset(); + JXL_ASSERT(value >= offset); + writer->Write(total_bits - 2, value - offset); + } + + return true; +} + +Status U32Coder::ChooseSelector(const U32Enc enc, const uint32_t value, + uint32_t* JXL_RESTRICT selector, + size_t* JXL_RESTRICT total_bits) { +#if JXL_ENABLE_ASSERT + const size_t bits_required = 32 - Num0BitsAboveMS1Bit(value); +#endif // JXL_ENABLE_ASSERT + JXL_ASSERT(bits_required <= 32); + + *selector = 0; + *total_bits = 0; + + // It is difficult to verify whether Dist32Byte are sorted, so check all + // selectors and keep the one with the fewest total_bits. + *total_bits = 64; // more than any valid encoding + for (uint32_t s = 0; s < 4; ++s) { + const U32Distr d = enc.GetDistr(s); + if (d.IsDirect()) { + if (d.Direct() == value) { + *selector = s; + *total_bits = 2; + return true; // Done, direct is always the best possible. + } + continue; + } + const size_t extra_bits = d.ExtraBits(); + const uint32_t offset = d.Offset(); + if (value < offset || value >= offset + (1ULL << extra_bits)) continue; + + // Better than prior encoding, remember it: + if (2 + extra_bits < *total_bits) { + *selector = s; + *total_bits = 2 + extra_bits; + } + } + + if (*total_bits == 64) { + return JXL_FAILURE("No feasible selector for %u", value); + } + + return true; +} + +uint64_t U64Coder::Read(BitReader* JXL_RESTRICT reader) { + uint64_t selector = reader->ReadFixedBits<2>(); + if (selector == 0) { + return 0; + } + if (selector == 1) { + return 1 + reader->ReadFixedBits<4>(); + } + if (selector == 2) { + return 17 + reader->ReadFixedBits<8>(); + } + + // selector 3, varint, groups have first 12, then 8, and last 4 bits. + uint64_t result = reader->ReadFixedBits<12>(); + + uint64_t shift = 12; + while (reader->ReadFixedBits<1>()) { + if (shift == 60) { + result |= static_cast(reader->ReadFixedBits<4>()) << shift; + break; + } + result |= static_cast(reader->ReadFixedBits<8>()) << shift; + shift += 8; + } + + return result; +} + +// Returns false if the value is too large to encode. +Status U64Coder::Write(uint64_t value, BitWriter* JXL_RESTRICT writer) { + if (value == 0) { + // Selector: use 0 bits, value 0 + writer->Write(2, 0); + } else if (value <= 16) { + // Selector: use 4 bits, value 1..16 + writer->Write(2, 1); + writer->Write(4, value - 1); + } else if (value <= 272) { + // Selector: use 8 bits, value 17..272 + writer->Write(2, 2); + writer->Write(8, value - 17); + } else { + // Selector: varint, first a 12-bit group, after that per 8-bit group. + writer->Write(2, 3); + writer->Write(12, value & 4095); + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) { + // Indicate varint not done + writer->Write(1, 1); + writer->Write(8, value & 255); + value >>= 8; + shift += 8; + } + if (value > 0) { + // This only could happen if shift == N - 4. + writer->Write(1, 1); + writer->Write(4, value & 15); + // Implicitly closed sequence, no extra stop bit is required. + } else { + // Indicate end of varint + writer->Write(1, 0); + } + } + + return true; +} + +// Can always encode, but useful because it also returns bit size. +Status U64Coder::CanEncode(uint64_t value, size_t* JXL_RESTRICT encoded_bits) { + if (value == 0) { + *encoded_bits = 2; // 2 selector bits + } else if (value <= 16) { + *encoded_bits = 2 + 4; // 2 selector bits + 4 payload bits + } else if (value <= 272) { + *encoded_bits = 2 + 8; // 2 selector bits + 8 payload bits + } else { + *encoded_bits = 2 + 12; // 2 selector bits + 12 payload bits + value >>= 12; + int shift = 12; + while (value > 0 && shift < 60) { + *encoded_bits += 1 + 8; // 1 continuation bit + 8 payload bits + value >>= 8; + shift += 8; + } + if (value > 0) { + // This only could happen if shift == N - 4. + *encoded_bits += 1 + 4; // 1 continuation bit + 4 payload bits + } else { + *encoded_bits += 1; // 1 stop bit + } + } + + return true; +} + +Status F16Coder::Read(BitReader* JXL_RESTRICT reader, + float* JXL_RESTRICT value) { + const uint32_t bits16 = reader->ReadFixedBits<16>(); + const uint32_t sign = bits16 >> 15; + const uint32_t biased_exp = (bits16 >> 10) & 0x1F; + const uint32_t mantissa = bits16 & 0x3FF; + + if (JXL_UNLIKELY(biased_exp == 31)) { + return JXL_FAILURE("F16 infinity or NaN are not supported"); + } + + // Subnormal or zero + if (JXL_UNLIKELY(biased_exp == 0)) { + *value = (1.0f / 16384) * (mantissa * (1.0f / 1024)); + if (sign) *value = -*value; + return true; + } + + // Normalized: convert the representation directly (faster than ldexp/tables). + const uint32_t biased_exp32 = biased_exp + (127 - 15); + const uint32_t mantissa32 = mantissa << (23 - 10); + const uint32_t bits32 = (sign << 31) | (biased_exp32 << 23) | mantissa32; + memcpy(value, &bits32, sizeof(bits32)); + return true; +} + +Status F16Coder::Write(float value, BitWriter* JXL_RESTRICT writer) { + uint32_t bits32; + memcpy(&bits32, &value, sizeof(bits32)); + const uint32_t sign = bits32 >> 31; + const uint32_t biased_exp32 = (bits32 >> 23) & 0xFF; + const uint32_t mantissa32 = bits32 & 0x7FFFFF; + + const int32_t exp = static_cast(biased_exp32) - 127; + if (JXL_UNLIKELY(exp > 15)) { + return JXL_FAILURE("Too big to encode, CanEncode should return false"); + } + + // Tiny or zero => zero. + if (exp < -24) { + writer->Write(16, 0); + return true; + } + + uint32_t biased_exp16, mantissa16; + + // exp = [-24, -15] => subnormal + if (JXL_UNLIKELY(exp < -14)) { + biased_exp16 = 0; + const uint32_t sub_exp = static_cast(-14 - exp); + JXL_ASSERT(1 <= sub_exp && sub_exp < 11); + mantissa16 = (1 << (10 - sub_exp)) + (mantissa32 >> (13 + sub_exp)); + } else { + // exp = [-14, 15] + biased_exp16 = static_cast(exp + 15); + JXL_ASSERT(1 <= biased_exp16 && biased_exp16 < 31); + mantissa16 = mantissa32 >> 13; + } + + JXL_ASSERT(mantissa16 < 1024); + const uint32_t bits16 = (sign << 15) | (biased_exp16 << 10) | mantissa16; + JXL_ASSERT(bits16 < 0x10000); + writer->Write(16, bits16); + return true; +} + +Status F16Coder::CanEncode(float value, size_t* JXL_RESTRICT encoded_bits) { + *encoded_bits = MaxEncodedBits(); + if (std::isnan(value) || std::isinf(value)) { + return JXL_FAILURE("Should not attempt to store NaN and infinity"); + } + return std::abs(value) <= 65504.0f; +} + +} // namespace jxl diff --git a/lib/jxl/fields.h b/lib/jxl/fields.h new file mode 100644 index 0000000..244b96f --- /dev/null +++ b/lib/jxl/fields.h @@ -0,0 +1,300 @@ +// 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_FIELDS_H_ +#define LIB_JXL_FIELDS_H_ + +// Forward/backward-compatible 'bundles' with auto-serialized 'fields'. + +#include +#include +#include +#include +#include + +#include +#include // abs +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +// Integer coders: BitsCoder (raw), U32Coder (table), U64Coder (varint). + +// Reads/writes a given (fixed) number of bits <= 32. +class BitsCoder { + public: + static size_t MaxEncodedBits(const size_t bits) { return bits; } + + static Status CanEncode(const size_t bits, const uint32_t value, + size_t* JXL_RESTRICT encoded_bits) { + *encoded_bits = bits; + if (value >= (1ULL << bits)) { + return JXL_FAILURE("Value %u too large for %zu bits", value, bits); + } + return true; + } + + static uint32_t Read(const size_t bits, BitReader* JXL_RESTRICT reader) { + return reader->ReadBits(bits); + } + + // Returns false if the value is too large to encode. + static Status Write(const size_t bits, const uint32_t value, + BitWriter* JXL_RESTRICT writer) { + if (value >= (1ULL << bits)) { + return JXL_FAILURE("Value %d too large to encode in %zu bits", value, + bits); + } + writer->Write(bits, value); + return true; + } +}; + +// Encodes u32 using a lookup table and/or extra bits, governed by a per-field +// encoding `enc` which consists of four distributions `d` chosen via a 2-bit +// selector (least significant = 0). Each d may have two modes: +// - direct: if d.IsDirect(), the value is d.Direct(); +// - offset: the value is derived from d.ExtraBits() extra bits plus d.Offset(); +// This encoding is denser than Exp-Golomb or Gamma codes when both small and +// large values occur. +// +// Examples: +// Direct: U32Enc(Val(8), Val(16), Val(32), Bits(6)), value 32 => 10b. +// Offset: U32Enc(Val(0), BitsOffset(1, 1), BitsOffset(2, 3), BitsOffset(8, 8)) +// defines the following prefix code: +// 00 -> 0 +// 01x -> 1..2 +// 10xx -> 3..7 +// 11xxxxxxxx -> 8..263 +class U32Coder { + public: + static size_t MaxEncodedBits(U32Enc enc); + static Status CanEncode(U32Enc enc, uint32_t value, + size_t* JXL_RESTRICT encoded_bits); + static uint32_t Read(U32Enc enc, BitReader* JXL_RESTRICT reader); + + // Returns false if the value is too large to encode. + static Status Write(U32Enc enc, uint32_t value, + BitWriter* JXL_RESTRICT writer); + + private: + static Status ChooseSelector(U32Enc enc, uint32_t value, + uint32_t* JXL_RESTRICT selector, + size_t* JXL_RESTRICT total_bits); +}; + +// Encodes 64-bit unsigned integers with a fixed distribution, taking 2 bits +// to encode 0, 6 bits to encode 1 to 16, 10 bits to encode 17 to 272, 15 bits +// to encode up to 4095, and on the order of log2(value) * 1.125 bits for +// larger values. +class U64Coder { + public: + static constexpr size_t MaxEncodedBits() { + return 2 + 12 + 6 * (8 + 1) + (4 + 1); + } + + static uint64_t Read(BitReader* JXL_RESTRICT reader); + + // Returns false if the value is too large to encode. + static Status Write(uint64_t value, BitWriter* JXL_RESTRICT writer); + + // Can always encode, but useful because it also returns bit size. + static Status CanEncode(uint64_t value, size_t* JXL_RESTRICT encoded_bits); +}; + +// IEEE 754 half-precision (binary16). Refuses to read/write NaN/Inf. +class F16Coder { + public: + static constexpr size_t MaxEncodedBits() { return 16; } + + // Returns false if the bit representation is NaN or infinity + static Status Read(BitReader* JXL_RESTRICT reader, float* JXL_RESTRICT value); + + // Returns false if the value is too large to encode. + static Status Write(float value, BitWriter* JXL_RESTRICT writer); + static Status CanEncode(float value, size_t* JXL_RESTRICT encoded_bits); +}; + +// A "bundle" is a forward- and backward compatible collection of fields. +// They are used for SizeHeader/FrameHeader/GroupHeader. Bundles can be +// extended by appending(!) fields. Optional fields may be omitted from the +// bitstream by conditionally visiting them. When reading new bitstreams with +// old code, we skip unknown fields at the end of the bundle. This requires +// storing the amount of extra appended bits, and that fields are visited in +// chronological order of being added to the format, because old decoders +// cannot skip some future fields and resume reading old fields. Similarly, +// new readers query bits in an "extensions" field to skip (groups of) fields +// not present in old bitstreams. Note that each bundle must include an +// "extensions" field prior to freezing the format, otherwise it cannot be +// extended. +// +// To ensure interoperability, there will be no opaque fields. +// +// HOWTO: +// - basic usage: define a struct with member variables ("fields") and a +// VisitFields(v) member function that calls v->U32/Bool etc. for each +// field, specifying their default values. The ctor must call +// Bundle::Init(this). +// +// - print a trace of visitors: ensure each bundle has a static Name() member +// function, and change Bundle::Print* to return true. +// +// - optional fields: in VisitFields, add if (v->Conditional(your_condition)) +// { v->Bool(default, &field); }. This prevents reading/writing field +// if !your_condition, which is typically computed from a prior field. +// WARNING: to ensure all fields are initialized, do not add an else branch; +// instead add another if (v->Conditional(!your_condition)). +// +// - repeated fields: for dynamic sizes, use e.g. std::vector and in +// VisitFields, if (v->IsReading()) field.resize(size) before accessing field. +// For static or bounded sizes, use an array or std::array. In all cases, +// simply visit each array element as if it were a normal field. +// +// - nested bundles: add a bundle as a normal field and in VisitFields call +// JXL_RETURN_IF_ERROR(v->VisitNested(&nested)); +// +// - allow future extensions: define a "uint64_t extensions" field and call +// v->BeginExtensions(&extensions) after visiting all non-extension fields, +// and `return v->EndExtensions();` after the last extension field. +// +// - encode an entire bundle in one bit if ALL its fields equal their default +// values: add a "mutable bool all_default" field and as the first visitor: +// if (v->AllDefault(*this, &all_default)) { +// // Overwrite all serialized fields, but not any nonserialized_*. +// v->SetDefault(this); +// return true; +// } +// Note: if extensions are present, AllDefault() == false. + +class Bundle { + public: + static constexpr size_t kMaxExtensions = 64; // bits in u64 + + // Print the type of each visitor called. + static constexpr bool PrintVisitors() { return false; } + // Print default value for each field and AllDefault result. + static constexpr bool PrintAllDefault() { return false; } + // Print values decoded for each field in Read. + static constexpr bool PrintRead() { return false; } + // Print size for each field and CanEncode total_bits. + static constexpr bool PrintSizes() { return false; } + + // Initializes fields to the default values. It is not recursive to nested + // fields, this function is intended to be called in the constructors so + // each nested field will already Init itself. + static void Init(Fields* JXL_RESTRICT fields); + + // Similar to Init, but recursive to nested fields. + static void SetDefault(Fields* JXL_RESTRICT fields); + + // Returns whether ALL fields (including `extensions`, if present) are equal + // to their default value. + static bool AllDefault(const Fields& fields); + + // Returns max number of bits required to encode a T. + static size_t MaxBits(const Fields& fields); + + // Returns whether a header's fields can all be encoded, i.e. they have a + // valid representation. If so, "*total_bits" is the exact number of bits + // required. Called by Write. + static Status CanEncode(const Fields& fields, + size_t* JXL_RESTRICT extension_bits, + size_t* JXL_RESTRICT total_bits); + + static Status Read(BitReader* reader, Fields* JXL_RESTRICT fields); + + // Returns whether enough bits are available to fully read this bundle using + // Read. Also returns true in case of a codestream error (other than not being + // large enough): that means enough bits are available to determine there's an + // error, use Read to get such error status. + // NOTE: this advances the BitReader, a different one pointing back at the + // original bit position in the codestream must be created to use Read after + // this. + static bool CanRead(BitReader* reader, Fields* JXL_RESTRICT fields); + + static Status Write(const Fields& fields, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* aux_out); + + private: +}; + +// Different subclasses of Visitor are passed to implementations of Fields +// throughout their lifetime. Templates used to be used for this but dynamic +// polymorphism produces more compact executables than template reification did. +class Visitor { + public: + virtual ~Visitor() = default; + virtual Status Visit(Fields* fields, const char* visitor_name) = 0; + + virtual Status Bool(bool default_value, bool* JXL_RESTRICT value) = 0; + virtual Status U32(U32Enc, uint32_t, uint32_t*) = 0; + + // Helper to construct U32Enc from U32Distr. + Status U32(const U32Distr d0, const U32Distr d1, const U32Distr d2, + const U32Distr d3, const uint32_t default_value, + uint32_t* JXL_RESTRICT value) { + return U32(U32Enc(d0, d1, d2, d3), default_value, value); + } + + template + Status Enum(const EnumT default_value, EnumT* JXL_RESTRICT value) { + uint32_t u32 = static_cast(*value); + // 00 -> 0 + // 01 -> 1 + // 10xxxx -> 2..17 + // 11yyyyyy -> 18..81 + JXL_RETURN_IF_ERROR(U32(Val(0), Val(1), BitsOffset(4, 2), BitsOffset(6, 18), + static_cast(default_value), &u32)); + *value = static_cast(u32); + return EnumValid(*value); + } + + virtual Status Bits(size_t bits, uint32_t default_value, + uint32_t* JXL_RESTRICT value) = 0; + virtual Status U64(uint64_t default_value, uint64_t* JXL_RESTRICT value) = 0; + virtual Status F16(float default_value, float* JXL_RESTRICT value) = 0; + + // Returns whether VisitFields should visit some subsequent fields. + // "condition" is typically from prior fields, e.g. flags. + // Overridden by InitVisitor and MaxBitsVisitor. + virtual Status Conditional(bool condition) { return condition; } + + // Overridden by InitVisitor, AllDefaultVisitor and CanEncodeVisitor. + virtual Status AllDefault(const Fields& /*fields*/, + bool* JXL_RESTRICT all_default) { + JXL_RETURN_IF_ERROR(Bool(true, all_default)); + return *all_default; + } + + virtual void SetDefault(Fields* /*fields*/) { + // Do nothing by default, this is overridden by ReadVisitor. + } + + // Returns the result of visiting a nested Bundle. + // Overridden by InitVisitor. + virtual Status VisitNested(Fields* fields) { return Visit(fields, ""); } + + // Overridden by ReadVisitor. Enables dynamically-sized fields. + virtual bool IsReading() const { return false; } + + virtual Status BeginExtensions(uint64_t* JXL_RESTRICT extensions) = 0; + virtual Status EndExtensions() = 0; + + // For debugging + virtual const char* VisitorName() = 0; +}; + +} // namespace jxl + +#endif // LIB_JXL_FIELDS_H_ diff --git a/lib/jxl/fields_test.cc b/lib/jxl/fields_test.cc new file mode 100644 index 0000000..39497c4 --- /dev/null +++ b/lib/jxl/fields_test.cc @@ -0,0 +1,434 @@ +// 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/fields.h" + +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/common.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" + +namespace jxl { +namespace { + +// Ensures `value` round-trips and in exactly `expected_bits_written`. +void TestU32Coder(const uint32_t value, const size_t expected_bits_written) { + U32Coder coder; + const U32Enc enc(Val(0), Bits(4), Val(0x7FFFFFFF), Bits(32)); + + BitWriter writer; + BitWriter::Allotment allotment( + &writer, RoundUpBitsToByteMultiple(U32Coder::MaxEncodedBits(enc))); + + size_t precheck_pos; + EXPECT_TRUE(coder.CanEncode(enc, value, &precheck_pos)); + EXPECT_EQ(expected_bits_written, precheck_pos); + + EXPECT_TRUE(coder.Write(enc, value, &writer)); + EXPECT_EQ(expected_bits_written, writer.BitsWritten()); + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + + BitReader reader(writer.GetSpan()); + const uint32_t decoded_value = coder.Read(enc, &reader); + EXPECT_EQ(value, decoded_value); + EXPECT_TRUE(reader.Close()); +} + +TEST(FieldsTest, U32CoderTest) { + TestU32Coder(0, 2); + TestU32Coder(1, 6); + TestU32Coder(15, 6); + TestU32Coder(0x7FFFFFFF, 2); + TestU32Coder(128, 34); + TestU32Coder(0x7FFFFFFEu, 34); + TestU32Coder(0x80000000u, 34); + TestU32Coder(0xFFFFFFFFu, 34); +} + +void TestU64Coder(const uint64_t value, const size_t expected_bits_written) { + U64Coder coder; + + BitWriter writer; + BitWriter::Allotment allotment( + &writer, RoundUpBitsToByteMultiple(U64Coder::MaxEncodedBits())); + + size_t precheck_pos; + EXPECT_TRUE(coder.CanEncode(value, &precheck_pos)); + EXPECT_EQ(expected_bits_written, precheck_pos); + + EXPECT_TRUE(coder.Write(value, &writer)); + EXPECT_EQ(expected_bits_written, writer.BitsWritten()); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + + BitReader reader(writer.GetSpan()); + const uint64_t decoded_value = coder.Read(&reader); + EXPECT_EQ(value, decoded_value); + EXPECT_TRUE(reader.Close()); +} + +TEST(FieldsTest, U64CoderTest) { + // Values that should take 2 bits (selector 00): 0 + TestU64Coder(0, 2); + + // Values that should take 6 bits (2 for selector, 4 for value): 1..16 + TestU64Coder(1, 6); + TestU64Coder(2, 6); + TestU64Coder(8, 6); + TestU64Coder(15, 6); + TestU64Coder(16, 6); + + // Values that should take 10 bits (2 for selector, 8 for value): 17..272 + TestU64Coder(17, 10); + TestU64Coder(18, 10); + TestU64Coder(100, 10); + TestU64Coder(271, 10); + TestU64Coder(272, 10); + + // Values that should take 15 bits (2 for selector, 12 for value, 1 for varint + // end): (0)..273..4095 + TestU64Coder(273, 15); + TestU64Coder(274, 15); + TestU64Coder(1000, 15); + TestU64Coder(4094, 15); + TestU64Coder(4095, 15); + + // Take 24 bits (of which 20 actual value): (0)..4096..1048575 + TestU64Coder(4096, 24); + TestU64Coder(4097, 24); + TestU64Coder(10000, 24); + TestU64Coder(1048574, 24); + TestU64Coder(1048575, 24); + + // Take 33 bits (of which 28 actual value): (0)..1048576..268435455 + TestU64Coder(1048576, 33); + TestU64Coder(1048577, 33); + TestU64Coder(10000000, 33); + TestU64Coder(268435454, 33); + TestU64Coder(268435455, 33); + + // Take 42 bits (of which 36 actual value): (0)..268435456..68719476735 + TestU64Coder(268435456ull, 42); + TestU64Coder(268435457ull, 42); + TestU64Coder(1000000000ull, 42); + TestU64Coder(68719476734ull, 42); + TestU64Coder(68719476735ull, 42); + + // Take 51 bits (of which 44 actual value): (0)..68719476736..17592186044415 + TestU64Coder(68719476736ull, 51); + TestU64Coder(68719476737ull, 51); + TestU64Coder(1000000000000ull, 51); + TestU64Coder(17592186044414ull, 51); + TestU64Coder(17592186044415ull, 51); + + // Take 60 bits (of which 52 actual value): + // (0)..17592186044416..4503599627370495 + TestU64Coder(17592186044416ull, 60); + TestU64Coder(17592186044417ull, 60); + TestU64Coder(100000000000000ull, 60); + TestU64Coder(4503599627370494ull, 60); + TestU64Coder(4503599627370495ull, 60); + + // Take 69 bits (of which 60 actual value): + // (0)..4503599627370496..1152921504606846975 + TestU64Coder(4503599627370496ull, 69); + TestU64Coder(4503599627370497ull, 69); + TestU64Coder(10000000000000000ull, 69); + TestU64Coder(1152921504606846974ull, 69); + TestU64Coder(1152921504606846975ull, 69); + + // Take 73 bits (of which 64 actual value): + // (0)..1152921504606846976..18446744073709551615 + TestU64Coder(1152921504606846976ull, 73); + TestU64Coder(1152921504606846977ull, 73); + TestU64Coder(10000000000000000000ull, 73); + TestU64Coder(18446744073709551614ull, 73); + TestU64Coder(18446744073709551615ull, 73); +} + +Status TestF16Coder(const float value) { + F16Coder coder; + + size_t max_encoded_bits; + // It is not a fatal error if it can't be encoded. + if (!coder.CanEncode(value, &max_encoded_bits)) return false; + EXPECT_EQ(F16Coder::MaxEncodedBits(), max_encoded_bits); + + BitWriter writer; + BitWriter::Allotment allotment(&writer, + RoundUpBitsToByteMultiple(max_encoded_bits)); + + EXPECT_TRUE(coder.Write(value, &writer)); + EXPECT_EQ(F16Coder::MaxEncodedBits(), writer.BitsWritten()); + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + + BitReader reader(writer.GetSpan()); + float decoded_value; + EXPECT_TRUE(coder.Read(&reader, &decoded_value)); + // All values we test can be represented exactly. + EXPECT_EQ(value, decoded_value); + EXPECT_TRUE(reader.Close()); + return true; +} + +TEST(FieldsTest, F16CoderTest) { + for (float sign : {-1.0f, 1.0f}) { + // (anything less than 1E-3 are subnormals) + for (float mag : {0.0f, 0.5f, 1.0f, 2.0f, 2.5f, 16.015625f, 1.0f / 4096, + 1.0f / 16384, 65504.0f}) { + EXPECT_TRUE(TestF16Coder(sign * mag)); + } + } + + // Out of range + EXPECT_FALSE(TestF16Coder(65504.01f)); + EXPECT_FALSE(TestF16Coder(-65505.0f)); +} + +// Ensures Read(Write()) returns the same fields. +TEST(FieldsTest, TestRoundtripSize) { + for (int i = 0; i < 8; i++) { + SizeHeader size; + ASSERT_TRUE(size.Set(123 + 77 * i, 7 + i)); + + size_t extension_bits = 999, total_bits = 999; // Initialize as garbage. + ASSERT_TRUE(Bundle::CanEncode(size, &extension_bits, &total_bits)); + EXPECT_EQ(0u, extension_bits); + + BitWriter writer; + ASSERT_TRUE(WriteSizeHeader(size, &writer, 0, nullptr)); + EXPECT_EQ(total_bits, writer.BitsWritten()); + writer.ZeroPadToByte(); + + SizeHeader size2; + BitReader reader(writer.GetSpan()); + ASSERT_TRUE(ReadSizeHeader(&reader, &size2)); + EXPECT_EQ(total_bits, reader.TotalBitsConsumed()); + EXPECT_TRUE(reader.Close()); + + EXPECT_EQ(size.xsize(), size2.xsize()); + EXPECT_EQ(size.ysize(), size2.ysize()); + } +} + +// Ensure all values can be reached by the encoding. +TEST(FieldsTest, TestCropRect) { + CodecMetadata metadata; + for (int32_t i = -1000; i < 19000; ++i) { + FrameHeader f(&metadata); + f.custom_size_or_origin = true; + f.frame_origin.x0 = i; + f.frame_origin.y0 = i; + f.frame_size.xsize = 1000 + i; + f.frame_size.ysize = 1000 + i; + size_t extension_bits = 0, total_bits = 0; + ASSERT_TRUE(Bundle::CanEncode(f, &extension_bits, &total_bits)); + EXPECT_EQ(0u, extension_bits); + EXPECT_GE(total_bits, 9u); + } +} +TEST(FieldsTest, TestPreview) { + // (div8 cannot represent 4360, but !div8 can go a little higher) + for (uint32_t i = 1; i < 4360; ++i) { + PreviewHeader p; + ASSERT_TRUE(p.Set(i, i)); + size_t extension_bits = 0, total_bits = 0; + ASSERT_TRUE(Bundle::CanEncode(p, &extension_bits, &total_bits)); + EXPECT_EQ(0u, extension_bits); + EXPECT_GE(total_bits, 6u); + } +} + +// Ensures Read(Write()) returns the same fields. +TEST(FieldsTest, TestRoundtripFrame) { + CodecMetadata metadata; + FrameHeader h(&metadata); + h.extensions = 0x800; + + size_t extension_bits = 999, total_bits = 999; // Initialize as garbage. + ASSERT_TRUE(Bundle::CanEncode(h, &extension_bits, &total_bits)); + EXPECT_EQ(0u, extension_bits); + BitWriter writer; + ASSERT_TRUE(WriteFrameHeader(h, &writer, nullptr)); + EXPECT_EQ(total_bits, writer.BitsWritten()); + writer.ZeroPadToByte(); + + FrameHeader h2(&metadata); + BitReader reader(writer.GetSpan()); + ASSERT_TRUE(ReadFrameHeader(&reader, &h2)); + EXPECT_EQ(total_bits, reader.TotalBitsConsumed()); + EXPECT_TRUE(reader.Close()); + + EXPECT_EQ(h.extensions, h2.extensions); + EXPECT_EQ(h.flags, h2.flags); +} + +#ifndef JXL_CRASH_ON_ERROR +// Ensure out-of-bounds values cause an error. +TEST(FieldsTest, TestOutOfRange) { + SizeHeader h; + ASSERT_TRUE(h.Set(0xFFFFFFFFull, 0xFFFFFFFFull)); + size_t extension_bits = 999, total_bits = 999; // Initialize as garbage. + ASSERT_FALSE(Bundle::CanEncode(h, &extension_bits, &total_bits)); +} +#endif + +struct OldBundle : public Fields { + OldBundle() { Bundle::Init(this); } + const char* Name() const override { return "OldBundle"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Bits(2), Bits(3), Bits(4), 1, &old_small)); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(1.125f, &old_f)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Bits(7), Bits(12), Bits(16), Bits(32), 0, &old_large)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions)); + return visitor->EndExtensions(); + } + + uint32_t old_small; + float old_f; + uint32_t old_large; + uint64_t extensions; +}; + +struct NewBundle : public Fields { + NewBundle() { Bundle::Init(this); } + const char* Name() const override { return "NewBundle"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Bits(2), Bits(3), Bits(4), 1, &old_small)); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(1.125f, &old_f)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Bits(7), Bits(12), Bits(16), Bits(32), 0, &old_large)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions)); + if (visitor->Conditional(extensions & 1)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(2), Bits(2), Bits(3), Bits(4), 2, &new_small)); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(-2.0f, &new_f)); + } + if (visitor->Conditional(extensions & 2)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Bits(9), Bits(12), Bits(16), Bits(32), 0, &new_large)); + } + return visitor->EndExtensions(); + } + + uint32_t old_small; + float old_f; + uint32_t old_large; + uint64_t extensions; + + // If extensions & 1 + uint32_t new_small = 2; + float new_f = -2.0f; + // If extensions & 2 + uint32_t new_large = 0; +}; + +TEST(FieldsTest, TestNewDecoderOldData) { + OldBundle old_bundle; + old_bundle.old_large = 123; + old_bundle.old_f = 3.75f; + old_bundle.extensions = 0; + + // Write to bit stream + const size_t kMaxOutBytes = 999; + BitWriter writer; + // Make sure values are initialized by code under test. + size_t extension_bits = 12345, total_bits = 12345; + ASSERT_TRUE(Bundle::CanEncode(old_bundle, &extension_bits, &total_bits)); + ASSERT_LE(total_bits, kMaxOutBytes * kBitsPerByte); + EXPECT_EQ(0u, extension_bits); + AuxOut aux_out; + ASSERT_TRUE(Bundle::Write(old_bundle, &writer, kLayerHeader, &aux_out)); + + BitWriter::Allotment allotment(&writer, + kMaxOutBytes * kBitsPerByte - total_bits); + writer.Write(20, 0xA55A); // sentinel + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, kLayerHeader, nullptr); + + ASSERT_LE(writer.GetSpan().size(), kMaxOutBytes); + BitReader reader(writer.GetSpan()); + NewBundle new_bundle; + ASSERT_TRUE(Bundle::Read(&reader, &new_bundle)); + EXPECT_EQ(reader.TotalBitsConsumed(), + aux_out.layers[kLayerHeader].total_bits); + EXPECT_EQ(reader.ReadBits(20), 0xA55Au); + EXPECT_TRUE(reader.Close()); + + // Old fields are the same in both + EXPECT_EQ(old_bundle.extensions, new_bundle.extensions); + EXPECT_EQ(old_bundle.old_small, new_bundle.old_small); + EXPECT_EQ(old_bundle.old_f, new_bundle.old_f); + EXPECT_EQ(old_bundle.old_large, new_bundle.old_large); + // New fields match their defaults + EXPECT_EQ(2u, new_bundle.new_small); + EXPECT_EQ(-2.0f, new_bundle.new_f); + EXPECT_EQ(0u, new_bundle.new_large); +} + +TEST(FieldsTest, TestOldDecoderNewData) { + NewBundle new_bundle; + new_bundle.old_large = 123; + new_bundle.extensions = 3; + new_bundle.new_f = 999.0f; + new_bundle.new_large = 456; + + // Write to bit stream + constexpr size_t kMaxOutBytes = 999; + BitWriter writer; + // Make sure values are initialized by code under test. + size_t extension_bits = 12345, total_bits = 12345; + ASSERT_TRUE(Bundle::CanEncode(new_bundle, &extension_bits, &total_bits)); + EXPECT_NE(0u, extension_bits); + AuxOut aux_out; + ASSERT_TRUE(Bundle::Write(new_bundle, &writer, kLayerHeader, &aux_out)); + ASSERT_LE(aux_out.layers[kLayerHeader].total_bits, + kMaxOutBytes * kBitsPerByte); + + BitWriter::Allotment allotment( + &writer, + kMaxOutBytes * kBitsPerByte - aux_out.layers[kLayerHeader].total_bits); + // Ensure Read skips the additional fields + writer.Write(20, 0xA55A); // sentinel + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, kLayerHeader, nullptr); + + BitReader reader(writer.GetSpan()); + OldBundle old_bundle; + ASSERT_TRUE(Bundle::Read(&reader, &old_bundle)); + EXPECT_EQ(reader.TotalBitsConsumed(), + aux_out.layers[kLayerHeader].total_bits); + EXPECT_EQ(reader.ReadBits(20), 0xA55Au); + EXPECT_TRUE(reader.Close()); + + // Old fields are the same in both + EXPECT_EQ(new_bundle.extensions, old_bundle.extensions); + EXPECT_EQ(new_bundle.old_small, old_bundle.old_small); + EXPECT_EQ(new_bundle.old_f, old_bundle.old_f); + EXPECT_EQ(new_bundle.old_large, old_bundle.old_large); + // (Can't check new fields because old decoder doesn't know about them) +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/filters.cc b/lib/jxl/filters.cc new file mode 100644 index 0000000..9cb62c1 --- /dev/null +++ b/lib/jxl/filters.cc @@ -0,0 +1,112 @@ +// 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/filters.h" + +#include + +#include "lib/jxl/base/profiler.h" + +namespace jxl { + +Status FilterWeights::Init(const LoopFilter& lf, + const FrameDimensions& frame_dim) { + if (lf.epf_iters > 0) { + sigma = ImageF(frame_dim.xsize_blocks + 2 * kSigmaPadding, + frame_dim.ysize_blocks + 2 * kSigmaPadding); + } + if (lf.gab) { + JXL_RETURN_IF_ERROR(GaborishWeights(lf)); + } + return true; +} + +Status FilterWeights::GaborishWeights(const LoopFilter& lf) { + const float kZeroEpsilon = 1e-6; + + gab_weights[0] = 1; + gab_weights[1] = lf.gab_x_weight1; + gab_weights[2] = lf.gab_x_weight2; + gab_weights[3] = 1; + gab_weights[4] = lf.gab_y_weight1; + gab_weights[5] = lf.gab_y_weight2; + gab_weights[6] = 1; + gab_weights[7] = lf.gab_b_weight1; + gab_weights[8] = lf.gab_b_weight2; + // Normalize + for (size_t c = 0; c < 3; c++) { + const float div = gab_weights[3 * c] + + 4 * (gab_weights[3 * c + 1] + gab_weights[3 * c + 2]); + if (std::abs(div) < kZeroEpsilon) { + return JXL_FAILURE("Gaborish weights lead to near 0 unnormalized kernel"); + } + const float mul = 1.0f / div; + gab_weights[3 * c] *= mul; + gab_weights[3 * c + 1] *= mul; + gab_weights[3 * c + 2] *= mul; + } + return true; +} + +void FilterPipeline::ApplyFiltersRow(const LoopFilter& lf, + const FilterWeights& filter_weights, + ssize_t y) { + PROFILER_ZONE("Gaborish+EPF"); + JXL_DASSERT(num_filters != 0); // Must be initialized. + + JXL_ASSERT(y < static_cast(image_rect.ysize() + lf.Padding())); + + // The minimum value of the center row "y" needed to process the current + // filter. + ssize_t rows_needed = -static_cast(lf.Padding()); + + // We pass `image_rect.x0() - image_rect.x0() % kBlockDim` as the x0 for + // the row_sigma, so to go from an `x` value in the filter to the + // corresponding value in row_sigma we use the fact that we mapped + // image_rect.x0() in the original image to MaxLeftPadding(image_rect.x0()) in + // the input/output rows seen by the filters: + // x_in_sigma_row = + // ((x - (image_rect.x0() % kPaddingXRound) + image_rect.x0()) - + // (image_rect.x0() - image_rect.x0() % kBlockDim))) / kBlockDim + // x_in_sigma_row = + // x - image_rect.x0() % kPaddingXRound + image_rect.x0() % kBlockDim + const size_t sigma_x_offset = + image_rect.x0() % kBlockDim - + image_rect.x0() % GroupBorderAssigner::kPaddingXRound; + + for (size_t i = 0; i < num_filters; i++) { + const FilterStep& filter = filters[i]; + + rows_needed += filter.filter_def.border; + + // After this "y" points to the rect row for the center of the filter. + y -= filter.filter_def.border; + if (y < rows_needed) return; + + // Apply filter to the given region. + FilterRows rows(filter.filter_def.border); + filter.set_input_rows(filter, &rows, y); + filter.set_output_rows(filter, &rows, y); + + // The "y" coordinate used for the sigma image in EPF1. Sigma is padded + // with kMaxFilterPadding (or kMaxFilterPadding/kBlockDim rows in sigma) + // above and below. + const size_t sigma_y = kMaxFilterPadding + image_rect.y0() + y; + // The offset to subtract to a "x" value in the filter to obtain the + // corresponding x in the sigma row. + if (compute_sigma) { + rows.SetSigma(filter_weights.sigma, sigma_y, + image_rect.x0() - image_rect.x0() % kBlockDim); + } + + filter.filter_def.apply(rows, lf, filter_weights, filter.filter_x0, + filter.filter_x1, sigma_x_offset, + sigma_y % kBlockDim); + } + + JXL_DASSERT(rows_needed == 0); +} + +} // namespace jxl diff --git a/lib/jxl/filters.h b/lib/jxl/filters.h new file mode 100644 index 0000000..1dad66f --- /dev/null +++ b/lib/jxl/filters.h @@ -0,0 +1,348 @@ +// 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_FILTERS_H_ +#define LIB_JXL_FILTERS_H_ + +#include + +#include "lib/jxl/common.h" +#include "lib/jxl/dec_group_border.h" +#include "lib/jxl/filters_internal.h" +#include "lib/jxl/image.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/sanitizers.h" + +namespace jxl { + +struct FilterWeights { + // Initialize the FilterWeights for the passed LoopFilter and FrameDimensions. + // Returns an error if the weights are invalid. + Status Init(const LoopFilter& lf, const FrameDimensions& frame_dim); + + // Normalized weights for gaborish, in XYB order, each weight for Manhattan + // distance of 0, 1 and 2 respectively. + float gab_weights[9]; + + // Sigma values for EPF, if enabled. + // Note that, for speed reasons, this is actually kInvSigmaNum / sigma. + ImageF sigma; + + private: + Status GaborishWeights(const LoopFilter& lf); +}; + +static constexpr size_t kMaxFinalizeRectPadding = 9; + +// Line-based EPF only needs to keep in cache 21 lines of the image, so 256 is +// sufficient for everything to fit in the L2 cache. We add +// 2*RoundUpTo(kMaxFinalizeRectPadding, kBlockDim) pixels as we might have up to +// two extra borders on each side. +constexpr size_t kApplyImageFeaturesTileDim = + 256 + 2 * RoundUpToBlockDim(kMaxFinalizeRectPadding); + +// The maximum row storage needed by the filtering pipeline. This is the sum of +// the number of input rows needed by each step. +constexpr size_t kTotalStorageRows = 7 + 5 + 3; // max is EPF0 + EPF1 + EPF2. + +// The maximum sum of all the borders in a chain of filters. +constexpr size_t kMaxFilterBorder = 1 * kBlockDim; + +// The maximum horizontal filter padding ever needed to apply a chain of +// filters. Intermediate storage must have at least as much padding on each +// left and right sides. This value must be a multiple of kBlockDim. +constexpr size_t kMaxFilterPadding = kMaxFilterBorder + kBlockDim; +static_assert(kMaxFilterPadding % kBlockDim == 0, + "kMaxFilterPadding must be a multiple of block size."); + +// Same as FilterBorder and FilterPadding but for Sigma. +constexpr size_t kSigmaBorder = kMaxFilterBorder / kBlockDim; +constexpr size_t kSigmaPadding = kMaxFilterPadding / kBlockDim; + +// Utility struct to define input/output rows of row-based loop filters. +constexpr size_t kMaxBorderSize = 3; +struct FilterRows { + explicit FilterRows(int border_size) : border_size_(border_size) { + JXL_DASSERT(border_size <= static_cast(kMaxBorderSize)); + } + + JXL_INLINE const float* GetInputRow(int row, size_t c) const { + // Check that row is within range. + JXL_DASSERT(-border_size_ <= row && row <= border_size_); + return rows_in_[c] + offsets_in_[kMaxBorderSize + row]; + } + + float* GetOutputRow(size_t c) const { return rows_out_[c]; } + + const float* GetSigmaRow() const { + JXL_DASSERT(row_sigma_ != nullptr); + return row_sigma_; + } + + template + void SetInput(const Image3F& in, size_t y_offset, ssize_t y0, ssize_t x0, + ssize_t full_image_y_offset = 0, ssize_t image_ysize = 0) { + RowMap row_map(full_image_y_offset, image_ysize); + for (size_t c = 0; c < 3; c++) { + rows_in_[c] = in.ConstPlaneRow(c, 0); + } + for (int32_t i = -border_size_; i <= border_size_; i++) { + size_t y = row_map(y0 + i); + offsets_in_[i + kMaxBorderSize] = + static_cast((y + y_offset) * in.PixelsPerRow()) + x0; + } + } + + template + void SetOutput(Image3F* out, size_t y_offset, ssize_t y0, ssize_t x0) { + size_t y = RowMap()(y0); + for (size_t c = 0; c < 3; c++) { + rows_out_[c] = out->PlaneRow(c, y + y_offset) + x0; + } + } + + // Sets the sigma row for the given y0, x0 input image position. Sigma images + // have one pixel per input image block, although they are padded with two + // blocks (pixels in sigma) on each one of the four sides. The (x0, y0) values + // should include this padding. + void SetSigma(const ImageF& sigma, size_t y0, size_t x0) { + JXL_DASSERT(x0 % kBlockDim == 0); + row_sigma_ = sigma.ConstRow(y0 / kBlockDim) + x0 / kBlockDim; + } + + private: + // Base pointer to each one of the planes. + const float* JXL_RESTRICT rows_in_[3]; + + // Offset to the pixel x0 at the different rows. offsets_in_[kMaxBorderSize] + // references the center row, regardless of the border_size_. Only the center + // row, border_size_ before and border_size_ after are initialized. The offset + // is relative to the base pointer in rows_in_. + ssize_t offsets_in_[2 * kMaxBorderSize + 1]; + + float* JXL_RESTRICT rows_out_[3]; + + const float* JXL_RESTRICT row_sigma_{nullptr}; + + const int border_size_; +}; + +// Definition of a filter. This specifies the function to be used to apply the +// filter and its row and column padding requirements. +struct FilterDefinition { + // Function to apply the filter to a given row. The filter constant parameters + // are passed in LoopFilter lf and filter_weights. `sigma_x_offset` is needed + // to offset the `x0` value so that it will cause correct accesses to + // rows.GetSigmaRow(): there is just one sigma value per 8 pixels, and if the + // image rectangle is not aligned to multiples of 8 pixels, we need to + // compensate for the difference between x0 and the image position modulo 8. + void (*apply)(const FilterRows& rows, const LoopFilter& lf, + const FilterWeights& filter_weights, size_t x0, size_t x1, + size_t sigma_x_offset, size_t image_y_mod_8); + + // Number of source image rows and cols before and after an input pixel needed + // to compute the output of the filter. For a 3x3 convolution this border will + // be only 1. + size_t border; +}; + +// A chain of filters to be applied to a source image. This instance must be +// initialized by the FilterPipelineInit() function before it can be used. +class FilterPipeline { + public: + FilterPipeline() : FilterPipeline(kApplyImageFeaturesTileDim) {} + explicit FilterPipeline(size_t max_rect_xsize) + : storage{max_rect_xsize + 2 * kMaxFilterPadding + + GroupBorderAssigner::kPaddingXRound, + kTotalStorageRows} { +#if MEMORY_SANITIZER + // The padding of the storage may be used uninitialized since we process + // multiple SIMD lanes at a time, aligned to a multiple of lanes. + // For example, in a hypothetical 3-step filter process where all filters + // use 1 pixel border the first filter needs to process 2 pixels more on + // each side than the requested rect.x0(), rect.xsize(), while the second + // filter needs to process 1 more pixel on each side, however for + // performance reasons both will process Lanes(df) more pixels on each + // side assuming this Lanes(df) value is more than one. In that case the + // second filter will be using one pixel of uninitialized data to generate + // an output pixel that won't affect the final output but may cause msan + // failures. For this reason we initialize the padding region. + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < storage.ysize(); y++) { + float* row = storage.PlaneRow(c, y); + std::fill(row, row + kMaxFilterPadding, msan::kSanitizerSentinel); + std::fill(row + storage.xsize() - kMaxFilterPadding, + row + storage.xsize(), msan::kSanitizerSentinel); + } + } +#endif // MEMORY_SANITIZER + } + + FilterPipeline(const FilterPipeline&) = delete; + FilterPipeline(FilterPipeline&&) = default; + + // Apply the filter chain to a given row. To apply the filter chain to a whole + // image this must be called for `image_rect.ysize() + 2 * total_border` + // values of `y`, in increasing order, starting from `y = -total_border`. + // `image_rect` is the value passed to FilterPipelineInit(). + void ApplyFiltersRow(const LoopFilter& lf, + const FilterWeights& filter_weights, ssize_t y); + + struct FilterStep { + // We don't map self.input_rect.x0() directly to kMaxFilterPadding in + // input/output row since they might have a different alignment, instead we + // keep the alignment modulo kPaddingXRound. + static size_t MaxLeftPadding(size_t image_rect_x0) { + return kMaxFilterPadding + + image_rect_x0 % GroupBorderAssigner::kPaddingXRound; + } + + // Sets the input of the filter step as an image region. + void SetInput(const Image3F* im_input, const Rect& input_rect, + const Rect& image_rect, size_t image_ysize) { + input = im_input; + this->input_rect = input_rect; + this->image_rect = image_rect; + this->image_ysize = image_ysize; + JXL_DASSERT(SameSize(input_rect, image_rect)); + set_input_rows = [](const FilterStep& self, FilterRows* rows, + ssize_t y0) { + ssize_t full_image_y_offset = + static_cast(self.image_rect.y0()) - + static_cast(self.input_rect.y0()); + rows->SetInput(*(self.input), 0, + self.input_rect.y0() + y0, + self.input_rect.x0() - kMaxFilterPadding, + full_image_y_offset, self.image_ysize); + rows->SetInput( + *(self.input), 0, self.input_rect.y0() + y0, + self.input_rect.x0() - MaxLeftPadding(self.input_rect.x0()), + full_image_y_offset, self.image_ysize); + }; + } + + // Sets the input of the filter step as the temporary cyclic storage with + // num_rows rows. The value image_rect.x0() during application will be + // mapped to "kMaxFilterPadding + alignment" regardless of the rect being + // processed. + template + void SetInputCyclicStorage(const Image3F* storage, size_t offset_rows) { + input = storage; + input_y_offset = offset_rows; + set_input_rows = [](const FilterStep& self, FilterRows* rows, + ssize_t y0) { + rows->SetInput>(*(self.input), self.input_y_offset, + y0, 0); + }; + } + + // Sets the output of the filter step as the temporary cyclic storage with + // num_rows rows. The value image_rect.x0() during application will be + // mapped to "kMaxFilterPadding + alignment" regardless of the rect being + // processed. + template + void SetOutputCyclicStorage(Image3F* storage, size_t offset_rows) { + output = storage; + output_y_offset = offset_rows; + set_output_rows = [](const FilterStep& self, FilterRows* rows, + ssize_t y0) { + rows->SetOutput>(self.output, self.output_y_offset, + y0, 0); + }; + } + + // Set the output of the filter step as the output image. The value + // rect.x0() will be mapped to the same value in the output image. + void SetOutput(Image3F* im_output, const Rect& output_rect) { + output = im_output; + this->output_rect = output_rect; + set_output_rows = [](const FilterStep& self, FilterRows* rows, + ssize_t y0) { + rows->SetOutput(self.output, 0, self.output_rect.y0() + y0, + static_cast(self.output_rect.x0()) - + MaxLeftPadding(self.output_rect.x0())); + }; + } + + // The input and output image buffers for the current filter step. Note that + // the rows used from these images depends on the module used in + // set_input_rows and set_output_rows functions. + const Image3F* input; + size_t input_y_offset = 0; + Image3F* output; + size_t output_y_offset = 0; + + // Input/output rect for the first/last steps of the filter. + Rect input_rect; + Rect output_rect; + + // Information to properly do RowMapMirror(). + Rect image_rect; + size_t image_ysize; + + // Functions that compute the list of rows needed to process a region for + // the given row and starting column. + void (*set_input_rows)(const FilterStep&, FilterRows* rows, ssize_t y0); + void (*set_output_rows)(const FilterStep&, FilterRows* rows, ssize_t y0); + + // Actual filter descriptor. + FilterDefinition filter_def; + + // Range of output pixels of the step. The filter [x0, x1) range is always + // a multiple of Lanes(df) and is large enough to contain the input and + // border needed by the next stages, but values outside that range may be + // undefined values. Coordinates are relative to the FilterRows pointers. + size_t filter_x0, filter_x1; + + // Number of extra horizontal pixels needed on each side of the output of + // this filter to produce the requested rect at the end of the chain. This + // value is always 0 for the last filter of the chain but it depends on the + // actual filter chain used in other cases. + size_t output_col_border; + }; + + template + void AddStep(const FilterDefinition& filter_def) { + JXL_DASSERT(num_filters < kMaxFilters); + filters[num_filters].filter_def = filter_def; + + if (num_filters > 0) { + // If it is not the first step we need to set the previous step output to + // a portion of the cyclic storage. We only need as many rows as the + // input of the current stage. + constexpr size_t num_rows = 2 * border + 1; + filters[num_filters - 1].SetOutputCyclicStorage( + &storage, storage_rows_used); + filters[num_filters].SetInputCyclicStorage(&storage, + storage_rows_used); + storage_rows_used += num_rows; + JXL_DASSERT(storage_rows_used <= kTotalStorageRows); + } + num_filters++; + } + + // Tile storage for ApplyImageFeatures steps. Different groups of rows of this + // image are used for the intermediate steps. + Image3F storage; + size_t storage_rows_used = 0; + + static const size_t kMaxFilters = 4; + FilterStep filters[kMaxFilters]; + size_t num_filters = 0; + + // Whether we need to compute the sigma_row_ during application. + bool compute_sigma = false; + + // Rect to be processed in the image coordinates. This doesn't include any + // padding needed to produce the output. + Rect image_rect; + + // The total border needed to process this pipeline. + size_t total_border = 0; +}; + +} // namespace jxl + +#endif // LIB_JXL_FILTERS_H_ diff --git a/lib/jxl/filters_internal.h b/lib/jxl/filters_internal.h new file mode 100644 index 0000000..4ad90fa --- /dev/null +++ b/lib/jxl/filters_internal.h @@ -0,0 +1,55 @@ +// 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_FILTERS_INTERNAL_H_ +#define LIB_JXL_FILTERS_INTERNAL_H_ + +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +// Maps a row to the range [0, image_ysize) mirroring it when outside the [0, +// image_ysize) range. The input row is offset by `full_image_y_offset`, i.e. +// row `y` corresponds to row `y + full_image_y_offset` in the full frame. +struct RowMapMirror { + RowMapMirror(ssize_t full_image_y_offset, size_t image_ysize) + : full_image_y_offset_(full_image_y_offset), image_ysize_(image_ysize) {} + size_t operator()(ssize_t y) { + return Mirror(y + full_image_y_offset_, image_ysize_) - + full_image_y_offset_; + } + ssize_t full_image_y_offset_; + size_t image_ysize_; +}; + +// Maps a row in the range [-16, \inf) to a row number in the range [0, m) using +// the modulo operation. +template +struct RowMapMod { + RowMapMod() = default; + RowMapMod(ssize_t /*full_image_y_offset*/, size_t /*image_ysize*/) {} + size_t operator()(ssize_t y) { + JXL_DASSERT(y >= -16); + // The `m > 16 ? m : 16 * m` is evaluated at compile time and is a multiple + // of m of at least 16. This is to make sure that the left operand is + // positive. + return static_cast(y + (m > 16 ? m : 16 * m)) % m; + } +}; + +// Identity mapping. Maps a row in the range [0, ysize) to the same value. +struct RowMapId { + size_t operator()(ssize_t y) { + JXL_DASSERT(y >= 0); + return y; + } +}; + +} // namespace jxl + +#endif // LIB_JXL_FILTERS_INTERNAL_H_ diff --git a/lib/jxl/filters_internal_test.cc b/lib/jxl/filters_internal_test.cc new file mode 100644 index 0000000..a7dc1c5 --- /dev/null +++ b/lib/jxl/filters_internal_test.cc @@ -0,0 +1,50 @@ +// 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/filters_internal.h" + +#include "gtest/gtest.h" + +namespace jxl { + +class FiltersInternalTest : public ::testing::Test {}; + +// Test the mping of rows using RowMapMod. +TEST(FiltersInternalTest, RowMapModTest) { + RowMapMod<5> m; + // Identity part: + EXPECT_EQ(0u, m(0)); + EXPECT_EQ(4u, m(4)); + + // Larger than the module work. + EXPECT_EQ(0u, m(5)); + EXPECT_EQ(1u, m(11)); + + // Smaller than 0 up to a block. + EXPECT_EQ(4u, m(-1)); + EXPECT_EQ(2u, m(-8)); +} + +// Test the implementation for mirroring of rows. +TEST(FiltersInternalTest, RowMapMirrorTest) { + RowMapMirror m(0, 10); // Image size of 10 rows. + + EXPECT_EQ(2u, m(-3)); + EXPECT_EQ(1u, m(-2)); + EXPECT_EQ(0u, m(-1)); + + EXPECT_EQ(0u, m(0)); + EXPECT_EQ(9u, m(9)); + + EXPECT_EQ(9u, m(10)); + EXPECT_EQ(8u, m(11)); + EXPECT_EQ(7u, m(12)); + + // It mirrors the rows to infinity. + EXPECT_EQ(1u, m(21)); + EXPECT_EQ(1u, m(41)); +} + +} // namespace jxl diff --git a/lib/jxl/frame_header.cc b/lib/jxl/frame_header.cc new file mode 100644 index 0000000..55e9858 --- /dev/null +++ b/lib/jxl/frame_header.cc @@ -0,0 +1,369 @@ +// 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/frame_header.h" + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/fields.h" + +namespace jxl { + +constexpr uint8_t YCbCrChromaSubsampling::kHShift[]; +constexpr uint8_t YCbCrChromaSubsampling::kVShift[]; + +static Status VisitBlendMode(Visitor* JXL_RESTRICT visitor, + BlendMode default_value, BlendMode* blend_mode) { + uint32_t encoded = static_cast(*blend_mode); + + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val(static_cast(BlendMode::kReplace)), + Val(static_cast(BlendMode::kAdd)), + Val(static_cast(BlendMode::kBlend)), BitsOffset(2, 3), + static_cast(default_value), &encoded)); + if (encoded > 4) { + return JXL_FAILURE("Invalid blend_mode"); + } + *blend_mode = static_cast(encoded); + return true; +} + +static Status VisitFrameType(Visitor* JXL_RESTRICT visitor, + FrameType default_value, FrameType* frame_type) { + uint32_t encoded = static_cast(*frame_type); + + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(static_cast(FrameType::kRegularFrame)), + Val(static_cast(FrameType::kDCFrame)), + Val(static_cast(FrameType::kReferenceOnly)), + Val(static_cast(FrameType::kSkipProgressive)), + static_cast(default_value), &encoded)); + *frame_type = static_cast(encoded); + return true; +} + +BlendingInfo::BlendingInfo() { Bundle::Init(this); } + +Status BlendingInfo::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR( + VisitBlendMode(visitor, BlendMode::kReplace, &mode)); + if (visitor->Conditional(nonserialized_num_extra_channels > 0 && + (mode == BlendMode::kBlend || + mode == BlendMode::kAlphaWeightedAdd))) { + // Up to 11 alpha channels for blending. + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val(0), Val(1), Val(2), BitsOffset(3, 3), 0, &alpha_channel)); + if (visitor->IsReading() && + alpha_channel >= nonserialized_num_extra_channels) { + return JXL_FAILURE("Invalid alpha channel for blending"); + } + } + if (visitor->Conditional((nonserialized_num_extra_channels > 0 && + (mode == BlendMode::kBlend || + mode == BlendMode::kAlphaWeightedAdd)) || + mode == BlendMode::kMul)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &clamp)); + } + // 'old' frame for blending. Only necessary if this is not a full frame, or + // blending is not kReplace. + if (visitor->Conditional(mode != BlendMode::kReplace || + nonserialized_is_partial_frame)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(1), Val(2), Val(3), 0, &source)); + } + return true; +} + +AnimationFrame::AnimationFrame(const CodecMetadata* metadata) + : nonserialized_metadata(metadata) { + Bundle::Init(this); +} +Status AnimationFrame::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->Conditional(nonserialized_metadata != nullptr && + nonserialized_metadata->m.have_animation)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(1), Bits(8), Bits(32), 0, &duration)); + } + + if (visitor->Conditional( + nonserialized_metadata != nullptr && + nonserialized_metadata->m.animation.have_timecodes)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(32, 0, &timecode)); + } + return true; +} + +YCbCrChromaSubsampling::YCbCrChromaSubsampling() { Bundle::Init(this); } +Passes::Passes() { Bundle::Init(this); } +Status Passes::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), BitsOffset(3, 4), 1, &num_passes)); + JXL_ASSERT(num_passes <= kMaxNumPasses); // Cannot happen when reading + + if (visitor->Conditional(num_passes != 1)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val(0), Val(1), Val(2), BitsOffset(1, 3), 0, &num_downsample)); + JXL_ASSERT(num_downsample <= 4); // 1,2,4,8 + if (num_downsample > num_passes) { + return JXL_FAILURE("num_downsample %u > num_passes %u", num_downsample, + num_passes); + } + + for (uint32_t i = 0; i < num_passes - 1; i++) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 0, &shift[i])); + } + shift[num_passes - 1] = 0; + + for (uint32_t i = 0; i < num_downsample; ++i) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &downsample[i])); + } + for (uint32_t i = 0; i < num_downsample; ++i) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(1), Val(2), Bits(3), 0, &last_pass[i])); + if (last_pass[i] >= num_passes) { + return JXL_FAILURE("last_pass %u >= num_passes %u", last_pass[i], + num_passes); + } + } + } + + return true; +} +FrameHeader::FrameHeader(const CodecMetadata* metadata) + : animation_frame(metadata), nonserialized_metadata(metadata) { + Bundle::Init(this); +} + +Status ReadFrameHeader(BitReader* JXL_RESTRICT reader, + FrameHeader* JXL_RESTRICT frame) { + return Bundle::Read(reader, frame); +} + +Status WriteFrameHeader(const FrameHeader& frame, + BitWriter* JXL_RESTRICT writer, AuxOut* aux_out) { + return Bundle::Write(frame, writer, kLayerHeader, aux_out); +} + +Status FrameHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + JXL_QUIET_RETURN_IF_ERROR( + VisitFrameType(visitor, FrameType::kRegularFrame, &frame_type)); + if (visitor->IsReading() && nonserialized_is_preview && + frame_type != kRegularFrame) { + return JXL_FAILURE("Only regular frame could be a preview"); + } + + // FrameEncoding. + bool is_modular = (encoding == FrameEncoding::kModular); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &is_modular)); + encoding = (is_modular ? FrameEncoding::kModular : FrameEncoding::kVarDCT); + + // Flags + JXL_QUIET_RETURN_IF_ERROR(visitor->U64(0, &flags)); + + // Color transform + bool xyb_encoded = nonserialized_metadata == nullptr || + nonserialized_metadata->m.xyb_encoded; + + if (xyb_encoded) { + color_transform = ColorTransform::kXYB; + } else { + // Alternate if kYCbCr. + bool alternate = color_transform == ColorTransform::kYCbCr; + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &alternate)); + color_transform = + (alternate ? ColorTransform::kYCbCr : ColorTransform::kNone); + } + + // Chroma subsampling for YCbCr, if no DC frame is used. + if (visitor->Conditional(color_transform == ColorTransform::kYCbCr && + ((flags & kUseDcFrame) == 0))) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&chroma_subsampling)); + } + + size_t num_extra_channels = + nonserialized_metadata != nullptr + ? nonserialized_metadata->m.extra_channel_info.size() + : 0; + + // Upsampling + if (visitor->Conditional((flags & kUseDcFrame) == 0)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &upsampling)); + if (nonserialized_metadata != nullptr && + visitor->Conditional(num_extra_channels != 0)) { + const std::vector& extra_channels = + nonserialized_metadata->m.extra_channel_info; + extra_channel_upsampling.resize(extra_channels.size(), 1); + for (size_t i = 0; i < extra_channels.size(); ++i) { + uint32_t dim_shift = + nonserialized_metadata->m.extra_channel_info[i].dim_shift; + uint32_t& ec_upsampling = extra_channel_upsampling[i]; + ec_upsampling >>= dim_shift; + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(4), Val(8), 1, &ec_upsampling)); + ec_upsampling <<= dim_shift; + if (ec_upsampling < upsampling) { + return JXL_FAILURE( + "EC upsampling (%u) < color upsampling (%u), which is invalid.", + ec_upsampling, upsampling); + } + if (ec_upsampling > 8) { + return JXL_FAILURE("EC upsampling too large (%u)", ec_upsampling); + } + } + } else { + extra_channel_upsampling.clear(); + } + } + + // Modular- or VarDCT-specific data. + if (visitor->Conditional(encoding == FrameEncoding::kModular)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 1, &group_size_shift)); + } + if (visitor->Conditional(encoding == FrameEncoding::kVarDCT && + color_transform == ColorTransform::kXYB)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 3, &x_qm_scale)); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 2, &b_qm_scale)); + } else { + x_qm_scale = b_qm_scale = 2; // noop + } + + // Not useful for kPatchSource + if (visitor->Conditional(frame_type != FrameType::kReferenceOnly)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&passes)); + } + + if (visitor->Conditional(frame_type == FrameType::kDCFrame)) { + // Up to 4 pyramid levels - for up to 16384x downsampling. + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), Val(4), 1, &dc_level)); + } + if (frame_type != FrameType::kDCFrame) { + dc_level = 0; + } + + bool is_partial_frame = false; + if (visitor->Conditional(frame_type != FrameType::kDCFrame)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &custom_size_or_origin)); + if (visitor->Conditional(custom_size_or_origin)) { + const U32Enc enc(Bits(8), BitsOffset(11, 256), BitsOffset(14, 2304), + BitsOffset(30, 18688)); + // Frame offset, only if kRegularFrame or kSkipProgressive. + if (visitor->Conditional(frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive)) { + uint32_t ux0 = PackSigned(frame_origin.x0); + uint32_t uy0 = PackSigned(frame_origin.y0); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &ux0)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &uy0)); + frame_origin.x0 = UnpackSigned(ux0); + frame_origin.y0 = UnpackSigned(uy0); + } + // Frame size + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.xsize)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(enc, 0, &frame_size.ysize)); + int32_t image_xsize = default_xsize(); + int32_t image_ysize = default_ysize(); + if (frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive) { + is_partial_frame |= frame_origin.x0 > 0; + is_partial_frame |= frame_origin.y0 > 0; + is_partial_frame |= (static_cast(frame_size.xsize) + + frame_origin.x0) < image_xsize; + is_partial_frame |= (static_cast(frame_size.ysize) + + frame_origin.y0) < image_ysize; + } + } + } + + // Blending info, animation info and whether this is the last frame or not. + if (visitor->Conditional(frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive)) { + blending_info.nonserialized_num_extra_channels = num_extra_channels; + blending_info.nonserialized_is_partial_frame = is_partial_frame; + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&blending_info)); + bool replace_all = (blending_info.mode == BlendMode::kReplace); + extra_channel_blending_info.resize(num_extra_channels); + for (size_t i = 0; i < num_extra_channels; i++) { + auto& ec_blending_info = extra_channel_blending_info[i]; + ec_blending_info.nonserialized_is_partial_frame = is_partial_frame; + ec_blending_info.nonserialized_num_extra_channels = num_extra_channels; + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&ec_blending_info)); + replace_all &= (ec_blending_info.mode == BlendMode::kReplace); + } + if (visitor->IsReading() && nonserialized_is_preview) { + if (!replace_all || custom_size_or_origin) { + return JXL_FAILURE("Preview is not compatible with blending"); + } + } + if (visitor->Conditional(nonserialized_metadata != nullptr && + nonserialized_metadata->m.have_animation)) { + animation_frame.nonserialized_metadata = nonserialized_metadata; + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&animation_frame)); + } + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(true, &is_last)); + } + if (frame_type != FrameType::kRegularFrame) { + is_last = false; + } + + // ID of that can be used to refer to this frame. 0 for a non-zero-duration + // frame means that it will not be referenced. Not necessary for the last + // frame. + if (visitor->Conditional(frame_type != kDCFrame && !is_last)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(1), Val(2), Val(3), 0, &save_as_reference)); + } + + // If this frame is not blended on another frame post-color-transform, it may + // be stored for being referenced either before or after the color transform. + // If it is blended post-color-transform, it must be blended after. It must + // also be blended after if this is a kRegular frame that does not cover the + // full frame, as samples outside the partial region are from a + // post-color-transform frame. + if (frame_type != FrameType::kDCFrame) { + if (visitor->Conditional(CanBeReferenced() && + blending_info.mode == BlendMode::kReplace && + !is_partial_frame && + (frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive))) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->Bool(false, &save_before_color_transform)); + } else if (visitor->Conditional(frame_type == FrameType::kReferenceOnly)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->Bool(true, &save_before_color_transform)); + if (!save_before_color_transform && + (frame_size.xsize < nonserialized_metadata->xsize() || + frame_size.ysize < nonserialized_metadata->ysize() || + frame_origin.x0 != 0 || frame_origin.y0 != 0)) { + return JXL_FAILURE( + "non-patch reference frame with invalid crop: %zux%zu%+d%+d", + static_cast(frame_size.xsize), + static_cast(frame_size.ysize), + static_cast(frame_origin.x0), + static_cast(frame_origin.y0)); + } + } + } else { + save_before_color_transform = true; + } + + JXL_QUIET_RETURN_IF_ERROR(VisitNameString(visitor, &name)); + + loop_filter.nonserialized_is_modular = is_modular; + JXL_RETURN_IF_ERROR(visitor->VisitNested(&loop_filter)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions)); + // Extensions: in chronological order of being added to the format. + return visitor->EndExtensions(); +} + +} // namespace jxl diff --git a/lib/jxl/frame_header.h b/lib/jxl/frame_header.h new file mode 100644 index 0000000..d82afb2 --- /dev/null +++ b/lib/jxl/frame_header.h @@ -0,0 +1,492 @@ +// 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_FRAME_HEADER_H_ +#define LIB_JXL_FRAME_HEADER_H_ + +// Frame header with backward and forward-compatible extension capability and +// compressed integer fields. + +#include +#include + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image_metadata.h" +#include "lib/jxl/loop_filter.h" + +namespace jxl { + +// Also used by extra channel names. +static inline Status VisitNameString(Visitor* JXL_RESTRICT visitor, + std::string* name) { + uint32_t name_length = static_cast(name->length()); + // Allows layer name lengths up to 1071 bytes + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(0), Bits(4), BitsOffset(5, 16), + BitsOffset(10, 48), 0, &name_length)); + if (visitor->IsReading()) { + name->resize(name_length); + } + for (size_t i = 0; i < name_length; i++) { + uint32_t c = (*name)[i]; + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(8, 0, &c)); + (*name)[i] = static_cast(c); + } + return true; +} + +enum class FrameEncoding : uint32_t { + kVarDCT, + kModular, +}; + +enum class ColorTransform : uint32_t { + kXYB, // Values are encoded with XYB. May only be used if + // ImageBundle::xyb_encoded. + kNone, // Values are encoded according to the attached color profile. May + // only be used if !ImageBundle::xyb_encoded. + kYCbCr, // Values are encoded according to the attached color profile, but + // transformed to YCbCr. May only be used if + // !ImageBundle::xyb_encoded. +}; + +inline std::array JpegOrder(ColorTransform ct, bool is_gray) { + if (is_gray) { + return {{0, 0, 0}}; + } + JXL_ASSERT(ct != ColorTransform::kXYB); + if (ct == ColorTransform::kYCbCr) { + return {{1, 0, 2}}; + } else { + return {{0, 1, 2}}; + } +} + +struct YCbCrChromaSubsampling : public Fields { + YCbCrChromaSubsampling(); + const char* Name() const override { return "YCbCrChromaSubsampling"; } + size_t HShift(size_t c) const { return maxhs_ - kHShift[channel_mode_[c]]; } + size_t VShift(size_t c) const { return maxvs_ - kVShift[channel_mode_[c]]; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override { + // TODO(veluca): consider allowing 4x downsamples + for (size_t i = 0; i < 3; i++) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 0, &channel_mode_[i])); + } + Recompute(); + return true; + } + + uint8_t MaxHShift() const { return maxhs_; } + uint8_t MaxVShift() const { return maxvs_; } + + uint8_t RawHShift(size_t c) { return kHShift[channel_mode_[c]]; } + uint8_t RawVShift(size_t c) { return kVShift[channel_mode_[c]]; } + + // Uses JPEG channel order (Y, Cb, Cr). + Status Set(const uint8_t* hsample, const uint8_t* vsample) { + for (size_t c = 0; c < 3; c++) { + size_t cjpeg = c < 2 ? c ^ 1 : c; + size_t i = 0; + for (; i < 4; i++) { + if (1 << kHShift[i] == hsample[cjpeg] && + 1 << kVShift[i] == vsample[cjpeg]) { + channel_mode_[c] = i; + break; + } + } + if (i == 4) { + return JXL_FAILURE("Invalid subsample mode"); + } + } + Recompute(); + return true; + } + + bool Is444() const { + for (size_t c : {0, 2}) { + if (channel_mode_[c] != channel_mode_[1]) { + return false; + } + } + return true; + } + + bool Is420() const { + return channel_mode_[0] == 1 && channel_mode_[1] == 0 && + channel_mode_[2] == 1; + } + + bool Is422() const { + for (size_t c : {0, 2}) { + if (kHShift[channel_mode_[c]] == kHShift[channel_mode_[1]] + 1 && + kVShift[channel_mode_[c]] == kVShift[channel_mode_[1]]) { + return false; + } + } + return true; + } + + bool Is440() const { + for (size_t c : {0, 2}) { + if (kHShift[channel_mode_[c]] == kHShift[channel_mode_[1]] && + kVShift[channel_mode_[c]] == kVShift[channel_mode_[1]] + 1) { + return false; + } + } + return true; + } + + private: + void Recompute() { + maxhs_ = 0; + maxvs_ = 0; + for (size_t i = 0; i < 3; i++) { + maxhs_ = std::max(maxhs_, kHShift[channel_mode_[i]]); + maxvs_ = std::max(maxvs_, kVShift[channel_mode_[i]]); + } + } + static constexpr uint8_t kHShift[4] = {0, 1, 1, 0}; + static constexpr uint8_t kVShift[4] = {0, 1, 0, 1}; + uint32_t channel_mode_[3]; + uint8_t maxhs_; + uint8_t maxvs_; +}; + +// Indicates how to combine the current frame with a previously-saved one. Can +// be independently controlled for color and extra channels. Formulas are +// indicative and treat alpha as if it is in range 0.0-1.0. In descriptions +// below, alpha channel is the extra channel of type alpha used for blending +// according to the blend_channel, or fully opaque if there is no alpha channel. +// The blending specified here is used for performing blending *after* color +// transforms - in linear sRGB if blending a XYB-encoded frame on another +// XYB-encoded frame, in sRGB if blending a frame with kColorSpace == kSRGB, or +// in the original colorspace otherwise. Blending in XYB or YCbCr is done by +// using patches. +enum class BlendMode { + // The new values (in the crop) replace the old ones: sample = new + kReplace = 0, + // The new values (in the crop) get added to the old ones: sample = old + new + kAdd = 1, + // The new values (in the crop) replace the old ones if alpha>0: + // For the alpha channel that is used as source: + // alpha = old + new * (1 - old) + // For other channels if !alpha_associated: + // sample = ((1 - new_alpha) * old * old_alpha + new_alpha * new) / alpha + // For other channels if alpha_associated: + // sample = (1 - new_alpha) * old + new + // The alpha formula applies to the alpha used for the division in the other + // channels formula, and applies to the alpha channel itself if its + // blend_channel value matches itself. + kBlend = 2, + // The new values (in the crop) are added to the old ones if alpha>0: + // For the alpha channel that is used as source: + // sample = sample = old + new * (1 - old) + // For other channels: sample = old + alpha * new + kAlphaWeightedAdd = 3, + // The new values (in the crop) get multiplied by the old ones: + // sample = old * new + // The range of the new value matters for multiplication purposes, and its + // nominal range of 0..1 is computed the same way as this is done for the + // alpha values in kBlend and kAlphaWeightedAdd. + // If using kMul as a blend mode for color channels, no color transform is + // performed on the current frame. + kMul = 4, +}; + +struct BlendingInfo : public Fields { + BlendingInfo(); + const char* Name() const override { return "BlendingInfo"; } + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + BlendMode mode; + // Which extra channel to use as alpha channel for blending, only encoded + // for blend modes that involve alpha and if there are more than 1 extra + // channels. + uint32_t alpha_channel; + // Clamp alpha or channel values to 0-1 range. + bool clamp; + // Frame ID to copy from (0-3). Only encoded if blend_mode is not kReplace. + uint32_t source; + + size_t nonserialized_num_extra_channels = 0; + bool nonserialized_is_partial_frame = false; +}; + +// Origin of the current frame. Not present for frames of type +// kOnlyPatches. +struct FrameOrigin { + int32_t x0, y0; // can be negative. +}; + +// Size of the current frame. +struct FrameSize { + uint32_t xsize, ysize; +}; + +// AnimationFrame defines duration of animation frames. +struct AnimationFrame : public Fields { + explicit AnimationFrame(const CodecMetadata* metadata); + const char* Name() const override { return "AnimationFrame"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // How long to wait [in ticks, see Animation{}] after rendering. + // May be 0 if the current frame serves as a foundation for another frame. + uint32_t duration; + + uint32_t timecode; // 0xHHMMSSFF + + // Must be set to the one ImageMetadata acting as the full codestream header, + // with correct xyb_encoded, list of extra channels, etc... + const CodecMetadata* nonserialized_metadata = nullptr; +}; + +// For decoding to lower resolutions. Only used for kRegular frames. +struct Passes : public Fields { + Passes(); + const char* Name() const override { return "Passes"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + void GetDownsamplingBracket(size_t pass, int& minShift, int& maxShift) const { + maxShift = 2; + minShift = 0; + for (size_t i = 0;; i++) { + for (uint32_t j = 0; j < num_downsample; ++j) { + if (i <= last_pass[j]) { + if (downsample[j] == 8) minShift = 3; + if (downsample[j] == 4) minShift = 2; + if (downsample[j] == 2) minShift = 1; + if (downsample[j] == 1) minShift = 0; + } + } + if (i == num_passes - 1) minShift = 0; + if (i == pass) return; + maxShift = minShift - 1; + minShift = 0; + } + } + + uint32_t num_passes; // <= kMaxNumPasses + uint32_t num_downsample; // <= num_passes + + // Array of num_downsample pairs. downsample=1/last_pass=num_passes-1 and + // downsample=8/last_pass=0 need not be specified; they are implicit. + uint32_t downsample[kMaxNumPasses]; + uint32_t last_pass[kMaxNumPasses]; + // Array of shift values for each pass. It is implicitly assumed to be 0 for + // the last pass. + uint32_t shift[kMaxNumPasses]; +}; + +enum FrameType { + // A "regular" frame: might be a crop, and will be blended on a previous + // frame, if any, and displayed or blended in future frames. + kRegularFrame = 0, + // A DC frame: this frame is downsampled and will be *only* used as the DC of + // a future frame and, possibly, for previews. Cannot be cropped, blended, or + // referenced by patches or blending modes. Frames that *use* a DC frame + // cannot have non-default sizes either. + kDCFrame = 1, + // A PatchesSource frame: this frame will be only used as a source frame for + // taking patches. Can be cropped, but cannot have non-(0, 0) x0 and y0. + kReferenceOnly = 2, + // Same as kRegularFrame, but not used for progressive rendering. This also + // implies no early display of DC. + kSkipProgressive = 3, +}; + +// Image/frame := one of more of these, where the last has is_last = true. +// Starts at a byte-aligned address "a"; the next pass starts at "a + size". +struct FrameHeader : public Fields { + // Optional postprocessing steps. These flags are the source of truth; + // Override must set/clear them rather than change their meaning. Values + // chosen such that typical flags == 0 (encoded in only two bits). + enum Flags { + // Often but not always off => low bit value: + + // Inject noise into decoded output. + kNoise = 1, + + // Overlay patches. + kPatches = 2, + + // 4, 8 = reserved for future sometimes-off + + // Overlay splines. + kSplines = 16, + + kUseDcFrame = 32, // Implies kSkipAdaptiveDCSmoothing. + + // 64 = reserved for future often-off + + // Almost always on => negated: + + kSkipAdaptiveDCSmoothing = 128, + }; + + explicit FrameHeader(const CodecMetadata* metadata); + const char* Name() const override { return "FrameHeader"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Sets/clears `flag` based upon `condition`. + void UpdateFlag(const bool condition, const uint64_t flag) { + if (condition) { + flags |= flag; + } else { + flags &= ~flag; + } + } + + // Returns true if this frame is supposed to be saved for future usage by + // other frames. + bool CanBeReferenced() const { + // DC frames cannot be referenced. The last frame cannot be referenced. A + // duration 0 frame makes little sense if it is not referenced. A + // non-duration 0 frame may or may not be referenced. + return !is_last && frame_type != FrameType::kDCFrame && + (animation_frame.duration == 0 || save_as_reference != 0); + } + + mutable bool all_default; + + // Always present + FrameEncoding encoding; + // Some versions of UBSAN complain in VisitFrameType if not initialized. + FrameType frame_type = FrameType::kRegularFrame; + + uint64_t flags; + + ColorTransform color_transform; + YCbCrChromaSubsampling chroma_subsampling; + + uint32_t group_size_shift; // only if encoding == kModular; + + uint32_t x_qm_scale; // only if VarDCT and color_transform == kXYB + uint32_t b_qm_scale; // only if VarDCT and color_transform == kXYB + + std::string name; + + // Skipped for kReferenceOnly. + Passes passes; + + // Skipped for kDCFrame + bool custom_size_or_origin; + FrameSize frame_size; + + // upsampling factors for color and extra channels. + // Upsampling is always performed before applying any inverse color transform. + // Skipped (1) if kUseDCFrame + uint32_t upsampling; + std::vector extra_channel_upsampling; + + // Only for kRegular frames. + FrameOrigin frame_origin; + + BlendingInfo blending_info; + std::vector extra_channel_blending_info; + + // Animation info for this frame. + AnimationFrame animation_frame; + + // This is the last frame. + bool is_last; + + // ID to refer to this frame with. 0-3, not present if kDCFrame. + // 0 has a special meaning for kRegular frames of nonzero duration: it defines + // a frame that will not be referenced in the future. + uint32_t save_as_reference; + + // Whether to save this frame before or after the color transform. A frame + // that is saved before the color tansform can only be used for blending + // through patches. On the contrary, a frame that is saved after the color + // transform can only be used for blending through blending modes. + // Irrelevant for extra channel blending. Can only be true if + // blending_info.mode == kReplace and this is not a partial kRegularFrame; if + // this is a DC frame, it is always true. + bool save_before_color_transform; + + uint32_t dc_level; // 1-4 if kDCFrame (0 otherwise). + + // Must be set to the one ImageMetadata acting as the full codestream header, + // with correct xyb_encoded, list of extra channels, etc... + const CodecMetadata* nonserialized_metadata = nullptr; + + // NOTE: This is ignored by AllDefault. + LoopFilter loop_filter; + + bool nonserialized_is_preview = false; + + size_t default_xsize() const { + if (!nonserialized_metadata) return 0; + if (nonserialized_is_preview) { + return nonserialized_metadata->m.preview_size.xsize(); + } + return nonserialized_metadata->xsize(); + } + + size_t default_ysize() const { + if (!nonserialized_metadata) return 0; + if (nonserialized_is_preview) { + return nonserialized_metadata->m.preview_size.ysize(); + } + return nonserialized_metadata->ysize(); + } + + FrameDimensions ToFrameDimensions() const { + size_t xsize = default_xsize(); + size_t ysize = default_ysize(); + + xsize = frame_size.xsize ? frame_size.xsize : xsize; + ysize = frame_size.ysize ? frame_size.ysize : ysize; + + if (dc_level != 0) { + xsize = DivCeil(xsize, 1 << (3 * dc_level)); + ysize = DivCeil(ysize, 1 << (3 * dc_level)); + } + + FrameDimensions frame_dim; + frame_dim.Set(xsize, ysize, group_size_shift, + chroma_subsampling.MaxHShift(), + chroma_subsampling.MaxVShift(), + encoding == FrameEncoding::kModular, upsampling); + return frame_dim; + } + + // True if a color transform should be applied to this frame. + bool needs_color_transform() const { + return !save_before_color_transform || + frame_type == FrameType::kRegularFrame || + frame_type == FrameType::kSkipProgressive; + } + + uint64_t extensions; +}; + +Status ReadFrameHeader(BitReader* JXL_RESTRICT reader, + FrameHeader* JXL_RESTRICT frame); + +Status WriteFrameHeader(const FrameHeader& frame, + BitWriter* JXL_RESTRICT writer, AuxOut* aux_out); + +// Shared by enc/dec. 5F and 13 are by far the most common for d1/2/4/8, 0 +// ensures low overhead for small images. +static constexpr U32Enc kOrderEnc = + U32Enc(Val(0x5F), Val(0x13), Val(0), Bits(kNumOrders)); + +} // namespace jxl + +#endif // LIB_JXL_FRAME_HEADER_H_ diff --git a/lib/jxl/gaborish.cc b/lib/jxl/gaborish.cc new file mode 100644 index 0000000..6a187c4 --- /dev/null +++ b/lib/jxl/gaborish.cc @@ -0,0 +1,70 @@ +// 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/gaborish.h" + +#include + +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +void GaborishInverse(Image3F* in_out, float mul, ThreadPool* pool) { + JXL_ASSERT(mul >= 0.0f); + + // Only an approximation. One or even two 3x3, and rank-1 (separable) 5x5 + // are insufficient. + constexpr float kGaborish[5] = { + -0.092359145662814029f, -0.039253623634014627f, 0.016176494530216929f, + 0.00083458437774987476f, 0.004512465323949319f, + }; + /* + better would be: + 1.0 - mul * (4 * (kGaborish[0] + kGaborish[1] + + kGaborish[2] + kGaborish[4]) + + 8 * (kGaborish[3])); + */ + WeightsSymmetric5 weights = {{HWY_REP4(1.0f)}, + {HWY_REP4(mul * kGaborish[0])}, + {HWY_REP4(mul * kGaborish[2])}, + {HWY_REP4(mul * kGaborish[1])}, + {HWY_REP4(mul * kGaborish[4])}, + {HWY_REP4(mul * kGaborish[3])}}; + double sum = static_cast(weights.c[0]); + sum += 4 * weights.r[0]; + sum += 4 * weights.R[0]; + sum += 4 * weights.d[0]; + sum += 4 * weights.D[0]; + sum += 8 * weights.L[0]; + const float normalize = static_cast(1.0 / sum); + for (size_t i = 0; i < 4; ++i) { + weights.c[i] *= normalize; + weights.r[i] *= normalize; + weights.R[i] *= normalize; + weights.d[i] *= normalize; + weights.D[i] *= normalize; + weights.L[i] *= normalize; + } + + // Reduce memory footprint by only allocating a single plane and swapping it + // into the output Image3F. Better still would be tiling. + // Note that we cannot *allocate* a plane, as doing so might cause Image3F to + // have planes of different stride. Instead, we copy one plane in a temporary + // image and reuse the existing planes of the in/out image. + ImageF temp = CopyImage(in_out->Plane(2)); + Symmetric5(in_out->Plane(0), Rect(*in_out), weights, pool, &in_out->Plane(2)); + Symmetric5(in_out->Plane(1), Rect(*in_out), weights, pool, &in_out->Plane(0)); + Symmetric5(temp, Rect(*in_out), weights, pool, &in_out->Plane(1)); + // Now planes are 1, 2, 0. + in_out->Plane(0).Swap(in_out->Plane(1)); + // 2 1 0 + in_out->Plane(0).Swap(in_out->Plane(2)); +} + +} // namespace jxl diff --git a/lib/jxl/gaborish.h b/lib/jxl/gaborish.h new file mode 100644 index 0000000..e43411d --- /dev/null +++ b/lib/jxl/gaborish.h @@ -0,0 +1,26 @@ +// 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_GABORISH_H_ +#define LIB_JXL_GABORISH_H_ + +// Linear smoothing (3x3 convolution) for deblocking without too much blur. + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// Used in encoder to reduce the impact of the decoder's smoothing. +// This is not exact. Works in-place to reduce memory use. +// The input is typically in XYB space. +void GaborishInverse(Image3F* in_out, float mul, ThreadPool* pool); + +} // namespace jxl + +#endif // LIB_JXL_GABORISH_H_ diff --git a/lib/jxl/gaborish_test.cc b/lib/jxl/gaborish_test.cc new file mode 100644 index 0000000..55b17a0 --- /dev/null +++ b/lib/jxl/gaborish_test.cc @@ -0,0 +1,71 @@ +// 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/gaborish.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +// weight1,2 need not be normalized. +WeightsSymmetric3 GaborishKernel(float weight1, float weight2) { + constexpr float weight0 = 1.0f; + + // Normalize + const float mul = 1.0f / (weight0 + 4 * (weight1 + weight2)); + const float w0 = weight0 * mul; + const float w1 = weight1 * mul; + const float w2 = weight2 * mul; + + const WeightsSymmetric3 w = {{HWY_REP4(w0)}, {HWY_REP4(w1)}, {HWY_REP4(w2)}}; + return w; +} + +void ConvolveGaborish(const ImageF& in, float weight1, float weight2, + ThreadPool* pool, ImageF* JXL_RESTRICT out) { + JXL_CHECK(SameSize(in, *out)); + Symmetric3(in, Rect(in), GaborishKernel(weight1, weight2), pool, out); +} + +void TestRoundTrip(const Image3F& in, float max_l1) { + Image3F fwd(in.xsize(), in.ysize()); + ThreadPool* null_pool = nullptr; + ConvolveGaborish(in.Plane(0), 0, 0, null_pool, &fwd.Plane(0)); + ConvolveGaborish(in.Plane(1), 0, 0, null_pool, &fwd.Plane(1)); + ConvolveGaborish(in.Plane(2), 0, 0, null_pool, &fwd.Plane(2)); + GaborishInverse(&fwd, 0.92718927264540152f, null_pool); + VerifyRelativeError(in, fwd, max_l1, 1E-4f); +} + +TEST(GaborishTest, TestZero) { + Image3F in(20, 20); + ZeroFillImage(&in); + TestRoundTrip(in, 0.0f); +} + +// Disabled: large difference. +#if 0 +TEST(GaborishTest, TestDirac) { + Image3F in(20, 20); + ZeroFillImage(&in); + in.PlaneRow(1, 10)[10] = 10.0f; + TestRoundTrip(in, 0.26f); +} +#endif + +TEST(GaborishTest, TestFlat) { + Image3F in(20, 20); + FillImage(1.0f, &in); + TestRoundTrip(in, 1E-5f); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/gamma_correct_test.cc b/lib/jxl/gamma_correct_test.cc new file mode 100644 index 0000000..d17ce89 --- /dev/null +++ b/lib/jxl/gamma_correct_test.cc @@ -0,0 +1,37 @@ +// 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 + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/enc_gamma_correct.h" + +namespace jxl { +namespace { + +TEST(GammaCorrectTest, TestLinearToSrgbEdgeCases) { + EXPECT_EQ(0, LinearToSrgb8Direct(0.0)); + EXPECT_NEAR(0, LinearToSrgb8Direct(1E-6f), 2E-5); + EXPECT_EQ(0, LinearToSrgb8Direct(-1E-6f)); + EXPECT_EQ(0, LinearToSrgb8Direct(-1E6)); + EXPECT_NEAR(1, LinearToSrgb8Direct(1 - 1E-6f), 1E-5); + EXPECT_EQ(1, LinearToSrgb8Direct(1 + 1E-6f)); + EXPECT_EQ(1, LinearToSrgb8Direct(1E6)); +} + +TEST(GammaCorrectTest, TestRoundTrip) { + // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) + for (double linear = 0.0; linear <= 1.0; linear += 1E-7) { + const double srgb = LinearToSrgb8Direct(linear); + const double linear2 = Srgb8ToLinearDirect(srgb); + ASSERT_LT(std::abs(linear - linear2), 2E-13) + << "linear = " << linear << ", linear2 = " << linear2; + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/gauss_blur.cc b/lib/jxl/gauss_blur.cc new file mode 100644 index 0000000..f9babe7 --- /dev/null +++ b/lib/jxl/gauss_blur.cc @@ -0,0 +1,616 @@ +// 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/gauss_blur.h" + +#include + +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/gauss_blur.cc" +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/linalg.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Broadcast; +#if HWY_TARGET != HWY_SCALAR +using hwy::HWY_NAMESPACE::ShiftLeftLanes; +#endif +using hwy::HWY_NAMESPACE::Vec; + +void FastGaussian1D(const hwy::AlignedUniquePtr& rg, + const float* JXL_RESTRICT in, intptr_t width, + float* JXL_RESTRICT out) { + // Although the current output depends on the previous output, we can unroll + // up to 4x by precomputing up to fourth powers of the constants. Beyond that, + // numerical precision might become a problem. Macro because this is tested + // in #if alongside HWY_TARGET. +#define JXL_GAUSS_MAX_LANES 4 + using D = HWY_CAPPED(float, JXL_GAUSS_MAX_LANES); + using V = Vec; + const D d; + const V mul_in_1 = Load(d, rg->mul_in + 0 * 4); + const V mul_in_3 = Load(d, rg->mul_in + 1 * 4); + const V mul_in_5 = Load(d, rg->mul_in + 2 * 4); + const V mul_prev_1 = Load(d, rg->mul_prev + 0 * 4); + const V mul_prev_3 = Load(d, rg->mul_prev + 1 * 4); + const V mul_prev_5 = Load(d, rg->mul_prev + 2 * 4); + const V mul_prev2_1 = Load(d, rg->mul_prev2 + 0 * 4); + const V mul_prev2_3 = Load(d, rg->mul_prev2 + 1 * 4); + const V mul_prev2_5 = Load(d, rg->mul_prev2 + 2 * 4); + V prev_1 = Zero(d); + V prev_3 = Zero(d); + V prev_5 = Zero(d); + V prev2_1 = Zero(d); + V prev2_3 = Zero(d); + V prev2_5 = Zero(d); + + const intptr_t N = rg->radius; + + intptr_t n = -N + 1; + // Left side with bounds checks and only write output after n >= 0. + const intptr_t first_aligned = RoundUpTo(N + 1, Lanes(d)); + for (; n < std::min(first_aligned, width); ++n) { + const intptr_t left = n - N - 1; + const intptr_t right = n + N - 1; + const float left_val = left >= 0 ? in[left] : 0.0f; + const float right_val = right < width ? in[right] : 0.0f; + const V sum = Set(d, left_val + right_val); + + // (Only processing a single lane here, no need to broadcast) + V out_1 = sum * mul_in_1; + V out_3 = sum * mul_in_3; + V out_5 = sum * mul_in_5; + + out_1 = MulAdd(mul_prev2_1, prev2_1, out_1); + out_3 = MulAdd(mul_prev2_3, prev2_3, out_3); + out_5 = MulAdd(mul_prev2_5, prev2_5, out_5); + prev2_1 = prev_1; + prev2_3 = prev_3; + prev2_5 = prev_5; + + out_1 = MulAdd(mul_prev_1, prev_1, out_1); + out_3 = MulAdd(mul_prev_3, prev_3, out_3); + out_5 = MulAdd(mul_prev_5, prev_5, out_5); + prev_1 = out_1; + prev_3 = out_3; + prev_5 = out_5; + + if (n >= 0) { + out[n] = GetLane(out_1 + out_3 + out_5); + } + } + + // The above loop is effectively scalar but it is convenient to use the same + // prev/prev2 variables, so broadcast to each lane before the unrolled loop. +#if HWY_TARGET != HWY_SCALAR && JXL_GAUSS_MAX_LANES > 1 + prev2_1 = Broadcast<0>(prev2_1); + prev2_3 = Broadcast<0>(prev2_3); + prev2_5 = Broadcast<0>(prev2_5); + prev_1 = Broadcast<0>(prev_1); + prev_3 = Broadcast<0>(prev_3); + prev_5 = Broadcast<0>(prev_5); +#endif + + // Unrolled, no bounds checking needed. + for (; n < width - N + 1 - (JXL_GAUSS_MAX_LANES - 1); n += Lanes(d)) { + const V sum = LoadU(d, in + n - N - 1) + LoadU(d, in + n + N - 1); + + // To get a vector of output(s), we multiply broadcasted vectors (of each + // input plus the two previous outputs) and add them all together. + // Incremental broadcasting and shifting is expected to be cheaper than + // horizontal adds or transposing 4x4 values because they run on a different + // port, concurrently with the FMA. + const V in0 = Broadcast<0>(sum); + V out_1 = in0 * mul_in_1; + V out_3 = in0 * mul_in_3; + V out_5 = in0 * mul_in_5; + +#if HWY_TARGET != HWY_SCALAR && JXL_GAUSS_MAX_LANES >= 2 + const V in1 = Broadcast<1>(sum); + out_1 = MulAdd(ShiftLeftLanes<1>(mul_in_1), in1, out_1); + out_3 = MulAdd(ShiftLeftLanes<1>(mul_in_3), in1, out_3); + out_5 = MulAdd(ShiftLeftLanes<1>(mul_in_5), in1, out_5); + +#if JXL_GAUSS_MAX_LANES >= 4 + const V in2 = Broadcast<2>(sum); + out_1 = MulAdd(ShiftLeftLanes<2>(mul_in_1), in2, out_1); + out_3 = MulAdd(ShiftLeftLanes<2>(mul_in_3), in2, out_3); + out_5 = MulAdd(ShiftLeftLanes<2>(mul_in_5), in2, out_5); + + const V in3 = Broadcast<3>(sum); + out_1 = MulAdd(ShiftLeftLanes<3>(mul_in_1), in3, out_1); + out_3 = MulAdd(ShiftLeftLanes<3>(mul_in_3), in3, out_3); + out_5 = MulAdd(ShiftLeftLanes<3>(mul_in_5), in3, out_5); +#endif +#endif + + out_1 = MulAdd(mul_prev2_1, prev2_1, out_1); + out_3 = MulAdd(mul_prev2_3, prev2_3, out_3); + out_5 = MulAdd(mul_prev2_5, prev2_5, out_5); + + out_1 = MulAdd(mul_prev_1, prev_1, out_1); + out_3 = MulAdd(mul_prev_3, prev_3, out_3); + out_5 = MulAdd(mul_prev_5, prev_5, out_5); +#if HWY_TARGET == HWY_SCALAR || JXL_GAUSS_MAX_LANES == 1 + prev2_1 = prev_1; + prev2_3 = prev_3; + prev2_5 = prev_5; + prev_1 = out_1; + prev_3 = out_3; + prev_5 = out_5; +#else + prev2_1 = Broadcast(out_1); + prev2_3 = Broadcast(out_3); + prev2_5 = Broadcast(out_5); + prev_1 = Broadcast(out_1); + prev_3 = Broadcast(out_3); + prev_5 = Broadcast(out_5); +#endif + + Store(out_1 + out_3 + out_5, d, out + n); + } + + // Remainder handling with bounds checks + for (; n < width; ++n) { + const intptr_t left = n - N - 1; + const intptr_t right = n + N - 1; + const float left_val = left >= 0 ? in[left] : 0.0f; + const float right_val = right < width ? in[right] : 0.0f; + const V sum = Set(d, left_val + right_val); + + // (Only processing a single lane here, no need to broadcast) + V out_1 = sum * mul_in_1; + V out_3 = sum * mul_in_3; + V out_5 = sum * mul_in_5; + + out_1 = MulAdd(mul_prev2_1, prev2_1, out_1); + out_3 = MulAdd(mul_prev2_3, prev2_3, out_3); + out_5 = MulAdd(mul_prev2_5, prev2_5, out_5); + prev2_1 = prev_1; + prev2_3 = prev_3; + prev2_5 = prev_5; + + out_1 = MulAdd(mul_prev_1, prev_1, out_1); + out_3 = MulAdd(mul_prev_3, prev_3, out_3); + out_5 = MulAdd(mul_prev_5, prev_5, out_5); + prev_1 = out_1; + prev_3 = out_3; + prev_5 = out_5; + + out[n] = GetLane(out_1 + out_3 + out_5); + } +} + +// Ring buffer is for n, n-1, n-2; round up to 4 for faster modulo. +constexpr size_t kMod = 4; + +// Avoids an unnecessary store during warmup. +struct OutputNone { + template + void operator()(const V& /*unused*/, float* JXL_RESTRICT /*pos*/, + ptrdiff_t /*offset*/) const {} +}; + +// Common case: write output vectors in all VerticalBlock except warmup. +struct OutputStore { + template + void operator()(const V& out, float* JXL_RESTRICT pos, + ptrdiff_t offset) const { + // Stream helps for large images but is slower for images that fit in cache. + Store(out, HWY_FULL(float)(), pos + offset); + } +}; + +// At top/bottom borders, we don't have two inputs to load, so avoid addition. +// pos may even point to all zeros if the row is outside the input image. +class SingleInput { + public: + explicit SingleInput(const float* pos) : pos_(pos) {} + Vec operator()(const size_t offset) const { + return Load(HWY_FULL(float)(), pos_ + offset); + } + const float* pos_; +}; + +// In the middle of the image, we need to load from a row above and below, and +// return the sum. +class TwoInputs { + public: + TwoInputs(const float* pos1, const float* pos2) : pos1_(pos1), pos2_(pos2) {} + Vec operator()(const size_t offset) const { + const auto in1 = Load(HWY_FULL(float)(), pos1_ + offset); + const auto in2 = Load(HWY_FULL(float)(), pos2_ + offset); + return in1 + in2; + } + + private: + const float* pos1_; + const float* pos2_; +}; + +// Block := kVectors consecutive full vectors (one cache line except on the +// right boundary, where we can only rely on having one vector). Unrolling to +// the cache line size improves cache utilization. +template +void VerticalBlock(const V& d1_1, const V& d1_3, const V& d1_5, const V& n2_1, + const V& n2_3, const V& n2_5, const Input& input, + size_t& ctr, float* ring_buffer, const Output output, + float* JXL_RESTRICT out_pos) { + const HWY_FULL(float) d; + constexpr size_t kVN = MaxLanes(d); + // More cache-friendly to process an entirely cache line at a time + constexpr size_t kLanes = kVectors * kVN; + + float* JXL_RESTRICT y_1 = ring_buffer + 0 * kLanes * kMod; + float* JXL_RESTRICT y_3 = ring_buffer + 1 * kLanes * kMod; + float* JXL_RESTRICT y_5 = ring_buffer + 2 * kLanes * kMod; + + const size_t n_0 = (++ctr) % kMod; + const size_t n_1 = (ctr - 1) % kMod; + const size_t n_2 = (ctr - 2) % kMod; + + for (size_t idx_vec = 0; idx_vec < kVectors; ++idx_vec) { + const V sum = input(idx_vec * kVN); + + const V y_n1_1 = Load(d, y_1 + kLanes * n_1 + idx_vec * kVN); + const V y_n1_3 = Load(d, y_3 + kLanes * n_1 + idx_vec * kVN); + const V y_n1_5 = Load(d, y_5 + kLanes * n_1 + idx_vec * kVN); + const V y_n2_1 = Load(d, y_1 + kLanes * n_2 + idx_vec * kVN); + const V y_n2_3 = Load(d, y_3 + kLanes * n_2 + idx_vec * kVN); + const V y_n2_5 = Load(d, y_5 + kLanes * n_2 + idx_vec * kVN); + // (35) + const V y1 = MulAdd(n2_1, sum, NegMulSub(d1_1, y_n1_1, y_n2_1)); + const V y3 = MulAdd(n2_3, sum, NegMulSub(d1_3, y_n1_3, y_n2_3)); + const V y5 = MulAdd(n2_5, sum, NegMulSub(d1_5, y_n1_5, y_n2_5)); + Store(y1, d, y_1 + kLanes * n_0 + idx_vec * kVN); + Store(y3, d, y_3 + kLanes * n_0 + idx_vec * kVN); + Store(y5, d, y_5 + kLanes * n_0 + idx_vec * kVN); + output(y1 + y3 + y5, out_pos, idx_vec * kVN); + } + // NOTE: flushing cache line out_pos hurts performance - less so with + // clflushopt than clflush but still a significant slowdown. +} + +// Reads/writes one block (kVectors full vectors) in each row. +template +void VerticalStrip(const hwy::AlignedUniquePtr& rg, + const ImageF& in, const size_t x, ImageF* JXL_RESTRICT out) { + // We're iterating vertically, so use multiple full-length vectors (each lane + // is one column of row n). + using D = HWY_FULL(float); + using V = Vec; + const D d; + constexpr size_t kVN = MaxLanes(d); + // More cache-friendly to process an entirely cache line at a time + constexpr size_t kLanes = kVectors * kVN; +#if HWY_TARGET == HWY_SCALAR + const V d1_1 = Set(d, rg->d1[0 * 4]); + const V d1_3 = Set(d, rg->d1[1 * 4]); + const V d1_5 = Set(d, rg->d1[2 * 4]); + const V n2_1 = Set(d, rg->n2[0 * 4]); + const V n2_3 = Set(d, rg->n2[1 * 4]); + const V n2_5 = Set(d, rg->n2[2 * 4]); +#else + const V d1_1 = LoadDup128(d, rg->d1 + 0 * 4); + const V d1_3 = LoadDup128(d, rg->d1 + 1 * 4); + const V d1_5 = LoadDup128(d, rg->d1 + 2 * 4); + const V n2_1 = LoadDup128(d, rg->n2 + 0 * 4); + const V n2_3 = LoadDup128(d, rg->n2 + 1 * 4); + const V n2_5 = LoadDup128(d, rg->n2 + 2 * 4); +#endif + + const size_t N = rg->radius; + const size_t ysize = in.ysize(); + + size_t ctr = 0; + HWY_ALIGN float ring_buffer[3 * kLanes * kMod] = {0}; + HWY_ALIGN static constexpr float zero[kLanes] = {0}; + + // Warmup: top is out of bounds (zero padded), bottom is usually in-bounds. + ssize_t n = -static_cast(N) + 1; + for (; n < 0; ++n) { + // bottom is always non-negative since n is initialized in -N + 1. + const size_t bottom = n + N - 1; + VerticalBlock( + d1_1, d1_3, d1_5, n2_1, n2_3, n2_5, + SingleInput(bottom < ysize ? in.ConstRow(bottom) + x : zero), ctr, + ring_buffer, OutputNone(), nullptr); + } + JXL_DASSERT(n >= 0); + + // Start producing output; top is still out of bounds. + for (; static_cast(n) < std::min(N + 1, ysize); ++n) { + const size_t bottom = n + N - 1; + VerticalBlock( + d1_1, d1_3, d1_5, n2_1, n2_3, n2_5, + SingleInput(bottom < ysize ? in.ConstRow(bottom) + x : zero), ctr, + ring_buffer, OutputStore(), out->Row(n) + x); + } + + // Interior outputs with prefetching and without bounds checks. + constexpr size_t kPrefetchRows = 8; + for (; n < static_cast(ysize - N + 1 - kPrefetchRows); ++n) { + const size_t top = n - N - 1; + const size_t bottom = n + N - 1; + VerticalBlock( + d1_1, d1_3, d1_5, n2_1, n2_3, n2_5, + TwoInputs(in.ConstRow(top) + x, in.ConstRow(bottom) + x), ctr, + ring_buffer, OutputStore(), out->Row(n) + x); + hwy::Prefetch(in.ConstRow(top + kPrefetchRows) + x); + hwy::Prefetch(in.ConstRow(bottom + kPrefetchRows) + x); + } + + // Bottom border without prefetching and with bounds checks. + for (; static_cast(n) < ysize; ++n) { + const size_t top = n - N - 1; + const size_t bottom = n + N - 1; + VerticalBlock( + d1_1, d1_3, d1_5, n2_1, n2_3, n2_5, + TwoInputs(in.ConstRow(top) + x, + bottom < ysize ? in.ConstRow(bottom) + x : zero), + ctr, ring_buffer, OutputStore(), out->Row(n) + x); + } +} + +// Apply 1D vertical scan to multiple columns (one per vector lane). +// Not yet parallelized. +void FastGaussianVertical(const hwy::AlignedUniquePtr& rg, + const ImageF& in, ThreadPool* /*pool*/, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + JXL_CHECK(SameSize(in, *out)); + + constexpr size_t kCacheLineLanes = 64 / sizeof(float); + constexpr size_t kVN = MaxLanes(HWY_FULL(float)()); + constexpr size_t kCacheLineVectors = kCacheLineLanes / kVN; + + size_t x = 0; + for (; x + kCacheLineLanes <= in.xsize(); x += kCacheLineLanes) { + VerticalStrip(rg, in, x, out); + } + for (; x < in.xsize(); x += kVN) { + VerticalStrip<1>(rg, in, x, out); + } +} + +// TODO(veluca): consider replacing with FastGaussian. +ImageF ConvolveXSampleAndTranspose(const ImageF& in, + const std::vector& kernel, + const size_t res) { + JXL_ASSERT(kernel.size() % 2 == 1); + JXL_ASSERT(in.xsize() % res == 0); + const size_t offset = res / 2; + const size_t out_xsize = in.xsize() / res; + ImageF out(in.ysize(), out_xsize); + const int r = kernel.size() / 2; + HWY_FULL(float) df; + std::vector row_tmp(in.xsize() + 2 * r + Lanes(df)); + float* const JXL_RESTRICT rowp = &row_tmp[r]; + std::vector padded_k = kernel; + padded_k.resize(padded_k.size() + Lanes(df)); + const float* const kernelp = &padded_k[r]; + for (size_t y = 0; y < in.ysize(); ++y) { + ExtrapolateBorders(in.Row(y), rowp, in.xsize(), r); + size_t x = offset, ox = 0; + for (; x < static_cast(r) && x < in.xsize(); x += res, ++ox) { + float sum = 0.0f; + for (int i = -r; i <= r; ++i) { + sum += rowp[std::max( + 0, std::min(static_cast(x) + i, in.xsize()))] * + kernelp[i]; + } + out.Row(ox)[y] = sum; + } + for (; x + r < in.xsize(); x += res, ++ox) { + auto sum = Zero(df); + for (int i = -r; i <= r; i += Lanes(df)) { + sum = MulAdd(LoadU(df, rowp + x + i), LoadU(df, kernelp + i), sum); + } + out.Row(ox)[y] = GetLane(SumOfLanes(sum)); + } + for (; x < in.xsize(); x += res, ++ox) { + float sum = 0.0f; + for (int i = -r; i <= r; ++i) { + sum += rowp[std::max( + 0, std::min(static_cast(x) + i, in.xsize()))] * + kernelp[i]; + } + out.Row(ox)[y] = sum; + } + } + return out; +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(FastGaussian1D); +HWY_EXPORT(ConvolveXSampleAndTranspose); +void FastGaussian1D(const hwy::AlignedUniquePtr& rg, + const float* JXL_RESTRICT in, intptr_t width, + float* JXL_RESTRICT out) { + return HWY_DYNAMIC_DISPATCH(FastGaussian1D)(rg, in, width, out); +} + +HWY_EXPORT(FastGaussianVertical); // Local function. + +void ExtrapolateBorders(const float* const JXL_RESTRICT row_in, + float* const JXL_RESTRICT row_out, const int xsize, + const int radius) { + const int lastcol = xsize - 1; + for (int x = 1; x <= radius; ++x) { + row_out[-x] = row_in[std::min(x, xsize - 1)]; + } + memcpy(row_out, row_in, xsize * sizeof(row_out[0])); + for (int x = 1; x <= radius; ++x) { + row_out[lastcol + x] = row_in[std::max(0, lastcol - x)]; + } +} + +ImageF ConvolveXSampleAndTranspose(const ImageF& in, + const std::vector& kernel, + const size_t res) { + return HWY_DYNAMIC_DISPATCH(ConvolveXSampleAndTranspose)(in, kernel, res); +} + +Image3F ConvolveXSampleAndTranspose(const Image3F& in, + const std::vector& kernel, + const size_t res) { + return Image3F(ConvolveXSampleAndTranspose(in.Plane(0), kernel, res), + ConvolveXSampleAndTranspose(in.Plane(1), kernel, res), + ConvolveXSampleAndTranspose(in.Plane(2), kernel, res)); +} + +ImageF ConvolveAndSample(const ImageF& in, const std::vector& kernel, + const size_t res) { + ImageF tmp = ConvolveXSampleAndTranspose(in, kernel, res); + return ConvolveXSampleAndTranspose(tmp, kernel, res); +} + +// Implements "Recursive Implementation of the Gaussian Filter Using Truncated +// Cosine Functions" by Charalampidis [2016]. +hwy::AlignedUniquePtr CreateRecursiveGaussian(double sigma) { + PROFILER_FUNC; + auto rg = hwy::MakeUniqueAligned(); + constexpr double kPi = 3.141592653589793238; + + const double radius = roundf(3.2795 * sigma + 0.2546); // (57), "N" + + // Table I, first row + const double pi_div_2r = kPi / (2.0 * radius); + const double omega[3] = {pi_div_2r, 3.0 * pi_div_2r, 5.0 * pi_div_2r}; + + // (37), k={1,3,5} + const double p_1 = +1.0 / std::tan(0.5 * omega[0]); + const double p_3 = -1.0 / std::tan(0.5 * omega[1]); + const double p_5 = +1.0 / std::tan(0.5 * omega[2]); + + // (44), k={1,3,5} + const double r_1 = +p_1 * p_1 / std::sin(omega[0]); + const double r_3 = -p_3 * p_3 / std::sin(omega[1]); + const double r_5 = +p_5 * p_5 / std::sin(omega[2]); + + // (50), k={1,3,5} + const double neg_half_sigma2 = -0.5 * sigma * sigma; + const double recip_radius = 1.0 / radius; + double rho[3]; + for (size_t i = 0; i < 3; ++i) { + rho[i] = std::exp(neg_half_sigma2 * omega[i] * omega[i]) * recip_radius; + } + + // second part of (52), k1,k2 = 1,3; 3,5; 5,1 + const double D_13 = p_1 * r_3 - r_1 * p_3; + const double D_35 = p_3 * r_5 - r_3 * p_5; + const double D_51 = p_5 * r_1 - r_5 * p_1; + + // (52), k=5 + const double recip_d13 = 1.0 / D_13; + const double zeta_15 = D_35 * recip_d13; + const double zeta_35 = D_51 * recip_d13; + + double A[9] = {p_1, p_3, p_5, // + r_1, r_3, r_5, // (56) + zeta_15, zeta_35, 1}; + JXL_CHECK(Inv3x3Matrix(A)); + const double gamma[3] = {1, radius * radius - sigma * sigma, // (55) + zeta_15 * rho[0] + zeta_35 * rho[1] + rho[2]}; + double beta[3]; + MatMul(A, gamma, 3, 3, 1, beta); // (53) + + // Sanity check: correctly solved for beta (IIR filter weights are normalized) + const double sum = beta[0] * p_1 + beta[1] * p_3 + beta[2] * p_5; // (39) + JXL_ASSERT(std::abs(sum - 1) < 1E-12); + (void)sum; + + rg->radius = static_cast(radius); + + double n2[3]; + double d1[3]; + for (size_t i = 0; i < 3; ++i) { + n2[i] = -beta[i] * std::cos(omega[i] * (radius + 1.0)); // (33) + d1[i] = -2.0 * std::cos(omega[i]); // (33) + + for (size_t lane = 0; lane < 4; ++lane) { + rg->n2[4 * i + lane] = static_cast(n2[i]); + rg->d1[4 * i + lane] = static_cast(d1[i]); + } + + const double d_2 = d1[i] * d1[i]; + + // Obtained by expanding (35) for four consecutive outputs via sympy: + // n, d, p, pp = symbols('n d p pp') + // i0, i1, i2, i3 = symbols('i0 i1 i2 i3') + // o0, o1, o2, o3 = symbols('o0 o1 o2 o3') + // o0 = n*i0 - d*p - pp + // o1 = n*i1 - d*o0 - p + // o2 = n*i2 - d*o1 - o0 + // o3 = n*i3 - d*o2 - o1 + // Then expand(o3) and gather terms for p(prev), pp(prev2) etc. + rg->mul_prev[4 * i + 0] = -d1[i]; + rg->mul_prev[4 * i + 1] = d_2 - 1.0; + rg->mul_prev[4 * i + 2] = -d_2 * d1[i] + 2.0 * d1[i]; + rg->mul_prev[4 * i + 3] = d_2 * d_2 - 3.0 * d_2 + 1.0; + rg->mul_prev2[4 * i + 0] = -1.0; + rg->mul_prev2[4 * i + 1] = d1[i]; + rg->mul_prev2[4 * i + 2] = -d_2 + 1.0; + rg->mul_prev2[4 * i + 3] = d_2 * d1[i] - 2.0 * d1[i]; + rg->mul_in[4 * i + 0] = n2[i]; + rg->mul_in[4 * i + 1] = -d1[i] * n2[i]; + rg->mul_in[4 * i + 2] = d_2 * n2[i] - n2[i]; + rg->mul_in[4 * i + 3] = -d_2 * d1[i] * n2[i] + 2.0 * d1[i] * n2[i]; + } + return rg; +} + +namespace { + +// Apply 1D horizontal scan to each row. +void FastGaussianHorizontal(const hwy::AlignedUniquePtr& rg, + const ImageF& in, ThreadPool* pool, + ImageF* JXL_RESTRICT out) { + PROFILER_FUNC; + JXL_CHECK(SameSize(in, *out)); + + const intptr_t xsize = in.xsize(); + RunOnPool( + pool, 0, in.ysize(), ThreadPool::SkipInit(), + [&](const int task, const int /*thread*/) { + const size_t y = task; + const float* row_in = in.ConstRow(y); + float* JXL_RESTRICT row_out = out->Row(y); + FastGaussian1D(rg, row_in, xsize, row_out); + }, + "FastGaussianHorizontal"); +} + +} // namespace + +void FastGaussian(const hwy::AlignedUniquePtr& rg, + const ImageF& in, ThreadPool* pool, ImageF* JXL_RESTRICT temp, + ImageF* JXL_RESTRICT out) { + FastGaussianHorizontal(rg, in, pool, temp); + HWY_DYNAMIC_DISPATCH(FastGaussianVertical)(rg, *temp, pool, out); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/gauss_blur.h b/lib/jxl/gauss_blur.h new file mode 100644 index 0000000..fb4741f --- /dev/null +++ b/lib/jxl/gauss_blur.h @@ -0,0 +1,94 @@ +// 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_GAUSS_BLUR_H_ +#define LIB_JXL_GAUSS_BLUR_H_ + +#include + +#include +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" + +namespace jxl { + +template +std::vector GaussianKernel(int radius, T sigma) { + JXL_ASSERT(sigma > 0.0); + std::vector kernel(2 * radius + 1); + const T scaler = -1.0 / (2 * sigma * sigma); + double sum = 0.0; + for (int i = -radius; i <= radius; ++i) { + const T val = std::exp(scaler * i * i); + kernel[i + radius] = val; + sum += val; + } + for (size_t i = 0; i < kernel.size(); ++i) { + kernel[i] /= sum; + } + return kernel; +} + +// All convolution functions below apply mirroring of the input on the borders +// in the following way: +// +// input: [a0 a1 a2 ... aN] +// mirrored input: [aR ... a1 | a0 a1 a2 .... aN | aN-1 ... aN-R] +// +// where R is the radius of the kernel (i.e. kernel size is 2*R+1). + +// REQUIRES: in.xsize() and in.ysize() are integer multiples of res. +ImageF ConvolveAndSample(const ImageF& in, const std::vector& kernel, + const size_t res); + +// Private, used by test. +void ExtrapolateBorders(const float* const JXL_RESTRICT row_in, + float* const JXL_RESTRICT row_out, const int xsize, + const int radius); + +// Only for use by CreateRecursiveGaussian and FastGaussian*. +#pragma pack(push, 1) +struct RecursiveGaussian { + // For k={1,3,5} in that order, each broadcasted 4x for LoadDup128. Used only + // for vertical passes. + float n2[3 * 4]; + float d1[3 * 4]; + + // We unroll horizontal passes 4x - one output per lane. These are each lane's + // multiplier for the previous output (relative to the first of the four + // outputs). Indexing: 4 * 0..2 (for {1,3,5}) + 0..3 for the lane index. + float mul_prev[3 * 4]; + // Ditto for the second to last output. + float mul_prev2[3 * 4]; + + // We multiply a vector of inputs 0..3 by a vector shifted from this array. + // in=0 uses all 4 (nonzero) terms; for in=3, the lower three lanes are 0. + float mul_in[3 * 4]; + + size_t radius; +}; +#pragma pack(pop) + +// Precomputation for FastGaussian*; users may use the same pointer/storage in +// subsequent calls to FastGaussian* with the same sigma. +hwy::AlignedUniquePtr CreateRecursiveGaussian(double sigma); + +// 1D Gaussian with zero-pad boundary handling and runtime independent of sigma. +void FastGaussian1D(const hwy::AlignedUniquePtr& rg, + const float* JXL_RESTRICT in, intptr_t width, + float* JXL_RESTRICT out); + +// 2D Gaussian with zero-pad boundary handling and runtime independent of sigma. +void FastGaussian(const hwy::AlignedUniquePtr& rg, + const ImageF& in, ThreadPool* pool, ImageF* JXL_RESTRICT temp, + ImageF* JXL_RESTRICT out); + +} // namespace jxl + +#endif // LIB_JXL_GAUSS_BLUR_H_ diff --git a/lib/jxl/gauss_blur_test.cc b/lib/jxl/gauss_blur_test.cc new file mode 100644 index 0000000..cdde77e --- /dev/null +++ b/lib/jxl/gauss_blur_test.cc @@ -0,0 +1,610 @@ +// 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/gauss_blur.h" + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/time.h" +#include "lib/jxl/base/robust_statistics.h" +#include "lib/jxl/convolve.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { + +bool NearEdge(const int64_t width, const int64_t peak) { + // When around 3*sigma from the edge, there is negligible truncation. + return peak < 10 || peak > width - 10; +} + +// Follow the curve downwards by scanning right from `peak` and verifying +// identical values at the same offset to the left. +void VerifySymmetric(const int64_t width, const int64_t peak, + const float* out) { + const double tolerance = NearEdge(width, peak) ? 0.015 : 6E-7; + for (int64_t i = 1;; ++i) { + // Stop if we passed either end of the array + if (peak - i < 0 || peak + i >= width) break; + EXPECT_GT(out[peak + i - 1] + tolerance, out[peak + i]); // descending + EXPECT_NEAR(out[peak - i], out[peak + i], tolerance); // symmetric + } +} + +void TestImpulseResponse(size_t width, size_t peak) { + const auto rg3 = CreateRecursiveGaussian(3.0); + const auto rg4 = CreateRecursiveGaussian(4.0); + const auto rg5 = CreateRecursiveGaussian(5.0); + + // Extra padding for 4x unrolling + auto in = hwy::AllocateAligned(width + 3); + memset(in.get(), 0, sizeof(float) * (width + 3)); + in[peak] = 1.0f; + + auto out3 = hwy::AllocateAligned(width + 3); + auto out4 = hwy::AllocateAligned(width + 3); + auto out5 = hwy::AllocateAligned(width + 3); + FastGaussian1D(rg3, in.get(), width, out3.get()); + FastGaussian1D(rg4, out3.get(), width, out4.get()); + FastGaussian1D(rg5, in.get(), width, out5.get()); + + VerifySymmetric(width, peak, out3.get()); + VerifySymmetric(width, peak, out4.get()); + VerifySymmetric(width, peak, out5.get()); + + // Wider kernel has flatter peak + EXPECT_LT(out5[peak] + 0.05, out3[peak]); + + // Gauss3 o Gauss4 ~= Gauss5 + const double tolerance = NearEdge(width, peak) ? 0.04 : 0.01; + for (size_t i = 0; i < width; ++i) { + EXPECT_NEAR(out4[i], out5[i], tolerance); + } +} + +void TestImpulseResponseForWidth(size_t width) { + for (size_t i = 0; i < width; ++i) { + TestImpulseResponse(width, i); + } +} + +TEST(GaussBlurTest, ImpulseResponse) { + TestImpulseResponseForWidth(10); // tiny even + TestImpulseResponseForWidth(15); // small odd + TestImpulseResponseForWidth(32); // power of two + TestImpulseResponseForWidth(31); // power of two - 1 + TestImpulseResponseForWidth(33); // power of two + 1 +} + +ImageF Convolve(const ImageF& in, const std::vector& kernel) { + return ConvolveAndSample(in, kernel, 1); +} + +// Higher-precision version for accuracy test. +ImageF ConvolveAndTransposeF64(const ImageF& in, + const std::vector& kernel) { + JXL_ASSERT(kernel.size() % 2 == 1); + ImageF out(in.ysize(), in.xsize()); + const int r = kernel.size() / 2; + std::vector row_tmp(in.xsize() + 2 * r); + float* const JXL_RESTRICT rowp = &row_tmp[r]; + const double* const kernelp = &kernel[r]; + for (size_t y = 0; y < in.ysize(); ++y) { + ExtrapolateBorders(in.Row(y), rowp, in.xsize(), r); + for (size_t x = 0, ox = 0; x < in.xsize(); ++x, ++ox) { + double sum = 0.0; + for (int i = -r; i <= r; ++i) { + sum += rowp[std::max( + 0, std::min(static_cast(x) + i, in.xsize()))] * + kernelp[i]; + } + out.Row(ox)[y] = static_cast(sum); + } + } + return out; +} + +ImageF ConvolveF64(const ImageF& in, const std::vector& kernel) { + ImageF tmp = ConvolveAndTransposeF64(in, kernel); + return ConvolveAndTransposeF64(tmp, kernel); +} + +void TestDirac2D(size_t xsize, size_t ysize, double sigma) { + ImageF in(xsize, ysize); + ZeroFillImage(&in); + // We anyway ignore the border below, so might as well choose the middle. + in.Row(ysize / 2)[xsize / 2] = 1.0f; + + ImageF temp(xsize, ysize); + ImageF out(xsize, ysize); + const auto rg = CreateRecursiveGaussian(sigma); + ThreadPool* null_pool = nullptr; + FastGaussian(rg, in, null_pool, &temp, &out); + + const std::vector kernel = + GaussianKernel(static_cast(4 * sigma), static_cast(sigma)); + const ImageF expected = Convolve(in, kernel); + + const double max_l1 = sigma < 1.5 ? 5E-3 : 6E-4; + const size_t border = 2 * sigma; + VerifyRelativeError(expected, out, max_l1, 1E-8, border); +} + +TEST(GaussBlurTest, Test2D) { + const std::vector dimensions{6, 15, 17, 64, 50, 49}; + for (int xsize : dimensions) { + for (int ysize : dimensions) { + for (double sigma : {1.0, 2.5, 3.6, 7.0}) { + TestDirac2D(static_cast(xsize), static_cast(ysize), + sigma); + } + } + } +} + +// Slow (44 sec). To run, remove the disabled prefix. +TEST(GaussBlurTest, DISABLED_SlowTestDirac1D) { + const double sigma = 7.0; + const auto rg = CreateRecursiveGaussian(sigma); + + // IPOL accuracy test uses 10^-15 tolerance, this is 2*10^-11. + const size_t radius = static_cast(7 * sigma); + const std::vector kernel = GaussianKernel(radius, sigma); + + const size_t length = 16384; + ImageF inputs(length, 1); + ZeroFillImage(&inputs); + + auto outputs = hwy::AllocateAligned(length); + + // One per center position + auto sum_abs_err = hwy::AllocateAligned(length); + std::fill(sum_abs_err.get(), sum_abs_err.get() + length, 0.0); + + for (size_t center = radius; center < length - radius; ++center) { + inputs.Row(0)[center - 1] = 0.0f; // reset last peak, entire array now 0 + inputs.Row(0)[center] = 1.0f; + FastGaussian1D(rg, inputs.Row(0), length, outputs.get()); + + const ImageF outputs_fir = ConvolveF64(inputs, kernel); + + for (size_t i = 0; i < length; ++i) { + const float abs_err = std::abs(outputs[i] - outputs_fir.Row(0)[i]); + sum_abs_err[i] += static_cast(abs_err); + } + } + + const double max_abs_err = + *std::max_element(sum_abs_err.get(), sum_abs_err.get() + length); + printf("Max abs err: %.8e\n", max_abs_err); +} + +void TestRandom(size_t xsize, size_t ysize, float min, float max, double sigma, + double max_l1, double max_rel) { + printf("%4zu x %4zu %4.1f %4.1f sigma %.1f\n", xsize, ysize, min, max, sigma); + ImageF in(xsize, ysize); + RandomFillImage(&in, min, max, 65537 + xsize * 129 + ysize); + // FastGaussian/Convolve handle borders differently, so keep those pixels 0. + const size_t border = 4 * sigma; + SetBorder(border, 0.0f, &in); + + ImageF temp(xsize, ysize); + ImageF out(xsize, ysize); + const auto rg = CreateRecursiveGaussian(sigma); + ThreadPool* null_pool = nullptr; + FastGaussian(rg, in, null_pool, &temp, &out); + + const std::vector kernel = + GaussianKernel(static_cast(4 * sigma), static_cast(sigma)); + const ImageF expected = Convolve(in, kernel); + + VerifyRelativeError(expected, out, max_l1, max_rel, border); +} + +void TestRandomForSizes(float min, float max, double sigma) { + double max_l1 = 5E-3; + double max_rel = 3E-3; + TestRandom(128, 1, min, max, sigma, max_l1, max_rel); + TestRandom(1, 128, min, max, sigma, max_l1, max_rel); + TestRandom(30, 201, min, max, sigma, max_l1 * 1.6, max_rel * 1.2); + TestRandom(201, 30, min, max, sigma, max_l1 * 1.6, max_rel * 1.2); + TestRandom(201, 201, min, max, sigma, max_l1 * 2.0, max_rel * 1.2); +} + +TEST(GaussBlurTest, TestRandom) { + // small non-negative + TestRandomForSizes(0.0f, 10.0f, 3.0f); + TestRandomForSizes(0.0f, 10.0f, 7.0f); + + // small negative + TestRandomForSizes(-4.0f, -1.0f, 3.0f); + TestRandomForSizes(-4.0f, -1.0f, 7.0f); + + // mixed positive/negative + TestRandomForSizes(-6.0f, 6.0f, 3.0f); + TestRandomForSizes(-6.0f, 6.0f, 7.0f); +} + +TEST(GaussBlurTest, TestSign) { + const size_t xsize = 500; + const size_t ysize = 606; + ImageF in(xsize, ysize); + + ZeroFillImage(&in); + const float center[33 * 33] = { + -0.128445f, -0.098473f, -0.121883f, -0.093601f, 0.095665f, -0.271332f, + -0.705475f, -1.324005f, -2.020741f, -1.329464f, 1.834064f, 4.787300f, + 5.834560f, 5.272720f, 3.967960f, 3.547935f, 3.432732f, 3.383015f, + 3.239326f, 3.290806f, 3.298954f, 3.397808f, 3.359730f, 3.533844f, + 3.511856f, 3.436787f, 3.428310f, 3.460209f, 3.550011f, 3.590942f, + 3.593109f, 3.560005f, 3.443165f, 0.089741f, 0.179230f, -0.032997f, + -0.182610f, 0.005669f, -0.244759f, -0.395123f, -0.514961f, -1.003529f, + -1.798656f, -2.377975f, 0.222191f, 3.957664f, 5.946804f, 5.543129f, + 4.290096f, 3.621010f, 3.407257f, 3.392494f, 3.345367f, 3.391903f, + 3.441605f, 3.429260f, 3.444969f, 3.507130f, 3.518612f, 3.443111f, + 3.475948f, 3.536148f, 3.470333f, 3.628311f, 3.600243f, 3.292892f, + -0.226730f, -0.573616f, -0.762165f, -0.398739f, -0.189842f, -0.275921f, + -0.446739f, -0.550037f, -0.461033f, -0.724792f, -1.448349f, -1.814064f, + -0.491032f, 2.817703f, 5.213242f, 5.675629f, 4.864548f, 3.876324f, + 3.535587f, 3.530312f, 3.413765f, 3.386261f, 3.404854f, 3.383472f, + 3.420830f, 3.326496f, 3.257877f, 3.362152f, 3.489609f, 3.619587f, + 3.555805f, 3.423164f, 3.309708f, -0.483940f, -0.502926f, -0.592983f, + -0.492527f, -0.413616f, -0.482555f, -0.475506f, -0.447990f, -0.338120f, + -0.189072f, -0.376427f, -0.910828f, -1.878044f, -1.937927f, 1.423218f, + 4.871609f, 5.767548f, 5.103741f, 3.983868f, 3.633003f, 3.458263f, + 3.507309f, 3.247021f, 3.220612f, 3.326061f, 3.352814f, 3.291061f, + 3.322739f, 3.444302f, 3.506207f, 3.556839f, 3.529575f, 3.457024f, + -0.408161f, -0.431343f, -0.454369f, -0.356419f, -0.380924f, -0.399452f, + -0.439476f, -0.412189f, -0.306816f, -0.008213f, -0.325813f, -0.537842f, + -0.984100f, -1.805332f, -2.028198f, 0.773205f, 4.423046f, 5.604839f, + 5.231617f, 4.080299f, 3.603008f, 3.498741f, 3.517010f, 3.333897f, + 3.381336f, 3.342617f, 3.369686f, 3.434155f, 3.490452f, 3.607029f, + 3.555298f, 3.702297f, 3.618679f, -0.503609f, -0.578564f, -0.419014f, + -0.239883f, 0.269836f, 0.022984f, -0.455067f, -0.621777f, -0.304176f, + -0.163792f, -0.490250f, -0.466637f, -0.391792f, -0.657940f, -1.498035f, + -1.895836f, 0.036537f, 3.462456f, 5.586445f, 5.658791f, 4.434784f, + 3.423435f, 3.318848f, 3.202328f, 3.532764f, 3.436687f, 3.354881f, + 3.356941f, 3.382645f, 3.503902f, 3.512867f, 3.632366f, 3.537312f, + -0.274734f, -0.658829f, -0.726532f, -0.281254f, 0.053196f, -0.064991f, + -0.608517f, -0.720966f, -0.070602f, -0.111320f, -0.440956f, -0.492180f, + -0.488762f, -0.569283f, -1.012741f, -1.582779f, -2.101479f, -1.392380f, + 2.451153f, 5.555855f, 6.096313f, 5.230045f, 4.068172f, 3.404274f, + 3.392586f, 3.326065f, 3.156670f, 3.284828f, 3.347012f, 3.319252f, + 3.352310f, 3.610790f, 3.499847f, -0.150600f, -0.314445f, -0.093575f, + -0.057384f, 0.053688f, -0.189255f, -0.263515f, -0.318653f, 0.053246f, + 0.080627f, -0.119553f, -0.152454f, -0.305420f, -0.404869f, -0.385944f, + -0.689949f, -1.204914f, -1.985748f, -1.711361f, 1.260658f, 4.626896f, + 5.888351f, 5.450989f, 4.070587f, 3.539200f, 3.383492f, 3.296318f, + 3.267334f, 3.436028f, 3.463005f, 3.502625f, 3.522282f, 3.403763f, + -0.348049f, -0.302303f, -0.137016f, -0.041737f, -0.164001f, -0.358849f, + -0.469627f, -0.428291f, -0.375797f, -0.246346f, -0.118950f, -0.084229f, + -0.205681f, -0.241199f, -0.391796f, -0.323151f, -0.241211f, -0.834137f, + -1.684219f, -1.972137f, 0.448399f, 4.019985f, 5.648144f, 5.647846f, + 4.295094f, 3.641884f, 3.374790f, 3.197342f, 3.425545f, 3.507481f, + 3.478065f, 3.430889f, 3.341900f, -1.016304f, -0.959221f, -0.909466f, + -0.810715f, -0.590729f, -0.594467f, -0.646721f, -0.629364f, -0.528561f, + -0.551819f, -0.301086f, -0.149101f, -0.060146f, -0.162220f, -0.326210f, + -0.156548f, -0.036293f, -0.426098f, -1.145470f, -1.628998f, -2.003052f, + -1.142891f, 2.885162f, 5.652863f, 5.718426f, 4.911140f, 3.234222f, + 3.473373f, 3.577183f, 3.271603f, 3.410435f, 3.505489f, 3.434032f, + -0.508911f, -0.438797f, -0.437450f, -0.627426f, -0.511745f, -0.304874f, + -0.274246f, -0.261841f, -0.228466f, -0.342491f, -0.528206f, -0.490082f, + -0.516350f, -0.361694f, -0.398514f, -0.276020f, -0.210369f, -0.355938f, + -0.402622f, -0.538864f, -1.249573f, -2.100105f, -0.996178f, 1.886410f, + 4.929745f, 5.630871f, 5.444199f, 4.042740f, 3.739189f, 3.691399f, + 3.391956f, 3.469696f, 3.431232f, 0.204849f, 0.205433f, -0.131927f, + -0.367908f, -0.374378f, -0.126820f, -0.186951f, -0.228565f, -0.081776f, + -0.143143f, -0.379230f, -0.598701f, -0.458019f, -0.295586f, -0.407730f, + -0.245853f, -0.043140f, 0.024242f, -0.038998f, -0.044151f, -0.425991f, + -1.240753f, -1.943146f, -2.174755f, 0.523415f, 4.376751f, 5.956558f, + 5.850082f, 4.403152f, 3.517399f, 3.560753f, 3.554836f, 3.471985f, + -0.508503f, -0.109783f, 0.057747f, 0.190079f, -0.257153f, -0.591980f, + -0.666771f, -0.525391f, -0.293060f, -0.489731f, -0.304855f, -0.259644f, + -0.367825f, -0.346977f, -0.292889f, -0.215652f, -0.120705f, -0.176010f, + -0.422905f, -0.114647f, -0.289749f, -0.374203f, -0.606754f, -1.127949f, + -1.994583f, -0.588058f, 3.415840f, 5.603470f, 5.811581f, 4.959423f, + 3.721760f, 3.710499f, 3.785461f, -0.554588f, -0.565517f, -0.434578f, + -0.012482f, -0.284660f, -0.699795f, -0.957535f, -0.755135f, -0.382034f, + -0.321552f, -0.287571f, -0.279537f, -0.314972f, -0.256287f, -0.372818f, + -0.316017f, -0.287975f, -0.365639f, -0.512589f, -0.420692f, -0.436485f, + -0.295353f, -0.451958f, -0.755459f, -1.272358f, -2.301353f, -1.776161f, + 1.572483f, 4.826286f, 5.741898f, 5.162853f, 4.028049f, 3.686325f, + -0.495590f, -0.664413f, -0.760044f, -0.152634f, -0.286480f, -0.340462f, + 0.076477f, 0.187706f, -0.068787f, -0.293491f, -0.361145f, -0.292515f, + -0.140671f, -0.190723f, -0.333302f, -0.368168f, -0.192581f, -0.154499f, + -0.236544f, -0.124405f, -0.208321f, -0.465607f, -0.883080f, -1.104813f, + -1.210567f, -1.415665f, -1.924683f, -1.634758f, 0.601017f, 4.276672f, + 5.501350f, 5.331257f, 3.809288f, -0.727722f, -0.533619f, -0.511524f, + -0.470688f, -0.610710f, -0.575130f, -0.311115f, -0.090420f, -0.297676f, + -0.646118f, -0.742805f, -0.485050f, -0.330910f, -0.275417f, -0.357037f, + -0.425598f, -0.481876f, -0.488941f, -0.393551f, -0.051105f, -0.090755f, + -0.328674f, -0.536369f, -0.533684f, -0.336960f, -0.689194f, -1.187195f, + -1.860954f, -2.290253f, -0.424774f, 3.050060f, 5.083332f, 5.291920f, + -0.343605f, -0.190975f, -0.303692f, -0.456512f, -0.681820f, -0.690693f, + -0.416729f, -0.286446f, -0.442055f, -0.709148f, -0.569160f, -0.382423f, + -0.402321f, -0.383362f, -0.366413f, -0.290718f, -0.110069f, -0.220280f, + -0.279018f, -0.255424f, -0.262081f, -0.487556f, -0.444492f, -0.250500f, + -0.119583f, -0.291557f, -0.537781f, -1.104073f, -1.737091f, -1.697441f, + -0.323456f, 2.042049f, 4.605103f, -0.310631f, -0.279568f, -0.012695f, + -0.160130f, -0.358746f, -0.421101f, -0.559677f, -0.474136f, -0.416565f, + -0.561817f, -0.534672f, -0.519157f, -0.767197f, -0.605831f, -0.186523f, + 0.219872f, 0.264984f, -0.193432f, -0.363182f, -0.467472f, -0.462009f, + -0.571053f, -0.522476f, -0.315903f, -0.237427f, -0.147320f, -0.100201f, + -0.237568f, -0.763435f, -1.242043f, -2.135159f, -1.409485f, 1.236370f, + -0.474247f, -0.517906f, -0.410217f, -0.542244f, -0.795986f, -0.590004f, + -0.388863f, -0.462921f, -0.810627f, -0.778637f, -0.512486f, -0.718025f, + -0.710854f, -0.482513f, -0.318233f, -0.194962f, -0.220116f, -0.421673f, + -0.534233f, -0.403339f, -0.389332f, -0.407303f, -0.437355f, -0.469730f, + -0.359600f, -0.352745f, -0.466755f, -0.414585f, -0.430756f, -0.656822f, + -1.237038f, -2.046097f, -1.574898f, -0.593815f, -0.582165f, -0.336098f, + -0.372612f, -0.554386f, -0.410603f, -0.428276f, -0.647644f, -0.640720f, + -0.582207f, -0.414112f, -0.435547f, -0.435505f, -0.332561f, -0.248116f, + -0.340221f, -0.277855f, -0.352699f, -0.377319f, -0.230850f, -0.313267f, + -0.446270f, -0.346237f, -0.420422f, -0.530781f, -0.400341f, -0.463661f, + -0.209091f, -0.056705f, -0.011772f, -0.169388f, -0.736275f, -1.463017f, + -0.752701f, -0.668865f, -0.329765f, -0.299347f, -0.245667f, -0.286999f, + -0.520420f, -0.675438f, -0.255753f, 0.141357f, -0.079639f, -0.419476f, + -0.374069f, -0.046253f, 0.116116f, -0.145847f, -0.380371f, -0.563412f, + -0.638634f, -0.310116f, -0.260914f, -0.508404f, -0.465508f, -0.527824f, + -0.370979f, -0.305595f, -0.244694f, -0.254490f, 0.009968f, -0.050201f, + -0.331219f, -0.614960f, -0.788208f, -0.483242f, -0.367516f, -0.186951f, + -0.180031f, 0.129711f, -0.127811f, -0.384750f, -0.499542f, -0.418613f, + -0.121635f, 0.203197f, -0.167290f, -0.397270f, -0.355461f, -0.218746f, + -0.376785f, -0.521698f, -0.721581f, -0.845741f, -0.535439f, -0.220882f, + -0.309067f, -0.555248f, -0.690342f, -0.664948f, -0.390102f, 0.020355f, + -0.130447f, -0.173252f, -0.170059f, -0.633663f, -0.956001f, -0.621696f, + -0.388302f, -0.342262f, -0.244370f, -0.386948f, -0.401421f, -0.172979f, + -0.206163f, -0.450058f, -0.525789f, -0.549274f, -0.349251f, -0.474613f, + -0.667976f, -0.435600f, -0.175369f, -0.196877f, -0.202976f, -0.242481f, + -0.258369f, -0.189133f, -0.395397f, -0.765499f, -0.944016f, -0.850967f, + -0.631561f, -0.152493f, -0.046432f, -0.262066f, -0.195919f, 0.048218f, + 0.084972f, 0.039902f, 0.000618f, -0.404430f, -0.447456f, -0.418076f, + -0.631935f, -0.717415f, -0.502888f, -0.530514f, -0.747826f, -0.704041f, + -0.674969f, -0.516853f, -0.418446f, -0.327740f, -0.308815f, -0.481636f, + -0.440083f, -0.481720f, -0.341053f, -0.283897f, -0.324368f, -0.352829f, + -0.434349f, -0.545589f, -0.533104f, -0.472755f, -0.570496f, -0.557735f, + -0.708176f, -0.493332f, -0.194416f, -0.186249f, -0.256710f, -0.271835f, + -0.304752f, -0.431267f, -0.422398f, -0.646725f, -0.680801f, -0.249031f, + -0.058567f, -0.213890f, -0.383949f, -0.540291f, -0.549877f, -0.225567f, + -0.037174f, -0.499874f, -0.641010f, -0.628044f, -0.390549f, -0.311497f, + -0.542313f, -0.569565f, -0.473408f, -0.331245f, -0.357197f, -0.285599f, + -0.200157f, -0.201866f, -0.124428f, -0.346016f, -0.392311f, -0.264496f, + -0.285370f, -0.436974f, -0.523483f, -0.410461f, -0.267925f, -0.055016f, + -0.382458f, -0.319771f, -0.049927f, 0.124329f, 0.266102f, -0.106606f, + -0.773647f, -0.973053f, -0.708206f, -0.486137f, -0.319923f, -0.493900f, + -0.490860f, -0.324986f, -0.147346f, -0.146088f, -0.161758f, -0.084396f, + -0.379494f, 0.041626f, -0.113361f, -0.277767f, 0.083366f, 0.126476f, + 0.139057f, 0.038040f, 0.038162f, -0.242126f, -0.411736f, -0.370049f, + -0.455357f, -0.039257f, 0.264442f, -0.271492f, -0.425346f, -0.514847f, + -0.448650f, -0.580399f, -0.652603f, -0.774803f, -0.692524f, -0.579578f, + -0.465206f, -0.386265f, -0.458012f, -0.446594f, -0.284893f, -0.345448f, + -0.350876f, -0.440350f, -0.360378f, -0.270428f, 0.237213f, -0.063602f, + -0.364529f, -0.179867f, 0.078197f, 0.117947f, -0.093410f, -0.359119f, + -0.480961f, -0.540638f, -0.436287f, -0.598576f, -0.253735f, -0.060093f, + -0.549145f, -0.808327f, -0.698593f, -0.595764f, -0.582508f, -0.497353f, + -0.480892f, -0.584240f, -0.665791f, -0.690903f, -0.743446f, -0.796677f, + -0.782391f, -0.649010f, -0.628139f, -0.880848f, -0.829361f, -0.373272f, + -0.223667f, 0.174572f, -0.348743f, -0.798901f, -0.692307f, -0.607609f, + -0.401455f, -0.480919f, -0.450798f, -0.435413f, -0.322338f, -0.228382f, + -0.450466f, -0.504440f, -0.477402f, -0.662224f, -0.583397f, -0.217445f, + -0.157459f, -0.079584f, -0.226168f, -0.488720f, -0.669624f, -0.666878f, + -0.565311f, -0.549625f, -0.364601f, -0.497627f, -0.736897f, -0.763023f, + -0.741020f, -0.404503f, 0.184814f, -0.075315f, -0.281513f, -0.532906f, + -0.405800f, -0.313438f, -0.536652f, -0.403381f, 0.011967f, 0.103310f, + -0.269848f, -0.508656f, -0.445923f, -0.644859f, -0.617870f, -0.500927f, + -0.371559f, -0.125580f, 0.028625f, -0.154713f, -0.442024f, -0.492764f, + -0.199371f, 0.236305f, 0.225925f, 0.075577f, -0.285812f, -0.437145f, + -0.374260f, -0.156693f, -0.129635f, -0.243206f, -0.123058f, 0.162148f, + -0.313152f, -0.337982f, -0.358421f, 0.040070f, 0.038925f, -0.333313f, + -0.351662f, 0.023014f, 0.091362f, -0.282890f, -0.373253f, -0.389050f, + -0.532707f, -0.423347f, -0.349968f, -0.287045f, -0.202442f, -0.308430f, + -0.222801f, -0.106323f, -0.056358f, 0.027222f, 0.390732f, 0.033558f, + -0.160088f, -0.382217f, -0.535282f, -0.515900f, -0.022736f, 0.165665f, + -0.111408f, -0.233784f, -0.312357f, -0.541885f, -0.480022f, -0.482513f, + -0.246254f, 0.132244f, 0.090134f, 0.234634f, -0.089249f, -0.460854f, + -0.515457f, -0.450874f, -0.311031f, -0.387680f, -0.360554f, -0.179241f, + -0.283817f, -0.475815f, -0.246399f, -0.388958f, -0.551140f, -0.496239f, + -0.559879f, -0.379761f, -0.254288f, -0.395111f, -0.613018f, -0.459427f, + -0.263580f, -0.268929f, 0.080826f, 0.115616f, -0.097324f, -0.325310f, + -0.480450f, -0.313286f, -0.310371f, -0.517361f, -0.288288f, -0.112679f, + -0.173241f, -0.221664f, -0.039452f, -0.107578f, -0.089630f, -0.483768f, + -0.571087f, -0.497108f, -0.321533f, -0.375492f, -0.540363f, -0.406815f, + -0.388512f, -0.514561f, -0.540192f, -0.402412f, -0.232246f, -0.304749f, + -0.383724f, -0.679596f, -0.685463f, -0.694538f, -0.642937f, -0.425789f, + 0.103271f, -0.194862f, -0.487999f, -0.717281f, -0.681850f, -0.709286f, + -0.615398f, -0.554245f, -0.254681f, -0.049950f, -0.002914f, -0.095383f, + -0.370911f, -0.564224f, -0.242714f}; + const size_t xtest = xsize / 2; + const size_t ytest = ysize / 2; + + for (intptr_t dy = -16; dy <= 16; ++dy) { + float* row = in.Row(ytest + dy); + for (intptr_t dx = -16; dx <= 16; ++dx) + row[xtest + dx] = center[(dy + 16) * 33 + (dx + 16)]; + } + + const double sigma = 7.155933; + + ImageF temp(xsize, ysize); + ImageF out_rg(xsize, ysize); + const auto rg = CreateRecursiveGaussian(sigma); + ThreadPool* null_pool = nullptr; + FastGaussian(rg, in, null_pool, &temp, &out_rg); + + ImageF out_old; + { + const std::vector kernel = + GaussianKernel(static_cast(4 * sigma), static_cast(sigma)); + printf("old kernel size %zu\n", kernel.size()); + out_old = Convolve(in, kernel); + } + + printf("rg %.4f old %.4f\n", out_rg.Row(ytest)[xtest], + out_old.Row(ytest)[xtest]); +} + +// Returns megapixels/sec. "div" is a divisor for the number of repetitions, +// used to reduce benchmark duration. Func returns elapsed time. +template +double Measure(const size_t xsize, const size_t ysize, int div, + const Func& func) { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + int reps = 10 / div; +#else + int reps = 2000 / div; +#endif + if (reps < 2) reps = 2; + std::vector elapsed; + for (int i = 0; i < reps; ++i) { + elapsed.push_back(func(xsize, ysize)); + } + + double mean_elapsed; + // Potential loss of precision, and also enough samples for mode. + if (reps > 50) { + std::sort(elapsed.begin(), elapsed.end()); + mean_elapsed = jxl::HalfSampleMode()(elapsed.data(), elapsed.size()); + } else { + // Skip first(noisier) + mean_elapsed = Geomean(elapsed.data() + 1, elapsed.size() - 1); + } + return (xsize * ysize * 1E-6) / mean_elapsed; +} + +void Benchmark1D() { + // Uncomment to disable SIMD and force and scalar implementation + // hwy::DisableTargets(~HWY_SCALAR); + + const size_t length = 16384; // (same value used for running IPOL benchmark) + const double sigma = 7.0; // (from Butteraugli application) + // NOTE: MSVC and clang disagree on the required captures, so use =. + const double mps_rg1 = + Measure(length, 1, 1, [=](size_t /*xsize*/, size_t /*ysize*/) { + ImageF in(length, 1); + const float expected = length; + FillImage(expected, &in); + + ImageF temp(length, 1); + ImageF out(length, 1); + const auto rg = CreateRecursiveGaussian(sigma); + const double t0 = Now(); + FastGaussian1D(rg, in.Row(0), length, out.Row(0)); + const double t1 = Now(); + // Prevent optimizing out + const float actual = out.ConstRow(0)[length / 2]; + const float rel_err = std::abs(actual - expected) / expected; + EXPECT_LT(rel_err, 9E-5); + return t1 - t0; + }); + // Report milliseconds for comparison with IPOL benchmark + const double milliseconds = (1E-6 * length) / mps_rg1 * 1E3; + printf("%5zu @%.1f: rg 1D %e\n", length, sigma, milliseconds); +} + +void Benchmark(size_t xsize, size_t ysize, double sigma) { + // Uncomment to run AVX2 + // hwy::DisableTargets(HWY_AVX3); + + const double mps_rg = + Measure(xsize, ysize, 1, [sigma](size_t xsize, size_t ysize) { + ImageF in(xsize, ysize); + const float expected = xsize + ysize; + FillImage(expected, &in); + + ImageF temp(xsize, ysize); + ImageF out(xsize, ysize); + const auto rg = CreateRecursiveGaussian(sigma); + ThreadPool* null_pool = nullptr; + const double t0 = Now(); + FastGaussian(rg, in, null_pool, &temp, &out); + const double t1 = Now(); + // Prevent optimizing out + const float actual = out.ConstRow(ysize / 2)[xsize / 2]; + const float rel_err = std::abs(actual - expected) / expected; + EXPECT_LT(rel_err, 9E-5); + return t1 - t0; + }); + + const double mps_fir = + Measure(xsize, ysize, 100, [sigma](size_t xsize, size_t ysize) { + ImageF in(xsize, ysize); + const float expected = xsize + ysize; + FillImage(expected, &in); + const std::vector kernel = GaussianKernel( + static_cast(4 * sigma), static_cast(sigma)); + const double t0 = Now(); + const ImageF out = Convolve(in, kernel); + const double t1 = Now(); + + // Prevent optimizing out + const float actual = out.ConstRow(ysize / 2)[xsize / 2]; + const float rel_err = std::abs(actual - expected) / expected; + EXPECT_LT(rel_err, 5E-6); + return t1 - t0; + }); + + const double mps_simd7 = + Measure(xsize, ysize, 10, [](size_t xsize, size_t ysize) { + ImageF in(xsize, ysize); + const float expected = xsize + ysize; + FillImage(expected, &in); + ImageF out(xsize, ysize); + // Gaussian with sigma 1 + const WeightsSeparable7 weights = { + {HWY_REP4(0.383103f), HWY_REP4(0.241843f), HWY_REP4(0.060626f), + HWY_REP4(0.00598f)}, + {HWY_REP4(0.383103f), HWY_REP4(0.241843f), HWY_REP4(0.060626f), + HWY_REP4(0.00598f)}}; + ThreadPool* null_pool = nullptr; + const double t0 = Now(); + Separable7(in, Rect(in), weights, null_pool, &out); + const double t1 = Now(); + + // Prevent optimizing out + const float actual = out.ConstRow(ysize / 2)[xsize / 2]; + const float rel_err = std::abs(actual - expected) / expected; + EXPECT_LT(rel_err, 5E-6); + return t1 - t0; + }); + + printf("%zu,%zu,%.1f,%.1f,%.1f\n", xsize, ysize, mps_fir, mps_simd7, mps_rg); +} + +TEST(GaussBlurTest, BenchmarkTest) { + Benchmark1D(); + Benchmark(77, 177, 7); +} + +TEST(GaussBlurTest, DISABLED_SlowBenchmark) { + Benchmark1D(); + + // Euler's gamma as a nothing-up-my-sleeve number, so sizes are unlikely to + // interact with cache properties + const float g = 0.57721566; + const size_t d0 = 128; + const size_t d1 = static_cast(d0 / g); + const size_t d2 = static_cast(d1 / g); + const size_t d3 = static_cast(d2 / g); + Benchmark(d0, d0, 7); + Benchmark(d0, d1, 7); + Benchmark(d1, d0, 7); + Benchmark(d1, d1, 7); + Benchmark(d1, d2, 7); + Benchmark(d2, d1, 7); + Benchmark(d2, d2, 7); + Benchmark(d2, d3, 7); + Benchmark(d3, d2, 7); + Benchmark(d3, d3, 7); + + Benchmark(1920, 1080, 7); + + PROFILER_PRINT_RESULTS(); +} + +} // namespace jxl diff --git a/lib/jxl/gradient_test.cc b/lib/jxl/gradient_test.cc new file mode 100644 index 0000000..332684a --- /dev/null +++ b/lib/jxl/gradient_test.cc @@ -0,0 +1,205 @@ +// 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 +#include +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { +namespace { + +// Returns distance of point p to line p0..p1, the result is signed and is not +// normalized. +double PointLineDist(double x0, double y0, double x1, double y1, double x, + double y) { + return (y1 - y0) * x - (x1 - x0) * y + x1 * y0 - y1 * x0; +} + +// Generates a test image with a gradient from one color to another. +// Angle in degrees, colors can be given in hex as 0xRRGGBB. The angle is the +// angle in which the change direction happens. +Image3F GenerateTestGradient(uint32_t color0, uint32_t color1, double angle, + size_t xsize, size_t ysize) { + Image3F image(xsize, ysize); + + double x0 = xsize / 2; + double y0 = ysize / 2; + double x1 = x0 + std::sin(angle / 360.0 * 2.0 * kPi); + double y1 = y0 + std::cos(angle / 360.0 * 2.0 * kPi); + + double maxdist = + std::max(fabs(PointLineDist(x0, y0, x1, y1, 0, 0)), + fabs(PointLineDist(x0, y0, x1, y1, xsize, 0))); + + for (size_t c = 0; c < 3; ++c) { + float c0 = ((color0 >> (8 * (2 - c))) & 255); + float c1 = ((color1 >> (8 * (2 - c))) & 255); + for (size_t y = 0; y < ysize; ++y) { + float* row = image.PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + double dist = PointLineDist(x0, y0, x1, y1, x, y); + double v = ((dist / maxdist) + 1.0) / 2.0; + float color = c0 * (1.0 - v) + c1 * v; + row[x] = color; + } + } + } + + return image; +} + +// Computes the max of the horizontal and vertical second derivative for each +// pixel, where second derivative means absolute value of difference of left +// delta and right delta (top/bottom for vertical direction). +// The radius over which the derivative is computed is only 1 pixel and it only +// checks two angles (hor and ver), but this approximation works well enough. +static ImageF Gradient2(const ImageF& image) { + size_t xsize = image.xsize(); + size_t ysize = image.ysize(); + ImageF image2(image.xsize(), image.ysize()); + for (size_t y = 1; y + 1 < ysize; y++) { + const auto* JXL_RESTRICT row0 = image.Row(y - 1); + const auto* JXL_RESTRICT row1 = image.Row(y); + const auto* JXL_RESTRICT row2 = image.Row(y + 1); + auto* row_out = image2.Row(y); + for (size_t x = 1; x + 1 < xsize; x++) { + float ddx = (row1[x] - row1[x - 1]) - (row1[x + 1] - row1[x]); + float ddy = (row1[x] - row0[x]) - (row2[x] - row1[x]); + row_out[x] = std::max(fabsf(ddx), fabsf(ddy)); + } + } + // Copy to the borders + if (ysize > 2) { + auto* JXL_RESTRICT row0 = image2.Row(0); + const auto* JXL_RESTRICT row1 = image2.Row(1); + const auto* JXL_RESTRICT row2 = image2.Row(ysize - 2); + auto* JXL_RESTRICT row3 = image2.Row(ysize - 1); + for (size_t x = 1; x + 1 < xsize; x++) { + row0[x] = row1[x]; + row3[x] = row2[x]; + } + } else { + const auto* row0_in = image.Row(0); + const auto* row1_in = image.Row(ysize - 1); + auto* row0_out = image2.Row(0); + auto* row1_out = image2.Row(ysize - 1); + for (size_t x = 1; x + 1 < xsize; x++) { + // Image too narrow, take first derivative instead + row0_out[x] = row1_out[x] = fabsf(row0_in[x] - row1_in[x]); + } + } + if (xsize > 2) { + for (size_t y = 0; y < ysize; y++) { + auto* row = image2.Row(y); + row[0] = row[1]; + row[xsize - 1] = row[xsize - 2]; + } + } else { + for (size_t y = 0; y < ysize; y++) { + const auto* JXL_RESTRICT row_in = image.Row(y); + auto* row_out = image2.Row(y); + // Image too narrow, take first derivative instead + row_out[0] = row_out[xsize - 1] = fabsf(row_in[0] - row_in[xsize - 1]); + } + } + return image2; +} + +static Image3F Gradient2(const Image3F& image) { + return Image3F(Gradient2(image.Plane(0)), Gradient2(image.Plane(1)), + Gradient2(image.Plane(2))); +} + +/* +Tests if roundtrip with jxl on a gradient image doesn't cause banding. +Only tests if use_gradient is true. Set to false for debugging to see the +distance values. +Angle in degrees, colors can be given in hex as 0xRRGGBB. +*/ +void TestGradient(ThreadPool* pool, uint32_t color0, uint32_t color1, + size_t xsize, size_t ysize, float angle, bool fast_mode, + float butteraugli_distance, bool use_gradient = true) { + CompressParams cparams; + cparams.butteraugli_distance = butteraugli_distance; + if (fast_mode) { + cparams.speed_tier = SpeedTier::kSquirrel; + } + DecompressParams dparams; + + Image3F gradient = GenerateTestGradient(color0, color1, angle, xsize, ysize); + + CodecInOut io; + io.metadata.m.SetUintSamples(8); + io.metadata.m.color_encoding = ColorEncoding::SRGB(); + io.SetFromImage(std::move(gradient), io.metadata.m.color_encoding); + + CodecInOut io2; + + PaddedBytes compressed; + AuxOut* aux_out = nullptr; + PassesEncoderState enc_state; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(io2.Main().TransformTo(io2.metadata.m.color_encoding, pool)); + + if (use_gradient) { + // Test that the gradient map worked. For that, we take a second derivative + // of the image with Gradient2 to measure how linear the change is in x and + // y direction. For a well handled gradient, we expect max values around + // 0.1, while if there is noticeable banding, which means the gradient map + // failed, the values are around 0.5-1.0 (regardless of + // butteraugli_distance). + Image3F gradient2 = Gradient2(*io2.Main().color()); + + std::array image_max; + Image3Max(gradient2, &image_max); + + // TODO(jyrki): These values used to work with 0.2, 0.2, 0.2. + EXPECT_LE(image_max[0], 3.15); + EXPECT_LE(image_max[1], 1.72); + EXPECT_LE(image_max[2], 5.05); + } +} + +static constexpr bool fast_mode = true; + +TEST(GradientTest, SteepGradient) { + ThreadPoolInternal pool(8); + // Relatively steep gradients, colors from the sky of stp.png + TestGradient(&pool, 0xd99d58, 0x889ab1, 512, 512, 90, fast_mode, 3.0); +} + +TEST(GradientTest, SubtleGradient) { + ThreadPoolInternal pool(8); + // Very subtle gradient + TestGradient(&pool, 0xb89b7b, 0xa89b8d, 512, 512, 90, fast_mode, 4.0); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/headers.cc b/lib/jxl/headers.cc new file mode 100644 index 0000000..cc3077a --- /dev/null +++ b/lib/jxl/headers.cc @@ -0,0 +1,212 @@ +// 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/headers.h" + +#include "lib/jxl/common.h" +#include "lib/jxl/fields.h" + +namespace jxl { +namespace { + +struct Rational { + constexpr explicit Rational(uint32_t num, uint32_t den) + : num(num), den(den) {} + + // Returns floor(multiplicand * rational). + constexpr uint32_t MulTruncate(uint32_t multiplicand) const { + return uint64_t(multiplicand) * num / den; + } + + uint32_t num; + uint32_t den; +}; + +Rational FixedAspectRatios(uint32_t ratio) { + JXL_ASSERT(0 != ratio && ratio < 8); + // Other candidates: 5/4, 7/5, 14/9, 16/10, 5/3, 21/9, 12/5 + constexpr Rational kRatios[7] = {Rational(1, 1), // square + Rational(12, 10), // + Rational(4, 3), // camera + Rational(3, 2), // mobile camera + Rational(16, 9), // camera/display + Rational(5, 4), // + Rational(2, 1)}; // + return kRatios[ratio - 1]; +} + +uint32_t FindAspectRatio(uint32_t xsize, uint32_t ysize) { + for (uint32_t r = 1; r < 8; ++r) { + if (xsize == FixedAspectRatios(r).MulTruncate(ysize)) { + return r; + } + } + return 0; // Must send xsize instead +} + +} // namespace + +size_t SizeHeader::xsize() const { + if (ratio_ != 0) { + return FixedAspectRatios(ratio_).MulTruncate( + static_cast(ysize())); + } + return small_ ? ((xsize_div8_minus_1_ + 1) * 8) : xsize_; +} + +Status SizeHeader::Set(size_t xsize64, size_t ysize64) { + if (xsize64 > 0xFFFFFFFFull || ysize64 > 0xFFFFFFFFull) { + return JXL_FAILURE("Image too large"); + } + const uint32_t xsize32 = static_cast(xsize64); + const uint32_t ysize32 = static_cast(ysize64); + if (xsize64 == 0 || ysize64 == 0) return JXL_FAILURE("Empty image"); + ratio_ = FindAspectRatio(xsize32, ysize32); + small_ = ysize64 <= 256 && (ysize64 % kBlockDim) == 0 && + (ratio_ != 0 || (xsize64 <= 256 && (xsize64 % kBlockDim) == 0)); + if (small_) { + ysize_div8_minus_1_ = ysize32 / 8 - 1; + } else { + ysize_ = ysize32; + } + + if (ratio_ == 0) { + if (small_) { + xsize_div8_minus_1_ = xsize32 / 8 - 1; + } else { + xsize_ = xsize32; + } + } + JXL_ASSERT(xsize() == xsize64); + JXL_ASSERT(ysize() == ysize64); + return true; +} + +Status PreviewHeader::Set(size_t xsize64, size_t ysize64) { + const uint32_t xsize32 = static_cast(xsize64); + const uint32_t ysize32 = static_cast(ysize64); + if (xsize64 == 0 || ysize64 == 0) return JXL_FAILURE("Empty preview"); + div8_ = (xsize64 % kBlockDim) == 0 && (ysize64 % kBlockDim) == 0; + if (div8_) { + ysize_div8_ = ysize32 / 8; + } else { + ysize_ = ysize32; + } + + ratio_ = FindAspectRatio(xsize32, ysize32); + if (ratio_ == 0) { + if (div8_) { + xsize_div8_ = xsize32 / 8; + } else { + xsize_ = xsize32; + } + } + JXL_ASSERT(xsize() == xsize64); + JXL_ASSERT(ysize() == ysize64); + return true; +} + +size_t PreviewHeader::xsize() const { + if (ratio_ != 0) { + return FixedAspectRatios(ratio_).MulTruncate( + static_cast(ysize())); + } + return div8_ ? (xsize_div8_ * 8) : xsize_; +} + +SizeHeader::SizeHeader() { Bundle::Init(this); } +Status SizeHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &small_)); + + if (visitor->Conditional(small_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(5, 0, &ysize_div8_minus_1_)); + } + if (visitor->Conditional(!small_)) { + // (Could still be small, but non-multiple of 8.) + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(BitsOffset(9, 1), BitsOffset(13, 1), + BitsOffset(18, 1), BitsOffset(30, 1), + 1, &ysize_)); + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 0, &ratio_)); + if (visitor->Conditional(ratio_ == 0 && small_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(5, 0, &xsize_div8_minus_1_)); + } + if (visitor->Conditional(ratio_ == 0 && !small_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(BitsOffset(9, 1), BitsOffset(13, 1), + BitsOffset(18, 1), BitsOffset(30, 1), + 1, &xsize_)); + } + + return true; +} + +PreviewHeader::PreviewHeader() { Bundle::Init(this); } +Status PreviewHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &div8_)); + + if (visitor->Conditional(div8_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(16), Val(32), BitsOffset(5, 1), + BitsOffset(9, 33), 1, &ysize_div8_)); + } + if (visitor->Conditional(!div8_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(BitsOffset(6, 1), BitsOffset(8, 65), + BitsOffset(10, 321), + BitsOffset(12, 1345), 1, &ysize_)); + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 0, &ratio_)); + if (visitor->Conditional(ratio_ == 0 && div8_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(16), Val(32), BitsOffset(5, 1), + BitsOffset(9, 33), 1, &xsize_div8_)); + } + if (visitor->Conditional(ratio_ == 0 && !div8_)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(BitsOffset(6, 1), BitsOffset(8, 65), + BitsOffset(10, 321), + BitsOffset(12, 1345), 1, &xsize_)); + } + + return true; +} + +AnimationHeader::AnimationHeader() { Bundle::Init(this); } +Status AnimationHeader::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(100), Val(1000), BitsOffset(10, 1), + BitsOffset(30, 1), 1, &tps_numerator)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(1), Val(1001), BitsOffset(8, 1), + BitsOffset(10, 1), 1, + &tps_denominator)); + + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Bits(3), Bits(16), Bits(32), 0, &num_loops)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &have_timecodes)); + return true; +} + +Status ReadSizeHeader(BitReader* JXL_RESTRICT reader, + SizeHeader* JXL_RESTRICT size) { + return Bundle::Read(reader, size); +} + +Status WriteSizeHeader(const SizeHeader& size, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* aux_out) { + const size_t max_bits = Bundle::MaxBits(size); + if (max_bits != SizeHeader::kMaxBits) { + JXL_ABORT("Please update SizeHeader::kMaxBits from %zu to %zu\n", + SizeHeader::kMaxBits, max_bits); + } + + // Only check the number of non-extension bits (extensions are unbounded). + // (Bundle::Write will call CanEncode again, but it is fast because SizeHeader + // is tiny.) + size_t extension_bits, total_bits; + JXL_RETURN_IF_ERROR(Bundle::CanEncode(size, &extension_bits, &total_bits)); + JXL_ASSERT(total_bits - extension_bits < SizeHeader::kMaxBits); + + return Bundle::Write(size, writer, layer, aux_out); +} + +} // namespace jxl diff --git a/lib/jxl/headers.h b/lib/jxl/headers.h new file mode 100644 index 0000000..d33e2b5 --- /dev/null +++ b/lib/jxl/headers.h @@ -0,0 +1,106 @@ +// 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_HEADERS_H_ +#define LIB_JXL_HEADERS_H_ + +// Codestream headers, also stored in CodecInOut. + +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +// Reserved by ISO/IEC 10918-1. LF causes files opened in text mode to be +// rejected because the marker changes to 0x0D instead. The 0xFF prefix also +// ensures there were no 7-bit transmission limitations. +static constexpr uint8_t kCodestreamMarker = 0x0A; + +// Compact representation of image dimensions (best case: 9 bits) so decoders +// can preallocate early. +class SizeHeader : public Fields { + public: + // All fields are valid after reading at most this many bits. WriteSizeHeader + // verifies this matches Bundle::MaxBits(SizeHeader). + static constexpr size_t kMaxBits = 78; + + SizeHeader(); + const char* Name() const override { return "SizeHeader"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + Status Set(size_t xsize, size_t ysize); + + size_t xsize() const; + size_t ysize() const { + return small_ ? ((ysize_div8_minus_1_ + 1) * 8) : ysize_; + } + + private: + bool small_; // xsize and ysize <= 256 and divisible by 8. + + uint32_t ysize_div8_minus_1_; + uint32_t ysize_; + + uint32_t ratio_; + uint32_t xsize_div8_minus_1_; + uint32_t xsize_; +}; + +// (Similar to SizeHeader but different encoding because previews are smaller) +class PreviewHeader : public Fields { + public: + PreviewHeader(); + const char* Name() const override { return "PreviewHeader"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + Status Set(size_t xsize, size_t ysize); + + size_t xsize() const; + size_t ysize() const { return div8_ ? (ysize_div8_ * 8) : ysize_; } + + private: + bool div8_; // xsize and ysize divisible by 8. + + uint32_t ysize_div8_; + uint32_t ysize_; + + uint32_t ratio_; + uint32_t xsize_div8_; + uint32_t xsize_; +}; + +struct AnimationHeader : public Fields { + AnimationHeader(); + const char* Name() const override { return "AnimationHeader"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Ticks per second (expressed as rational number to support NTSC) + uint32_t tps_numerator; + uint32_t tps_denominator; + + uint32_t num_loops; // 0 means to repeat infinitely. + + bool have_timecodes; +}; + +Status ReadSizeHeader(BitReader* JXL_RESTRICT reader, + SizeHeader* JXL_RESTRICT size); + +Status WriteSizeHeader(const SizeHeader& size, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* aux_out); + +} // namespace jxl + +#endif // LIB_JXL_HEADERS_H_ diff --git a/lib/jxl/huffman_table.cc b/lib/jxl/huffman_table.cc new file mode 100644 index 0000000..9ae7865 --- /dev/null +++ b/lib/jxl/huffman_table.cc @@ -0,0 +1,161 @@ +// 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/huffman_table.h" + +#include /* for memcpy */ +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/dec_huffman.h" + +namespace jxl { + +/* Returns reverse(reverse(key, len) + 1, len), where reverse(key, len) is the + bit-wise reversal of the len least significant bits of key. */ +static inline int GetNextKey(int key, int len) { + int step = 1u << (len - 1); + while (key & step) { + step >>= 1; + } + return (key & (step - 1)) + step; +} + +/* Stores code in table[0], table[step], table[2*step], ..., table[end] */ +/* Assumes that end is an integer multiple of step */ +static inline void ReplicateValue(HuffmanCode* table, int step, int end, + HuffmanCode code) { + do { + end -= step; + table[end] = code; + } while (end > 0); +} + +/* Returns the table width of the next 2nd level table. count is the histogram + of bit lengths for the remaining symbols, len is the code length of the next + processed symbol */ +static inline size_t NextTableBitSize(const uint16_t* const count, size_t len, + int root_bits) { + size_t left = 1u << (len - root_bits); + while (len < PREFIX_MAX_BITS) { + if (left <= count[len]) break; + left -= count[len]; + ++len; + left <<= 1; + } + return len - root_bits; +} + +uint32_t BuildHuffmanTable(HuffmanCode* root_table, int root_bits, + const uint8_t* const code_lengths, + size_t code_lengths_size, uint16_t* count) { + HuffmanCode code; /* current table entry */ + HuffmanCode* table; /* next available space in table */ + size_t len; /* current code length */ + size_t symbol; /* symbol index in original or sorted table */ + int key; /* reversed prefix code */ + int step; /* step size to replicate values in current table */ + int low; /* low bits for current root entry */ + int mask; /* mask for low bits */ + size_t table_bits; /* key length of current table */ + int table_size; /* size of current table */ + int total_size; /* sum of root table size and 2nd level table sizes */ + /* offsets in sorted table for each length */ + uint16_t offset[PREFIX_MAX_BITS + 1]; + size_t max_length = 1; + + if (code_lengths_size > 1u << PREFIX_MAX_BITS) return 0; + + /* symbols sorted by code length */ + std::vector sorted_storage(code_lengths_size); + uint16_t* sorted = sorted_storage.data(); + + /* generate offsets into sorted symbol table by code length */ + { + uint16_t sum = 0; + for (len = 1; len <= PREFIX_MAX_BITS; len++) { + offset[len] = sum; + if (count[len]) { + sum = static_cast(sum + count[len]); + max_length = len; + } + } + } + + /* sort symbols by length, by symbol order within each length */ + for (symbol = 0; symbol < code_lengths_size; symbol++) { + if (code_lengths[symbol] != 0) { + sorted[offset[code_lengths[symbol]]++] = symbol; + } + } + + table = root_table; + table_bits = root_bits; + table_size = 1u << table_bits; + total_size = table_size; + + /* special case code with only one value */ + if (offset[PREFIX_MAX_BITS] == 1) { + code.bits = 0; + code.value = static_cast(sorted[0]); + for (key = 0; key < total_size; ++key) { + table[key] = code; + } + return total_size; + } + + /* fill in root table */ + /* let's reduce the table size to a smaller size if possible, and */ + /* create the repetitions by memcpy if possible in the coming loop */ + if (table_bits > max_length) { + table_bits = max_length; + table_size = 1u << table_bits; + } + key = 0; + symbol = 0; + code.bits = 1; + step = 2; + do { + for (; count[code.bits] != 0; --count[code.bits]) { + code.value = static_cast(sorted[symbol++]); + ReplicateValue(&table[key], step, table_size, code); + key = GetNextKey(key, code.bits); + } + step <<= 1; + } while (++code.bits <= table_bits); + + /* if root_bits != table_bits we only created one fraction of the */ + /* table, and we need to replicate it now. */ + while (total_size != table_size) { + memcpy(&table[table_size], &table[0], table_size * sizeof(table[0])); + table_size <<= 1; + } + + /* fill in 2nd level tables and add pointers to root table */ + mask = total_size - 1; + low = -1; + for (len = root_bits + 1, step = 2; len <= max_length; ++len, step <<= 1) { + for (; count[len] != 0; --count[len]) { + if ((key & mask) != low) { + table += table_size; + table_bits = NextTableBitSize(count, len, root_bits); + table_size = 1u << table_bits; + total_size += table_size; + low = key & mask; + root_table[low].bits = static_cast(table_bits + root_bits); + root_table[low].value = + static_cast((table - root_table) - low); + } + code.bits = static_cast(len - root_bits); + code.value = static_cast(sorted[symbol++]); + ReplicateValue(&table[key >> root_bits], step, table_size, code); + key = GetNextKey(key, len); + } + } + + return total_size; +} + +} // namespace jxl diff --git a/lib/jxl/huffman_table.h b/lib/jxl/huffman_table.h new file mode 100644 index 0000000..11cdb2f --- /dev/null +++ b/lib/jxl/huffman_table.h @@ -0,0 +1,28 @@ +// 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_HUFFMAN_TABLE_H_ +#define LIB_JXL_HUFFMAN_TABLE_H_ + +#include +#include + +namespace jxl { + +struct HuffmanCode { + uint8_t bits; /* number of bits used for this symbol */ + uint16_t value; /* symbol value or table offset */ +}; + +/* Builds Huffman lookup table assuming code lengths are in symbol order. */ +/* Returns 0 in case of error (invalid tree or memory error), otherwise + populated size of table. */ +uint32_t BuildHuffmanTable(HuffmanCode* root_table, int root_bits, + const uint8_t* code_lengths, + size_t code_lengths_size, uint16_t* count); + +} // namespace jxl + +#endif // LIB_JXL_HUFFMAN_TABLE_H_ diff --git a/lib/jxl/huffman_tree.cc b/lib/jxl/huffman_tree.cc new file mode 100644 index 0000000..77107b0 --- /dev/null +++ b/lib/jxl/huffman_tree.cc @@ -0,0 +1,328 @@ +// 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/huffman_tree.h" + +#include +#include +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { + +void SetDepth(const HuffmanTree& p, HuffmanTree* pool, uint8_t* depth, + uint8_t level) { + if (p.index_left >= 0) { + ++level; + SetDepth(pool[p.index_left], pool, depth, level); + SetDepth(pool[p.index_right_or_value], pool, depth, level); + } else { + depth[p.index_right_or_value] = level; + } +} + +// Sort the root nodes, least popular first. +static JXL_INLINE bool Compare(const HuffmanTree& v0, const HuffmanTree& v1) { + return v0.total_count < v1.total_count; +} + +// This function will create a Huffman tree. +// +// The catch here is that the tree cannot be arbitrarily deep. +// Brotli specifies a maximum depth of 15 bits for "code trees" +// and 7 bits for "code length code trees." +// +// count_limit is the value that is to be faked as the minimum value +// and this minimum value is raised until the tree matches the +// maximum length requirement. +// +// This algorithm is not of excellent performance for very long data blocks, +// especially when population counts are longer than 2**tree_limit, but +// we are not planning to use this with extremely long blocks. +// +// See http://en.wikipedia.org/wiki/Huffman_coding +void CreateHuffmanTree(const uint32_t* data, const size_t length, + const int tree_limit, uint8_t* depth) { + // For block sizes below 64 kB, we never need to do a second iteration + // of this loop. Probably all of our block sizes will be smaller than + // that, so this loop is mostly of academic interest. If we actually + // would need this, we would be better off with the Katajainen algorithm. + for (uint32_t count_limit = 1;; count_limit *= 2) { + std::vector tree; + tree.reserve(2 * length + 1); + + for (size_t i = length; i != 0;) { + --i; + if (data[i]) { + const uint32_t count = std::max(data[i], count_limit - 1); + tree.emplace_back(count, -1, static_cast(i)); + } + } + + const size_t n = tree.size(); + if (n == 1) { + // Fake value; will be fixed on upper level. + depth[tree[0].index_right_or_value] = 1; + break; + } + + std::stable_sort(tree.begin(), tree.end(), Compare); + + // The nodes are: + // [0, n): the sorted leaf nodes that we start with. + // [n]: we add a sentinel here. + // [n + 1, 2n): new parent nodes are added here, starting from + // (n+1). These are naturally in ascending order. + // [2n]: we add a sentinel at the end as well. + // There will be (2n+1) elements at the end. + const HuffmanTree sentinel(std::numeric_limits::max(), -1, -1); + tree.push_back(sentinel); + tree.push_back(sentinel); + + size_t i = 0; // Points to the next leaf node. + size_t j = n + 1; // Points to the next non-leaf node. + for (size_t k = n - 1; k != 0; --k) { + size_t left, right; + if (tree[i].total_count <= tree[j].total_count) { + left = i; + ++i; + } else { + left = j; + ++j; + } + if (tree[i].total_count <= tree[j].total_count) { + right = i; + ++i; + } else { + right = j; + ++j; + } + + // The sentinel node becomes the parent node. + size_t j_end = tree.size() - 1; + tree[j_end].total_count = + tree[left].total_count + tree[right].total_count; + tree[j_end].index_left = static_cast(left); + tree[j_end].index_right_or_value = static_cast(right); + + // Add back the last sentinel node. + tree.push_back(sentinel); + } + JXL_DASSERT(tree.size() == 2 * n + 1); + SetDepth(tree[2 * n - 1], &tree[0], depth, 0); + + // We need to pack the Huffman tree in tree_limit bits. + // If this was not successful, add fake entities to the lowest values + // and retry. + if (*std::max_element(&depth[0], &depth[length]) <= tree_limit) { + break; + } + } +} + +void Reverse(uint8_t* v, size_t start, size_t end) { + --end; + while (start < end) { + uint8_t tmp = v[start]; + v[start] = v[end]; + v[end] = tmp; + ++start; + --end; + } +} + +void WriteHuffmanTreeRepetitions(const uint8_t previous_value, + const uint8_t value, size_t repetitions, + size_t* tree_size, uint8_t* tree, + uint8_t* extra_bits_data) { + JXL_DASSERT(repetitions > 0); + if (previous_value != value) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions == 7) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions < 3) { + for (size_t i = 0; i < repetitions; ++i) { + tree[*tree_size] = value; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + } + } else { + repetitions -= 3; + size_t start = *tree_size; + while (true) { + tree[*tree_size] = 16; + extra_bits_data[*tree_size] = repetitions & 0x3; + ++(*tree_size); + repetitions >>= 2; + if (repetitions == 0) { + break; + } + --repetitions; + } + Reverse(tree, start, *tree_size); + Reverse(extra_bits_data, start, *tree_size); + } +} + +void WriteHuffmanTreeRepetitionsZeros(size_t repetitions, size_t* tree_size, + uint8_t* tree, uint8_t* extra_bits_data) { + if (repetitions == 11) { + tree[*tree_size] = 0; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + --repetitions; + } + if (repetitions < 3) { + for (size_t i = 0; i < repetitions; ++i) { + tree[*tree_size] = 0; + extra_bits_data[*tree_size] = 0; + ++(*tree_size); + } + } else { + repetitions -= 3; + size_t start = *tree_size; + while (true) { + tree[*tree_size] = 17; + extra_bits_data[*tree_size] = repetitions & 0x7; + ++(*tree_size); + repetitions >>= 3; + if (repetitions == 0) { + break; + } + --repetitions; + } + Reverse(tree, start, *tree_size); + Reverse(extra_bits_data, start, *tree_size); + } +} + +static void DecideOverRleUse(const uint8_t* depth, const size_t length, + bool* use_rle_for_non_zero, + bool* use_rle_for_zero) { + size_t total_reps_zero = 0; + size_t total_reps_non_zero = 0; + size_t count_reps_zero = 1; + size_t count_reps_non_zero = 1; + for (size_t i = 0; i < length;) { + const uint8_t value = depth[i]; + size_t reps = 1; + for (size_t k = i + 1; k < length && depth[k] == value; ++k) { + ++reps; + } + if (reps >= 3 && value == 0) { + total_reps_zero += reps; + ++count_reps_zero; + } + if (reps >= 4 && value != 0) { + total_reps_non_zero += reps; + ++count_reps_non_zero; + } + i += reps; + } + *use_rle_for_non_zero = total_reps_non_zero > count_reps_non_zero * 2; + *use_rle_for_zero = total_reps_zero > count_reps_zero * 2; +} + +void WriteHuffmanTree(const uint8_t* depth, size_t length, size_t* tree_size, + uint8_t* tree, uint8_t* extra_bits_data) { + uint8_t previous_value = 8; + + // Throw away trailing zeros. + size_t new_length = length; + for (size_t i = 0; i < length; ++i) { + if (depth[length - i - 1] == 0) { + --new_length; + } else { + break; + } + } + + // First gather statistics on if it is a good idea to do rle. + bool use_rle_for_non_zero = false; + bool use_rle_for_zero = false; + if (length > 50) { + // Find rle coding for longer codes. + // Shorter codes seem not to benefit from rle. + DecideOverRleUse(depth, new_length, &use_rle_for_non_zero, + &use_rle_for_zero); + } + + // Actual rle coding. + for (size_t i = 0; i < new_length;) { + const uint8_t value = depth[i]; + size_t reps = 1; + if ((value != 0 && use_rle_for_non_zero) || + (value == 0 && use_rle_for_zero)) { + for (size_t k = i + 1; k < new_length && depth[k] == value; ++k) { + ++reps; + } + } + if (value == 0) { + WriteHuffmanTreeRepetitionsZeros(reps, tree_size, tree, extra_bits_data); + } else { + WriteHuffmanTreeRepetitions(previous_value, value, reps, tree_size, tree, + extra_bits_data); + previous_value = value; + } + i += reps; + } +} + +namespace { + +uint16_t ReverseBits(int num_bits, uint16_t bits) { + static const size_t kLut[16] = {// Pre-reversed 4-bit values. + 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, + 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf}; + size_t retval = kLut[bits & 0xf]; + for (int i = 4; i < num_bits; i += 4) { + retval <<= 4; + bits = static_cast(bits >> 4); + retval |= kLut[bits & 0xf]; + } + retval >>= (-num_bits & 0x3); + return static_cast(retval); +} + +} // namespace + +void ConvertBitDepthsToSymbols(const uint8_t* depth, size_t len, + uint16_t* bits) { + // In Brotli, all bit depths are [1..15] + // 0 bit depth means that the symbol does not exist. + const int kMaxBits = 16; // 0..15 are values for bits + uint16_t bl_count[kMaxBits] = {0}; + { + for (size_t i = 0; i < len; ++i) { + ++bl_count[depth[i]]; + } + bl_count[0] = 0; + } + uint16_t next_code[kMaxBits]; + next_code[0] = 0; + { + int code = 0; + for (size_t i = 1; i < kMaxBits; ++i) { + code = (code + bl_count[i - 1]) << 1; + next_code[i] = static_cast(code); + } + } + for (size_t i = 0; i < len; ++i) { + if (depth[i]) { + bits[i] = ReverseBits(depth[i], next_code[depth[i]]++); + } + } +} + +} // namespace jxl diff --git a/lib/jxl/huffman_tree.h b/lib/jxl/huffman_tree.h new file mode 100644 index 0000000..e4ccac4 --- /dev/null +++ b/lib/jxl/huffman_tree.h @@ -0,0 +1,52 @@ +// 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. + +// Library for creating Huffman codes from population counts. + +#ifndef LIB_JXL_HUFFMAN_TREE_H_ +#define LIB_JXL_HUFFMAN_TREE_H_ + +#include +#include + +namespace jxl { + +// A node of a Huffman tree. +struct HuffmanTree { + HuffmanTree(uint32_t count, int16_t left, int16_t right) + : total_count(count), index_left(left), index_right_or_value(right) {} + uint32_t total_count; + int16_t index_left; + int16_t index_right_or_value; +}; + +void SetDepth(const HuffmanTree& p, HuffmanTree* pool, uint8_t* depth, + uint8_t level); + +// This function will create a Huffman tree. +// +// The (data,length) contains the population counts. +// The tree_limit is the maximum bit depth of the Huffman codes. +// +// The depth contains the tree, i.e., how many bits are used for +// the symbol. +// +// See http://en.wikipedia.org/wiki/Huffman_coding +void CreateHuffmanTree(const uint32_t* data, const size_t length, + const int tree_limit, uint8_t* depth); + +// Write a Huffman tree from bit depths into the bitstream representation +// of a Huffman tree. The generated Huffman tree is to be compressed once +// more using a Huffman tree +void WriteHuffmanTree(const uint8_t* depth, size_t length, size_t* tree_size, + uint8_t* tree, uint8_t* extra_bits_data); + +// Get the actual bit values for a tree of bit depths. +void ConvertBitDepthsToSymbols(const uint8_t* depth, size_t len, + uint16_t* bits); + +} // namespace jxl + +#endif // LIB_JXL_HUFFMAN_TREE_H_ diff --git a/lib/jxl/iaca_test.cc b/lib/jxl/iaca_test.cc new file mode 100644 index 0000000..9b2e8ea --- /dev/null +++ b/lib/jxl/iaca_test.cc @@ -0,0 +1,21 @@ +// 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/iaca.h" + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(IacaTest, MarkersDefaultToDisabledAndDoNotCrash) { + BeginIACA(); + EndIACA(); +} + +TEST(IacaTest, ScopeDefaultToDisabledAndDoNotCrash) { ScopeIACA iaca; } + +} // namespace +} // namespace jxl diff --git a/lib/jxl/icc_codec.cc b/lib/jxl/icc_codec.cc new file mode 100644 index 0000000..619c814 --- /dev/null +++ b/lib/jxl/icc_codec.cc @@ -0,0 +1,404 @@ +// 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/icc_codec.h" + +#include + +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/icc_codec_common.h" + +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 +// last elements of the rightmost column are missing, the missing spots are +// transposed along with the filled spots, and the result has the missing +// elements at the end of the bottom row. The input is the input matrix in +// scanline order but with missing elements skipped (which may occur in multiple +// locations), the output is the result matrix in scanline order (with +// no need to skip missing elements as they are past the end of the data). +void Shuffle(uint8_t* data, size_t size, size_t width) { + size_t height = (size + width - 1) / width; // amount of rows of output + PaddedBytes result(size); + // i = output index, j input index + size_t s = 0, j = 0; + for (size_t i = 0; i < size; i++) { + result[i] = data[j]; + j += height; + if (j >= size) j = ++s; + } + + for (size_t i = 0; i < size; i++) { + data[i] = result[i]; + } +} + +// TODO(eustas): should be 20, or even 18, once DecodeVarInt is improved; +// currently DecodeVarInt does not signal the errors, and marks +// 11 bytes as used even if only 10 are used (and 9 is enough for +// 63-bit values). +constexpr const size_t kPreambleSize = 22; // enough for reading 2 VarInts + +} // namespace + +// Mimics the beginning of UnpredictICC for quick validity check. +// At least kPreambleSize bytes of data should be valid at invocation time. +Status CheckPreamble(const PaddedBytes& data, size_t enc_size, + size_t output_limit) { + const uint8_t* enc = data.data(); + size_t size = data.size(); + size_t pos = 0; + uint64_t osize = DecodeVarInt(enc, size, &pos); + JXL_RETURN_IF_ERROR(CheckIs32Bit(osize)); + if (pos >= size) return JXL_FAILURE("Out of bounds"); + uint64_t csize = DecodeVarInt(enc, size, &pos); + JXL_RETURN_IF_ERROR(CheckIs32Bit(csize)); + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, csize, size)); + // We expect that UnpredictICC inflates input, not the other way round. + if (osize + 65536 < enc_size) return JXL_FAILURE("Malformed ICC"); + if (output_limit && osize > output_limit) { + return JXL_FAILURE("Decoded ICC is too large"); + } + return true; +} + +// Decodes the result of PredictICC back to a valid ICC profile. +Status UnpredictICC(const uint8_t* enc, size_t size, PaddedBytes* result) { + if (!result->empty()) return JXL_FAILURE("result must be empty initially"); + size_t pos = 0; + // TODO(lode): technically speaking we need to check that the entire varint + // decoding never goes out of bounds, not just the first byte. This requires + // a DecodeVarInt function that returns an error code. It is safe to use + // DecodeVarInt with out of bounds values, it silently returns, but the + // specification requires an error. Idem for all DecodeVarInt below. + if (pos >= size) return JXL_FAILURE("Out of bounds"); + uint64_t osize = DecodeVarInt(enc, size, &pos); // Output size + JXL_RETURN_IF_ERROR(CheckIs32Bit(osize)); + if (pos >= size) return JXL_FAILURE("Out of bounds"); + uint64_t csize = DecodeVarInt(enc, size, &pos); // Commands size + // Every command is translated to at least on byte. + JXL_RETURN_IF_ERROR(CheckIs32Bit(csize)); + size_t cpos = pos; // pos in commands stream + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, csize, size)); + size_t commands_end = cpos + csize; + pos = commands_end; // pos in data stream + + // Header + PaddedBytes header = ICCInitialHeaderPrediction(); + EncodeUint32(0, osize, &header); + for (size_t i = 0; i <= kICCHeaderSize; i++) { + if (result->size() == osize) { + if (cpos != commands_end) return JXL_FAILURE("Not all commands used"); + if (pos != size) return JXL_FAILURE("Not all data used"); + return true; // Valid end + } + if (i == kICCHeaderSize) break; // Done + ICCPredictHeader(result->data(), result->size(), header.data(), i); + if (pos >= size) return JXL_FAILURE("Out of bounds"); + result->push_back(enc[pos++] + header[i]); + } + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + + // Tag list + uint64_t numtags = DecodeVarInt(enc, size, &cpos); + + if (numtags != 0) { + numtags--; + JXL_RETURN_IF_ERROR(CheckIs32Bit(numtags)); + AppendUint32(numtags, result); + uint64_t prevtagstart = kICCHeaderSize + numtags * 12; + uint64_t prevtagsize = 0; + for (;;) { + if (result->size() > osize) return JXL_FAILURE("Invalid result size"); + if (cpos > commands_end) return JXL_FAILURE("Out of bounds"); + if (cpos == commands_end) break; // Valid end + uint8_t command = enc[cpos++]; + uint8_t tagcode = command & 63; + Tag tag; + if (tagcode == 0) { + break; + } else if (tagcode == kCommandTagUnknown) { + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, 4, size)); + tag = DecodeKeyword(enc, size, pos); + pos += 4; + } else if (tagcode == kCommandTagTRC) { + tag = kRtrcTag; + } else if (tagcode == kCommandTagXYZ) { + tag = kRxyzTag; + } else { + if (tagcode - kCommandTagStringFirst >= kNumTagStrings) { + return JXL_FAILURE("Unknown tagcode"); + } + tag = *kTagStrings[tagcode - kCommandTagStringFirst]; + } + AppendKeyword(tag, result); + + uint64_t tagstart; + uint64_t tagsize = prevtagsize; + if (tag == kRxyzTag || tag == kGxyzTag || tag == kBxyzTag || + tag == kKxyzTag || tag == kWtptTag || tag == kBkptTag || + tag == kLumiTag) { + tagsize = 20; + } + + if (command & kFlagBitOffset) { + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + tagstart = DecodeVarInt(enc, size, &cpos); + } else { + JXL_RETURN_IF_ERROR(CheckIs32Bit(prevtagstart)); + tagstart = prevtagstart + prevtagsize; + } + JXL_RETURN_IF_ERROR(CheckIs32Bit(tagstart)); + AppendUint32(tagstart, result); + if (command & kFlagBitSize) { + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + tagsize = DecodeVarInt(enc, size, &cpos); + } + JXL_RETURN_IF_ERROR(CheckIs32Bit(tagsize)); + AppendUint32(tagsize, result); + prevtagstart = tagstart; + prevtagsize = tagsize; + + if (tagcode == kCommandTagTRC) { + AppendKeyword(kGtrcTag, result); + AppendUint32(tagstart, result); + AppendUint32(tagsize, result); + AppendKeyword(kBtrcTag, result); + AppendUint32(tagstart, result); + AppendUint32(tagsize, result); + } + + if (tagcode == kCommandTagXYZ) { + JXL_RETURN_IF_ERROR(CheckIs32Bit(tagstart + tagsize * 2)); + AppendKeyword(kGxyzTag, result); + AppendUint32(tagstart + tagsize, result); + AppendUint32(tagsize, result); + AppendKeyword(kBxyzTag, result); + AppendUint32(tagstart + tagsize * 2, result); + AppendUint32(tagsize, result); + } + } + } + + // Main Content + for (;;) { + if (result->size() > osize) return JXL_FAILURE("Invalid result size"); + if (cpos > commands_end) return JXL_FAILURE("Out of bounds"); + if (cpos == commands_end) break; // Valid end + uint8_t command = enc[cpos++]; + if (command == kCommandInsert) { + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + uint64_t num = DecodeVarInt(enc, size, &cpos); + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, num, size)); + for (size_t i = 0; i < num; i++) { + result->push_back(enc[pos++]); + } + } else if (command == kCommandShuffle2 || command == kCommandShuffle4) { + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + uint64_t num = DecodeVarInt(enc, size, &cpos); + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, num, size)); + PaddedBytes shuffled(num); + for (size_t i = 0; i < num; i++) { + shuffled[i] = enc[pos + i]; + } + if (command == kCommandShuffle2) { + Shuffle(shuffled.data(), num, 2); + } else if (command == kCommandShuffle4) { + Shuffle(shuffled.data(), num, 4); + } + for (size_t i = 0; i < num; i++) { + result->push_back(shuffled[i]); + pos++; + } + } else if (command == kCommandPredict) { + JXL_RETURN_IF_ERROR(CheckOutOfBounds(cpos, 2, commands_end)); + uint8_t flags = enc[cpos++]; + + size_t width = (flags & 3) + 1; + if (width == 3) return JXL_FAILURE("Invalid width"); + + int order = (flags & 12) >> 2; + if (order == 3) return JXL_FAILURE("Invalid order"); + + uint64_t stride = width; + if (flags & 16) { + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + stride = DecodeVarInt(enc, size, &cpos); + if (stride < width) { + return JXL_FAILURE("Invalid stride"); + } + } + // If stride * 4 >= result->size(), return failure. The check + // "size == 0 || ((size - 1) >> 2) < stride" corresponds to + // "stride * 4 >= size", but does not suffer from integer overflow. + // This check is more strict than necessary but follows the specification + // and the encoder should ensure this is followed. + if (result->empty() || ((result->size() - 1u) >> 2u) < stride) { + return JXL_FAILURE("Invalid stride"); + } + + if (cpos >= commands_end) return JXL_FAILURE("Out of bounds"); + uint64_t num = DecodeVarInt(enc, size, &cpos); // in bytes + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, num, size)); + + PaddedBytes shuffled(num); + for (size_t i = 0; i < num; i++) { + shuffled[i] = enc[pos + i]; + } + if (width > 1) Shuffle(shuffled.data(), num, width); + + size_t start = result->size(); + for (size_t i = 0; i < num; i++) { + uint8_t predicted = LinearPredictICCValue(result->data(), start, i, + stride, width, order); + result->push_back(predicted + shuffled[i]); + } + pos += num; + } else if (command == kCommandXYZ) { + AppendKeyword(kXyz_Tag, result); + for (int i = 0; i < 4; i++) result->push_back(0); + JXL_RETURN_IF_ERROR(CheckOutOfBounds(pos, 12, size)); + for (size_t i = 0; i < 12; i++) { + result->push_back(enc[pos++]); + } + } else if (command >= kCommandTypeStartFirst && + command < kCommandTypeStartFirst + kNumTypeStrings) { + AppendKeyword(*kTypeStrings[command - kCommandTypeStartFirst], result); + for (size_t i = 0; i < 4; i++) { + result->push_back(0); + } + } else { + return JXL_FAILURE("Unknown command"); + } + } + + if (pos != size) return JXL_FAILURE("Not all data used"); + if (result->size() != osize) return JXL_FAILURE("Invalid result size"); + + return true; +} + +Status ICCReader::Init(BitReader* reader, size_t output_limit) { + JXL_RETURN_IF_ERROR(CheckEOI(reader)); + used_bits_base_ = reader->TotalBitsConsumed(); + if (bits_to_skip_ == 0) { + enc_size_ = U64Coder::Read(reader); + if (enc_size_ > 268435456) { + // Avoid too large memory allocation for invalid file. + return JXL_FAILURE("Too large encoded profile"); + } + JXL_RETURN_IF_ERROR( + DecodeHistograms(reader, kNumICCContexts, &code_, &context_map_)); + ans_reader_ = ANSSymbolReader(&code_, reader); + i_ = 0; + decompressed_.resize(std::min(i_ + 0x400, enc_size_)); + for (; i_ < std::min(2, enc_size_); i_++) { + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, i_ > 0 ? decompressed_[i_ - 1] : 0, + i_ > 1 ? decompressed_[i_ - 2] : 0), + reader, context_map_); + } + if (enc_size_ > kPreambleSize) { + for (; i_ < kPreambleSize; i_++) { + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, decompressed_[i_ - 1], decompressed_[i_ - 2]), + reader, context_map_); + } + JXL_RETURN_IF_ERROR(CheckEOI(reader)); + JXL_RETURN_IF_ERROR( + CheckPreamble(decompressed_, enc_size_, output_limit)); + } + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + } else { + reader->SkipBits(bits_to_skip_); + } + return true; +} + +Status ICCReader::Process(BitReader* reader, PaddedBytes* icc) { + ANSSymbolReader::Checkpoint checkpoint; + size_t saved_i = 0; + auto save = [&]() { + ans_reader_.Save(&checkpoint); + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + saved_i = i_; + }; + save(); + auto check_and_restore = [&]() { + Status status = CheckEOI(reader); + if (!status) { + // not enough bytes. + ans_reader_.Restore(checkpoint); + i_ = saved_i; + return status; + } + return Status(true); + }; + for (; i_ < enc_size_; i_++) { + if (i_ % ANSSymbolReader::kMaxCheckpointInterval == 0 && i_ > 0) { + JXL_RETURN_IF_ERROR(check_and_restore()); + save(); + if ((i_ > 0) && (((i_ & 0xFFFF) == 0))) { + float used_bytes = + (reader->TotalBitsConsumed() - used_bits_base_) / 8.0f; + if (i_ > used_bytes * 256) return JXL_FAILURE("Corrupted stream"); + } + decompressed_.resize(std::min(i_ + 0x400, enc_size_)); + } + JXL_DASSERT(i_ >= 2); + decompressed_[i_] = ans_reader_.ReadHybridUint( + ICCANSContext(i_, decompressed_[i_ - 1], decompressed_[i_ - 2]), reader, + context_map_); + } + JXL_RETURN_IF_ERROR(check_and_restore()); + bits_to_skip_ = reader->TotalBitsConsumed() - used_bits_base_; + if (!ans_reader_.CheckANSFinalState()) { + return JXL_FAILURE("Corrupted ICC profile"); + } + + icc->clear(); + return UnpredictICC(decompressed_.data(), decompressed_.size(), icc); +} + +Status ICCReader::CheckEOI(BitReader* reader) { + if (reader->AllReadsWithinBounds()) return true; + return JXL_STATUS(StatusCode::kNotEnoughBytes, + "Not enough bytes for reading ICC profile"); +} + +Status ReadICC(BitReader* JXL_RESTRICT reader, PaddedBytes* JXL_RESTRICT icc, + size_t output_limit) { + ICCReader icc_reader; + JXL_RETURN_IF_ERROR(icc_reader.Init(reader, output_limit)); + JXL_RETURN_IF_ERROR(icc_reader.Process(reader, icc)); + return true; +} + +} // namespace jxl diff --git a/lib/jxl/icc_codec.h b/lib/jxl/icc_codec.h new file mode 100644 index 0000000..d55b316 --- /dev/null +++ b/lib/jxl/icc_codec.h @@ -0,0 +1,64 @@ +// 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_ICC_CODEC_H_ +#define LIB_JXL_ICC_CODEC_H_ + +// Compressed representation of ICC profiles. + +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" + +namespace jxl { + +// Should still be called if `icc.empty()` - if so, writes only 1 bit. +Status WriteICC(const PaddedBytes& icc, BitWriter* JXL_RESTRICT writer, + size_t layer, AuxOut* JXL_RESTRICT aux_out); + +struct ICCReader { + Status Init(BitReader* reader, size_t output_limit); + Status Process(BitReader* reader, PaddedBytes* icc); + void Reset() { + bits_to_skip_ = 0; + decompressed_.clear(); + } + + private: + Status CheckEOI(BitReader* reader); + size_t i_ = 0; + size_t bits_to_skip_ = 0; + size_t used_bits_base_ = 0; + uint64_t enc_size_ = 0; + std::vector context_map_; + ANSCode code_; + ANSSymbolReader ans_reader_; + PaddedBytes decompressed_; +}; + +// `icc` may be empty afterwards - if so, call CreateProfile. Does not append, +// clears any original data that was in icc. +// If `output_limit` is not 0, then returns error if resulting profile would be +// longer than `output_limit` +Status ReadICC(BitReader* JXL_RESTRICT reader, PaddedBytes* JXL_RESTRICT icc, + size_t output_limit = 0); + +// Exposed only for testing +Status PredictICC(const uint8_t* icc, size_t size, PaddedBytes* result); + +// Exposed only for testing +Status UnpredictICC(const uint8_t* enc, size_t size, PaddedBytes* result); + +} // namespace jxl + +#endif // LIB_JXL_ICC_CODEC_H_ diff --git a/lib/jxl/icc_codec_common.cc b/lib/jxl/icc_codec_common.cc new file mode 100644 index 0000000..3e60048 --- /dev/null +++ b/lib/jxl/icc_codec_common.cc @@ -0,0 +1,192 @@ +// 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/icc_codec_common.h" + +#include + +#include +#include +#include + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/common.h" +#include "lib/jxl/fields.h" + +namespace jxl { +namespace { +static uint8_t ByteKind1(uint8_t b) { + if ('a' <= b && b <= 'z') return 0; + if ('A' <= b && b <= 'Z') return 0; + if ('0' <= b && b <= '9') return 1; + if (b == '.' || b == ',') return 1; + if (b == 0) return 2; + if (b == 1) return 3; + if (b < 16) return 4; + if (b == 255) return 6; + if (b > 240) return 5; + return 7; +} + +static uint8_t ByteKind2(uint8_t b) { + if ('a' <= b && b <= 'z') return 0; + if ('A' <= b && b <= 'Z') return 0; + if ('0' <= b && b <= '9') return 1; + if (b == '.' || b == ',') return 1; + if (b < 16) return 2; + if (b > 240) return 3; + return 4; +} + +template +T PredictValue(T p1, T p2, T p3, int order) { + if (order == 0) return p1; + if (order == 1) return 2 * p1 - p2; + if (order == 2) return 3 * p1 - 3 * p2 + p3; + return 0; +} +} // namespace + +uint32_t DecodeUint32(const uint8_t* data, size_t size, size_t pos) { + return pos + 4 > size ? 0 : LoadBE32(data + pos); +} + +void EncodeUint32(size_t pos, uint32_t value, PaddedBytes* data) { + if (pos + 4 > data->size()) return; + StoreBE32(value, data->data() + pos); +} + +void AppendUint32(uint32_t value, PaddedBytes* data) { + data->resize(data->size() + 4); + EncodeUint32(data->size() - 4, value, data); +} + +typedef std::array Tag; + +Tag DecodeKeyword(const uint8_t* data, size_t size, size_t pos) { + if (pos + 4 > size) return {{' ', ' ', ' ', ' '}}; + return {{data[pos], data[pos + 1], data[pos + 2], data[pos + 3]}}; +} + +void EncodeKeyword(const Tag& keyword, uint8_t* data, size_t size, size_t pos) { + if (keyword.size() != 4 || pos + 3 >= size) return; + for (size_t i = 0; i < 4; ++i) data[pos + i] = keyword[i]; +} + +void AppendKeyword(const Tag& keyword, PaddedBytes* data) { + JXL_ASSERT(keyword.size() == 4); + data->append(keyword); +} + +// Checks if a + b > size, taking possible integer overflow into account. +Status CheckOutOfBounds(size_t a, size_t b, size_t size) { + size_t pos = a + b; + if (pos > size) return JXL_FAILURE("Out of bounds"); + if (pos < a) return JXL_FAILURE("Out of bounds"); // overflow happened + return true; +} + +Status CheckIs32Bit(uint64_t v) { + static constexpr const uint64_t kUpper32 = ~static_cast(0xFFFFFFFF); + if ((v & kUpper32) != 0) return JXL_FAILURE("32-bit value expected"); + return true; +} + +PaddedBytes ICCInitialHeaderPrediction() { + PaddedBytes result(kICCHeaderSize); + for (size_t i = 0; i < kICCHeaderSize; i++) { + result[i] = 0; + } + result[8] = 4; + EncodeKeyword(kMntrTag, result.data(), result.size(), 12); + EncodeKeyword(kRgb_Tag, result.data(), result.size(), 16); + EncodeKeyword(kXyz_Tag, result.data(), result.size(), 20); + EncodeKeyword(kAcspTag, result.data(), result.size(), 36); + result[68] = 0; + result[69] = 0; + result[70] = 246; + result[71] = 214; + result[72] = 0; + result[73] = 1; + result[74] = 0; + result[75] = 0; + result[76] = 0; + result[77] = 0; + result[78] = 211; + result[79] = 45; + return result; +} + +void ICCPredictHeader(const uint8_t* icc, size_t size, uint8_t* header, + size_t pos) { + if (pos == 8 && size >= 8) { + header[80] = icc[4]; + header[81] = icc[5]; + header[82] = icc[6]; + header[83] = icc[7]; + } + if (pos == 41 && size >= 41) { + if (icc[40] == 'A') { + header[41] = 'P'; + header[42] = 'P'; + header[43] = 'L'; + } + if (icc[40] == 'M') { + header[41] = 'S'; + header[42] = 'F'; + header[43] = 'T'; + } + } + if (pos == 42 && size >= 42) { + if (icc[40] == 'S' && icc[41] == 'G') { + header[42] = 'I'; + header[43] = ' '; + } + if (icc[40] == 'S' && icc[41] == 'U') { + header[42] = 'N'; + header[43] = 'W'; + } + } +} + +// Predicts a value with linear prediction of given order (0-2), for integers +// with width bytes and given stride in bytes between values. +// The start position is at start + i, and the relevant modulus of i describes +// which byte of the multi-byte integer is being handled. +// The value start + i must be at least stride * 4. +uint8_t LinearPredictICCValue(const uint8_t* data, size_t start, size_t i, + size_t stride, size_t width, int order) { + size_t pos = start + i; + if (width == 1) { + uint8_t p1 = data[pos - stride]; + uint8_t p2 = data[pos - stride * 2]; + uint8_t p3 = data[pos - stride * 3]; + return PredictValue(p1, p2, p3, order); + } else if (width == 2) { + size_t p = start + (i & ~1); + uint16_t p1 = (data[p - stride * 1] << 8) + data[p - stride * 1 + 1]; + uint16_t p2 = (data[p - stride * 2] << 8) + data[p - stride * 2 + 1]; + uint16_t p3 = (data[p - stride * 3] << 8) + data[p - stride * 3 + 1]; + uint16_t pred = PredictValue(p1, p2, p3, order); + return (i & 1) ? (pred & 255) : ((pred >> 8) & 255); + } else { + size_t p = start + (i & ~3); + uint32_t p1 = DecodeUint32(data, pos, p - stride); + uint32_t p2 = DecodeUint32(data, pos, p - stride * 2); + uint32_t p3 = DecodeUint32(data, pos, p - stride * 3); + uint32_t pred = PredictValue(p1, p2, p3, order); + unsigned shiftbytes = 3 - (i & 3); + return (pred >> (shiftbytes * 8)) & 255; + } +} + +size_t ICCANSContext(size_t i, size_t b1, size_t b2) { + if (i <= 128) return 0; + return 1 + ByteKind1(b1) + ByteKind2(b2) * 8; +} + +} // namespace jxl diff --git a/lib/jxl/icc_codec_common.h b/lib/jxl/icc_codec_common.h new file mode 100644 index 0000000..e91e908 --- /dev/null +++ b/lib/jxl/icc_codec_common.h @@ -0,0 +1,106 @@ +// 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_ICC_CODEC_COMMON_H_ +#define LIB_JXL_ICC_CODEC_COMMON_H_ + +// Compressed representation of ICC profiles. + +#include +#include + +#include + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +static constexpr size_t kICCHeaderSize = 128; + +typedef std::array Tag; + +static const Tag kAcspTag = {{'a', 'c', 's', 'p'}}; +static const Tag kBkptTag = {{'b', 'k', 'p', 't'}}; +static const Tag kBtrcTag = {{'b', 'T', 'R', 'C'}}; +static const Tag kBxyzTag = {{'b', 'X', 'Y', 'Z'}}; +static const Tag kChadTag = {{'c', 'h', 'a', 'd'}}; +static const Tag kChrmTag = {{'c', 'h', 'r', 'm'}}; +static const Tag kCprtTag = {{'c', 'p', 'r', 't'}}; +static const Tag kCurvTag = {{'c', 'u', 'r', 'v'}}; +static const Tag kDescTag = {{'d', 'e', 's', 'c'}}; +static const Tag kDmddTag = {{'d', 'm', 'd', 'd'}}; +static const Tag kDmndTag = {{'d', 'm', 'n', 'd'}}; +static const Tag kGbd_Tag = {{'g', 'b', 'd', ' '}}; +static const Tag kGtrcTag = {{'g', 'T', 'R', 'C'}}; +static const Tag kGxyzTag = {{'g', 'X', 'Y', 'Z'}}; +static const Tag kKtrcTag = {{'k', 'T', 'R', 'C'}}; +static const Tag kKxyzTag = {{'k', 'X', 'Y', 'Z'}}; +static const Tag kLumiTag = {{'l', 'u', 'm', 'i'}}; +static const Tag kMab_Tag = {{'m', 'A', 'B', ' '}}; +static const Tag kMba_Tag = {{'m', 'B', 'A', ' '}}; +static const Tag kMlucTag = {{'m', 'l', 'u', 'c'}}; +static const Tag kMntrTag = {{'m', 'n', 't', 'r'}}; +static const Tag kParaTag = {{'p', 'a', 'r', 'a'}}; +static const Tag kRgb_Tag = {{'R', 'G', 'B', ' '}}; +static const Tag kRtrcTag = {{'r', 'T', 'R', 'C'}}; +static const Tag kRxyzTag = {{'r', 'X', 'Y', 'Z'}}; +static const Tag kSf32Tag = {{'s', 'f', '3', '2'}}; +static const Tag kTextTag = {{'t', 'e', 'x', 't'}}; +static const Tag kVcgtTag = {{'v', 'c', 'g', 't'}}; +static const Tag kWtptTag = {{'w', 't', 'p', 't'}}; +static const Tag kXyz_Tag = {{'X', 'Y', 'Z', ' '}}; + +// Tag names focused on RGB and GRAY monitor profiles +static constexpr size_t kNumTagStrings = 17; +static constexpr const Tag* kTagStrings[kNumTagStrings] = { + &kCprtTag, &kWtptTag, &kBkptTag, &kRxyzTag, &kGxyzTag, &kBxyzTag, + &kKxyzTag, &kRtrcTag, &kGtrcTag, &kBtrcTag, &kKtrcTag, &kChadTag, + &kDescTag, &kChrmTag, &kDmndTag, &kDmddTag, &kLumiTag}; + +static constexpr size_t kCommandTagUnknown = 1; +static constexpr size_t kCommandTagTRC = 2; +static constexpr size_t kCommandTagXYZ = 3; +static constexpr size_t kCommandTagStringFirst = 4; + +// Tag types focused on RGB and GRAY monitor profiles +static constexpr size_t kNumTypeStrings = 8; +static constexpr const Tag* kTypeStrings[kNumTypeStrings] = { + &kXyz_Tag, &kDescTag, &kTextTag, &kMlucTag, + &kParaTag, &kCurvTag, &kSf32Tag, &kGbd_Tag}; + +static constexpr size_t kCommandInsert = 1; +static constexpr size_t kCommandShuffle2 = 2; +static constexpr size_t kCommandShuffle4 = 3; +static constexpr size_t kCommandPredict = 4; +static constexpr size_t kCommandXYZ = 10; +static constexpr size_t kCommandTypeStartFirst = 16; + +static constexpr size_t kFlagBitOffset = 64; +static constexpr size_t kFlagBitSize = 128; + +static constexpr size_t kNumICCContexts = 41; + +uint32_t DecodeUint32(const uint8_t* data, size_t size, size_t pos); +void EncodeUint32(size_t pos, uint32_t value, PaddedBytes* data); +void AppendUint32(uint32_t value, PaddedBytes* data); +Tag DecodeKeyword(const uint8_t* data, size_t size, size_t pos); +void EncodeKeyword(const Tag& keyword, uint8_t* data, size_t size, size_t pos); +void AppendKeyword(const Tag& keyword, PaddedBytes* data); + +// Checks if a + b > size, taking possible integer overflow into account. +Status CheckOutOfBounds(size_t a, size_t b, size_t size); +Status CheckIs32Bit(uint64_t v); + +PaddedBytes ICCInitialHeaderPrediction(); +void ICCPredictHeader(const uint8_t* icc, size_t size, uint8_t* header, + size_t pos); +uint8_t LinearPredictICCValue(const uint8_t* data, size_t start, size_t i, + size_t stride, size_t width, int order); +size_t ICCANSContext(size_t i, size_t b1, size_t b2); + +} // namespace jxl + +#endif // LIB_JXL_ICC_CODEC_COMMON_H_ diff --git a/lib/jxl/icc_codec_test.cc b/lib/jxl/icc_codec_test.cc new file mode 100644 index 0000000..d365471 --- /dev/null +++ b/lib/jxl/icc_codec_test.cc @@ -0,0 +1,207 @@ +// 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/icc_codec.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/enc_icc_codec.h" + +namespace jxl { +namespace { + +void TestProfile(const PaddedBytes& icc) { + BitWriter writer; + ASSERT_TRUE(WriteICC(icc, &writer, 0, nullptr)); + writer.ZeroPadToByte(); + PaddedBytes dec; + BitReader reader(writer.GetSpan()); + ASSERT_TRUE(ReadICC(&reader, &dec)); + ASSERT_TRUE(reader.Close()); + EXPECT_EQ(icc.size(), dec.size()); + if (icc.size() == dec.size()) { + for (size_t i = 0; i < icc.size(); i++) { + EXPECT_EQ(icc[i], dec[i]); + if (icc[i] != dec[i]) break; // One output is enough + } + } +} + +void TestProfile(const std::string& icc) { + PaddedBytes bytes(icc.size()); + for (size_t i = 0; i < icc.size(); i++) { + bytes[i] = icc[i]; + } + TestProfile(bytes); +} + +// Valid profile from one of the images output by the decoder. +static const unsigned char kTestProfile[] = { + 0x00, 0x00, 0x03, 0x80, 0x6c, 0x63, 0x6d, 0x73, 0x04, 0x30, 0x00, 0x00, + 0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20, + 0x07, 0xe3, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x0f, 0x00, 0x32, 0x00, 0x2e, + 0x61, 0x63, 0x73, 0x70, 0x41, 0x50, 0x50, 0x4c, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xf6, 0xd6, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x6c, 0x63, 0x6d, 0x73, + 0x5f, 0x07, 0x0d, 0x3e, 0x4d, 0x32, 0xf2, 0x6e, 0x5d, 0x77, 0x26, 0xcc, + 0x23, 0xb0, 0x6a, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, + 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x42, + 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x64, 0x00, 0x00, 0x01, 0x00, + 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x02, 0x64, 0x00, 0x00, 0x00, 0x14, + 0x63, 0x68, 0x61, 0x64, 0x00, 0x00, 0x02, 0x78, 0x00, 0x00, 0x00, 0x2c, + 0x72, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xa4, 0x00, 0x00, 0x00, 0x14, + 0x62, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xb8, 0x00, 0x00, 0x00, 0x14, + 0x67, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0xcc, 0x00, 0x00, 0x00, 0x14, + 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, 0xe0, 0x00, 0x00, 0x00, 0x20, + 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x24, + 0x64, 0x6d, 0x6e, 0x64, 0x00, 0x00, 0x03, 0x24, 0x00, 0x00, 0x00, 0x28, + 0x64, 0x6d, 0x64, 0x64, 0x00, 0x00, 0x03, 0x4c, 0x00, 0x00, 0x00, 0x32, + 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0x26, + 0x00, 0x00, 0x00, 0x1c, 0x00, 0x52, 0x00, 0x47, 0x00, 0x42, 0x00, 0x5f, + 0x00, 0x44, 0x00, 0x36, 0x00, 0x35, 0x00, 0x5f, 0x00, 0x53, 0x00, 0x52, + 0x00, 0x47, 0x00, 0x5f, 0x00, 0x52, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x5f, + 0x00, 0x37, 0x00, 0x30, 0x00, 0x39, 0x00, 0x00, 0x6d, 0x6c, 0x75, 0x63, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, + 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x1c, + 0x00, 0x43, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x79, 0x00, 0x72, 0x00, 0x69, + 0x00, 0x67, 0x00, 0x68, 0x00, 0x74, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, + 0x00, 0x31, 0x00, 0x38, 0x00, 0x20, 0x00, 0x47, 0x00, 0x6f, 0x00, 0x6f, + 0x00, 0x67, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x20, 0x00, 0x4c, 0x00, 0x4c, + 0x00, 0x43, 0x00, 0x2c, 0x00, 0x20, 0x00, 0x43, 0x00, 0x43, 0x00, 0x2d, + 0x00, 0x42, 0x00, 0x59, 0x00, 0x2d, 0x00, 0x53, 0x00, 0x41, 0x00, 0x20, + 0x00, 0x33, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x20, 0x00, 0x55, 0x00, 0x6e, + 0x00, 0x70, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x74, 0x00, 0x65, 0x00, 0x64, + 0x00, 0x20, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x73, 0x00, 0x65, 0x00, 0x28, 0x00, 0x68, 0x00, 0x74, 0x00, 0x74, + 0x00, 0x70, 0x00, 0x73, 0x00, 0x3a, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0x63, + 0x00, 0x72, 0x00, 0x65, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, + 0x00, 0x65, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x6d, 0x00, 0x6f, + 0x00, 0x6e, 0x00, 0x73, 0x00, 0x2e, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x67, + 0x00, 0x2f, 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x6e, + 0x00, 0x73, 0x00, 0x65, 0x00, 0x73, 0x00, 0x2f, 0x00, 0x62, 0x00, 0x79, + 0x00, 0x2d, 0x00, 0x73, 0x00, 0x61, 0x00, 0x2f, 0x00, 0x33, 0x00, 0x2e, + 0x00, 0x30, 0x00, 0x2f, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x67, 0x00, 0x61, + 0x00, 0x6c, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x65, 0x00, 0x29, + 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0xd6, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x73, 0x66, 0x33, 0x32, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x42, 0x00, 0x00, 0x05, 0xde, + 0xff, 0xff, 0xf3, 0x25, 0x00, 0x00, 0x07, 0x93, 0x00, 0x00, 0xfd, 0x90, + 0xff, 0xff, 0xfb, 0xa1, 0xff, 0xff, 0xfd, 0xa2, 0x00, 0x00, 0x03, 0xdc, + 0x00, 0x00, 0xc0, 0x6e, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x6f, 0xa0, 0x00, 0x00, 0x38, 0xf5, 0x00, 0x00, 0x03, 0x90, + 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x9f, + 0x00, 0x00, 0x0f, 0x84, 0x00, 0x00, 0xb6, 0xc4, 0x58, 0x59, 0x5a, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x97, 0x00, 0x00, 0xb7, 0x87, + 0x00, 0x00, 0x18, 0xd9, 0x70, 0x61, 0x72, 0x61, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x38, 0xe4, 0x00, 0x00, 0xe8, 0xf0, + 0x00, 0x00, 0x17, 0x10, 0x00, 0x00, 0x38, 0xe4, 0x00, 0x00, 0x14, 0xbc, + 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xa3, 0xd7, 0x00, 0x00, 0x54, 0x7c, 0x00, 0x00, 0x4c, 0xcd, + 0x00, 0x00, 0x99, 0x9a, 0x00, 0x00, 0x26, 0x67, 0x00, 0x00, 0x0f, 0x5c, + 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0x0c, + 0x00, 0x00, 0x00, 0x1c, 0x00, 0x47, 0x00, 0x6f, 0x00, 0x6f, 0x00, 0x67, + 0x00, 0x6c, 0x00, 0x65, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, + 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x49, 0x00, 0x6d, + 0x00, 0x61, 0x00, 0x67, 0x00, 0x65, 0x00, 0x20, 0x00, 0x63, 0x00, 0x6f, + 0x00, 0x64, 0x00, 0x65, 0x00, 0x63, 0x00, 0x00, +}; + +} // namespace + +TEST(IccCodecTest, Icc) { + // Empty string cannot be tested, encoder checks against writing it. + TestProfile("a"); + TestProfile("ab"); + TestProfile("aaaa"); + + { + // Exactly the ICC header size + PaddedBytes profile(128); + for (size_t i = 0; i < 128; i++) { + profile[i] = 0; + } + TestProfile(profile); + } + + { + PaddedBytes profile; + profile.append(kTestProfile, kTestProfile + sizeof(kTestProfile)); + TestProfile(profile); + } + + // Test substrings of full profile + { + PaddedBytes profile; + for (size_t i = 0; i <= 256; i++) { + profile.push_back(kTestProfile[i]); + TestProfile(profile); + } + } +} + +// kTestProfile after encoding with the ICC codec +static const unsigned char kEncodedTestProfile[] = { + 0x1f, 0x8b, 0x1, 0x13, 0x10, 0x0, 0x0, 0x0, 0x20, 0x4c, 0xcc, 0x3, + 0xe7, 0xa0, 0xa5, 0xa2, 0x90, 0xa4, 0x27, 0xe8, 0x79, 0x1d, 0xe3, 0x26, + 0x57, 0x54, 0xef, 0x0, 0xe8, 0x97, 0x2, 0xce, 0xa1, 0xd7, 0x85, 0x16, + 0xb4, 0x29, 0x94, 0x58, 0xf2, 0x56, 0xc0, 0x76, 0xea, 0x23, 0xec, 0x7c, + 0x73, 0x51, 0x41, 0x40, 0x23, 0x21, 0x95, 0x4, 0x75, 0x12, 0xc9, 0xcc, + 0x16, 0xbd, 0xb6, 0x99, 0xad, 0xf8, 0x75, 0x35, 0xb6, 0x42, 0xae, 0xae, + 0xae, 0x86, 0x56, 0xf8, 0xcc, 0x16, 0x30, 0xb3, 0x45, 0xad, 0xd, 0x40, + 0xd6, 0xd1, 0xd6, 0x99, 0x40, 0xbe, 0xe2, 0xdc, 0x31, 0x7, 0xa6, 0xb9, + 0x27, 0x92, 0x38, 0x0, 0x3, 0x5e, 0x2c, 0xbe, 0xe6, 0xfb, 0x19, 0xbf, + 0xf3, 0x6d, 0xbc, 0x4d, 0x64, 0xe5, 0xba, 0x76, 0xde, 0x31, 0x65, 0x66, + 0x14, 0xa6, 0x3a, 0xc5, 0x8f, 0xb1, 0xb4, 0xba, 0x1f, 0xb1, 0xb8, 0xd4, + 0x75, 0xba, 0x18, 0x86, 0x95, 0x3c, 0x26, 0xf6, 0x25, 0x62, 0x53, 0xfd, + 0x9c, 0x94, 0x76, 0xf6, 0x95, 0x2c, 0xb1, 0xfd, 0xdc, 0xc0, 0xe4, 0x3f, + 0xb3, 0xff, 0x67, 0xde, 0xd5, 0x94, 0xcc, 0xb0, 0x83, 0x2f, 0x28, 0x93, + 0x92, 0x3, 0xa1, 0x41, 0x64, 0x60, 0x62, 0x70, 0x80, 0x87, 0xaf, 0xe7, + 0x60, 0x4a, 0x20, 0x23, 0xb3, 0x11, 0x7, 0x38, 0x38, 0xd4, 0xa, 0x66, + 0xb5, 0x93, 0x41, 0x90, 0x19, 0x17, 0x18, 0x60, 0xa5, 0xb, 0x7a, 0x24, + 0xaa, 0x20, 0x81, 0xac, 0xa9, 0xa1, 0x70, 0xa6, 0x12, 0x8a, 0x4a, 0xa3, + 0xa0, 0xf9, 0x9a, 0x97, 0xe7, 0xa8, 0xac, 0x8, 0xa8, 0xc4, 0x2a, 0x86, + 0xa7, 0x69, 0x1e, 0x67, 0xe6, 0xbe, 0xa4, 0xd3, 0xff, 0x91, 0x61, 0xf6, + 0x8a, 0xe6, 0xb5, 0xb3, 0x61, 0x9f, 0x19, 0x17, 0x98, 0x27, 0x6b, 0xe9, + 0x8, 0x98, 0xe1, 0x21, 0x4a, 0x9, 0xb5, 0xd7, 0xca, 0xfa, 0x94, 0xd0, + 0x69, 0x1a, 0xeb, 0x52, 0x1, 0x4e, 0xf5, 0xf6, 0xdf, 0x7f, 0xe7, 0x29, + 0x70, 0xee, 0x4, 0xda, 0x2f, 0xa4, 0xff, 0xfe, 0xbb, 0x6f, 0xa8, 0xff, + 0xfe, 0xdb, 0xaf, 0x8, 0xf6, 0x72, 0xa1, 0x40, 0x5d, 0xf0, 0x2d, 0x8, + 0x82, 0x5b, 0x87, 0xbd, 0x10, 0x8, 0xe9, 0x7, 0xee, 0x4b, 0x80, 0xda, + 0x4a, 0x4, 0xc5, 0x5e, 0xa0, 0xb7, 0x1e, 0x60, 0xb0, 0x59, 0x76, 0x60, + 0xb, 0x2e, 0x19, 0x8a, 0x2e, 0x1c, 0xe6, 0x6, 0x20, 0xb8, 0x64, 0x18, + 0x2a, 0xcf, 0x51, 0x94, 0xd4, 0xee, 0xc3, 0xfe, 0x39, 0x74, 0xd4, 0x2b, + 0x48, 0xc9, 0x83, 0x4c, 0x9b, 0xd0, 0x4c, 0x35, 0x10, 0xe3, 0x9, 0xf7, + 0x72, 0xf0, 0x7a, 0xe, 0xbf, 0x7d, 0x36, 0x2e, 0x19, 0x7e, 0x3f, 0xc, + 0xf7, 0x93, 0xe7, 0xf4, 0x1d, 0x32, 0xc6, 0xb0, 0x89, 0xad, 0xe0, 0x28, + 0xc1, 0xa7, 0x59, 0xe3, 0x0, +}; + +// Tests that the decoded kEncodedTestProfile matches kTestProfile. +TEST(IccCodecTest, EncodedIccProfile) { + jxl::BitReader reader(jxl::Span(kEncodedTestProfile, + sizeof(kEncodedTestProfile))); + jxl::PaddedBytes dec; + ASSERT_TRUE(ReadICC(&reader, &dec)); + ASSERT_TRUE(reader.Close()); + EXPECT_EQ(sizeof(kTestProfile), dec.size()); + if (sizeof(kTestProfile) == dec.size()) { + for (size_t i = 0; i < dec.size(); i++) { + EXPECT_EQ(kTestProfile[i], dec[i]); + if (kTestProfile[i] != dec[i]) break; // One output is enough + } + } +} + +} // namespace jxl diff --git a/lib/jxl/image.cc b/lib/jxl/image.cc new file mode 100644 index 0000000..0d63d79 --- /dev/null +++ b/lib/jxl/image.cc @@ -0,0 +1,313 @@ +// 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/image.h" + +#include // swap + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/image.cc" +#include +#include + +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/sanitizers.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { + +namespace HWY_NAMESPACE { +size_t GetVectorSize() { return HWY_LANES(uint8_t); } +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE + +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +namespace { + +HWY_EXPORT(GetVectorSize); // Local function. + +size_t VectorSize() { + static size_t bytes = HWY_DYNAMIC_DISPATCH(GetVectorSize)(); + return bytes; +} + +// Returns distance [bytes] between the start of two consecutive rows, a +// multiple of vector/cache line size but NOT CacheAligned::kAlias - see below. +size_t BytesPerRow(const size_t xsize, const size_t sizeof_t) { + const size_t vec_size = VectorSize(); + size_t valid_bytes = xsize * sizeof_t; + + // Allow unaligned accesses starting at the last valid value - this may raise + // msan errors unless the user calls InitializePaddingForUnalignedAccesses. + // Skip for the scalar case because no extra lanes will be loaded. + if (vec_size != 0) { + valid_bytes += vec_size - sizeof_t; + } + + // Round up to vector and cache line size. + const size_t align = std::max(vec_size, CacheAligned::kAlignment); + size_t bytes_per_row = RoundUpTo(valid_bytes, align); + + // During the lengthy window before writes are committed to memory, CPUs + // guard against read after write hazards by checking the address, but + // only the lower 11 bits. We avoid a false dependency between writes to + // consecutive rows by ensuring their sizes are not multiples of 2 KiB. + // Avoid2K prevents the same problem for the planes of an Image3. + if (bytes_per_row % CacheAligned::kAlias == 0) { + bytes_per_row += align; + } + + JXL_ASSERT(bytes_per_row % align == 0); + return bytes_per_row; +} + +} // namespace + +PlaneBase::PlaneBase(const size_t xsize, const size_t ysize, + const size_t sizeof_t) + : xsize_(static_cast(xsize)), + ysize_(static_cast(ysize)), + orig_xsize_(static_cast(xsize)), + orig_ysize_(static_cast(ysize)) { + // (Can't profile CacheAligned itself because it is used by profiler.h) + PROFILER_FUNC; + + JXL_CHECK(xsize == xsize_); + JXL_CHECK(ysize == ysize_); + + JXL_ASSERT(sizeof_t == 1 || sizeof_t == 2 || sizeof_t == 4 || sizeof_t == 8); + + bytes_per_row_ = 0; + // Dimensions can be zero, e.g. for lazily-allocated images. Only allocate + // if nonzero, because "zero" bytes still have padding/bookkeeping overhead. + if (xsize != 0 && ysize != 0) { + bytes_per_row_ = BytesPerRow(xsize, sizeof_t); + bytes_ = AllocateArray(bytes_per_row_ * ysize); + JXL_CHECK(bytes_.get()); + InitializePadding(sizeof_t, Padding::kRoundUp); + } +} + +void PlaneBase::InitializePadding(const size_t sizeof_t, Padding padding) { +#if defined(MEMORY_SANITIZER) || HWY_IDE + if (xsize_ == 0 || ysize_ == 0) return; + + const size_t vec_size = VectorSize(); + if (vec_size == 0) return; // Scalar mode: no padding needed + + const size_t valid_size = xsize_ * sizeof_t; + const size_t initialize_size = padding == Padding::kRoundUp + ? RoundUpTo(valid_size, vec_size) + : valid_size + vec_size - sizeof_t; + if (valid_size == initialize_size) return; + + for (size_t y = 0; y < ysize_; ++y) { + uint8_t* JXL_RESTRICT row = static_cast(VoidRow(y)); +#if defined(__clang__) && (__clang_major__ <= 6) + // There's a bug in msan in clang-6 when handling AVX2 operations. This + // workaround allows tests to pass on msan, although it is slower and + // prevents msan warnings from uninitialized images. + std::fill(row, msan::kSanitizerSentinelByte, initialize_size); +#else + memset(row + valid_size, msan::kSanitizerSentinelByte, + initialize_size - valid_size); +#endif // clang6 + } +#endif // MEMORY_SANITIZER +} + +void PlaneBase::Swap(PlaneBase& other) { + std::swap(xsize_, other.xsize_); + std::swap(ysize_, other.ysize_); + std::swap(orig_xsize_, other.orig_xsize_); + std::swap(orig_ysize_, other.orig_ysize_); + std::swap(bytes_per_row_, other.bytes_per_row_); + 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(); + size_t ysize = in.ysize(); + Image3F out(xsize + 2 * xborder, ysize + 2 * yborder); + if (xborder > xsize || yborder > ysize) { + for (size_t c = 0; c < 3; c++) { + for (int32_t y = 0; y < static_cast(out.ysize()); y++) { + float* row_out = out.PlaneRow(c, y); + const float* row_in = in.PlaneRow( + c, Mirror(y - static_cast(yborder), in.ysize())); + for (int32_t x = 0; x < static_cast(out.xsize()); x++) { + int32_t xin = Mirror(x - static_cast(xborder), in.xsize()); + row_out[x] = row_in[xin]; + } + } + } + return out; + } + CopyImageTo(in, Rect(xborder, yborder, xsize, ysize), &out); + for (size_t c = 0; c < 3; c++) { + // Horizontal pad. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xborder; x++) { + out.PlaneRow(c, y + yborder)[x] = + in.ConstPlaneRow(c, y)[xborder - x - 1]; + out.PlaneRow(c, y + yborder)[x + xsize + xborder] = + in.ConstPlaneRow(c, y)[xsize - 1 - x]; + } + } + // Vertical pad. + for (size_t y = 0; y < yborder; y++) { + memcpy(out.PlaneRow(c, y), out.ConstPlaneRow(c, 2 * yborder - 1 - y), + out.xsize() * sizeof(float)); + memcpy(out.PlaneRow(c, y + ysize + yborder), + out.ConstPlaneRow(c, ysize + yborder - 1 - y), + out.xsize() * sizeof(float)); + } + } + 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(); + const size_t ysize_orig = in->ysize(); + const size_t xsize = RoundUpToBlockDim(xsize_orig); + const size_t ysize = RoundUpToBlockDim(ysize_orig); + // Expands image size to the originally-allocated size. + in->ShrinkTo(xsize, ysize); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < ysize_orig; y++) { + float* JXL_RESTRICT row = in->PlaneRow(c, y); + for (size_t x = xsize_orig; x < xsize; x++) { + row[x] = row[xsize_orig - 1]; + } + } + const float* JXL_RESTRICT row_src = in->ConstPlaneRow(c, ysize_orig - 1); + for (size_t y = ysize_orig; y < ysize; y++) { + memcpy(in->PlaneRow(c, y), row_src, xsize * sizeof(float)); + } + } +} + +float DotProduct(const ImageF& a, const ImageF& b) { + double sum = 0.0; + for (size_t y = 0; y < a.ysize(); ++y) { + const float* const JXL_RESTRICT row_a = a.ConstRow(y); + const float* const JXL_RESTRICT row_b = b.ConstRow(y); + for (size_t x = 0; x < a.xsize(); ++x) { + sum += row_a[x] * row_b[x]; + } + } + return sum; +} + +static void DownsampleImage(const ImageF& input, size_t factor, + ImageF* output) { + JXL_ASSERT(factor != 1); + output->ShrinkTo(DivCeil(input.xsize(), factor), + DivCeil(input.ysize(), factor)); + size_t in_stride = input.PixelsPerRow(); + for (size_t y = 0; y < output->ysize(); y++) { + float* row_out = output->Row(y); + const float* row_in = input.Row(factor * y); + for (size_t x = 0; x < output->xsize(); x++) { + size_t cnt = 0; + float sum = 0; + for (size_t iy = 0; iy < factor && iy + factor * y < input.ysize(); + iy++) { + for (size_t ix = 0; ix < factor && ix + factor * x < input.xsize(); + ix++) { + sum += row_in[iy * in_stride + x * factor + ix]; + cnt++; + } + } + row_out[x] = sum / cnt; + } + } +} + +void DownsampleImage(ImageF* image, size_t factor) { + // Allocate extra space to avoid a reallocation when padding. + ImageF downsampled(DivCeil(image->xsize(), factor) + kBlockDim, + DivCeil(image->ysize(), factor) + kBlockDim); + DownsampleImage(*image, factor, &downsampled); + *image = std::move(downsampled); +} + +void DownsampleImage(Image3F* opsin, size_t factor) { + JXL_ASSERT(factor != 1); + // Allocate extra space to avoid a reallocation when padding. + Image3F downsampled(DivCeil(opsin->xsize(), factor) + kBlockDim, + DivCeil(opsin->ysize(), factor) + kBlockDim); + downsampled.ShrinkTo(downsampled.xsize() - kBlockDim, + downsampled.ysize() - kBlockDim); + for (size_t c = 0; c < 3; c++) { + DownsampleImage(opsin->Plane(c), factor, &downsampled.Plane(c)); + } + *opsin = std::move(downsampled); +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/image.h b/lib/jxl/image.h new file mode 100644 index 0000000..1af5826 --- /dev/null +++ b/lib/jxl/image.h @@ -0,0 +1,443 @@ +// 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_IMAGE_H_ +#define LIB_JXL_IMAGE_H_ + +// SIMD/multicore-friendly planar image representation with row accessors. + +#include +#include +#include + +#include +#include // std::move + +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Type-independent parts of Plane<> - reduces code duplication and facilitates +// moving member function implementations to cc file. +struct PlaneBase { + PlaneBase() + : xsize_(0), + ysize_(0), + orig_xsize_(0), + orig_ysize_(0), + bytes_per_row_(0), + bytes_(nullptr) {} + PlaneBase(size_t xsize, size_t ysize, size_t sizeof_t); + + // Copy construction/assignment is forbidden to avoid inadvertent copies, + // which can be very expensive. Use CopyImageTo() instead. + PlaneBase(const PlaneBase& other) = delete; + PlaneBase& operator=(const PlaneBase& other) = delete; + + // Move constructor (required for returning Image from function) + PlaneBase(PlaneBase&& other) noexcept = default; + + // Move assignment (required for std::vector) + PlaneBase& operator=(PlaneBase&& other) noexcept = default; + + void Swap(PlaneBase& other); + + // Useful for pre-allocating image with some padding for alignment purposes + // and later reporting the actual valid dimensions. May also be used to + // un-shrink the image. Caller is responsible for ensuring xsize/ysize are <= + // the original dimensions. + void ShrinkTo(const size_t xsize, const size_t ysize) { + JXL_CHECK(xsize <= orig_xsize_); + JXL_CHECK(ysize <= orig_ysize_); + xsize_ = static_cast(xsize); + ysize_ = static_cast(ysize); + // NOTE: we can't recompute bytes_per_row for more compact storage and + // better locality because that would invalidate the image contents. + } + + // How many pixels. + JXL_INLINE size_t xsize() const { return xsize_; } + JXL_INLINE size_t ysize() const { return ysize_; } + + // NOTE: do not use this for copying rows - the valid xsize may be much less. + JXL_INLINE size_t bytes_per_row() const { return bytes_per_row_; } + + // Raw access to byte contents, for interfacing with other libraries. + // Unsigned char instead of char to avoid surprises (sign extension). + JXL_INLINE uint8_t* bytes() { + void* p = bytes_.get(); + return static_cast(JXL_ASSUME_ALIGNED(p, 64)); + } + JXL_INLINE const uint8_t* bytes() const { + const void* p = bytes_.get(); + return static_cast(JXL_ASSUME_ALIGNED(p, 64)); + } + + protected: + // Returns pointer to the start of a row. + JXL_INLINE void* VoidRow(const size_t y) const { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + if (y >= ysize_) { + JXL_ABORT("Row(%zu) in (%u x %u) image\n", y, xsize_, ysize_); + } +#endif + + void* row = bytes_.get() + y * bytes_per_row_; + return JXL_ASSUME_ALIGNED(row, 64); + } + + enum class Padding { + // Allow Load(d, row + x) for x = 0; x < xsize(); x += Lanes(d). Default. + kRoundUp, + // Allow LoadU(d, row + x) for x = xsize() - 1. This requires an extra + // vector to be initialized. If done by default, this would suppress + // legitimate msan warnings. We therefore require users to explicitly call + // InitializePadding before using unaligned loads (e.g. convolution). + kUnaligned + }; + + // Initializes the minimum bytes required to suppress msan warnings from + // legitimate (according to Padding mode) vector loads/stores on the right + // border, where some lanes are uninitialized and assumed to be unused. + void InitializePadding(size_t sizeof_t, Padding padding); + + // (Members are non-const to enable assignment during move-assignment.) + uint32_t xsize_; // In valid pixels, not including any padding. + uint32_t ysize_; + uint32_t orig_xsize_; + uint32_t orig_ysize_; + size_t bytes_per_row_; // Includes padding. + CacheAlignedUniquePtr bytes_; +}; + +// Single channel, aligned rows separated by padding. T must be POD. +// +// 'Single channel' (one 2D array per channel) simplifies vectorization +// (repeating the same operation on multiple adjacent components) without the +// complexity of a hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients +// can easily iterate over all components in a row and Image requires no +// knowledge of the pixel format beyond the component type "T". +// +// 'Aligned' means each row is aligned to the L1 cache line size. This prevents +// false sharing between two threads operating on adjacent rows. +// +// 'Padding' is still relevant because vectors could potentially be larger than +// a cache line. By rounding up row sizes to the vector size, we allow +// reading/writing ALIGNED vectors whose first lane is a valid sample. This +// avoids needing a separate loop to handle remaining unaligned lanes. +// +// This image layout could also be achieved with a vector and a row accessor +// function, but a class wrapper with support for "deleter" allows wrapping +// existing memory allocated by clients without copying the pixels. It also +// provides convenient accessors for xsize/ysize, which shortens function +// argument lists. Supports move-construction so it can be stored in containers. +template +class Plane : public PlaneBase { + public: + using T = ComponentType; + static constexpr size_t kNumPlanes = 1; + + Plane() = default; + Plane(const size_t xsize, const size_t ysize) + : PlaneBase(xsize, ysize, sizeof(T)) {} + + void InitializePaddingForUnalignedAccesses() { + InitializePadding(sizeof(T), Padding::kUnaligned); + } + + JXL_INLINE T* Row(const size_t y) { return static_cast(VoidRow(y)); } + + // Returns pointer to const (see above). + JXL_INLINE const T* Row(const size_t y) const { + return static_cast(VoidRow(y)); + } + + // Documents that the access is const. + JXL_INLINE const T* ConstRow(const size_t y) const { + return static_cast(VoidRow(y)); + } + + // Returns number of pixels (some of which are padding) per row. Useful for + // computing other rows via pointer arithmetic. WARNING: this must + // NOT be used to determine xsize. + JXL_INLINE intptr_t PixelsPerRow() const { + return static_cast(bytes_per_row_ / sizeof(T)); + } +}; + +using ImageSB = Plane; +using ImageB = Plane; +using ImageS = Plane; // signed integer or half-float +using ImageU = Plane; +using ImageI = Plane; +using ImageF = Plane; +using ImageD = Plane; + +// Also works for Image3 and mixed argument types. +template +bool SameSize(const Image1& image1, const Image2& image2) { + return image1.xsize() == image2.xsize() && image1.ysize() == image2.ysize(); +} + +template +class Image3; + +// Rectangular region in image(s). Factoring this out of Image instead of +// 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 { + 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) + : 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) + : x0_(xbegin), y0_(ybegin), xsize_(xsize), ysize_(ysize) {} + + // Construct a rect that covers a whole image/plane/ImageBundle etc. + template + explicit Rect(const Image& image) + : Rect(0, 0, image.xsize(), image.ysize()) {} + + Rect() : Rect(0, 0, 0, 0) {} + + Rect(const Rect&) = default; + Rect& operator=(const Rect&) = default; + + // Construct a subrect that resides in an image/plane/ImageBundle etc. + template + Rect Crop(const Image& image) const { + return Rect(x0_, y0_, xsize_, ysize_, image.xsize(), image.ysize()); + } + + // 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); + } + + // Returns a rect that only contains `num` lines with offset `y` from `y0()`. + Rect Lines(size_t y, size_t num) const { + JXL_DASSERT(y + num <= ysize_); + return Rect(x0_, y0_ + y, xsize_, num); + } + + Rect 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 Rect Translate(int64_t x_offset, int64_t y_offset) const { + return Rect(x0_ + x_offset, y0_ + y_offset, xsize_, ysize_); + } + + template + T* Row(Plane* image, size_t y) const { + return image->Row(y + y0_) + x0_; + } + + template + const T* Row(const Plane* image, size_t y) const { + return image->Row(y + y0_) + x0_; + } + + template + T* PlaneRow(Image3* image, const size_t c, size_t y) const { + return image->PlaneRow(c, y + y0_) + x0_; + } + + template + const T* ConstRow(const Plane& image, size_t y) const { + return image.ConstRow(y + y0_) + x0_; + } + + template + const T* ConstPlaneRow(const Image3& image, size_t c, size_t y) const { + return image.ConstPlaneRow(c, y + y0_) + x0_; + } + + bool IsInside(const Rect& other) const { + return x0_ >= other.x0() && x0_ + xsize_ <= other.x0() + other.xsize_ && + y0_ >= other.y0() && y0_ + ysize_ <= other.y0() + other.ysize(); + } + + // Returns true if this Rect fully resides in the given image. ImageT could be + // Plane or Image3; however if ImageT is Rect, results are nonsensical. + template + bool IsInside(const ImageT& image) const { + return (x0_ + xsize_ <= image.xsize()) && (y0_ + ysize_ <= image.ysize()); + } + + size_t x0() const { return x0_; } + size_t y0() const { return y0_; } + size_t xsize() const { return xsize_; } + size_t ysize() const { return 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); + } + + size_t x0_; + size_t y0_; + + size_t xsize_; + size_t ysize_; +}; + +// 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". +// Hopefully, the "assign to me" meaning will go away and most images in the +// codebase will not be backed by own storage. When this happens we can redesign +// Image to be a non-storage-holding view class and introduce BackedImage in +// those places that actually need it. + +// NOTE: we can't use Image as a view because invariants are violated +// (alignment and the presence of padding before/after each "row"). + +// A bundle of 3 same-sized images. Typically constructed by moving from three +// rvalue references to Image. To overwrite an existing Image3 using +// single-channel producers, we also need access to Image*. Constructing +// temporary non-owning Image pointing to one plane of an existing Image3 risks +// dangling references, especially if the wrapper is moved. Therefore, we +// store an array of Image (which are compact enough that size is not a concern) +// and provide Plane+Row accessors. +template +class Image3 { + public: + using T = ComponentType; + using PlaneT = jxl::Plane; + static constexpr size_t kNumPlanes = 3; + + Image3() : planes_{PlaneT(), PlaneT(), PlaneT()} {} + + Image3(const size_t xsize, const size_t ysize) + : planes_{PlaneT(xsize, ysize), PlaneT(xsize, ysize), + PlaneT(xsize, ysize)} {} + + Image3(Image3&& other) noexcept { + for (size_t i = 0; i < kNumPlanes; i++) { + planes_[i] = std::move(other.planes_[i]); + } + } + + Image3(PlaneT&& plane0, PlaneT&& plane1, PlaneT&& plane2) { + JXL_CHECK(SameSize(plane0, plane1)); + JXL_CHECK(SameSize(plane0, plane2)); + planes_[0] = std::move(plane0); + planes_[1] = std::move(plane1); + planes_[2] = std::move(plane2); + } + + // Copy construction/assignment is forbidden to avoid inadvertent copies, + // which can be very expensive. Use CopyImageTo instead. + Image3(const Image3& other) = delete; + Image3& operator=(const Image3& other) = delete; + + Image3& operator=(Image3&& other) noexcept { + for (size_t i = 0; i < kNumPlanes; i++) { + planes_[i] = std::move(other.planes_[i]); + } + return *this; + } + + // Returns row pointer; usage: PlaneRow(idx_plane, y)[x] = val. + JXL_INLINE T* PlaneRow(const size_t c, const size_t y) { + // Custom implementation instead of calling planes_[c].Row ensures only a + // single multiplication is needed for PlaneRow(0..2, y). + PlaneRowBoundsCheck(c, y); + const size_t row_offset = y * planes_[0].bytes_per_row(); + void* row = planes_[c].bytes() + row_offset; + return static_cast(JXL_ASSUME_ALIGNED(row, 64)); + } + + // Returns const row pointer; usage: val = PlaneRow(idx_plane, y)[x]. + JXL_INLINE const T* PlaneRow(const size_t c, const size_t y) const { + PlaneRowBoundsCheck(c, y); + const size_t row_offset = y * planes_[0].bytes_per_row(); + const void* row = planes_[c].bytes() + row_offset; + return static_cast(JXL_ASSUME_ALIGNED(row, 64)); + } + + // Returns const row pointer, even if called from a non-const Image3. + JXL_INLINE const T* ConstPlaneRow(const size_t c, const size_t y) const { + PlaneRowBoundsCheck(c, y); + return PlaneRow(c, y); + } + + JXL_INLINE const PlaneT& Plane(size_t idx) const { return planes_[idx]; } + + JXL_INLINE PlaneT& Plane(size_t idx) { return planes_[idx]; } + + void Swap(Image3& other) { + for (size_t c = 0; c < 3; ++c) { + other.planes_[c].Swap(planes_[c]); + } + } + + // Useful for pre-allocating image with some padding for alignment purposes + // and later reporting the actual valid dimensions. May also be used to + // un-shrink the image. Caller is responsible for ensuring xsize/ysize are <= + // the original dimensions. + void ShrinkTo(const size_t xsize, const size_t ysize) { + for (PlaneT& plane : planes_) { + plane.ShrinkTo(xsize, ysize); + } + } + + // Sizes of all three images are guaranteed to be equal. + JXL_INLINE size_t xsize() const { return planes_[0].xsize(); } + JXL_INLINE size_t ysize() const { return planes_[0].ysize(); } + // Returns offset [bytes] from one row to the next row of the same plane. + // WARNING: this must NOT be used to determine xsize, nor for copying rows - + // the valid xsize may be much less. + JXL_INLINE size_t bytes_per_row() const { return planes_[0].bytes_per_row(); } + // Returns number of pixels (some of which are padding) per row. Useful for + // computing other rows via pointer arithmetic. WARNING: this must NOT be used + // to determine xsize. + JXL_INLINE intptr_t PixelsPerRow() const { return planes_[0].PixelsPerRow(); } + + private: + void PlaneRowBoundsCheck(const size_t c, const size_t y) const { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + if (c >= kNumPlanes || y >= ysize()) { + JXL_ABORT("PlaneRow(%zu, %zu) in (%zu x %zu) image\n", c, y, xsize(), + ysize()); + } +#endif + } + + private: + PlaneT planes_[kNumPlanes]; +}; + +using Image3B = Image3; +using Image3S = Image3; +using Image3U = Image3; +using Image3I = Image3; +using Image3F = Image3; +using Image3D = Image3; + +} // namespace jxl + +#endif // LIB_JXL_IMAGE_H_ diff --git a/lib/jxl/image_bundle.cc b/lib/jxl/image_bundle.cc new file mode 100644 index 0000000..0221903 --- /dev/null +++ b/lib/jxl/image_bundle.cc @@ -0,0 +1,149 @@ +// 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/image_bundle.h" + +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/luminance.h" + +namespace jxl { + +void ImageBundle::ShrinkTo(size_t xsize, size_t ysize) { + if (HasColor()) color_.ShrinkTo(xsize, ysize); + for (ImageF& ec : extra_channels_) { + ec.ShrinkTo(xsize, ysize); + } +} + +// Called by all other SetFrom*. +void ImageBundle::SetFromImage(Image3F&& color, + const ColorEncoding& c_current) { + JXL_CHECK(color.xsize() != 0 && color.ysize() != 0); + JXL_CHECK(metadata_->color_encoding.IsGray() == c_current.IsGray()); + color_ = std::move(color); + c_current_ = c_current; + VerifySizes(); +} + +void ImageBundle::VerifyMetadata() const { + JXL_CHECK(!c_current_.ICC().empty()); + JXL_CHECK(metadata_->color_encoding.IsGray() == IsGray()); + + if (metadata_->HasAlpha() && alpha().xsize() == 0) { + JXL_ABORT("MD alpha_bits %u IB alpha %zu x %zu\n", + metadata_->GetAlphaBits(), alpha().xsize(), alpha().ysize()); + } + const uint32_t alpha_bits = metadata_->GetAlphaBits(); + JXL_CHECK(alpha_bits <= 32); + + // metadata_->num_extra_channels may temporarily differ from + // extra_channels_.size(), e.g. after SetAlpha. They are synced by the next + // call to VisitFields. +} + +void ImageBundle::VerifySizes() const { + const size_t xs = xsize(); + const size_t ys = ysize(); + + if (HasExtraChannels()) { + JXL_CHECK(xs != 0 && ys != 0); + for (const ImageF& ec : extra_channels_) { + JXL_CHECK(ec.xsize() == xs); + JXL_CHECK(ec.ysize() == ys); + } + } +} + +size_t ImageBundle::DetectRealBitdepth() const { + return metadata_->bit_depth.bits_per_sample; + + // TODO(lode): let this function return lower bit depth if possible, e.g. + // return 8 bits in case the original image came from a 16-bit PNG that + // was in fact representable as 8-bit PNG. Ensure that the implementation + // returns 16 if e.g. two consecutive 16-bit values appeared in the original + // image (such as 32768 and 32769), take into account that e.g. the values + // 3-bit can represent is not a superset of the values 2-bit can represent, + // and there may be slight imprecisions in the floating point image. +} + +const ImageF& ImageBundle::alpha() const { + JXL_ASSERT(HasAlpha()); + const size_t ec = metadata_->Find(ExtraChannel::kAlpha) - + metadata_->extra_channel_info.data(); + JXL_ASSERT(ec < extra_channels_.size()); + return extra_channels_[ec]; +} +ImageF* ImageBundle::alpha() { + JXL_ASSERT(HasAlpha()); + const size_t ec = metadata_->Find(ExtraChannel::kAlpha) - + metadata_->extra_channel_info.data(); + JXL_ASSERT(ec < extra_channels_.size()); + return &extra_channels_[ec]; +} + +const ImageF& ImageBundle::depth() const { + JXL_ASSERT(HasDepth()); + const size_t ec = metadata_->Find(ExtraChannel::kDepth) - + metadata_->extra_channel_info.data(); + JXL_ASSERT(ec < extra_channels_.size()); + return extra_channels_[ec]; +} + +void ImageBundle::SetAlpha(ImageF&& alpha, bool alpha_is_premultiplied) { + const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); + // Must call SetAlphaBits first, otherwise we don't know which channel index + JXL_CHECK(eci != nullptr); + JXL_CHECK(alpha.xsize() != 0 && alpha.ysize() != 0); + JXL_CHECK(eci->alpha_associated == alpha_is_premultiplied); + extra_channels_.insert( + extra_channels_.begin() + (eci - metadata_->extra_channel_info.data()), + std::move(alpha)); + // num_extra_channels is automatically set in visitor + VerifySizes(); +} +void ImageBundle::PremultiplyAlpha() { + if (!HasAlpha()) return; + if (!HasColor()) return; + const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); + if (eci->alpha_associated) return; // already premultiplied + JXL_CHECK(color_.ysize() == alpha()->ysize()); + JXL_CHECK(color_.xsize() == alpha()->xsize()); + for (size_t y = 0; y < color_.ysize(); y++) { + ::jxl::PremultiplyAlpha(color_.PlaneRow(0, y), color_.PlaneRow(1, y), + color_.PlaneRow(2, y), alpha()->Row(y), + color_.xsize()); + } +} +void ImageBundle::UnpremultiplyAlpha() { + if (!HasAlpha()) return; + if (!HasColor()) return; + const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); + if (!eci->alpha_associated) return; // already unpremultiplied + JXL_CHECK(color_.ysize() == alpha()->ysize()); + JXL_CHECK(color_.xsize() == alpha()->xsize()); + for (size_t y = 0; y < color_.ysize(); y++) { + ::jxl::UnpremultiplyAlpha(color_.PlaneRow(0, y), color_.PlaneRow(1, y), + color_.PlaneRow(2, y), alpha()->Row(y), + color_.xsize()); + } +} + +void ImageBundle::SetExtraChannels(std::vector&& extra_channels) { + for (const ImageF& plane : extra_channels) { + JXL_CHECK(plane.xsize() != 0 && plane.ysize() != 0); + } + extra_channels_ = std::move(extra_channels); + VerifySizes(); +} +} // namespace jxl diff --git a/lib/jxl/image_bundle.h b/lib/jxl/image_bundle.h new file mode 100644 index 0000000..83f5f7b --- /dev/null +++ b/lib/jxl/image_bundle.h @@ -0,0 +1,263 @@ +// 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_IMAGE_BUNDLE_H_ +#define LIB_JXL_IMAGE_BUNDLE_H_ + +// The main image or frame consists of a bundle of associated images. + +#include +#include + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_metadata.h" +#include "lib/jxl/jpeg/jpeg_data.h" +#include "lib/jxl/opsin_params.h" +#include "lib/jxl/quantizer.h" + +namespace jxl { + +// A bundle of color/alpha/depth/plane images. +class ImageBundle { + public: + // Uninitialized state for use as output parameter. + ImageBundle() : metadata_(nullptr) {} + // Caller is responsible for setting metadata before calling Set*. + explicit ImageBundle(const ImageMetadata* metadata) : metadata_(metadata) {} + + // Move-only (allows storing in std::vector). + ImageBundle(ImageBundle&&) = default; + ImageBundle& operator=(ImageBundle&&) = default; + + ImageBundle Copy() const { + ImageBundle copy(metadata_); + copy.color_ = CopyImage(color_); + copy.c_current_ = c_current_; + copy.extra_channels_.reserve(extra_channels_.size()); + for (const ImageF& plane : extra_channels_) { + copy.extra_channels_.emplace_back(CopyImage(plane)); + } + + copy.jpeg_data = + jpeg_data ? make_unique(*jpeg_data) : nullptr; + copy.color_transform = color_transform; + copy.chroma_subsampling = chroma_subsampling; + + return copy; + } + + // -- SIZE + + size_t xsize() const { + if (IsJPEG()) return jpeg_data->width; + if (color_.xsize() != 0) return color_.xsize(); + return extra_channels_.empty() ? 0 : extra_channels_[0].xsize(); + } + size_t ysize() const { + if (IsJPEG()) return jpeg_data->height; + if (color_.ysize() != 0) return color_.ysize(); + return extra_channels_.empty() ? 0 : extra_channels_[0].ysize(); + } + void ShrinkTo(size_t xsize, size_t ysize); + + // sizes taking orientation into account + size_t oriented_xsize() const { + if (static_cast(metadata_->GetOrientation()) > 4) { + return ysize(); + } else { + return xsize(); + } + } + size_t oriented_ysize() const { + if (static_cast(metadata_->GetOrientation()) > 4) { + return xsize(); + } else { + return ysize(); + } + } + + // -- COLOR + + // Whether color() is valid/usable. Returns true in most cases. Even images + // with spot colors (one example of when !planes().empty()) typically have a + // part that can be converted to RGB. + bool HasColor() const { return color_.xsize() != 0; } + + // For resetting the size when switching from a reference to main frame. + void RemoveColor() { color_ = Image3F(); } + + // Do not use if !HasColor(). + const Image3F& color() const { + // If this fails, Set* was not called - perhaps because decoding failed? + JXL_DASSERT(HasColor()); + return color_; + } + + // Do not use if !HasColor(). + Image3F* color() { + JXL_DASSERT(HasColor()); + return &color_; + } + + // If c_current.IsGray(), all planes must be identical. NOTE: c_current is + // independent of metadata()->color_encoding, which is the original, whereas + // a decoder might return pixels in a different c_current. + // This only sets the color channels, you must also make extra channels + // match the amount that is in the metadata. + void SetFromImage(Image3F&& color, const ColorEncoding& c_current); + + // -- COLOR ENCODING + + const ColorEncoding& c_current() const { return c_current_; } + + // Returns whether the color image has identical planes. Once established by + // Set*, remains unchanged until a subsequent Set* or TransformTo. + bool IsGray() const { return c_current_.IsGray(); } + + bool IsSRGB() const { return c_current_.IsSRGB(); } + bool IsLinearSRGB() const { + return c_current_.white_point == WhitePoint::kD65 && + c_current_.primaries == Primaries::kSRGB && c_current_.tf.IsLinear(); + } + + // Set the c_current profile without doing any transformation, e.g. if the + // transformation was already applied. + void OverrideProfile(const ColorEncoding& new_c_current) { + c_current_ = new_c_current; + } + + // TODO(lode): TransformTo and CopyTo are implemented in enc_image_bundle.cc, + // move these functions out of this header file and class, to + // enc_image_bundle.h. + + // Transforms color to c_desired and sets c_current to c_desired. Alpha and + // metadata remains unchanged. + Status TransformTo(const ColorEncoding& c_desired, + ThreadPool* pool = nullptr); + // Copies this:rect, converts to c_desired, and allocates+fills out. + Status CopyTo(const Rect& rect, const ColorEncoding& c_desired, Image3B* out, + ThreadPool* pool = nullptr) const; + Status CopyTo(const Rect& rect, const ColorEncoding& c_desired, Image3F* out, + ThreadPool* pool = nullptr) const; + Status CopyToSRGB(const Rect& rect, Image3B* out, + ThreadPool* pool = nullptr) const; + + // Detect 'real' bit depth, which can be lower than nominal bit depth + // (this is common in PNG), returns 'real' bit depth + size_t DetectRealBitdepth() const; + + // -- ALPHA + + void SetAlpha(ImageF&& alpha, bool alpha_is_premultiplied); + bool HasAlpha() const { + return metadata_->Find(ExtraChannel::kAlpha) != nullptr; + } + bool AlphaIsPremultiplied() const { + const ExtraChannelInfo* eci = metadata_->Find(ExtraChannel::kAlpha); + return (eci == nullptr) ? false : eci->alpha_associated; + } + // Premultiply alpha (if it isn't already premultiplied) + void PremultiplyAlpha(); + // Unpremultiply alpha (if it isn't already non-premultiplied) + void UnpremultiplyAlpha(); + const ImageF& alpha() const; + ImageF* alpha(); + + // -- DEPTH + bool HasDepth() const { + return metadata_->Find(ExtraChannel::kDepth) != nullptr; + } + const ImageF& depth() const; + + // -- EXTRA CHANNELS + + // Extra channels of unknown interpretation (e.g. spot colors). + void SetExtraChannels(std::vector&& extra_channels); + void ClearExtraChannels() { extra_channels_.clear(); } + bool HasExtraChannels() const { return !extra_channels_.empty(); } + const std::vector& extra_channels() const { return extra_channels_; } + std::vector& extra_channels() { return extra_channels_; } + + const ImageMetadata* metadata() const { return metadata_; } + + void VerifyMetadata() const; + + void SetDecodedBytes(size_t decoded_bytes) { decoded_bytes_ = decoded_bytes; } + size_t decoded_bytes() const { return decoded_bytes_; } + + // -- JPEG transcoding: + + // Returns true if image does or will represent quantized DCT-8 coefficients, + // stored in 8x8 pixel regions. + bool IsJPEG() const { +#if JPEGXL_ENABLE_TRANSCODE_JPEG + return jpeg_data != nullptr; +#else // JPEGXL_ENABLE_TRANSCODE_JPEG + return false; +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + } + + std::unique_ptr jpeg_data; + // these fields are used to signal the input JPEG color space + // NOTE: JPEG doesn't actually provide a way to determine whether YCbCr was + // applied or not. + ColorTransform color_transform = ColorTransform::kNone; + YCbCrChromaSubsampling chroma_subsampling; + + FrameOrigin origin{0, 0}; + // Animation-related information. This assumes GIF- and APNG- like animation. + uint32_t duration = 0; + bool use_for_next_frame = false; + bool blend = false; + BlendMode blendmode = BlendMode::kBlend; + std::string name; + + private: + // Called after any Set* to ensure their sizes are compatible. + void VerifySizes() const; + + // Required for TransformTo so that an ImageBundle is self-sufficient. Always + // points to the same thing, but cannot be const-pointer because that prevents + // the compiler from generating a move ctor. + const ImageMetadata* metadata_; + + // Initialized by Set*: + Image3F color_; // If empty, planes_ is not; all planes equal if IsGray(). + ColorEncoding c_current_; // of color_ + + // Initialized by SetPlanes; size = ImageMetadata.num_extra_channels + std::vector extra_channels_; + + // How many bytes of the input were actually read. + size_t decoded_bytes_ = 0; +}; + +// Does color transformation from in.c_current() to c_desired if the color +// encodings are different, or nothing if they are already the same. +// If color transformation is done, stores the transformed values into store and +// sets the out pointer to store, else leaves store untouched and sets the out +// pointer to &in. +// Returns false if color transform fails. +Status TransformIfNeeded(const ImageBundle& in, const ColorEncoding& c_desired, + ThreadPool* pool, ImageBundle* store, + const ImageBundle** out); + +} // namespace jxl + +#endif // LIB_JXL_IMAGE_BUNDLE_H_ diff --git a/lib/jxl/image_bundle_test.cc b/lib/jxl/image_bundle_test.cc new file mode 100644 index 0000000..6de2e49 --- /dev/null +++ b/lib/jxl/image_bundle_test.cc @@ -0,0 +1,36 @@ +// 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/image_bundle.h" + +#include "gtest/gtest.h" +#include "lib/jxl/aux_out.h" + +namespace jxl { +namespace { + +TEST(ImageBundleTest, ExtraChannelName) { + AuxOut aux_out; + BitWriter writer; + BitWriter::Allotment allotment(&writer, 99); + + ImageMetadata metadata; + ExtraChannelInfo eci; + eci.type = ExtraChannel::kBlack; + eci.name = "testK"; + metadata.extra_channel_info.push_back(std::move(eci)); + ASSERT_TRUE(WriteImageMetadata(metadata, &writer, /*layer=*/0, &aux_out)); + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, /*layer=*/0, &aux_out); + + BitReader reader(writer.GetSpan()); + ImageMetadata metadata_out; + ASSERT_TRUE(ReadImageMetadata(&reader, &metadata_out)); + EXPECT_TRUE(reader.Close()); + EXPECT_EQ("testK", metadata_out.Find(ExtraChannel::kBlack)->name); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/image_metadata.cc b/lib/jxl/image_metadata.cc new file mode 100644 index 0000000..2d9d62e --- /dev/null +++ b/lib/jxl/image_metadata.cc @@ -0,0 +1,414 @@ +// 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/image_metadata.h" + +#include +#include + +#include "lib/jxl/alpha.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/fields.h" + +namespace jxl { +BitDepth::BitDepth() { Bundle::Init(this); } +Status BitDepth::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &floating_point_sample)); + // The same fields (bits_per_sample and exponent_bits_per_sample) are read + // in a different way depending on floating_point_sample's value. It's still + // default-initialized correctly so using visitor->Conditional is not + // required. + if (!floating_point_sample) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val(8), Val(10), Val(12), BitsOffset(6, 1), 8, &bits_per_sample)); + exponent_bits_per_sample = 0; + } else { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val(32), Val(16), Val(24), BitsOffset(6, 1), 32, &bits_per_sample)); + // The encoded value is exponent_bits_per_sample - 1, encoded in 3 bits + // so the value can be in range [1, 8]. + const uint32_t offset = 1; + exponent_bits_per_sample -= offset; + JXL_QUIET_RETURN_IF_ERROR( + visitor->Bits(4, 8 - offset, &exponent_bits_per_sample)); + exponent_bits_per_sample += offset; + } + + // Error-checking for floating point ranges. + if (floating_point_sample) { + if (exponent_bits_per_sample < 2 || exponent_bits_per_sample > 8) { + return JXL_FAILURE("Invalid exponent_bits_per_sample: %u", + exponent_bits_per_sample); + } + int mantissa_bits = + static_cast(bits_per_sample) - exponent_bits_per_sample - 1; + if (mantissa_bits < 2 || mantissa_bits > 23) { + return JXL_FAILURE("Invalid bits_per_sample: %u", bits_per_sample); + } + } else { + if (bits_per_sample > 31) { + return JXL_FAILURE("Invalid bits_per_sample: %u", bits_per_sample); + } + } + return true; +} + +CustomTransformData::CustomTransformData() { Bundle::Init(this); } +Status CustomTransformData::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + if (visitor->Conditional(nonserialized_xyb_encoded)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&opsin_inverse_matrix)); + } + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 0, &custom_weights_mask)); + if (visitor->Conditional((custom_weights_mask & 0x1) != 0)) { + // 4 5x5 kernels, but all of them can be obtained by symmetry from one, + // which is symmetric along its main diagonal. The top-left kernel is + // defined by + // + // 0 1 2 3 4 + // 1 5 6 7 8 + // 2 6 9 10 11 + // 3 7 10 12 13 + // 4 8 11 13 14 + float constexpr kWeights2[15] = { + -0.01716200f, -0.03452303f, -0.04022174f, -0.02921014f, -0.00624645f, + 0.14111091f, 0.28896755f, 0.00278718f, -0.01610267f, 0.56661550f, + 0.03777607f, -0.01986694f, -0.03144731f, -0.01185068f, -0.00213539f}; + for (size_t i = 0; i < 15; i++) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kWeights2[i], &upsampling2_weights[i])); + } + } + if (visitor->Conditional((custom_weights_mask & 0x2) != 0)) { + // 16 5x5 kernels, but all of them can be obtained by symmetry from + // three, two of which are symmetric along their main diagonals. The top + // left 4 kernels are defined by + // + // 0 1 2 3 4 5 6 7 8 9 + // 1 10 11 12 13 14 15 16 17 18 + // 2 11 19 20 21 22 23 24 25 26 + // 3 12 20 27 28 29 30 31 32 33 + // 4 13 21 28 34 35 36 37 38 39 + // + // 5 14 22 29 35 40 41 42 43 44 + // 6 15 23 30 36 41 45 46 47 48 + // 7 16 24 31 37 42 46 49 50 51 + // 8 17 25 32 38 43 47 50 52 53 + // 9 18 26 33 39 44 48 51 53 54 + constexpr float kWeights4[55] = { + -0.02419067f, -0.03491987f, -0.03693351f, -0.03094285f, -0.00529785f, + -0.01663432f, -0.03556863f, -0.03888905f, -0.03516850f, -0.00989469f, + 0.23651958f, 0.33392945f, -0.01073543f, -0.01313181f, -0.03556694f, + 0.13048175f, 0.40103025f, 0.03951150f, -0.02077584f, 0.46914198f, + -0.00209270f, -0.01484589f, -0.04064806f, 0.18942530f, 0.56279892f, + 0.06674400f, -0.02335494f, -0.03551682f, -0.00754830f, -0.02267919f, + -0.02363578f, 0.00315804f, -0.03399098f, -0.01359519f, -0.00091653f, + -0.00335467f, -0.01163294f, -0.01610294f, -0.00974088f, -0.00191622f, + -0.01095446f, -0.03198464f, -0.04455121f, -0.02799790f, -0.00645912f, + 0.06390599f, 0.22963888f, 0.00630981f, -0.01897349f, 0.67537268f, + 0.08483369f, -0.02534994f, -0.02205197f, -0.01667999f, -0.00384443f}; + for (size_t i = 0; i < 55; i++) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kWeights4[i], &upsampling4_weights[i])); + } + } + if (visitor->Conditional((custom_weights_mask & 0x4) != 0)) { + // 64 5x5 kernels, all of them can be obtained by symmetry from + // 10, 4 of which are symmetric along their main diagonals. The top + // left 16 kernels are defined by + // 0 1 2 3 4 5 6 7 8 9 a b c d e f 10 11 12 13 + // 1 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 + // 2 15 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 + // 3 16 28 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 + // 4 17 29 3a 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 + + // 5 18 2a 3b 4b 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 67 68 + // 6 19 2b 3c 4c 5b 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 + // 7 1a 2c 3d 4d 5c 6a 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 + // 8 1b 2d 3e 4e 5d 6b 78 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f + // 9 1c 2e 3f 4f 5e 6c 79 85 90 91 92 93 94 95 96 97 98 99 9a + + // a 1d 2f 40 50 5f 6d 7a 86 91 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 + // b 1e 30 41 51 60 6e 7b 87 92 9c a5 a6 a7 a8 a9 aa ab ac ad + // c 1f 31 42 52 61 6f 7c 88 93 9d a6 ae af b0 b1 b2 b3 b4 b5 + // d 20 32 43 53 62 70 7d 89 94 9e a7 af b6 b7 b8 b9 ba bb bc + // e 21 33 44 54 63 71 7e 8a 95 9f a8 b0 b7 bd be bf c0 c1 c2 + + // f 22 34 45 55 64 72 7f 8b 96 a0 a9 b1 b8 be c3 c4 c5 c6 c7 + // 10 23 35 46 56 65 73 80 8c 97 a1 aa b2 b9 bf c4 c8 c9 ca cb + // 11 24 36 47 57 66 74 81 8d 98 a2 ab b3 ba c0 c5 c9 cc cd ce + // 12 25 37 48 58 67 75 82 8e 99 a3 ac b4 bb c1 c6 ca cd cf d0 + // 13 26 38 49 59 68 76 83 8f 9a a4 ad b5 bc c2 c7 cb ce d0 d1 + constexpr float kWeights8[210] = { + -0.02928613f, -0.03706353f, -0.03783812f, -0.03324558f, -0.00447632f, + -0.02519406f, -0.03752601f, -0.03901508f, -0.03663285f, -0.00646649f, + -0.02066407f, -0.03838633f, -0.04002101f, -0.03900035f, -0.00901973f, + -0.01626393f, -0.03954148f, -0.04046620f, -0.03979621f, -0.01224485f, + 0.29895328f, 0.35757708f, -0.02447552f, -0.01081748f, -0.04314594f, + 0.23903219f, 0.41119301f, -0.00573046f, -0.01450239f, -0.04246845f, + 0.17567618f, 0.45220643f, 0.02287757f, -0.01936783f, -0.03583255f, + 0.11572472f, 0.47416733f, 0.06284440f, -0.02685066f, 0.42720050f, + -0.02248939f, -0.01155273f, -0.04562755f, 0.28689496f, 0.49093869f, + -0.00007891f, -0.01545926f, -0.04562659f, 0.21238920f, 0.53980934f, + 0.03369474f, -0.02070211f, -0.03866988f, 0.14229550f, 0.56593398f, + 0.08045181f, -0.02888298f, -0.03680918f, -0.00542229f, -0.02920477f, + -0.02788574f, -0.02118180f, -0.03942402f, -0.00775547f, -0.02433614f, + -0.03193943f, -0.02030828f, -0.04044014f, -0.01074016f, -0.01930822f, + -0.03620399f, -0.01974125f, -0.03919545f, -0.01456093f, -0.00045072f, + -0.00360110f, -0.01020207f, -0.01231907f, -0.00638988f, -0.00071592f, + -0.00279122f, -0.00957115f, -0.01288327f, -0.00730937f, -0.00107783f, + -0.00210156f, -0.00890705f, -0.01317668f, -0.00813895f, -0.00153491f, + -0.02128481f, -0.04173044f, -0.04831487f, -0.03293190f, -0.00525260f, + -0.01720322f, -0.04052736f, -0.05045706f, -0.03607317f, -0.00738030f, + -0.01341764f, -0.03965629f, -0.05151616f, -0.03814886f, -0.01005819f, + 0.18968273f, 0.33063684f, -0.01300105f, -0.01372950f, -0.04017465f, + 0.13727832f, 0.36402234f, 0.01027890f, -0.01832107f, -0.03365072f, + 0.08734506f, 0.38194295f, 0.04338228f, -0.02525993f, 0.56408126f, + 0.00458352f, -0.01648227f, -0.04887868f, 0.24585519f, 0.62026135f, + 0.04314807f, -0.02213737f, -0.04158014f, 0.16637289f, 0.65027023f, + 0.09621636f, -0.03101388f, -0.04082742f, -0.00904519f, -0.02790922f, + -0.02117818f, 0.00798662f, -0.03995711f, -0.01243427f, -0.02231705f, + -0.02946266f, 0.00992055f, -0.03600283f, -0.01684920f, -0.00111684f, + -0.00411204f, -0.01297130f, -0.01723725f, -0.01022545f, -0.00165306f, + -0.00313110f, -0.01218016f, -0.01763266f, -0.01125620f, -0.00231663f, + -0.01374149f, -0.03797620f, -0.05142937f, -0.03117307f, -0.00581914f, + -0.01064003f, -0.03608089f, -0.05272168f, -0.03375670f, -0.00795586f, + 0.09628104f, 0.27129991f, -0.00353779f, -0.01734151f, -0.03153981f, + 0.05686230f, 0.28500998f, 0.02230594f, -0.02374955f, 0.68214326f, + 0.05018048f, -0.02320852f, -0.04383616f, 0.18459474f, 0.71517975f, + 0.10805613f, -0.03263677f, -0.03637639f, -0.01394373f, -0.02511203f, + -0.01728636f, 0.05407331f, -0.02867568f, -0.01893131f, -0.00240854f, + -0.00446511f, -0.01636187f, -0.02377053f, -0.01522848f, -0.00333334f, + -0.00819975f, -0.02964169f, -0.04499287f, -0.02745350f, -0.00612408f, + 0.02727416f, 0.19446600f, 0.00159832f, -0.02232473f, 0.74982506f, + 0.11452620f, -0.03348048f, -0.01605681f, -0.02070339f, -0.00458223f}; + for (size_t i = 0; i < 210; i++) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kWeights8[i], &upsampling8_weights[i])); + } + } + return true; +} + +ExtraChannelInfo::ExtraChannelInfo() { Bundle::Init(this); } +Status ExtraChannelInfo::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + // General + JXL_QUIET_RETURN_IF_ERROR(visitor->Enum(ExtraChannel::kAlpha, &type)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&bit_depth)); + + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(3), Val(4), BitsOffset(3, 1), 0, &dim_shift)); + if ((1U << dim_shift) > 8) { + return JXL_FAILURE("dim_shift %u too large", dim_shift); + } + + JXL_QUIET_RETURN_IF_ERROR(VisitNameString(visitor, &name)); + + // Conditional + if (visitor->Conditional(type == ExtraChannel::kAlpha)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &alpha_associated)); + } + if (visitor->Conditional(type == ExtraChannel::kSpotColor)) { + for (float& c : spot_color) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0, &c)); + } + } + if (visitor->Conditional(type == ExtraChannel::kCFA)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(1), Bits(2), BitsOffset(4, 3), + BitsOffset(8, 19), 1, &cfa_channel)); + } + return true; +} + +ImageMetadata::ImageMetadata() { Bundle::Init(this); } +Status ImageMetadata::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + // Bundle::AllDefault does not allow usage when reading (it may abort the + // program when a codestream has invalid values), but when reading we + // overwrite the extra_fields value, so do not need to call AllDefault. + bool tone_mapping_default = + visitor->IsReading() ? false : Bundle::AllDefault(tone_mapping); + + bool extra_fields = (orientation != 1 || have_preview || have_animation || + have_intrinsic_size || !tone_mapping_default); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &extra_fields)); + if (visitor->Conditional(extra_fields)) { + orientation--; + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(3, 0, &orientation)); + orientation++; + // (No need for bounds checking because we read exactly 3 bits) + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &have_intrinsic_size)); + if (visitor->Conditional(have_intrinsic_size)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&intrinsic_size)); + } + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &have_preview)); + if (visitor->Conditional(have_preview)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&preview_size)); + } + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &have_animation)); + if (visitor->Conditional(have_animation)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&animation)); + } + } else { + orientation = 1; // identity + have_intrinsic_size = false; + have_preview = false; + have_animation = false; + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&bit_depth)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->Bool(true, &modular_16_bit_buffer_sufficient)); + + num_extra_channels = extra_channel_info.size(); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(0), Val(1), BitsOffset(4, 2), + BitsOffset(12, 1), 0, + &num_extra_channels)); + + if (visitor->Conditional(num_extra_channels != 0)) { + if (visitor->IsReading()) { + extra_channel_info.resize(num_extra_channels); + } + for (ExtraChannelInfo& eci : extra_channel_info) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&eci)); + } + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(true, &xyb_encoded)); + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&color_encoding)); + if (visitor->Conditional(extra_fields)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&tone_mapping)); + } + + // Treat as if only the fields up to extra channels exist. + if (visitor->IsReading() && nonserialized_only_parse_basic_info) { + return true; + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions)); + // Extensions: in chronological order of being added to the format. + return visitor->EndExtensions(); +} + +OpsinInverseMatrix::OpsinInverseMatrix() { Bundle::Init(this); } +Status OpsinInverseMatrix::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + for (int i = 0; i < 9; ++i) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16( + DefaultInverseOpsinAbsorbanceMatrix()[i], &inverse_matrix[i])); + } + for (int i = 0; i < 3; ++i) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kNegOpsinAbsorbanceBiasRGB[i], &opsin_biases[i])); + } + for (int i = 0; i < 4; ++i) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kDefaultQuantBias[i], &quant_biases[i])); + } + return true; +} + +ToneMapping::ToneMapping() { Bundle::Init(this); } +Status ToneMapping::VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(kDefaultIntensityTarget, &intensity_target)); + if (intensity_target <= 0.f) { + return JXL_FAILURE("invalid intensity target"); + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.0f, &min_nits)); + if (min_nits < 0.f || min_nits > intensity_target) { + return JXL_FAILURE("invalid min %f vs max %f", min_nits, intensity_target); + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &relative_to_max_display)); + + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.0f, &linear_below)); + if (linear_below < 0 || (relative_to_max_display && linear_below > 1.0f)) { + return JXL_FAILURE("invalid linear_below %f (%s)", linear_below, + relative_to_max_display ? "relative" : "absolute"); + } + + return true; +} + +Status ReadImageMetadata(BitReader* JXL_RESTRICT reader, + ImageMetadata* JXL_RESTRICT metadata) { + return Bundle::Read(reader, metadata); +} + +Status WriteImageMetadata(const ImageMetadata& metadata, + BitWriter* JXL_RESTRICT writer, size_t layer, + AuxOut* aux_out) { + return Bundle::Write(metadata, writer, layer, aux_out); +} + +void ImageMetadata::SetAlphaBits(uint32_t bits, bool alpha_is_premultiplied) { + std::vector& eciv = extra_channel_info; + ExtraChannelInfo* alpha = Find(ExtraChannel::kAlpha); + if (bits == 0) { + if (alpha != nullptr) { + // Remove the alpha channel from the extra channel info. It's + // theoretically possible that there are multiple, remove all in that + // case. This ensure a next HasAlpha() will return false. + const auto is_alpha = [](const ExtraChannelInfo& eci) { + return eci.type == ExtraChannel::kAlpha; + }; + eciv.erase(std::remove_if(eciv.begin(), eciv.end(), is_alpha), + eciv.end()); + } + } else { + if (alpha == nullptr) { + ExtraChannelInfo info; + info.type = ExtraChannel::kAlpha; + info.bit_depth.bits_per_sample = bits; + info.dim_shift = 0; + info.alpha_associated = alpha_is_premultiplied; + // Prepend rather than append: in case there already are other extra + // channels, prefer alpha channel to be listed first. + eciv.insert(eciv.begin(), info); + } else { + // Ignores potential extra alpha channels, only sets to first one. + alpha->bit_depth.bits_per_sample = bits; + alpha->bit_depth.floating_point_sample = false; + alpha->bit_depth.exponent_bits_per_sample = 0; + alpha->alpha_associated = alpha_is_premultiplied; + } + } + num_extra_channels = extra_channel_info.size(); + if (bits > 12) modular_16_bit_buffer_sufficient = false; +} +} // namespace jxl diff --git a/lib/jxl/image_metadata.h b/lib/jxl/image_metadata.h new file mode 100644 index 0000000..e5f7969 --- /dev/null +++ b/lib/jxl/image_metadata.h @@ -0,0 +1,410 @@ +// 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. + +// Main codestream header bundles, the metadata that applies to all frames. + +#ifndef LIB_JXL_IMAGE_METADATA_H_ +#define LIB_JXL_IMAGE_METADATA_H_ + +#include +#include + +#include + +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/jpeg/jpeg_data.h" +#include "lib/jxl/opsin_params.h" + +namespace jxl { + +// EXIF orientation of the image. This field overrides any field present in +// actual EXIF metadata. The value tells which transformation the decoder must +// apply after decoding to display the image with the correct orientation. +enum class Orientation : uint32_t { + // Values 1..8 match the EXIF definitions. + kIdentity = 1, + kFlipHorizontal, + kRotate180, + kFlipVertical, + kTranspose, + kRotate90, + kAntiTranspose, + kRotate270, +}; +// Don't need an EnumBits because Orientation is not read via Enum(). + +enum class ExtraChannel : uint32_t { + // First two enumerators (most common) are cheaper to encode + kAlpha, + kDepth, + + kSpotColor, + kSelectionMask, + kBlack, // for CMYK + kCFA, // Bayer channel + kThermal, + kReserved0, + kReserved1, + kReserved2, + kReserved3, + kReserved4, + kReserved5, + kReserved6, + kReserved7, + kUnknown, // disambiguated via name string, raise warning if unsupported + kOptional // like kUnknown but can silently be ignored +}; +static inline const char* EnumName(ExtraChannel /*unused*/) { + return "ExtraChannel"; +} +static inline constexpr uint64_t EnumBits(ExtraChannel /*unused*/) { + using EC = ExtraChannel; + return MakeBit(EC::kAlpha) | MakeBit(EC::kDepth) | MakeBit(EC::kSpotColor) | + MakeBit(EC::kSelectionMask) | MakeBit(EC::kBlack) | MakeBit(EC::kCFA) | + MakeBit(EC::kUnknown) | MakeBit(EC::kOptional); +} + +// Used in ImageMetadata and ExtraChannelInfo. +struct BitDepth : public Fields { + BitDepth(); + const char* Name() const override { return "BitDepth"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Whether the original (uncompressed) samples are floating point or + // unsigned integer. + bool floating_point_sample; + + // Bit depth of the original (uncompressed) image samples. Must be in the + // range [1, 32]. + uint32_t bits_per_sample; + + // Floating point exponent bits of the original (uncompressed) image samples, + // only used if floating_point_sample is true. + // If used, the samples are floating point with: + // - 1 sign bit + // - exponent_bits_per_sample exponent bits + // - (bits_per_sample - exponent_bits_per_sample - 1) mantissa bits + // If used, exponent_bits_per_sample must be in the range + // [2, 8] and amount of mantissa bits must be in the range [2, 23]. + // NOTE: exponent_bits_per_sample is 8 for single precision binary32 + // point, 5 for half precision binary16, 7 for fp24. + uint32_t exponent_bits_per_sample; +}; + +// Describes one extra channel. +struct ExtraChannelInfo : public Fields { + ExtraChannelInfo(); + const char* Name() const override { return "ExtraChannelInfo"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + mutable bool all_default; + + ExtraChannel type; + BitDepth bit_depth; + uint32_t dim_shift; // downsampled by 2^dim_shift on each axis + + std::string name; // UTF-8 + + // Conditional: + bool alpha_associated; // i.e. premultiplied + float spot_color[4]; // spot color in linear RGBA + uint32_t cfa_channel; +}; + +struct OpsinInverseMatrix : public Fields { + OpsinInverseMatrix(); + const char* Name() const override { return "OpsinInverseMatrix"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + mutable bool all_default; + + float inverse_matrix[9]; + float opsin_biases[3]; + float quant_biases[4]; +}; + +// Information useful for mapping HDR images to lower dynamic range displays. +struct ToneMapping : public Fields { + ToneMapping(); + const char* Name() const override { return "ToneMapping"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + mutable bool all_default; + + // Upper bound on the intensity level present in the image. For unsigned + // integer pixel encodings, this is the brightness of the largest + // representable value. The image does not necessarily contain a pixel + // actually this bright. An encoder is allowed to set 255 for SDR images + // without computing a histogram. + float intensity_target; // [nits] + + // Lower bound on the intensity level present in the image. This may be + // loose, i.e. lower than the actual darkest pixel. When tone mapping, a + // decoder will map [min_nits, intensity_target] to the display range. + float min_nits; + + bool relative_to_max_display; // see below + // The tone mapping will leave unchanged (linear mapping) any pixels whose + // brightness is strictly below this. The interpretation depends on + // relative_to_max_display. If true, this is a ratio [0, 1] of the maximum + // display brightness [nits], otherwise an absolute brightness [nits]. + float linear_below; +}; + +// Contains weights to customize some trasnforms - in particular, XYB and +// upsampling. +struct CustomTransformData : public Fields { + CustomTransformData(); + const char* Name() const override { return "CustomTransformData"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Must be set before calling VisitFields. Must equal xyb_encoded of + // ImageMetadata, should be set by ImageMetadata during VisitFields. + bool nonserialized_xyb_encoded = false; + + mutable bool all_default; + + OpsinInverseMatrix opsin_inverse_matrix; + + uint32_t custom_weights_mask; + float upsampling2_weights[15]; + float upsampling4_weights[55]; + float upsampling8_weights[210]; +}; + +// Properties of the original image bundle. This enables Encode(Decode()) to +// re-create an equivalent image without user input. +struct ImageMetadata : public Fields { + ImageMetadata(); + const char* Name() const override { return "ImageMetadata"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + // Returns bit depth of the JPEG XL compressed alpha channel, or 0 if no alpha + // channel present. In the theoretical case that there are multiple alpha + // channels, returns the bit depht of the first. + uint32_t GetAlphaBits() const { + const ExtraChannelInfo* alpha = Find(ExtraChannel::kAlpha); + if (alpha == nullptr) return 0; + JXL_ASSERT(alpha->bit_depth.bits_per_sample != 0); + return alpha->bit_depth.bits_per_sample; + } + + // Sets bit depth of alpha channel, adding extra channel if needed, or + // removing all alpha channels if bits is 0. + // Assumes integer alpha channel and not designed to support multiple + // alpha channels (it's possible to use those features by manipulating + // extra_channel_info directly). + // + // Callers must insert the actual channel image at the same index before any + // further modifications to extra_channel_info. + void SetAlphaBits(uint32_t bits, bool alpha_is_premultiplied = false); + + bool HasAlpha() const { return GetAlphaBits() != 0; } + + // Sets the original bit depth fields to indicate unsigned integer of the + // given bit depth. + // TODO(lode): move function to BitDepth + void SetUintSamples(uint32_t bits) { + bit_depth.bits_per_sample = bits; + bit_depth.exponent_bits_per_sample = 0; + bit_depth.floating_point_sample = false; + // RCT / Squeeze may add one bit each, and this is about int16_t, + // so uint13 should still be OK but limiting it to 12 seems safer. + // TODO(jon): figure out a better way to set this header field. + // (in particular, if modular mode is not used it doesn't matter, + // and if transforms are restricted, up to 15-bit could be done) + if (bits > 12) modular_16_bit_buffer_sufficient = false; + } + // Sets the original bit depth fields to indicate single precision floating + // point. + // TODO(lode): move function to BitDepth + void SetFloat32Samples() { + bit_depth.bits_per_sample = 32; + bit_depth.exponent_bits_per_sample = 8; + bit_depth.floating_point_sample = true; + modular_16_bit_buffer_sufficient = false; + } + + void SetFloat16Samples() { + bit_depth.bits_per_sample = 16; + bit_depth.exponent_bits_per_sample = 5; + bit_depth.floating_point_sample = true; + modular_16_bit_buffer_sufficient = false; + } + + void SetIntensityTarget(float intensity_target) { + tone_mapping.intensity_target = intensity_target; + } + float IntensityTarget() const { + JXL_ASSERT(tone_mapping.intensity_target != 0); + return tone_mapping.intensity_target; + } + + // Returns first ExtraChannelInfo of the given type, or nullptr if none. + const ExtraChannelInfo* Find(ExtraChannel type) const { + for (const ExtraChannelInfo& eci : extra_channel_info) { + if (eci.type == type) return &eci; + } + return nullptr; + } + + // Returns first ExtraChannelInfo of the given type, or nullptr if none. + ExtraChannelInfo* Find(ExtraChannel type) { + for (ExtraChannelInfo& eci : extra_channel_info) { + if (eci.type == type) return &eci; + } + return nullptr; + } + + Orientation GetOrientation() const { + return static_cast(orientation); + } + + bool ExtraFieldsDefault() const; + + mutable bool all_default; + + BitDepth bit_depth; + bool modular_16_bit_buffer_sufficient; // otherwise 32 is. + + // Whether the colors values of the pixels of frames are encoded in the + // codestream using the absolute XYB color space, or the using values that + // follow the color space defined by the ColorEncoding or ICC profile. This + // determines when or whether a CMS (Color Management System) is needed to get + // the pixels in a desired color space. In one case, the pixels have one known + // color space and a CMS is needed to convert them to the original image's + // color space, in the other case the pixels have the color space of the + // original image and a CMS is required if a different display space, or a + // single known consistent color space for multiple decoded images, is + // desired. In all cases, the color space of all frames from a single image is + // the same, both VarDCT and modular frames. + // + // If true: then frames can be decoded to XYB (which can also be converted to + // linear and non-linear sRGB with the built in conversion without CMS). The + // attached ColorEncoding or ICC profile has no effect on the meaning of the + // pixel's color values, but instead indicates what the color profile of the + // original image was, and what color profile one should convert to when + // decoding to integers to prevent clipping and precision loss. To do that + // conversion requires a CMS. + // + // If false: then the color values of decoded frames are in the space defined + // by the attached ColorEncoding or ICC profile. To instead get the pixels in + // a chosen known color space, such as sRGB, requires a CMS, since the + // attached ColorEncoding or ICC profile could be any arbitrary color space. + // This mode is typically used for lossless images encoded as integers. + // Frames can also use YCbCr encoding, some frames may and some may not, but + // this is not a different color space but a certain encoding of the RGB + // values. + // + // Note: if !xyb_encoded, but the attached color profile indicates XYB (which + // can happen either if it's a ColorEncoding with color_space_ == + // ColorSpace::kXYB, or if it's an ICC Profile that has been crafted to + // represent XYB), then the frames still may not use ColorEncoding kXYB, they + // must still use kNone (or kYCbCr, which would mean applying the YCbCr + // transform to the 3-channel XYB data), since with !xyb_encoded, the 3 + // channels are stored as-is, no matter what meaning the color profile assigns + // to them. To use ColorEncoding::kXYB, xyb_encoded must be true. + // + // This value is defined in image metadata because this is the global + // codestream header. This value does not affect the image itself, so is not + // image metadata per se, it only affects the encoding, and what color space + // the decoder can receive the pixels in without needing a CMS. + bool xyb_encoded; + + ColorEncoding color_encoding; + + // These values are initialized to defaults such that the 'extra_fields' + // condition in VisitFields uses correctly initialized values. + uint32_t orientation = 1; + bool have_preview = false; + bool have_animation = false; + bool have_intrinsic_size = false; + + // If present, the stored image has the dimensions of the first SizeHeader, + // but decoders are advised to resample or display per `intrinsic_size`. + SizeHeader intrinsic_size; // only if have_intrinsic_size + + ToneMapping tone_mapping; + + // When reading: deserialized. When writing: automatically set from vector. + uint32_t num_extra_channels; + std::vector extra_channel_info; + + // Only present if m.have_preview. + PreviewHeader preview_size; + // Only present if m.have_animation. + AnimationHeader animation; + + uint64_t extensions; + + // Option to stop parsing after basic info, and treat as if the later + // fields do not participate. Use to parse only basic image information + // excluding the final larger or variable sized data. + bool nonserialized_only_parse_basic_info = false; +}; + +Status ReadImageMetadata(BitReader* JXL_RESTRICT reader, + ImageMetadata* JXL_RESTRICT metadata); + +Status WriteImageMetadata(const ImageMetadata& metadata, + BitWriter* JXL_RESTRICT writer, size_t layer, + AuxOut* aux_out); + +// All metadata applicable to the entire codestream (dimensions, extra channels, +// ...) +struct CodecMetadata { + // TODO(lode): use the preview and animation fields too, in place of the + // nonserialized_ ones in ImageMetadata. + ImageMetadata m; + // The size of the codestream: this is the nominal size applicable to all + // frames, although some frames can have a different effective size through + // crop, dc_level or representing a the preview. + SizeHeader size; + // Often default. + CustomTransformData transform_data; + + size_t xsize() const { return size.xsize(); } + size_t ysize() const { return size.ysize(); } + size_t oriented_xsize(bool keep_orientation) const { + if (static_cast(m.GetOrientation()) > 4 && !keep_orientation) { + return ysize(); + } else { + return xsize(); + } + } + size_t oriented_preview_xsize(bool keep_orientation) const { + if (static_cast(m.GetOrientation()) > 4 && !keep_orientation) { + return m.preview_size.ysize(); + } else { + return m.preview_size.xsize(); + } + } + size_t oriented_ysize(bool keep_orientation) const { + if (static_cast(m.GetOrientation()) > 4 && !keep_orientation) { + return xsize(); + } else { + return ysize(); + } + } + size_t oriented_preview_ysize(bool keep_orientation) const { + if (static_cast(m.GetOrientation()) > 4 && !keep_orientation) { + return m.preview_size.xsize(); + } else { + return m.preview_size.ysize(); + } + } +}; + +} // namespace jxl + +#endif // LIB_JXL_IMAGE_METADATA_H_ diff --git a/lib/jxl/image_ops.h b/lib/jxl/image_ops.h new file mode 100644 index 0000000..f3c2b59 --- /dev/null +++ b/lib/jxl/image_ops.h @@ -0,0 +1,814 @@ +// 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_IMAGE_OPS_H_ +#define LIB_JXL_IMAGE_OPS_H_ + +// Operations on images. + +#include +#include +#include +#include + +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" + +namespace jxl { + +template +void CopyImageTo(const Plane& from, Plane* JXL_RESTRICT to) { + PROFILER_ZONE("CopyImage1"); + JXL_ASSERT(SameSize(from, *to)); + if (from.ysize() == 0 || from.xsize() == 0) return; + for (size_t y = 0; y < from.ysize(); ++y) { + const T* JXL_RESTRICT row_from = from.ConstRow(y); + T* JXL_RESTRICT row_to = to->Row(y); + memcpy(row_to, row_from, from.xsize() * sizeof(T)); + } +} + +// DEPRECATED - prefer to preallocate result. +template +Plane CopyImage(const Plane& from) { + Plane to(from.xsize(), from.ysize()); + CopyImageTo(from, &to); + return to; +} + +// Copies `from:rect_from` to `to:rect_to`. +template +void CopyImageTo(const Rect& rect_from, const Plane& from, + const Rect& rect_to, Plane* JXL_RESTRICT to) { + PROFILER_ZONE("CopyImageR"); + JXL_DASSERT(SameSize(rect_from, rect_to)); + JXL_DASSERT(rect_from.IsInside(from)); + JXL_DASSERT(rect_to.IsInside(*to)); + if (rect_from.xsize() == 0) return; + for (size_t y = 0; y < rect_from.ysize(); ++y) { + const T* JXL_RESTRICT row_from = rect_from.ConstRow(from, y); + T* JXL_RESTRICT row_to = rect_to.Row(to, y); + memcpy(row_to, row_from, rect_from.xsize() * sizeof(T)); + } +} + +// DEPRECATED - Returns a copy of the "image" pixels that lie in "rect". +template +Plane CopyImage(const Rect& rect, const Plane& image) { + Plane copy(rect.xsize(), rect.ysize()); + CopyImageTo(rect, image, ©); + return copy; +} + +// Copies `from:rect_from` to `to:rect_to`. +template +void CopyImageTo(const Rect& rect_from, const Image3& from, + const Rect& rect_to, Image3* JXL_RESTRICT to) { + PROFILER_ZONE("CopyImageR"); + JXL_ASSERT(SameSize(rect_from, rect_to)); + for (size_t c = 0; c < 3; c++) { + CopyImageTo(rect_from, from.Plane(c), rect_to, &to->Plane(c)); + } +} + +template +void ConvertPlaneAndClamp(const Rect& rect_from, const Plane& from, + const Rect& rect_to, Plane* JXL_RESTRICT to) { + PROFILER_ZONE("ConvertPlane"); + JXL_ASSERT(SameSize(rect_from, rect_to)); + using M = decltype(T() + U()); + for (size_t y = 0; y < rect_to.ysize(); ++y) { + const T* JXL_RESTRICT row_from = rect_from.ConstRow(from, y); + U* JXL_RESTRICT row_to = rect_to.Row(to, y); + for (size_t x = 0; x < rect_to.xsize(); ++x) { + row_to[x] = + std::min(std::max(row_from[x], std::numeric_limits::min()), + std::numeric_limits::max()); + } + } +} + +// Copies `from` to `to`. +template +void CopyImageTo(const T& from, T* JXL_RESTRICT to) { + return CopyImageTo(Rect(from), from, Rect(*to), to); +} + +// Copies `from:rect_from` to `to`. +template +void CopyImageTo(const Rect& rect_from, const T& from, T* JXL_RESTRICT to) { + return CopyImageTo(rect_from, from, Rect(*to), to); +} + +// Copies `from` to `to:rect_to`. +template +void CopyImageTo(const T& from, const Rect& rect_to, T* JXL_RESTRICT to) { + return CopyImageTo(Rect(from), from, rect_to, to); +} + +// Copies `from:rect_from` to `to:rect_to`; also copies `padding` pixels of +// border around `from:rect_from`, in all directions, whenever they are inside +// the first image. +template +void CopyImageToWithPadding(const Rect& from_rect, const T& from, + size_t padding, const Rect& to_rect, T* to) { + size_t xextra0 = std::min(padding, from_rect.x0()); + size_t xextra1 = + std::min(padding, from.xsize() - from_rect.x0() - from_rect.xsize()); + size_t yextra0 = std::min(padding, from_rect.y0()); + size_t yextra1 = + std::min(padding, from.ysize() - from_rect.y0() - from_rect.ysize()); + JXL_DASSERT(to_rect.x0() >= xextra0); + JXL_DASSERT(to_rect.y0() >= yextra0); + + return CopyImageTo(Rect(from_rect.x0() - xextra0, from_rect.y0() - yextra0, + from_rect.xsize() + xextra0 + xextra1, + from_rect.ysize() + yextra0 + yextra1), + from, + Rect(to_rect.x0() - xextra0, to_rect.y0() - yextra0, + to_rect.xsize() + xextra0 + xextra1, + to_rect.ysize() + yextra0 + yextra1), + to); +} + +// DEPRECATED - prefer to preallocate result. +template +Image3 CopyImage(const Image3& from) { + Image3 copy(from.xsize(), from.ysize()); + CopyImageTo(from, ©); + return copy; +} + +// DEPRECATED - prefer to preallocate result. +template +Image3 CopyImage(const Rect& rect, const Image3& from) { + Image3 to(rect.xsize(), rect.ysize()); + CopyImageTo(rect, from.Plane(0), to.Plane(0)); + CopyImageTo(rect, from.Plane(1), to.Plane(1)); + CopyImageTo(rect, from.Plane(2), to.Plane(2)); + return to; +} + +// Sets "thickness" pixels on each border to "value". This is faster than +// initializing the entire image and overwriting valid/interior pixels. +template +void SetBorder(const size_t thickness, const T value, Image3* image) { + const size_t xsize = image->xsize(); + const size_t ysize = image->ysize(); + // Top: fill entire row + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < std::min(thickness, ysize); ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + std::fill(row, row + xsize, value); + } + + // Bottom: fill entire row + for (size_t y = ysize - thickness; y < ysize; ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + std::fill(row, row + xsize, value); + } + + // Left/right: fill the 'columns' on either side, but only if the image is + // big enough that they don't already belong to the top/bottom rows. + if (ysize >= 2 * thickness) { + for (size_t y = thickness; y < ysize - thickness; ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + std::fill(row, row + thickness, value); + std::fill(row + xsize - thickness, row + xsize, value); + } + } + } +} + +template +void Subtract(const ImageIn& image1, const ImageIn& image2, ImageOut* out) { + using T = typename ImageIn::T; + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + + for (size_t y = 0; y < ysize; ++y) { + const T* const JXL_RESTRICT row1 = image1.Row(y); + const T* const JXL_RESTRICT row2 = image2.Row(y); + T* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] - row2[x]; + } + } +} + +// In-place. +template +void SubtractFrom(const Plane& what, Plane* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstRow(y); + Tout* JXL_RESTRICT row_to = to->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] -= row_what[x]; + } + } +} + +// In-place. +template +void AddTo(const Plane& what, Plane* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstRow(y); + Tout* JXL_RESTRICT row_to = to->Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } +} + +template +void AddTo(Rect rectFrom, const Plane& what, Rect rectTo, + Plane* to) { + JXL_ASSERT(SameSize(rectFrom, rectTo)); + const size_t xsize = rectTo.xsize(); + const size_t ysize = rectTo.ysize(); + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = rectFrom.ConstRow(what, y); + Tout* JXL_RESTRICT row_to = rectTo.Row(to, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } +} + +// Returns linear combination of two grayscale images. +template +Plane LinComb(const T lambda1, const Plane& image1, const T lambda2, + const Plane& image2) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + Plane out(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const T* const JXL_RESTRICT row1 = image1.Row(y); + const T* const JXL_RESTRICT row2 = image2.Row(y); + T* const JXL_RESTRICT row_out = out.Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = lambda1 * row1[x] + lambda2 * row2[x]; + } + } + return out; +} + +// Returns a pixel-by-pixel multiplication of image by lambda. +template +Plane ScaleImage(const T lambda, const Plane& image) { + Plane out(image.xsize(), image.ysize()); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* const JXL_RESTRICT row = image.Row(y); + T* const JXL_RESTRICT row_out = out.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = lambda * row[x]; + } + } + return out; +} + +// Multiplies image by lambda in-place +template +void ScaleImage(const T lambda, Plane* image) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* const JXL_RESTRICT row = image->Row(y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = lambda * row[x]; + } + } +} + +template +Plane Product(const Plane& a, const Plane& b) { + Plane c(a.xsize(), a.ysize()); + for (size_t y = 0; y < a.ysize(); ++y) { + const T* const JXL_RESTRICT row_a = a.Row(y); + const T* const JXL_RESTRICT row_b = b.Row(y); + T* const JXL_RESTRICT row_c = c.Row(y); + for (size_t x = 0; x < a.xsize(); ++x) { + row_c[x] = row_a[x] * row_b[x]; + } + } + return c; +} + +float DotProduct(const ImageF& a, const ImageF& b); + +template +void FillImage(const T value, Plane* image) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* const JXL_RESTRICT row = image->Row(y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = value; + } + } +} + +template +void ZeroFillImage(Plane* image) { + if (image->xsize() == 0) return; + for (size_t y = 0; y < image->ysize(); ++y) { + T* const JXL_RESTRICT row = image->Row(y); + memset(row, 0, image->xsize() * sizeof(T)); + } +} + +// Mirrors out of bounds coordinates and returns valid coordinates unchanged. +// We assume the radius (distance outside the image) is small compared to the +// image size, otherwise this might not terminate. +// The mirror is outside the last column (border pixel is also replicated). +static inline int64_t Mirror(int64_t x, const int64_t xsize) { + JXL_DASSERT(xsize != 0); + + // TODO(janwas): replace with branchless version + while (x < 0 || x >= xsize) { + if (x < 0) { + x = -x - 1; + } else { + x = 2 * xsize - 1 - x; + } + } + return x; +} + +// Wrap modes for ensuring X/Y coordinates are in the valid range [0, size): + +// Mirrors (repeating the edge pixel once). Useful for convolutions. +struct WrapMirror { + JXL_INLINE int64_t operator()(const int64_t coord, const int64_t size) const { + return Mirror(coord, size); + } +}; + +// Returns the same coordinate: required for TFNode with Border(), or useful +// when we know "coord" is already valid (e.g. interior of an image). +struct WrapUnchanged { + JXL_INLINE int64_t operator()(const int64_t coord, int64_t /*size*/) const { + return coord; + } +}; + +// Similar to Wrap* but for row pointers (reduces Row() multiplications). + +class WrapRowMirror { + public: + template + WrapRowMirror(const ImageOrView& image, size_t ysize) + : first_row_(image.ConstRow(0)), last_row_(image.ConstRow(ysize - 1)) {} + + const float* operator()(const float* const JXL_RESTRICT row, + const int64_t stride) const { + if (row < first_row_) { + const int64_t num_before = first_row_ - row; + // Mirrored; one row before => row 0, two before = row 1, ... + return first_row_ + num_before - stride; + } + if (row > last_row_) { + const int64_t num_after = row - last_row_; + // Mirrored; one row after => last row, two after = last - 1, ... + return last_row_ - num_after + stride; + } + return row; + } + + private: + const float* const JXL_RESTRICT first_row_; + const float* const JXL_RESTRICT last_row_; +}; + +struct WrapRowUnchanged { + JXL_INLINE const float* operator()(const float* const JXL_RESTRICT row, + int64_t /*stride*/) const { + return row; + } +}; + +// Sets "thickness" pixels on each border to "value". This is faster than +// initializing the entire image and overwriting valid/interior pixels. +template +void SetBorder(const size_t thickness, const T value, Plane* image) { + const size_t xsize = image->xsize(); + const size_t ysize = image->ysize(); + // Top: fill entire row + for (size_t y = 0; y < std::min(thickness, ysize); ++y) { + T* const JXL_RESTRICT row = image->Row(y); + std::fill(row, row + xsize, value); + } + + // Bottom: fill entire row + for (size_t y = ysize - thickness; y < ysize; ++y) { + T* const JXL_RESTRICT row = image->Row(y); + std::fill(row, row + xsize, value); + } + + // Left/right: fill the 'columns' on either side, but only if the image is + // big enough that they don't already belong to the top/bottom rows. + if (ysize >= 2 * thickness) { + for (size_t y = thickness; y < ysize - thickness; ++y) { + T* const JXL_RESTRICT row = image->Row(y); + std::fill(row, row + thickness, value); + std::fill(row + xsize - thickness, row + xsize, value); + } + } +} + +// Computes the minimum and maximum pixel value. +template +void ImageMinMax(const Plane& image, T* const JXL_RESTRICT min, + T* const JXL_RESTRICT max) { + *min = std::numeric_limits::max(); + *max = std::numeric_limits::lowest(); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* const JXL_RESTRICT row = image.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + *min = std::min(*min, row[x]); + *max = std::max(*max, row[x]); + } + } +} + +// Copies pixels, scaling their value relative to the "from" min/max by +// "to_range". Example: U8 [0, 255] := [0.0, 1.0], to_range = 1.0 => +// outputs [0.0, 1.0]. +template +void ImageConvert(const Plane& from, const float to_range, + Plane* const JXL_RESTRICT to) { + JXL_ASSERT(SameSize(from, *to)); + FromType min_from, max_from; + ImageMinMax(from, &min_from, &max_from); + const float scale = to_range / (max_from - min_from); + for (size_t y = 0; y < from.ysize(); ++y) { + const FromType* const JXL_RESTRICT row_from = from.Row(y); + ToType* const JXL_RESTRICT row_to = to->Row(y); + for (size_t x = 0; x < from.xsize(); ++x) { + row_to[x] = static_cast((row_from[x] - min_from) * scale); + } + } +} + +template +Plane ConvertToFloat(const Plane& from) { + float factor = 1.0f / std::numeric_limits::max(); + if (std::is_same::value || std::is_same::value) { + factor = 1.0f; + } + Plane to(from.xsize(), from.ysize()); + for (size_t y = 0; y < from.ysize(); ++y) { + const From* const JXL_RESTRICT row_from = from.Row(y); + float* const JXL_RESTRICT row_to = to.Row(y); + for (size_t x = 0; x < from.xsize(); ++x) { + row_to[x] = row_from[x] * factor; + } + } + return to; +} + +template +Plane ImageFromPacked(const std::vector& packed, const size_t xsize, + const size_t ysize) { + Plane out(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + T* const JXL_RESTRICT row = out.Row(y); + const T* const JXL_RESTRICT packed_row = &packed[y * xsize]; + memcpy(row, packed_row, xsize * sizeof(T)); + } + return out; +} + +// Computes independent minimum and maximum values for each plane. +template +void Image3MinMax(const Image3& image, const Rect& rect, + std::array* out_min, std::array* out_max) { + for (size_t c = 0; c < 3; ++c) { + T min = std::numeric_limits::max(); + T max = std::numeric_limits::min(); + for (size_t y = 0; y < rect.ysize(); ++y) { + const T* JXL_RESTRICT row = rect.ConstPlaneRow(image, c, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + min = std::min(min, row[x]); + max = std::max(max, row[x]); + } + } + (*out_min)[c] = min; + (*out_max)[c] = max; + } +} + +// Computes independent minimum and maximum values for each plane. +template +void Image3MinMax(const Image3& image, std::array* out_min, + std::array* out_max) { + Image3MinMax(image, Rect(image), out_min, out_max); +} + +template +void Image3Max(const Image3& image, std::array* out_max) { + for (size_t c = 0; c < 3; ++c) { + T max = std::numeric_limits::min(); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* JXL_RESTRICT row = image.ConstPlaneRow(c, y); + for (size_t x = 0; x < image.xsize(); ++x) { + max = std::max(max, row[x]); + } + } + (*out_max)[c] = max; + } +} + +// Computes the sum of the pixels in `rect`. +template +T ImageSum(const Plane& image, const Rect& rect) { + T result = 0; + for (size_t y = 0; y < rect.ysize(); ++y) { + const T* JXL_RESTRICT row = rect.ConstRow(image, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + result += row[x]; + } + } + return result; +} + +template +T ImageSum(const Plane& image) { + return ImageSum(image, Rect(image)); +} + +template +std::array Image3Sum(const Image3& image, const Rect& rect) { + std::array out_sum = 0; + for (size_t c = 0; c < 3; ++c) { + (out_sum)[c] = ImageSum(image.Plane(c), rect); + } + return out_sum; +} + +template +std::array Image3Sum(const Image3& image) { + return Image3Sum(image, Rect(image)); +} + +template +std::vector PackedFromImage(const Plane& image, const Rect& rect) { + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + std::vector packed(xsize * ysize); + for (size_t y = 0; y < rect.ysize(); ++y) { + memcpy(&packed[y * xsize], rect.ConstRow(image, y), xsize * sizeof(T)); + } + return packed; +} + +template +std::vector PackedFromImage(const Plane& image) { + return PackedFromImage(image, Rect(image)); +} + +// Computes the median pixel value. +template +T ImageMedian(const Plane& image, const Rect& rect) { + std::vector pixels = PackedFromImage(image, rect); + return Median(&pixels); +} + +template +T ImageMedian(const Plane& image) { + return ImageMedian(image, Rect(image)); +} + +template +std::array Image3Median(const Image3& image, const Rect& rect) { + std::array out_median; + for (size_t c = 0; c < 3; ++c) { + (out_median)[c] = ImageMedian(image.Plane(c), rect); + } + return out_median; +} + +template +std::array Image3Median(const Image3& image) { + return Image3Median(image, Rect(image)); +} + +template +void Image3Convert(const Image3& from, const float to_range, + Image3* const JXL_RESTRICT to) { + JXL_ASSERT(SameSize(from, *to)); + std::array min_from, max_from; + Image3MinMax(from, &min_from, &max_from); + float scales[3]; + for (size_t c = 0; c < 3; ++c) { + scales[c] = to_range / (max_from[c] - min_from[c]); + } + float scale = std::min(scales[0], std::min(scales[1], scales[2])); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < from.ysize(); ++y) { + const FromType* JXL_RESTRICT row_from = from.ConstPlaneRow(c, y); + ToType* JXL_RESTRICT row_to = to->PlaneRow(c, y); + for (size_t x = 0; x < from.xsize(); ++x) { + const float to = (row_from[x] - min_from[c]) * scale; + row_to[x] = static_cast(to); + } + } + } +} + +template +Image3F ConvertToFloat(const Image3& from) { + return Image3F(ConvertToFloat(from.Plane(0)), ConvertToFloat(from.Plane(1)), + ConvertToFloat(from.Plane(2))); +} + +template +void Subtract(const Image3& image1, const Image3& image2, + Image3* out) { + const size_t xsize = image1.xsize(); + const size_t ysize = image1.ysize(); + JXL_CHECK(xsize == image2.xsize()); + JXL_CHECK(ysize == image2.ysize()); + + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* const JXL_RESTRICT row1 = image1.ConstPlaneRow(c, y); + const Tin* const JXL_RESTRICT row2 = image2.ConstPlaneRow(c, y); + Tout* const JXL_RESTRICT row_out = out->PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = row1[x] - row2[x]; + } + } + } +} + +template +void SubtractFrom(const Image3& what, Image3* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y); + Tout* JXL_RESTRICT row_to = to->PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] -= row_what[x]; + } + } + } +} + +template +void AddTo(const Image3& what, Image3* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y); + Tout* JXL_RESTRICT row_to = to->PlaneRow(c, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } + } +} + +// Adds `what` of the size of `rect` to `to` in the position of `rect`. +template +void AddTo(const Rect& rect, const Image3& what, Image3* to) { + const size_t xsize = what.xsize(); + const size_t ysize = what.ysize(); + JXL_ASSERT(xsize == rect.xsize()); + JXL_ASSERT(ysize == rect.ysize()); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y); + Tout* JXL_RESTRICT row_to = rect.PlaneRow(to, c, y); + for (size_t x = 0; x < xsize; ++x) { + row_to[x] += row_what[x]; + } + } + } +} + +template +Image3 ScaleImage(const T lambda, const Image3& image) { + Image3 out(image.xsize(), image.ysize()); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image.ysize(); ++y) { + const T* JXL_RESTRICT row = image.ConstPlaneRow(c, y); + T* JXL_RESTRICT row_out = out.PlaneRow(c, y); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = lambda * row[x]; + } + } + } + return out; +} + +// Multiplies image by lambda in-place +template +void ScaleImage(const T lambda, Image3* image) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* const JXL_RESTRICT row = image->PlaneRow(c, y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = lambda * row[x]; + } + } + } +} + +// Initializes all planes to the same "value". +template +void FillImage(const T value, Image3* image) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = value; + } + } + } +} + +template +void FillPlane(const T value, Plane* image) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* JXL_RESTRICT row = image->Row(y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = value; + } + } +} + +template +void FillImage(const T value, Image3* image, Rect rect) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < rect.ysize(); ++y) { + T* JXL_RESTRICT row = rect.PlaneRow(image, c, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row[x] = value; + } + } + } +} + +template +void FillPlane(const T value, Plane* image, Rect rect) { + for (size_t y = 0; y < rect.ysize(); ++y) { + T* JXL_RESTRICT row = rect.Row(image, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row[x] = value; + } + } +} + +template +void ZeroFillImage(Image3* image) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + memset(row, 0, image->xsize() * sizeof(T)); + } + } +} + +template +void ZeroFillPlane(Plane* image, Rect rect) { + for (size_t y = 0; y < rect.ysize(); ++y) { + T* JXL_RESTRICT row = rect.Row(image, y); + memset(row, 0, rect.xsize() * sizeof(T)); + } +} + +// 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); + +// Downsamples an image by a given factor. +void DownsampleImage(Image3F* opsin, size_t factor); +void DownsampleImage(ImageF* image, size_t factor); + +} // namespace jxl + +#endif // LIB_JXL_IMAGE_OPS_H_ diff --git a/lib/jxl/image_ops_test.cc b/lib/jxl/image_ops_test.cc new file mode 100644 index 0000000..7979c45 --- /dev/null +++ b/lib/jxl/image_ops_test.cc @@ -0,0 +1,166 @@ +// 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/image_ops.h" + +#include +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +template +void TestPacked(const size_t xsize, const size_t ysize) { + Plane image1(xsize, ysize); + RandomFillImage(&image1); + const std::vector& packed = PackedFromImage(image1); + const Plane& image2 = ImageFromPacked(packed, xsize, ysize); + EXPECT_TRUE(SamePixels(image1, image2)); +} + +TEST(ImageTest, TestPacked) { + TestPacked(1, 1); + TestPacked(7, 1); + TestPacked(1, 7); + + TestPacked(1, 1); + TestPacked(7, 1); + TestPacked(1, 7); + + TestPacked(1, 1); + TestPacked(7, 1); + TestPacked(1, 7); + + TestPacked(1, 1); + TestPacked(7, 1); + TestPacked(1, 7); +} + +// Ensure entire payload is readable/writable for various size/offset combos. +TEST(ImageTest, TestAllocator) { + std::mt19937 rng(129); + const size_t k32 = 32; + const size_t kAlign = CacheAligned::kAlignment; + for (size_t size : {k32 * 1, k32 * 2, k32 * 3, k32 * 4, k32 * 5, + CacheAligned::kAlias, 2 * CacheAligned::kAlias + 4}) { + for (size_t offset = 0; offset <= CacheAligned::kAlias; offset += kAlign) { + uint8_t* bytes = + static_cast(CacheAligned::Allocate(size, offset)); + JXL_CHECK(reinterpret_cast(bytes) % kAlign == 0); + // Ensure we can write/read the last byte. Use RNG to fool the compiler + // into thinking the write is necessary. + memset(bytes, 0, size); + bytes[size - 1] = 1; // greatest element + std::uniform_int_distribution dist(0, size - 1); + uint32_t pos = dist(rng); // random but != greatest + while (pos == size - 1) { + pos = dist(rng); + } + JXL_CHECK(bytes[pos] < bytes[size - 1]); + + CacheAligned::Free(bytes); + } + } +} + +template +void TestFillImpl(Image3* img, const char* layout) { + FillImage(T(1), img); + for (size_t y = 0; y < img->ysize(); ++y) { + for (size_t c = 0; c < 3; ++c) { + T* JXL_RESTRICT row = img->PlaneRow(c, y); + for (size_t x = 0; x < img->xsize(); ++x) { + if (row[x] != T(1)) { + printf("Not 1 at c=%zu %zu, %zu (%zu x %zu) (%s)\n", c, x, y, + img->xsize(), img->ysize(), layout); + abort(); + } + row[x] = T(2); + } + } + } + + // Same for ZeroFillImage and swapped c/y loop ordering. + ZeroFillImage(img); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < img->ysize(); ++y) { + T* JXL_RESTRICT row = img->PlaneRow(c, y); + for (size_t x = 0; x < img->xsize(); ++x) { + if (row[x] != T(0)) { + printf("Not 0 at c=%zu %zu, %zu (%zu x %zu) (%s)\n", c, x, y, + img->xsize(), img->ysize(), layout); + abort(); + } + row[x] = T(3); + } + } + } +} + +template +void TestFillT() { + for (uint32_t xsize : {0, 1, 15, 16, 31, 32}) { + for (uint32_t ysize : {0, 1, 15, 16, 31, 32}) { + Image3 image(xsize, ysize); + TestFillImpl(&image, "size ctor"); + + Image3 planar(Plane(xsize, ysize), Plane(xsize, ysize), + Plane(xsize, ysize)); + TestFillImpl(&planar, "planar"); + } + } +} + +// Ensure y/c/x and c/y/x loops visit pixels no more than once. +TEST(ImageTest, TestFill) { + TestFillT(); + TestFillT(); + TestFillT(); + TestFillT(); +} + +TEST(ImageTest, CopyImageToWithPaddingTest) { + Plane src(100, 61); + for (size_t y = 0; y < src.ysize(); y++) { + for (size_t x = 0; x < src.xsize(); x++) { + src.Row(y)[x] = x * 1000 + y; + } + } + Rect src_rect(10, 20, 30, 40); + EXPECT_TRUE(src_rect.IsInside(src)); + + Plane dst(60, 50); + FillImage(0u, &dst); + Rect dst_rect(20, 5, 30, 40); + EXPECT_TRUE(dst_rect.IsInside(dst)); + + CopyImageToWithPadding(src_rect, src, /*padding=*/2, dst_rect, &dst); + + // ysize is + 3 instead of + 4 because we are at the y image boundary on the + // source image. + Rect padded_dst_rect(20 - 2, 5 - 2, 30 + 4, 40 + 3); + for (size_t y = 0; y < dst.ysize(); y++) { + for (size_t x = 0; x < dst.xsize(); x++) { + if (Rect(x, y, 1, 1).IsInside(padded_dst_rect)) { + EXPECT_EQ((x - dst_rect.x0() + src_rect.x0()) * 1000 + + (y - dst_rect.y0() + src_rect.y0()), + dst.Row(y)[x]); + } else { + EXPECT_EQ(0u, dst.Row(y)[x]); + } + } + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/image_test_utils.h b/lib/jxl/image_test_utils.h new file mode 100644 index 0000000..e484307 --- /dev/null +++ b/lib/jxl/image_test_utils.h @@ -0,0 +1,313 @@ +// 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_IMAGE_TEST_UTILS_H_ +#define LIB_JXL_IMAGE_TEST_UTILS_H_ + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/image.h" + +namespace jxl { + +template +void VerifyEqual(const Plane& expected, const Plane& actual) { + JXL_CHECK(SameSize(expected, actual)); + for (size_t y = 0; y < expected.ysize(); ++y) { + const T* const JXL_RESTRICT row_expected = expected.Row(y); + const T* const JXL_RESTRICT row_actual = actual.Row(y); + for (size_t x = 0; x < expected.xsize(); ++x) { + ASSERT_EQ(row_expected[x], row_actual[x]) << x << " " << y; + } + } +} + +template +void VerifyEqual(const Image3& expected, const Image3& actual) { + for (size_t c = 0; c < 3; ++c) { + VerifyEqual(expected.Plane(c), actual.Plane(c)); + } +} + +template +bool SamePixels(const Plane& image1, const Plane& image2, + const Rect rect) { + if (!rect.IsInside(image1) || !rect.IsInside(image2)) { + ADD_FAILURE() << "requested rectangle is not fully inside the image"; + return false; + } + size_t mismatches = 0; + for (size_t y = rect.y0(); y < rect.ysize(); ++y) { + const T* const JXL_RESTRICT row1 = image1.Row(y); + const T* const JXL_RESTRICT row2 = image2.Row(y); + for (size_t x = rect.x0(); x < rect.xsize(); ++x) { + if (row1[x] != row2[x]) { + ADD_FAILURE() << "pixel mismatch" << x << ", " << y << ": " + << double(row1[x]) << " != " << double(row2[x]); + if (++mismatches > 4) { + return false; + } + } + } + } + return mismatches == 0; +} + +template +bool SamePixels(const Plane& image1, const Plane& image2) { + JXL_CHECK(SameSize(image1, image2)); + return SamePixels(image1, image2, Rect(image1)); +} + +template +bool SamePixels(const Image3& image1, const Image3& image2) { + JXL_CHECK(SameSize(image1, image2)); + for (size_t c = 0; c < 3; ++c) { + if (!SamePixels(image1.Plane(c), image2.Plane(c))) { + return false; + } + } + return true; +} + +// Use for floating-point images with fairly large numbers; tolerates small +// absolute errors and/or small relative errors. Returns max_relative. +template +void VerifyRelativeError(const Plane& expected, const Plane& actual, + const double threshold_l1, + const double threshold_relative, + const intptr_t border = 0, const size_t c = 0) { + JXL_CHECK(SameSize(expected, actual)); + const intptr_t xsize = expected.xsize(); + const intptr_t ysize = expected.ysize(); + + // Max over current scanline to give a better idea whether there are + // systematic errors or just one outlier. Invalid if negative. + double max_l1 = -1; + double max_relative = -1; + bool any_bad = false; + for (intptr_t y = border; y < ysize - border; ++y) { + const T* const JXL_RESTRICT row_expected = expected.Row(y); + const T* const JXL_RESTRICT row_actual = actual.Row(y); + for (intptr_t x = border; x < xsize - border; ++x) { + const double l1 = std::abs(row_expected[x] - row_actual[x]); + + // Cannot compute relative, only check/update L1. + if (std::abs(row_expected[x]) < 1E-10) { + if (l1 > threshold_l1) { + any_bad = true; + max_l1 = std::max(max_l1, l1); + } + } else { + const double relative = l1 / std::abs(double(row_expected[x])); + if (l1 > threshold_l1 && relative > threshold_relative) { + // Fails both tolerances => will exit below, update max_*. + any_bad = true; + max_l1 = std::max(max_l1, l1); + max_relative = std::max(max_relative, relative); + } + } + } + } + if (any_bad) { + // Never had a valid relative value, don't print it. + if (max_relative < 0) { + fprintf(stderr, "c=%zu: max +/- %E exceeds +/- %.2E\n", c, max_l1, + threshold_l1); + } else { + fprintf(stderr, "c=%zu: max +/- %E, x %E exceeds +/- %.2E, x %.2E\n", c, + max_l1, max_relative, threshold_l1, threshold_relative); + } + // Dump the expected image and actual image if the region is small enough. + const intptr_t kMaxTestDumpSize = 16; + if (xsize <= kMaxTestDumpSize + 2 * border && + ysize <= kMaxTestDumpSize + 2 * border) { + fprintf(stderr, "Expected image:\n"); + for (intptr_t y = border; y < ysize - border; ++y) { + const T* const JXL_RESTRICT row_expected = expected.Row(y); + for (intptr_t x = border; x < xsize - border; ++x) { + fprintf(stderr, "%10lf ", static_cast(row_expected[x])); + } + fprintf(stderr, "\n"); + } + + fprintf(stderr, "Actual image:\n"); + for (intptr_t y = border; y < ysize - border; ++y) { + const T* const JXL_RESTRICT row_expected = expected.Row(y); + const T* const JXL_RESTRICT row_actual = actual.Row(y); + for (intptr_t x = border; x < xsize - border; ++x) { + const double l1 = std::abs(row_expected[x] - row_actual[x]); + + bool bad = l1 > threshold_l1; + if (row_expected[x] > 1E-10) { + const double relative = l1 / std::abs(double(row_expected[x])); + bad &= relative > threshold_relative; + } + if (bad) { + fprintf(stderr, "%10lf ", static_cast(row_actual[x])); + } else { + fprintf(stderr, "%10s ", "=="); + } + } + fprintf(stderr, "\n"); + } + } + + // Find first failing x for further debugging. + for (intptr_t y = border; y < ysize - border; ++y) { + const T* const JXL_RESTRICT row_expected = expected.Row(y); + const T* const JXL_RESTRICT row_actual = actual.Row(y); + + for (intptr_t x = border; x < xsize - border; ++x) { + const double l1 = std::abs(row_expected[x] - row_actual[x]); + + bool bad = l1 > threshold_l1; + if (row_expected[x] > 1E-10) { + const double relative = l1 / std::abs(double(row_expected[x])); + bad &= relative > threshold_relative; + } + if (bad) { + FAIL() << x << ", " << y << " (" << expected.xsize() << " x " + << expected.ysize() << ") expected " + << static_cast(row_expected[x]) << " actual " + << static_cast(row_actual[x]); + } + } + } + return; // if any_bad, we should have exited. + } +} + +template +void VerifyRelativeError(const Image3& expected, const Image3& actual, + const float threshold_l1, + const float threshold_relative, + const intptr_t border = 0) { + for (size_t c = 0; c < 3; ++c) { + VerifyRelativeError(expected.Plane(c), actual.Plane(c), threshold_l1, + threshold_relative, border, c); + } +} + +// Generator for independent, uniformly distributed integers [0, max]. +template +class GeneratorRandom { + public: + GeneratorRandom(Random* rng, const T max) : rng_(*rng), dist_(0, max) {} + + GeneratorRandom(Random* rng, const T min, const T max) + : rng_(*rng), dist_(min, max) {} + + T operator()(const size_t x, const size_t y, const int c) const { + return dist_(rng_); + } + + private: + Random& rng_; + mutable std::uniform_int_distribution<> dist_; +}; + +template +class GeneratorRandom { + public: + GeneratorRandom(Random* rng, const float max) + : rng_(*rng), dist_(0.0f, max) {} + + GeneratorRandom(Random* rng, const float min, const float max) + : rng_(*rng), dist_(min, max) {} + + float operator()(const size_t x, const size_t y, const int c) const { + return dist_(rng_); + } + + private: + Random& rng_; + mutable std::uniform_real_distribution dist_; +}; + +template +class GeneratorRandom { + public: + GeneratorRandom(Random* rng, const double max) + : rng_(*rng), dist_(0.0, max) {} + + GeneratorRandom(Random* rng, const double min, const double max) + : rng_(*rng), dist_(min, max) {} + + double operator()(const size_t x, const size_t y, const int c) const { + return dist_(rng_); + } + + private: + Random& rng_; + mutable std::uniform_real_distribution<> dist_; +}; + +// Assigns generator(x, y, 0) to each pixel (x, y). +template +void GenerateImage(const Generator& generator, Image* image) { + using T = typename Image::T; + for (size_t y = 0; y < image->ysize(); ++y) { + T* const JXL_RESTRICT row = image->Row(y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = generator(x, y, 0); + } + } +} + +template +void RandomFillImage(Plane* image, + const T max = std::numeric_limits::max()) { + std::mt19937_64 rng(129); + const GeneratorRandom generator(&rng, max); + GenerateImage(generator, image); +} + +template +void RandomFillImage(Plane* image, const T min, const T max, + const int seed) { + std::mt19937_64 rng(seed); + const GeneratorRandom generator(&rng, min, max); + GenerateImage(generator, image); +} + +// Assigns generator(x, y, c) to each pixel (x, y). +template +void GenerateImage(const Generator& generator, Image3* image) { + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < image->ysize(); ++y) { + T* JXL_RESTRICT row = image->PlaneRow(c, y); + for (size_t x = 0; x < image->xsize(); ++x) { + row[x] = generator(x, y, c); + } + } + } +} + +template +void RandomFillImage(Image3* image, + const T max = std::numeric_limits::max()) { + std::mt19937_64 rng(129); + const GeneratorRandom generator(&rng, max); + GenerateImage(generator, image); +} + +template +void RandomFillImage(Image3* image, const T min, const T max, + const int seed) { + std::mt19937_64 rng(seed); + const GeneratorRandom generator(&rng, min, max); + GenerateImage(generator, image); +} + +} // namespace jxl + +#endif // LIB_JXL_IMAGE_TEST_UTILS_H_ diff --git a/lib/jxl/jpeg/dec_jpeg_data.cc b/lib/jxl/jpeg/dec_jpeg_data.cc new file mode 100644 index 0000000..f57f697 --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_data.cc @@ -0,0 +1,140 @@ +// 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/jpeg/dec_jpeg_data.h" + +#include + +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" + +namespace jxl { +namespace jpeg { +Status DecodeJPEGData(Span encoded, JPEGData* jpeg_data) { + Status ret = true; + const uint8_t* in = encoded.data(); + size_t available_in = encoded.size(); + { + BitReader br(encoded); + BitReaderScopedCloser br_closer(&br, &ret); + JXL_RETURN_IF_ERROR(Bundle::Read(&br, jpeg_data)); + JXL_RETURN_IF_ERROR(br.JumpToByteBoundary()); + in += br.TotalBitsConsumed() / 8; + available_in -= br.TotalBitsConsumed() / 8; + } + JXL_RETURN_IF_ERROR(ret); + + BrotliDecoderState* brotli_dec = + BrotliDecoderCreateInstance(nullptr, nullptr, nullptr); + + struct BrotliDecDeleter { + BrotliDecoderState* brotli_dec; + ~BrotliDecDeleter() { BrotliDecoderDestroyInstance(brotli_dec); } + } brotli_dec_deleter{brotli_dec}; + + BrotliDecoderResult result = + BrotliDecoderResult::BROTLI_DECODER_RESULT_SUCCESS; + + auto br_read = [&](std::vector& data) -> Status { + size_t available_out = data.size(); + uint8_t* out = data.data(); + while (available_out != 0) { + if (BrotliDecoderIsFinished(brotli_dec)) { + return JXL_FAILURE("Not enough decompressed output"); + } + result = BrotliDecoderDecompressStream(brotli_dec, &available_in, &in, + &available_out, &out, nullptr); + if (result != + BrotliDecoderResult::BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT && + result != BrotliDecoderResult::BROTLI_DECODER_RESULT_SUCCESS) { + return JXL_FAILURE( + "Brotli decoding error: %s\n", + BrotliDecoderErrorString(BrotliDecoderGetErrorCode(brotli_dec))); + } + } + return true; + }; + size_t num_icc = 0; + for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { + auto& marker = jpeg_data->app_data[i]; + if (jpeg_data->app_marker_type[i] != AppMarkerType::kUnknown) { + // Set the size of the marker. + size_t size_minus_1 = marker.size() - 1; + marker[1] = size_minus_1 >> 8; + marker[2] = size_minus_1 & 0xFF; + if (jpeg_data->app_marker_type[i] == AppMarkerType::kICC) { + if (marker.size() < 17) { + return JXL_FAILURE("ICC markers must be at least 17 bytes"); + } + marker[0] = 0xE2; + memcpy(&marker[3], kIccProfileTag, sizeof kIccProfileTag); + marker[15] = ++num_icc; + } + } else { + JXL_RETURN_IF_ERROR(br_read(marker)); + if (marker[1] * 256u + marker[2] + 1u != marker.size()) { + return JXL_FAILURE("Incorrect marker size"); + } + } + } + for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { + auto& marker = jpeg_data->app_data[i]; + if (jpeg_data->app_marker_type[i] == AppMarkerType::kICC) { + marker[16] = num_icc; + } + if (jpeg_data->app_marker_type[i] == AppMarkerType::kExif) { + marker[0] = 0xE1; + if (marker.size() < 3 + sizeof kExifTag) { + return JXL_FAILURE("Incorrect Exif marker size"); + } + memcpy(&marker[3], kExifTag, sizeof kExifTag); + } + if (jpeg_data->app_marker_type[i] == AppMarkerType::kXMP) { + marker[0] = 0xE1; + if (marker.size() < 3 + sizeof kXMPTag) { + return JXL_FAILURE("Incorrect XMP marker size"); + } + memcpy(&marker[3], kXMPTag, sizeof kXMPTag); + } + } + // TODO(eustas): actually inject ICC profile and check it fits perfectly. + for (size_t i = 0; i < jpeg_data->com_data.size(); i++) { + auto& marker = jpeg_data->com_data[i]; + JXL_RETURN_IF_ERROR(br_read(marker)); + if (marker[1] * 256u + marker[2] + 1u != marker.size()) { + return JXL_FAILURE("Incorrect marker size"); + } + } + for (size_t i = 0; i < jpeg_data->inter_marker_data.size(); i++) { + JXL_RETURN_IF_ERROR(br_read(jpeg_data->inter_marker_data[i])); + } + JXL_RETURN_IF_ERROR(br_read(jpeg_data->tail_data)); + + // Check if there is more decompressed output. + size_t available_out = 1; + uint64_t dummy; + uint8_t* next_out = reinterpret_cast(&dummy); + result = BrotliDecoderDecompressStream(brotli_dec, &available_in, &in, + &available_out, &next_out, nullptr); + if (available_out == 0 || + result == BrotliDecoderResult::BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { + return JXL_FAILURE("Excess data in compressed stream"); + } + if (result == BrotliDecoderResult::BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { + return JXL_FAILURE("Incomplete brotli-stream"); + } + if (!BrotliDecoderIsFinished(brotli_dec) || + result != BrotliDecoderResult::BROTLI_DECODER_RESULT_SUCCESS) { + return JXL_FAILURE("Corrupted brotli-stream"); + } + if (available_in != 0) { + return JXL_FAILURE("Unused data after brotli stream"); + } + + return true; +} +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/dec_jpeg_data.h b/lib/jxl/jpeg/dec_jpeg_data.h new file mode 100644 index 0000000..b9d50bf --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_data.h @@ -0,0 +1,19 @@ +// 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_JPEG_DEC_JPEG_DATA_H_ +#define LIB_JXL_JPEG_DEC_JPEG_DATA_H_ + +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { +Status DecodeJPEGData(Span encoded, JPEGData* jpeg_data); +} +} // namespace jxl + +#endif // LIB_JXL_JPEG_DEC_JPEG_DATA_H_ diff --git a/lib/jxl/jpeg/dec_jpeg_data_writer.cc b/lib/jxl/jpeg/dec_jpeg_data_writer.cc new file mode 100644 index 0000000..c321344 --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_data_writer.cc @@ -0,0 +1,983 @@ +// 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/jpeg/dec_jpeg_data_writer.h" + +#include +#include /* for memset, memcpy */ + +#include +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/common.h" +#include "lib/jxl/jpeg/dec_jpeg_serialization_state.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { + +namespace { + +enum struct SerializationStatus { + NEEDS_MORE_INPUT, + NEEDS_MORE_OUTPUT, + ERROR, + DONE +}; + +const int kJpegPrecision = 8; + +// JpegBitWriter: buffer size +const size_t kJpegBitWriterChunkSize = 16384; + +// DCTCodingState: maximum number of correction bits to buffer +const int kJPEGMaxCorrectionBits = 1u << 16; + +// Returns non-zero if and only if x has a zero byte, i.e. one of +// x & 0xff, x & 0xff00, ..., x & 0xff00000000000000 is zero. +static JXL_INLINE uint64_t HasZeroByte(uint64_t x) { + return (x - 0x0101010101010101ULL) & ~x & 0x8080808080808080ULL; +} + +void JpegBitWriterInit(JpegBitWriter* bw, + std::deque* output_queue) { + bw->output = output_queue; + bw->chunk = OutputChunk(kJpegBitWriterChunkSize); + bw->pos = 0; + bw->put_buffer = 0; + bw->put_bits = 64; + bw->healthy = true; + bw->data = bw->chunk.buffer->data(); +} + +static JXL_NOINLINE void SwapBuffer(JpegBitWriter* bw) { + bw->chunk.len = bw->pos; + bw->output->emplace_back(std::move(bw->chunk)); + bw->chunk = OutputChunk(kJpegBitWriterChunkSize); + bw->data = bw->chunk.buffer->data(); + bw->pos = 0; +} + +static JXL_INLINE void Reserve(JpegBitWriter* bw, size_t n_bytes) { + if (JXL_UNLIKELY((bw->pos + n_bytes) > kJpegBitWriterChunkSize)) { + SwapBuffer(bw); + } +} + +/** + * Writes the given byte to the output, writes an extra zero if byte is 0xFF. + * + * This method is "careless" - caller must make sure that there is enough + * space in the output buffer. Emits up to 2 bytes to buffer. + */ +static JXL_INLINE void EmitByte(JpegBitWriter* bw, int byte) { + bw->data[bw->pos++] = byte; + if (byte == 0xFF) bw->data[bw->pos++] = 0; +} + +static JXL_INLINE void DischargeBitBuffer(JpegBitWriter* bw) { + // At this point we are ready to emit the most significant 6 bytes of + // put_buffer_ to the output. + // The JPEG format requires that after every 0xff byte in the entropy + // coded section, there is a zero byte, therefore we first check if any of + // the 6 most significant bytes of put_buffer_ is 0xFF. + Reserve(bw, 12); + if (HasZeroByte(~bw->put_buffer | 0xFFFF)) { + // We have a 0xFF byte somewhere, examine each byte and append a zero + // byte if necessary. + EmitByte(bw, (bw->put_buffer >> 56) & 0xFF); + EmitByte(bw, (bw->put_buffer >> 48) & 0xFF); + EmitByte(bw, (bw->put_buffer >> 40) & 0xFF); + EmitByte(bw, (bw->put_buffer >> 32) & 0xFF); + EmitByte(bw, (bw->put_buffer >> 24) & 0xFF); + EmitByte(bw, (bw->put_buffer >> 16) & 0xFF); + } else { + // We don't have any 0xFF bytes, output all 6 bytes without checking. + bw->data[bw->pos] = (bw->put_buffer >> 56) & 0xFF; + bw->data[bw->pos + 1] = (bw->put_buffer >> 48) & 0xFF; + bw->data[bw->pos + 2] = (bw->put_buffer >> 40) & 0xFF; + bw->data[bw->pos + 3] = (bw->put_buffer >> 32) & 0xFF; + bw->data[bw->pos + 4] = (bw->put_buffer >> 24) & 0xFF; + bw->data[bw->pos + 5] = (bw->put_buffer >> 16) & 0xFF; + bw->pos += 6; + } + bw->put_buffer <<= 48; + bw->put_bits += 48; +} + +static JXL_INLINE void WriteBits(JpegBitWriter* bw, int nbits, uint64_t bits) { + // This is an optimization; if everything goes well, + // then |nbits| is positive; if non-existing Huffman symbol is going to be + // encoded, its length should be zero; later encoder could check the + // "health" of JpegBitWriter. + if (nbits == 0) { + bw->healthy = false; + return; + } + bw->put_bits -= nbits; + bw->put_buffer |= (bits << bw->put_bits); + if (bw->put_bits <= 16) DischargeBitBuffer(bw); +} + +void EmitMarker(JpegBitWriter* bw, int marker) { + Reserve(bw, 2); + JXL_DASSERT(marker != 0xFF); + bw->data[bw->pos++] = 0xFF; + bw->data[bw->pos++] = marker; +} + +bool JumpToByteBoundary(JpegBitWriter* bw, const uint8_t** pad_bits, + const uint8_t* pad_bits_end) { + size_t n_bits = bw->put_bits & 7u; + uint8_t pad_pattern; + if (*pad_bits == nullptr) { + pad_pattern = (1u << n_bits) - 1; + } else { + pad_pattern = 0; + const uint8_t* src = *pad_bits; + // TODO(eustas): bitwise reading looks insanely ineffective... + while (n_bits--) { + pad_pattern <<= 1; + if (src >= pad_bits_end) return false; + // TODO(eustas): DCHECK *src == {0, 1} + pad_pattern |= !!*(src++); + } + *pad_bits = src; + } + + Reserve(bw, 16); + + while (bw->put_bits <= 56) { + int c = (bw->put_buffer >> 56) & 0xFF; + EmitByte(bw, c); + bw->put_buffer <<= 8; + bw->put_bits += 8; + } + if (bw->put_bits < 64) { + int pad_mask = 0xFFu >> (64 - bw->put_bits); + int c = ((bw->put_buffer >> 56) & ~pad_mask) | pad_pattern; + EmitByte(bw, c); + } + bw->put_buffer = 0; + bw->put_bits = 64; + + return true; +} + +void JpegBitWriterFinish(JpegBitWriter* bw) { + if (bw->pos == 0) return; + bw->chunk.len = bw->pos; + bw->output->emplace_back(std::move(bw->chunk)); + bw->chunk = OutputChunk(nullptr, 0); + bw->data = nullptr; + bw->pos = 0; +} + +void DCTCodingStateInit(DCTCodingState* s) { + s->eob_run_ = 0; + s->cur_ac_huff_ = nullptr; + s->refinement_bits_.clear(); + s->refinement_bits_.reserve(kJPEGMaxCorrectionBits); +} + +// Emit all buffered data to the bit stream using the given Huffman code and +// bit writer. +static JXL_INLINE void Flush(DCTCodingState* s, JpegBitWriter* bw) { + if (s->eob_run_ > 0) { + int nbits = FloorLog2Nonzero(s->eob_run_); + int symbol = nbits << 4u; + WriteBits(bw, s->cur_ac_huff_->depth[symbol], + s->cur_ac_huff_->code[symbol]); + if (nbits > 0) { + WriteBits(bw, nbits, s->eob_run_ & ((1 << nbits) - 1)); + } + s->eob_run_ = 0; + } + for (size_t i = 0; i < s->refinement_bits_.size(); ++i) { + WriteBits(bw, 1, s->refinement_bits_[i]); + } + s->refinement_bits_.clear(); +} + +// Buffer some more data at the end-of-band (the last non-zero or newly +// non-zero coefficient within the [Ss, Se] spectral band). +static JXL_INLINE void BufferEndOfBand(DCTCodingState* s, + const HuffmanCodeTable* ac_huff, + const std::vector* new_bits, + JpegBitWriter* bw) { + if (s->eob_run_ == 0) { + s->cur_ac_huff_ = ac_huff; + } + ++s->eob_run_; + if (new_bits) { + s->refinement_bits_.insert(s->refinement_bits_.end(), new_bits->begin(), + new_bits->end()); + } + if (s->eob_run_ == 0x7FFF || + s->refinement_bits_.size() > kJPEGMaxCorrectionBits - kDCTBlockSize + 1) { + Flush(s, bw); + } +} + +bool BuildHuffmanCodeTable(const JPEGHuffmanCode& huff, + HuffmanCodeTable* table) { + int huff_code[kJpegHuffmanAlphabetSize]; + // +1 for a sentinel element. + uint32_t huff_size[kJpegHuffmanAlphabetSize + 1]; + int p = 0; + for (size_t l = 1; l <= kJpegHuffmanMaxBitLength; ++l) { + int i = huff.counts[l]; + if (p + i > kJpegHuffmanAlphabetSize + 1) { + return false; + } + while (i--) huff_size[p++] = l; + } + + if (p == 0) { + return true; + } + + // Reuse sentinel element. + int last_p = p - 1; + huff_size[last_p] = 0; + + int code = 0; + uint32_t si = huff_size[0]; + p = 0; + while (huff_size[p]) { + while ((huff_size[p]) == si) { + huff_code[p++] = code; + code++; + } + code <<= 1; + si++; + } + for (p = 0; p < last_p; p++) { + int i = huff.values[p]; + table->depth[i] = huff_size[p]; + table->code[i] = huff_code[p]; + } + return true; +} + +bool EncodeSOI(SerializationState* state) { + state->output_queue.push_back(OutputChunk({0xFF, 0xD8})); + return true; +} + +bool EncodeEOI(const JPEGData& jpg, SerializationState* state) { + state->output_queue.push_back(OutputChunk({0xFF, 0xD9})); + state->output_queue.emplace_back(jpg.tail_data); + return true; +} + +bool EncodeSOF(const JPEGData& jpg, uint8_t marker, SerializationState* state) { + if (marker <= 0xC2) state->is_progressive = (marker == 0xC2); + + const size_t n_comps = jpg.components.size(); + const size_t marker_len = 8 + 3 * n_comps; + state->output_queue.emplace_back(marker_len + 2); + uint8_t* data = state->output_queue.back().buffer->data(); + size_t pos = 0; + data[pos++] = 0xFF; + data[pos++] = marker; + data[pos++] = marker_len >> 8u; + data[pos++] = marker_len & 0xFFu; + data[pos++] = kJpegPrecision; + data[pos++] = jpg.height >> 8u; + data[pos++] = jpg.height & 0xFFu; + data[pos++] = jpg.width >> 8u; + data[pos++] = jpg.width & 0xFFu; + data[pos++] = n_comps; + for (size_t i = 0; i < n_comps; ++i) { + data[pos++] = jpg.components[i].id; + data[pos++] = ((jpg.components[i].h_samp_factor << 4u) | + (jpg.components[i].v_samp_factor)); + const size_t quant_idx = jpg.components[i].quant_idx; + if (quant_idx >= jpg.quant.size()) return false; + data[pos++] = jpg.quant[quant_idx].index; + } + return true; +} + +bool EncodeSOS(const JPEGData& jpg, const JPEGScanInfo& scan_info, + SerializationState* state) { + const size_t n_scans = scan_info.num_components; + const size_t marker_len = 6 + 2 * n_scans; + state->output_queue.emplace_back(marker_len + 2); + uint8_t* data = state->output_queue.back().buffer->data(); + size_t pos = 0; + data[pos++] = 0xFF; + data[pos++] = 0xDA; + data[pos++] = marker_len >> 8u; + data[pos++] = marker_len & 0xFFu; + data[pos++] = n_scans; + for (size_t i = 0; i < n_scans; ++i) { + const JPEGComponentScanInfo& si = scan_info.components[i]; + if (si.comp_idx >= jpg.components.size()) return false; + data[pos++] = jpg.components[si.comp_idx].id; + data[pos++] = (si.dc_tbl_idx << 4u) + si.ac_tbl_idx; + } + data[pos++] = scan_info.Ss; + data[pos++] = scan_info.Se; + data[pos++] = ((scan_info.Ah << 4u) | (scan_info.Al)); + return true; +} + +bool EncodeDHT(const JPEGData& jpg, SerializationState* state) { + const std::vector& huffman_code = jpg.huffman_code; + + size_t marker_len = 2; + for (size_t i = state->dht_index; i < huffman_code.size(); ++i) { + const JPEGHuffmanCode& huff = huffman_code[i]; + marker_len += kJpegHuffmanMaxBitLength; + for (size_t j = 0; j < huff.counts.size(); ++j) { + marker_len += huff.counts[j]; + } + if (huff.is_last) break; + } + state->output_queue.emplace_back(marker_len + 2); + uint8_t* data = state->output_queue.back().buffer->data(); + size_t pos = 0; + data[pos++] = 0xFF; + data[pos++] = 0xC4; + data[pos++] = marker_len >> 8u; + data[pos++] = marker_len & 0xFFu; + while (true) { + const size_t huffman_code_index = state->dht_index++; + if (huffman_code_index >= huffman_code.size()) { + return false; + } + const JPEGHuffmanCode& huff = huffman_code[huffman_code_index]; + size_t index = huff.slot_id; + HuffmanCodeTable* huff_table; + if (index & 0x10) { + index -= 0x10; + huff_table = &state->ac_huff_table[index]; + } else { + huff_table = &state->dc_huff_table[index]; + } + // TODO(eustas): cache + // TODO(eustas): set up non-existing symbols + if (!BuildHuffmanCodeTable(huff, huff_table)) { + return false; + } + size_t total_count = 0; + size_t max_length = 0; + for (size_t i = 0; i < huff.counts.size(); ++i) { + if (huff.counts[i] != 0) { + max_length = i; + } + total_count += huff.counts[i]; + } + --total_count; + data[pos++] = huff.slot_id; + for (size_t i = 1; i <= kJpegHuffmanMaxBitLength; ++i) { + data[pos++] = (i == max_length ? huff.counts[i] - 1 : huff.counts[i]); + } + for (size_t i = 0; i < total_count; ++i) { + data[pos++] = huff.values[i]; + } + if (huff.is_last) break; + } + return true; +} + +bool EncodeDQT(const JPEGData& jpg, SerializationState* state) { + int marker_len = 2; + for (size_t i = state->dqt_index; i < jpg.quant.size(); ++i) { + const JPEGQuantTable& table = jpg.quant[i]; + marker_len += 1 + (table.precision ? 2 : 1) * kDCTBlockSize; + if (table.is_last) break; + } + state->output_queue.emplace_back(marker_len + 2); + uint8_t* data = state->output_queue.back().buffer->data(); + size_t pos = 0; + data[pos++] = 0xFF; + data[pos++] = 0xDB; + data[pos++] = marker_len >> 8u; + data[pos++] = marker_len & 0xFFu; + while (true) { + const size_t idx = state->dqt_index++; + if (idx >= jpg.quant.size()) { + return false; // corrupt input + } + const JPEGQuantTable& table = jpg.quant[idx]; + data[pos++] = (table.precision << 4u) + table.index; + for (size_t i = 0; i < kDCTBlockSize; ++i) { + int val_idx = kJPEGNaturalOrder[i]; + int val = table.values[val_idx]; + if (table.precision) { + data[pos++] = val >> 8u; + } + data[pos++] = val & 0xFFu; + } + if (table.is_last) break; + } + return true; +} + +bool EncodeDRI(const JPEGData& jpg, SerializationState* state) { + state->seen_dri_marker = true; + OutputChunk dri_marker = {0xFF, + 0xDD, + 0, + 4, + static_cast(jpg.restart_interval >> 8), + static_cast(jpg.restart_interval & 0xFF)}; + state->output_queue.push_back(std::move(dri_marker)); + return true; +} + +bool EncodeRestart(uint8_t marker, SerializationState* state) { + state->output_queue.push_back(OutputChunk({0xFF, marker})); + return true; +} + +bool EncodeAPP(const JPEGData& jpg, uint8_t marker, SerializationState* state) { + // TODO(eustas): check that marker corresponds to payload? + (void)marker; + + size_t app_index = state->app_index++; + if (app_index >= jpg.app_data.size()) return false; + state->output_queue.push_back(OutputChunk({0xFF})); + state->output_queue.emplace_back(jpg.app_data[app_index]); + return true; +} + +bool EncodeCOM(const JPEGData& jpg, SerializationState* state) { + size_t com_index = state->com_index++; + if (com_index >= jpg.com_data.size()) return false; + state->output_queue.push_back(OutputChunk({0xFF})); + state->output_queue.emplace_back(jpg.com_data[com_index]); + return true; +} + +bool EncodeInterMarkerData(const JPEGData& jpg, SerializationState* state) { + size_t index = state->data_index++; + if (index >= jpg.inter_marker_data.size()) return false; + state->output_queue.emplace_back(jpg.inter_marker_data[index]); + return true; +} + +bool EncodeDCTBlockSequential(const coeff_t* coeffs, + const HuffmanCodeTable& dc_huff, + const HuffmanCodeTable& ac_huff, + int num_zero_runs, coeff_t* last_dc_coeff, + JpegBitWriter* bw) { + coeff_t temp2; + coeff_t temp; + temp2 = coeffs[0]; + temp = temp2 - *last_dc_coeff; + *last_dc_coeff = temp2; + temp2 = temp; + if (temp < 0) { + temp = -temp; + temp2--; + } + int dc_nbits = (temp == 0) ? 0 : (FloorLog2Nonzero(temp) + 1); + WriteBits(bw, dc_huff.depth[dc_nbits], dc_huff.code[dc_nbits]); + if (dc_nbits >= 12) return false; + if (dc_nbits > 0) { + WriteBits(bw, dc_nbits, temp2 & ((1u << dc_nbits) - 1)); + } + int r = 0; + for (int k = 1; k < 64; ++k) { + if ((temp = coeffs[kJPEGNaturalOrder[k]]) == 0) { + r++; + continue; + } + if (temp < 0) { + temp = -temp; + temp2 = ~temp; + } else { + temp2 = temp; + } + while (r > 15) { + WriteBits(bw, ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + } + int ac_nbits = FloorLog2Nonzero(temp) + 1; + if (ac_nbits >= 16) return false; + int symbol = (r << 4u) + ac_nbits; + WriteBits(bw, ac_huff.depth[symbol], ac_huff.code[symbol]); + WriteBits(bw, ac_nbits, temp2 & ((1 << ac_nbits) - 1)); + r = 0; + } + for (int i = 0; i < num_zero_runs; ++i) { + WriteBits(bw, ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + } + if (r > 0) { + WriteBits(bw, ac_huff.depth[0], ac_huff.code[0]); + } + return true; +} + +bool EncodeDCTBlockProgressive(const coeff_t* coeffs, + const HuffmanCodeTable& dc_huff, + const HuffmanCodeTable& ac_huff, int Ss, int Se, + int Al, int num_zero_runs, + DCTCodingState* coding_state, + coeff_t* last_dc_coeff, JpegBitWriter* bw) { + bool eob_run_allowed = Ss > 0; + coeff_t temp2; + coeff_t temp; + if (Ss == 0) { + temp2 = coeffs[0] >> Al; + temp = temp2 - *last_dc_coeff; + *last_dc_coeff = temp2; + temp2 = temp; + if (temp < 0) { + temp = -temp; + temp2--; + } + int nbits = (temp == 0) ? 0 : (FloorLog2Nonzero(temp) + 1); + WriteBits(bw, dc_huff.depth[nbits], dc_huff.code[nbits]); + if (nbits > 0) { + WriteBits(bw, nbits, temp2 & ((1 << nbits) - 1)); + } + ++Ss; + } + if (Ss > Se) { + return true; + } + int r = 0; + for (int k = Ss; k <= Se; ++k) { + if ((temp = coeffs[kJPEGNaturalOrder[k]]) == 0) { + r++; + continue; + } + if (temp < 0) { + temp = -temp; + temp >>= Al; + temp2 = ~temp; + } else { + temp >>= Al; + temp2 = temp; + } + if (temp == 0) { + r++; + continue; + } + Flush(coding_state, bw); + while (r > 15) { + WriteBits(bw, ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + } + int nbits = FloorLog2Nonzero(temp) + 1; + int symbol = (r << 4u) + nbits; + WriteBits(bw, ac_huff.depth[symbol], ac_huff.code[symbol]); + WriteBits(bw, nbits, temp2 & ((1 << nbits) - 1)); + r = 0; + } + if (num_zero_runs > 0) { + Flush(coding_state, bw); + for (int i = 0; i < num_zero_runs; ++i) { + WriteBits(bw, ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + } + } + if (r > 0) { + BufferEndOfBand(coding_state, &ac_huff, nullptr, bw); + if (!eob_run_allowed) { + Flush(coding_state, bw); + } + } + return true; +} + +bool EncodeRefinementBits(const coeff_t* coeffs, + const HuffmanCodeTable& ac_huff, int Ss, int Se, + int Al, DCTCodingState* coding_state, + JpegBitWriter* bw) { + bool eob_run_allowed = Ss > 0; + if (Ss == 0) { + // Emit next bit of DC component. + WriteBits(bw, 1, (coeffs[0] >> Al) & 1); + ++Ss; + } + if (Ss > Se) { + return true; + } + int abs_values[kDCTBlockSize]; + int eob = 0; + for (int k = Ss; k <= Se; k++) { + const coeff_t abs_val = std::abs(coeffs[kJPEGNaturalOrder[k]]); + abs_values[k] = abs_val >> Al; + if (abs_values[k] == 1) { + eob = k; + } + } + int r = 0; + std::vector refinement_bits; + refinement_bits.reserve(kDCTBlockSize); + for (int k = Ss; k <= Se; k++) { + if (abs_values[k] == 0) { + r++; + continue; + } + while (r > 15 && k <= eob) { + Flush(coding_state, bw); + WriteBits(bw, ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + for (int bit : refinement_bits) { + WriteBits(bw, 1, bit); + } + refinement_bits.clear(); + } + if (abs_values[k] > 1) { + refinement_bits.push_back(abs_values[k] & 1u); + continue; + } + Flush(coding_state, bw); + int symbol = (r << 4u) + 1; + int new_non_zero_bit = (coeffs[kJPEGNaturalOrder[k]] < 0) ? 0 : 1; + WriteBits(bw, ac_huff.depth[symbol], ac_huff.code[symbol]); + WriteBits(bw, 1, new_non_zero_bit); + for (int bit : refinement_bits) { + WriteBits(bw, 1, bit); + } + refinement_bits.clear(); + r = 0; + } + if (r > 0 || !refinement_bits.empty()) { + BufferEndOfBand(coding_state, &ac_huff, &refinement_bits, bw); + if (!eob_run_allowed) { + Flush(coding_state, bw); + } + } + return true; +} + +template +SerializationStatus JXL_NOINLINE DoEncodeScan(const JPEGData& jpg, + SerializationState* state) { + const JPEGScanInfo& scan_info = jpg.scan_info[state->scan_index]; + EncodeScanState& ss = state->scan_state; + + const int restart_interval = + state->seen_dri_marker ? jpg.restart_interval : 0; + + const auto get_next_extra_zero_run_index = [&ss, &scan_info]() -> int { + if (ss.extra_zero_runs_pos < scan_info.extra_zero_runs.size()) { + return scan_info.extra_zero_runs[ss.extra_zero_runs_pos].block_idx; + } else { + return -1; + } + }; + + const auto get_next_reset_point = [&ss, &scan_info]() -> int { + if (ss.next_reset_point_pos < scan_info.reset_points.size()) { + return scan_info.reset_points[ss.next_reset_point_pos++]; + } else { + return -1; + } + }; + + if (ss.stage == EncodeScanState::HEAD) { + if (!EncodeSOS(jpg, scan_info, state)) return SerializationStatus::ERROR; + JpegBitWriterInit(&ss.bw, &state->output_queue); + DCTCodingStateInit(&ss.coding_state); + ss.restarts_to_go = restart_interval; + ss.next_restart_marker = 0; + ss.block_scan_index = 0; + ss.extra_zero_runs_pos = 0; + ss.next_extra_zero_run_index = get_next_extra_zero_run_index(); + ss.next_reset_point_pos = 0; + ss.next_reset_point = get_next_reset_point(); + ss.mcu_y = 0; + memset(ss.last_dc_coeff, 0, sizeof(ss.last_dc_coeff)); + ss.stage = EncodeScanState::BODY; + } + JpegBitWriter* bw = &ss.bw; + DCTCodingState* coding_state = &ss.coding_state; + + JXL_DASSERT(ss.stage == EncodeScanState::BODY); + + // "Non-interleaved" means color data comes in separate scans, in other words + // each scan can contain only one color component. + const bool is_interleaved = (scan_info.num_components > 1); + int MCUs_per_row = 0; + int MCU_rows = 0; + jpg.CalculateMcuSize(scan_info, &MCUs_per_row, &MCU_rows); + const bool is_progressive = state->is_progressive; + const int Al = is_progressive ? scan_info.Al : 0; + const int Ss = is_progressive ? scan_info.Ss : 0; + const int Se = is_progressive ? scan_info.Se : 63; + + // DC-only is defined by [0..0] spectral range. + const bool want_ac = ((Ss != 0) || (Se != 0)); + // TODO: support streaming decoding again. + const bool complete_ac = true; + const bool has_ac = true; + if (want_ac && !has_ac) return SerializationStatus::NEEDS_MORE_INPUT; + + // |has_ac| implies |complete_dc| but not vice versa; for the sake of + // simplicity we pretend they are equal, because they are separated by just a + // few bytes of input. + const bool complete_dc = has_ac; + const bool complete = want_ac ? complete_ac : complete_dc; + // When "incomplete" |ac_dc| tracks information about current ("incomplete") + // band parsing progress. + + // FIXME: Is this always complete? + // const int last_mcu_y = + // complete ? MCU_rows : parsing_state.internal->ac_dc.next_mcu_y * + // v_group; + (void)complete; + const int last_mcu_y = complete ? MCU_rows : 0; + + for (; ss.mcu_y < last_mcu_y; ++ss.mcu_y) { + for (int mcu_x = 0; mcu_x < MCUs_per_row; ++mcu_x) { + // Possibly emit a restart marker. + if (restart_interval > 0 && ss.restarts_to_go == 0) { + Flush(coding_state, bw); + if (!JumpToByteBoundary(bw, &state->pad_bits, state->pad_bits_end)) { + return SerializationStatus::ERROR; + } + EmitMarker(bw, 0xD0 + ss.next_restart_marker); + ss.next_restart_marker += 1; + ss.next_restart_marker &= 0x7; + ss.restarts_to_go = restart_interval; + memset(ss.last_dc_coeff, 0, sizeof(ss.last_dc_coeff)); + } + // Encode one MCU + for (size_t i = 0; i < scan_info.num_components; ++i) { + const JPEGComponentScanInfo& si = scan_info.components[i]; + const JPEGComponent& c = jpg.components[si.comp_idx]; + const HuffmanCodeTable& dc_huff = state->dc_huff_table[si.dc_tbl_idx]; + const HuffmanCodeTable& ac_huff = state->ac_huff_table[si.ac_tbl_idx]; + int n_blocks_y = is_interleaved ? c.v_samp_factor : 1; + int n_blocks_x = is_interleaved ? c.h_samp_factor : 1; + for (int iy = 0; iy < n_blocks_y; ++iy) { + for (int ix = 0; ix < n_blocks_x; ++ix) { + int block_y = ss.mcu_y * n_blocks_y + iy; + int block_x = mcu_x * n_blocks_x + ix; + int block_idx = block_y * c.width_in_blocks + block_x; + if (ss.block_scan_index == ss.next_reset_point) { + Flush(coding_state, bw); + ss.next_reset_point = get_next_reset_point(); + } + int num_zero_runs = 0; + if (ss.block_scan_index == ss.next_extra_zero_run_index) { + num_zero_runs = scan_info.extra_zero_runs[ss.extra_zero_runs_pos] + .num_extra_zero_runs; + ++ss.extra_zero_runs_pos; + ss.next_extra_zero_run_index = get_next_extra_zero_run_index(); + } + const coeff_t* coeffs = &c.coeffs[block_idx << 6]; + bool ok; + if (kMode == 0) { + ok = EncodeDCTBlockSequential(coeffs, dc_huff, ac_huff, + num_zero_runs, + ss.last_dc_coeff + si.comp_idx, bw); + } else if (kMode == 1) { + ok = EncodeDCTBlockProgressive( + coeffs, dc_huff, ac_huff, Ss, Se, Al, num_zero_runs, + coding_state, ss.last_dc_coeff + si.comp_idx, bw); + } else { + ok = EncodeRefinementBits(coeffs, ac_huff, Ss, Se, Al, + coding_state, bw); + } + if (!ok) return SerializationStatus::ERROR; + ++ss.block_scan_index; + } + } + } + --ss.restarts_to_go; + } + } + if (ss.mcu_y < MCU_rows) { + if (!bw->healthy) return SerializationStatus::ERROR; + return SerializationStatus::NEEDS_MORE_INPUT; + } + Flush(coding_state, bw); + if (!JumpToByteBoundary(bw, &state->pad_bits, state->pad_bits_end)) { + return SerializationStatus::ERROR; + } + JpegBitWriterFinish(bw); + ss.stage = EncodeScanState::HEAD; + state->scan_index++; + if (!bw->healthy) return SerializationStatus::ERROR; + + return SerializationStatus::DONE; +} + +static SerializationStatus JXL_INLINE EncodeScan(const JPEGData& jpg, + SerializationState* state) { + const JPEGScanInfo& scan_info = jpg.scan_info[state->scan_index]; + const bool is_progressive = state->is_progressive; + const int Al = is_progressive ? scan_info.Al : 0; + const int Ah = is_progressive ? scan_info.Ah : 0; + const int Ss = is_progressive ? scan_info.Ss : 0; + const int Se = is_progressive ? scan_info.Se : 63; + const bool need_sequential = + !is_progressive || (Ah == 0 && Al == 0 && Ss == 0 && Se == 63); + if (need_sequential) { + return DoEncodeScan<0>(jpg, state); + } else if (Ah == 0) { + return DoEncodeScan<1>(jpg, state); + } else { + return DoEncodeScan<2>(jpg, state); + } +} + +SerializationStatus SerializeSection(uint8_t marker, SerializationState* state, + const JPEGData& jpg) { + const auto to_status = [](bool result) { + return result ? SerializationStatus::DONE : SerializationStatus::ERROR; + }; + // TODO(eustas): add and use marker enum + switch (marker) { + case 0xC0: + case 0xC1: + case 0xC2: + case 0xC9: + case 0xCA: + return to_status(EncodeSOF(jpg, marker, state)); + + case 0xC4: + return to_status(EncodeDHT(jpg, state)); + + case 0xD0: + case 0xD1: + case 0xD2: + case 0xD3: + case 0xD4: + case 0xD5: + case 0xD6: + case 0xD7: + return to_status(EncodeRestart(marker, state)); + + case 0xD9: + return to_status(EncodeEOI(jpg, state)); + + case 0xDA: + return EncodeScan(jpg, state); + + case 0xDB: + return to_status(EncodeDQT(jpg, state)); + + case 0xDD: + return to_status(EncodeDRI(jpg, state)); + + case 0xE0: + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xED: + case 0xEE: + case 0xEF: + return to_status(EncodeAPP(jpg, marker, state)); + + case 0xFE: + return to_status(EncodeCOM(jpg, state)); + + case 0xFF: + return to_status(EncodeInterMarkerData(jpg, state)); + + default: + return SerializationStatus::ERROR; + } +} + +} // namespace + +// TODO(veluca): add streaming support again. +Status WriteJpeg(const JPEGData& jpg, const JPEGOutput& out) { + SerializationState ss; + + size_t written = 0; + const auto maybe_push_output = [&]() -> Status { + if (ss.stage != SerializationState::ERROR) { + while (!ss.output_queue.empty()) { + auto& chunk = ss.output_queue.front(); + size_t num_written = out(chunk.next, chunk.len); + if (num_written == 0 && chunk.len > 0) { + return StatusMessage(Status(StatusCode::kNotEnoughBytes), + "Failed to write output"); + } + chunk.len -= num_written; + written += num_written; + if (chunk.len == 0) { + ss.output_queue.pop_front(); + } + } + } + return true; + }; + + while (true) { + switch (ss.stage) { + case SerializationState::INIT: { + // Valid Brunsli requires, at least, 0xD9 marker. + // This might happen on corrupted stream, or on unconditioned JPEGData. + // TODO(eustas): check D9 in the only one and is the last one. + if (jpg.marker_order.empty()) { + ss.stage = SerializationState::ERROR; + break; + } + + ss.dc_huff_table.resize(kMaxHuffmanTables); + ss.ac_huff_table.resize(kMaxHuffmanTables); + if (jpg.has_zero_padding_bit) { + ss.pad_bits = jpg.padding_bits.data(); + ss.pad_bits_end = ss.pad_bits + jpg.padding_bits.size(); + } + + EncodeSOI(&ss); + JXL_QUIET_RETURN_IF_ERROR(maybe_push_output()); + ss.stage = SerializationState::SERIALIZE_SECTION; + break; + } + + case SerializationState::SERIALIZE_SECTION: { + if (ss.section_index >= jpg.marker_order.size()) { + ss.stage = SerializationState::DONE; + break; + } + uint8_t marker = jpg.marker_order[ss.section_index]; + SerializationStatus status = SerializeSection(marker, &ss, jpg); + if (status == SerializationStatus::ERROR) { + JXL_WARNING("Failed to encode marker 0x%.2x", marker); + ss.stage = SerializationState::ERROR; + break; + } + JXL_QUIET_RETURN_IF_ERROR(maybe_push_output()); + if (status == SerializationStatus::NEEDS_MORE_INPUT) { + return JXL_FAILURE("Incomplete serialization data"); + } else if (status != SerializationStatus::DONE) { + JXL_DASSERT(false); + ss.stage = SerializationState::ERROR; + break; + } + ++ss.section_index; + break; + } + + case SerializationState::DONE: + JXL_ASSERT(ss.output_queue.empty()); + return true; + + case SerializationState::ERROR: + return JXL_FAILURE("JPEG serialization error"); + } + } +} + +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/dec_jpeg_data_writer.h b/lib/jxl/jpeg/dec_jpeg_data_writer.h new file mode 100644 index 0000000..28f5141 --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_data_writer.h @@ -0,0 +1,30 @@ +// 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. + +// Functions for writing a JPEGData object into a jpeg byte stream. + +#ifndef LIB_JXL_JPEG_DEC_JPEG_DATA_WRITER_H_ +#define LIB_JXL_JPEG_DEC_JPEG_DATA_WRITER_H_ + +#include +#include + +#include + +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { + +// Function type used to write len bytes into buf. Returns the number of bytes +// written. +using JPEGOutput = std::function; + +Status WriteJpeg(const JPEGData& jpg, const JPEGOutput& out); + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_DEC_JPEG_DATA_WRITER_H_ diff --git a/lib/jxl/jpeg/dec_jpeg_output_chunk.h b/lib/jxl/jpeg/dec_jpeg_output_chunk.h new file mode 100644 index 0000000..e003c04 --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_output_chunk.h @@ -0,0 +1,72 @@ +// 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_JPEG_DEC_JPEG_OUTPUT_CHUNK_H_ +#define LIB_JXL_JPEG_DEC_JPEG_OUTPUT_CHUNK_H_ + +#include +#include + +#include +#include +#include + +namespace jxl { +namespace jpeg { + +/** + * A chunk of output data. + * + * Data producer creates OutputChunks and adds them to the end output queue. + * Once control flow leaves the producer code, it is considered that chunk of + * data is final and can not be changed; to underline this fact |next| is a + * const-pointer. + * + * Data consumer removes OutputChunks from the beginning of the output queue. + * It is possible to consume OutputChunks partially, by updating |next| and + * |len|. + * + * There are 2 types of output chunks: + * - owning: actual data is stored in |buffer| field; producer fills data after + * the instance it created; it is legal to reduce |len| to show that not all + * the capacity of |buffer| is used + * - non-owning: represents the data stored (owned) somewhere else + */ +struct OutputChunk { + // Non-owning + template + explicit OutputChunk(Bytes& bytes) : len(bytes.size()) { + // Deal both with const qualifier and data type. + const void* src = bytes.data(); + next = reinterpret_cast(src); + } + + // Non-owning + OutputChunk(const uint8_t* data, size_t size) : next(data), len(size) {} + + // Owning + explicit OutputChunk(size_t size = 0) { + buffer.reset(new std::vector(size)); + next = buffer->data(); + len = size; + } + + // Owning + OutputChunk(std::initializer_list bytes) { + buffer.reset(new std::vector(bytes)); + next = buffer->data(); + len = bytes.size(); + } + + const uint8_t* next; + size_t len; + // TODO(veluca): consider removing the unique_ptr. + std::unique_ptr> buffer; +}; + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_DEC_JPEG_OUTPUT_CHUNK_H_ diff --git a/lib/jxl/jpeg/dec_jpeg_serialization_state.h b/lib/jxl/jpeg/dec_jpeg_serialization_state.h new file mode 100644 index 0000000..a25c335 --- /dev/null +++ b/lib/jxl/jpeg/dec_jpeg_serialization_state.h @@ -0,0 +1,95 @@ +// 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_JPEG_DEC_JPEG_SERIALIZATION_STATE_H_ +#define LIB_JXL_JPEG_DEC_JPEG_SERIALIZATION_STATE_H_ + +#include +#include + +#include "lib/jxl/jpeg/dec_jpeg_output_chunk.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { + +struct HuffmanCodeTable { + int depth[256]; + int code[256]; +}; + +// Handles the packing of bits into output bytes. +struct JpegBitWriter { + bool healthy; + std::deque* output; + OutputChunk chunk; + uint8_t* data; + size_t pos; + uint64_t put_buffer; + int put_bits; +}; + +// Holds data that is buffered between 8x8 blocks in progressive mode. +struct DCTCodingState { + // The run length of end-of-band symbols in a progressive scan. + int eob_run_; + // The huffman table to be used when flushing the state. + const HuffmanCodeTable* cur_ac_huff_; + // The sequence of currently buffered refinement bits for a successive + // approximation scan (one where Ah > 0). + std::vector refinement_bits_; +}; + +struct EncodeScanState { + enum Stage { HEAD, BODY }; + + Stage stage = HEAD; + + int mcu_y; + JpegBitWriter bw; + coeff_t last_dc_coeff[kMaxComponents] = {0}; + int restarts_to_go; + int next_restart_marker; + int block_scan_index; + DCTCodingState coding_state; + size_t extra_zero_runs_pos; + int next_extra_zero_run_index; + size_t next_reset_point_pos; + int next_reset_point; +}; + +struct SerializationState { + enum Stage { + INIT, + SERIALIZE_SECTION, + DONE, + ERROR, + }; + + Stage stage = INIT; + + std::deque output_queue; + + size_t section_index = 0; + int dht_index = 0; + int dqt_index = 0; + int app_index = 0; + int com_index = 0; + int data_index = 0; + int scan_index = 0; + std::vector dc_huff_table; + std::vector ac_huff_table; + const uint8_t* pad_bits = nullptr; + const uint8_t* pad_bits_end = nullptr; + bool seen_dri_marker = false; + bool is_progressive = false; + + EncodeScanState scan_state; +}; + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_DEC_JPEG_SERIALIZATION_STATE_H_ diff --git a/lib/jxl/jpeg/enc_jpeg_data.cc b/lib/jxl/jpeg/enc_jpeg_data.cc new file mode 100644 index 0000000..079c6ef --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_data.cc @@ -0,0 +1,370 @@ +// 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/jpeg/enc_jpeg_data.h" + +#include +#include + +#include "lib/jxl/jpeg/enc_jpeg_data_reader.h" + +namespace jxl { +namespace jpeg { + +namespace { + +constexpr int BITS_IN_JSAMPLE = 8; +using ByteSpan = Span; + +// TODO(eustas): move to jpeg_data, to use from codec_jpg as well. +// See if there is a canonically chunked ICC profile and mark corresponding +// app-tags with AppMarkerType::kICC. +Status DetectIccProfile(JPEGData& jpeg_data) { + JXL_DASSERT(jpeg_data.app_data.size() == jpeg_data.app_marker_type.size()); + size_t num_icc = 0; + size_t num_icc_jpeg = 0; + for (size_t i = 0; i < jpeg_data.app_data.size(); i++) { + const auto& app = jpeg_data.app_data[i]; + size_t pos = 0; + if (app[pos++] != 0xE2) continue; + // At least APPn + size; otherwise it should be intermarker-data. + JXL_DASSERT(app.size() >= 3); + size_t tag_length = (app[pos] << 8) + app[pos + 1]; + pos += 2; + JXL_DASSERT(app.size() == tag_length + 1); + // Empty payload is 2 bytes for tag length itself + signature + if (tag_length < 2 + sizeof kIccProfileTag) continue; + + if (memcmp(&app[pos], kIccProfileTag, sizeof kIccProfileTag) != 0) continue; + pos += sizeof kIccProfileTag; + uint8_t chunk_id = app[pos++]; + uint8_t num_chunks = app[pos++]; + if (chunk_id != num_icc + 1) continue; + if (num_icc_jpeg == 0) num_icc_jpeg = num_chunks; + if (num_icc_jpeg != num_chunks) continue; + num_icc++; + jpeg_data.app_marker_type[i] = AppMarkerType::kICC; + } + if (num_icc != num_icc_jpeg) { + return JXL_FAILURE("Invalid ICC chunks"); + } + return true; +} + +bool GetMarkerPayload(const uint8_t* data, size_t size, ByteSpan* payload) { + if (size < 3) { + return false; + } + size_t hi = data[1]; + size_t lo = data[2]; + size_t internal_size = (hi << 8u) | lo; + // Second byte of marker is not counted towards size. + if (internal_size != size - 1) { + return false; + } + // cut second marker byte and "length" from payload. + *payload = ByteSpan(data, size); + payload->remove_prefix(3); + return true; +} + +Status DetectBlobs(jpeg::JPEGData& jpeg_data) { + JXL_DASSERT(jpeg_data.app_data.size() == jpeg_data.app_marker_type.size()); + bool have_exif = false, have_xmp = false; + for (size_t i = 0; i < jpeg_data.app_data.size(); i++) { + auto& marker = jpeg_data.app_data[i]; + if (marker.empty() || marker[0] != kApp1) { + continue; + } + ByteSpan payload; + if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) { + // Something is wrong with this marker; does not care. + continue; + } + if (!have_exif && payload.size() >= sizeof kExifTag && + !memcmp(payload.data(), kExifTag, sizeof kExifTag)) { + jpeg_data.app_marker_type[i] = AppMarkerType::kExif; + have_exif = true; + } + if (!have_xmp && payload.size() >= sizeof kXMPTag && + !memcmp(payload.data(), kXMPTag, sizeof kXMPTag)) { + jpeg_data.app_marker_type[i] = AppMarkerType::kXMP; + have_xmp = true; + } + } + return true; +} + +Status ParseChunkedMarker(const jpeg::JPEGData& src, uint8_t marker_type, + const ByteSpan& tag, PaddedBytes* output, + bool allow_permutations = false) { + output->clear(); + + std::vector chunks; + std::vector presence; + size_t expected_number_of_parts = 0; + bool is_first_chunk = true; + size_t ordinal = 0; + for (const auto& marker : src.app_data) { + if (marker.empty() || marker[0] != marker_type) { + continue; + } + ByteSpan payload; + if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) { + // Something is wrong with this marker; does not care. + continue; + } + if ((payload.size() < tag.size()) || + memcmp(payload.data(), tag.data(), tag.size()) != 0) { + continue; + } + payload.remove_prefix(tag.size()); + if (payload.size() < 2) { + return JXL_FAILURE("Chunk is too small."); + } + uint8_t index = payload[0]; + uint8_t total = payload[1]; + ordinal++; + if (!allow_permutations) { + if (index != ordinal) return JXL_FAILURE("Invalid chunk order."); + } + + payload.remove_prefix(2); + + JXL_RETURN_IF_ERROR(total != 0); + if (is_first_chunk) { + is_first_chunk = false; + expected_number_of_parts = total; + // 1-based indices; 0-th element is added for convenience. + chunks.resize(total + 1); + presence.resize(total + 1); + } else { + JXL_RETURN_IF_ERROR(expected_number_of_parts == total); + } + + if (index == 0 || index > total) { + return JXL_FAILURE("Invalid chunk index."); + } + + if (presence[index]) { + return JXL_FAILURE("Duplicate chunk."); + } + presence[index] = true; + chunks[index] = payload; + } + + for (size_t i = 0; i < expected_number_of_parts; ++i) { + // 0-th element is not used. + size_t index = i + 1; + if (!presence[index]) { + return JXL_FAILURE("Missing chunk."); + } + output->append(chunks[index]); + } + + return true; +} + +Status SetBlobsFromJpegData(const jpeg::JPEGData& jpeg_data, Blobs* blobs) { + for (size_t i = 0; i < jpeg_data.app_data.size(); i++) { + auto& marker = jpeg_data.app_data[i]; + if (marker.empty() || marker[0] != kApp1) { + continue; + } + ByteSpan payload; + if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) { + // Something is wrong with this marker; does not care. + continue; + } + if (payload.size() >= sizeof kExifTag && + !memcmp(payload.data(), kExifTag, sizeof kExifTag)) { + if (blobs->exif.empty()) { + blobs->exif.resize(payload.size() - sizeof kExifTag); + memcpy(blobs->exif.data(), payload.data() + sizeof kExifTag, + payload.size() - sizeof kExifTag); + } else { + JXL_WARNING( + "ReJPEG: multiple Exif blobs, storing only first one in the JPEG " + "XL container\n"); + } + } + if (payload.size() >= sizeof kXMPTag && + !memcmp(payload.data(), kXMPTag, sizeof kXMPTag)) { + if (blobs->xmp.empty()) { + blobs->xmp.resize(payload.size() - sizeof kXMPTag); + memcpy(blobs->xmp.data(), payload.data() + sizeof kXMPTag, + payload.size() - sizeof kXMPTag); + } else { + JXL_WARNING( + "ReJPEG: multiple XMP blobs, storing only first one in the JPEG " + "XL container\n"); + } + } + } + return true; +} + +} // namespace + +Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg, + ColorEncoding* color_encoding) { + PaddedBytes icc_profile; + if (!ParseChunkedMarker(jpg, kApp2, ByteSpan(kIccProfileTag), &icc_profile)) { + JXL_WARNING("ReJPEG: corrupted ICC profile\n"); + icc_profile.clear(); + } + + if (icc_profile.empty()) { + bool is_gray = (jpg.components.size() == 1); + *color_encoding = ColorEncoding::SRGB(is_gray); + return true; + } + + return color_encoding->SetICC(std::move(icc_profile)); +} + +Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes) { + jpeg_data.app_marker_type.resize(jpeg_data.app_data.size(), + AppMarkerType::kUnknown); + JXL_RETURN_IF_ERROR(DetectIccProfile(jpeg_data)); + JXL_RETURN_IF_ERROR(DetectBlobs(jpeg_data)); + BitWriter writer; + JXL_RETURN_IF_ERROR(Bundle::Write(jpeg_data, &writer, 0, nullptr)); + writer.ZeroPadToByte(); + *bytes = std::move(writer).TakeBytes(); + BrotliEncoderState* brotli_enc = + BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); + BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, 11); + 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) { + continue; + } + total_data += jpeg_data.app_data[i].size(); + } + for (size_t i = 0; i < jpeg_data.com_data.size(); i++) { + total_data += jpeg_data.com_data[i].size(); + } + for (size_t i = 0; i < jpeg_data.inter_marker_data.size(); i++) { + total_data += jpeg_data.inter_marker_data[i].size(); + } + total_data += jpeg_data.tail_data.size(); + size_t initial_size = bytes->size(); + size_t brotli_capacity = BrotliEncoderMaxCompressedSize(total_data); + BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_SIZE_HINT, total_data); + bytes->resize(bytes->size() + brotli_capacity); + size_t enc_size = 0; + auto br_append = [&](const std::vector& data, bool last) { + size_t available_in = data.size(); + const uint8_t* in = data.data(); + uint8_t* out = &(*bytes)[initial_size + enc_size]; + do { + JXL_CHECK(BrotliEncoderCompressStream( + brotli_enc, last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS, + &available_in, &in, &brotli_capacity, &out, &enc_size)); + } while (BrotliEncoderHasMoreOutput(brotli_enc) || available_in > 0); + }; + + for (size_t i = 0; i < jpeg_data.app_data.size(); i++) { + if (jpeg_data.app_marker_type[i] != AppMarkerType::kUnknown) { + continue; + } + br_append(jpeg_data.app_data[i], /*last=*/false); + } + for (size_t i = 0; i < jpeg_data.com_data.size(); i++) { + br_append(jpeg_data.com_data[i], /*last=*/false); + } + for (size_t i = 0; i < jpeg_data.inter_marker_data.size(); i++) { + br_append(jpeg_data.inter_marker_data[i], /*last=*/false); + } + br_append(jpeg_data.tail_data, /*last=*/true); + BrotliEncoderDestroyInstance(brotli_enc); + bytes->resize(initial_size + enc_size); + return true; +} + +Status DecodeImageJPG(const Span bytes, CodecInOut* io) { + io->frames.clear(); + io->frames.reserve(1); + io->frames.emplace_back(&io->metadata.m); + io->Main().jpeg_data = make_unique(); + jpeg::JPEGData* jpeg_data = io->Main().jpeg_data.get(); + if (!jpeg::ReadJpeg(bytes.data(), bytes.size(), jpeg::JpegReadMode::kReadAll, + jpeg_data)) { + return JXL_FAILURE("Error reading JPEG"); + } + JXL_RETURN_IF_ERROR( + SetColorEncodingFromJpegData(*jpeg_data, &io->metadata.m.color_encoding)); + JXL_RETURN_IF_ERROR(SetBlobsFromJpegData(*jpeg_data, &io->blobs)); + size_t nbcomp = jpeg_data->components.size(); + if (nbcomp != 1 && nbcomp != 3) { + return JXL_FAILURE("Cannot recompress JPEGs with neither 1 nor 3 channels"); + } + YCbCrChromaSubsampling cs; + if (nbcomp == 3) { + uint8_t hsample[3], vsample[3]; + for (size_t i = 0; i < nbcomp; i++) { + hsample[i] = jpeg_data->components[i].h_samp_factor; + vsample[i] = jpeg_data->components[i].v_samp_factor; + } + JXL_RETURN_IF_ERROR(cs.Set(hsample, vsample)); + } else if (nbcomp == 1) { + uint8_t hsample[3], vsample[3]; + for (size_t i = 0; i < 3; i++) { + hsample[i] = jpeg_data->components[0].h_samp_factor; + vsample[i] = jpeg_data->components[0].v_samp_factor; + } + JXL_RETURN_IF_ERROR(cs.Set(hsample, vsample)); + } + bool is_rgb = false; + { + const auto& markers = jpeg_data->marker_order; + // If there is a JFIF marker, this is YCbCr. Otherwise... + if (std::find(markers.begin(), markers.end(), 0xE0) == markers.end()) { + // Try to find an 'Adobe' marker. + size_t app_markers = 0; + size_t i = 0; + for (; i < markers.size(); i++) { + // This is an APP marker. + if ((markers[i] & 0xF0) == 0xE0) { + JXL_CHECK(app_markers < jpeg_data->app_data.size()); + // APP14 marker + if (markers[i] == 0xEE) { + const auto& data = jpeg_data->app_data[app_markers]; + if (data.size() == 15 && data[3] == 'A' && data[4] == 'd' && + data[5] == 'o' && data[6] == 'b' && data[7] == 'e') { + // 'Adobe' marker. + is_rgb = data[14] == 0; + break; + } + } + app_markers++; + } + } + + if (i == markers.size()) { + // No 'Adobe' marker, guess from component IDs. + is_rgb = nbcomp == 3 && jpeg_data->components[0].id == 'R' && + jpeg_data->components[1].id == 'G' && + jpeg_data->components[2].id == 'B'; + } + } + } + + io->Main().chroma_subsampling = cs; + io->Main().color_transform = + (!is_rgb || nbcomp == 1) ? ColorTransform::kYCbCr : ColorTransform::kNone; + + io->metadata.m.SetIntensityTarget( + io->target_nits != 0 ? io->target_nits : kDefaultIntensityTarget); + io->metadata.m.SetUintSamples(BITS_IN_JSAMPLE); + io->SetFromImage(Image3F(jpeg_data->width, jpeg_data->height), + io->metadata.m.color_encoding); + SetIntensityTarget(io); + return true; +} + +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/enc_jpeg_data.h b/lib/jxl/jpeg/enc_jpeg_data.h new file mode 100644 index 0000000..b80ade7 --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_data.h @@ -0,0 +1,28 @@ +// 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_JPEG_ENC_JPEG_DATA_H_ +#define LIB_JXL_JPEG_ENC_JPEG_DATA_H_ + +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { +Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes); + +Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg, + ColorEncoding* color_encoding); + +/** + * Decodes bytes containing JPEG codestream into a CodecInOut as coefficients + * only, for lossless JPEG transcoding. + */ +Status DecodeImageJPG(const Span bytes, CodecInOut* io); +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_ENC_JPEG_DATA_H_ diff --git a/lib/jxl/jpeg/enc_jpeg_data_reader.cc b/lib/jxl/jpeg/enc_jpeg_data_reader.cc new file mode 100644 index 0000000..6e24557 --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_data_reader.cc @@ -0,0 +1,1142 @@ +// 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/jpeg/enc_jpeg_data_reader.h" + +#include +#include + +#include +#include +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/jpeg/enc_jpeg_huffman_decode.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +// By default only print debug messages when JXL_DEBUG_ON_ERROR is enabled. +#ifndef JXL_DEBUG_JPEG_DATA_READER +#define JXL_DEBUG_JPEG_DATA_READER JXL_DEBUG_ON_ERROR +#endif // JXL_DEBUG_JPEG_DATA_READER + +#define JXL_JPEG_DEBUG(format, ...) \ + JXL_DEBUG(JXL_DEBUG_JPEG_DATA_READER, format, ##__VA_ARGS__) + +namespace jxl { +namespace jpeg { + +namespace { +static const int kBrunsliMaxSampling = 15; +static const size_t kBrunsliMaxNumBlocks = 1ull << 24; + +// Macros for commonly used error conditions. + +#define JXL_JPEG_VERIFY_LEN(n) \ + if (*pos + (n) > len) { \ + JXL_JPEG_DEBUG("Unexpected end of input: pos=%zu need=%d len=%zu", *pos, \ + static_cast(n), len); \ + jpg->error = JPEGReadError::UNEXPECTED_EOF; \ + return false; \ + } + +#define JXL_JPEG_VERIFY_INPUT(var, low, high, code) \ + if ((var) < (low) || (var) > (high)) { \ + JXL_JPEG_DEBUG("Invalid " #var ": %d", static_cast(var)); \ + jpg->error = JPEGReadError::INVALID_##code; \ + return false; \ + } + +#define JXL_JPEG_VERIFY_MARKER_END() \ + if (start_pos + marker_len != *pos) { \ + JXL_JPEG_DEBUG("Invalid marker length: declared=%zu actual=%zu", \ + marker_len, (*pos - start_pos)); \ + jpg->error = JPEGReadError::WRONG_MARKER_SIZE; \ + return false; \ + } + +#define JXL_JPEG_EXPECT_MARKER() \ + if (pos + 2 > len || data[pos] != 0xff) { \ + JXL_JPEG_DEBUG( \ + "Marker byte (0xff) expected, found: 0x%.2x pos=%zu len=%zu", \ + (pos < len ? data[pos] : 0), pos, len); \ + jpg->error = JPEGReadError::MARKER_BYTE_NOT_FOUND; \ + return false; \ + } + +inline int ReadUint8(const uint8_t* data, size_t* pos) { + return data[(*pos)++]; +} + +inline int ReadUint16(const uint8_t* data, size_t* pos) { + int v = (data[*pos] << 8) + data[*pos + 1]; + *pos += 2; + return v; +} + +// Reads the Start of Frame (SOF) marker segment and fills in *jpg with the +// parsed data. +bool ProcessSOF(const uint8_t* data, const size_t len, JpegReadMode mode, + size_t* pos, JPEGData* jpg) { + if (jpg->width != 0) { + JXL_JPEG_DEBUG("Duplicate SOF marker."); + jpg->error = JPEGReadError::DUPLICATE_SOF; + return false; + } + const size_t start_pos = *pos; + JXL_JPEG_VERIFY_LEN(8); + size_t marker_len = ReadUint16(data, pos); + int precision = ReadUint8(data, pos); + int height = ReadUint16(data, pos); + int width = ReadUint16(data, pos); + int num_components = ReadUint8(data, pos); + JXL_JPEG_VERIFY_INPUT(precision, 8, 8, PRECISION); + JXL_JPEG_VERIFY_INPUT(height, 1, kMaxDimPixels, HEIGHT); + JXL_JPEG_VERIFY_INPUT(width, 1, kMaxDimPixels, WIDTH); + JXL_JPEG_VERIFY_INPUT(num_components, 1, kMaxComponents, NUMCOMP); + JXL_JPEG_VERIFY_LEN(3 * num_components); + jpg->height = height; + jpg->width = width; + jpg->components.resize(num_components); + + // Read sampling factors and quant table index for each component. + std::vector ids_seen(256, false); + int max_h_samp_factor = 1; + int max_v_samp_factor = 1; + for (size_t i = 0; i < jpg->components.size(); ++i) { + const int id = ReadUint8(data, pos); + if (ids_seen[id]) { // (cf. section B.2.2, syntax of Ci) + JXL_JPEG_DEBUG("Duplicate ID %d in SOF.", id); + jpg->error = JPEGReadError::DUPLICATE_COMPONENT_ID; + return false; + } + ids_seen[id] = true; + jpg->components[i].id = id; + int factor = ReadUint8(data, pos); + int h_samp_factor = factor >> 4; + int v_samp_factor = factor & 0xf; + JXL_JPEG_VERIFY_INPUT(h_samp_factor, 1, kBrunsliMaxSampling, SAMP_FACTOR); + JXL_JPEG_VERIFY_INPUT(v_samp_factor, 1, kBrunsliMaxSampling, SAMP_FACTOR); + jpg->components[i].h_samp_factor = h_samp_factor; + jpg->components[i].v_samp_factor = v_samp_factor; + jpg->components[i].quant_idx = ReadUint8(data, pos); + max_h_samp_factor = std::max(max_h_samp_factor, h_samp_factor); + max_v_samp_factor = std::max(max_v_samp_factor, v_samp_factor); + } + + // We have checked above that none of the sampling factors are 0, so the max + // sampling factors can not be 0. + int MCU_rows = DivCeil(jpg->height, max_v_samp_factor * 8); + int MCU_cols = DivCeil(jpg->width, max_h_samp_factor * 8); + // Compute the block dimensions for each component. + for (size_t i = 0; i < jpg->components.size(); ++i) { + JPEGComponent* c = &jpg->components[i]; + if (max_h_samp_factor % c->h_samp_factor != 0 || + max_v_samp_factor % c->v_samp_factor != 0) { + JXL_JPEG_DEBUG("Non-integral subsampling ratios."); + jpg->error = JPEGReadError::INVALID_SAMPLING_FACTORS; + return false; + } + c->width_in_blocks = MCU_cols * c->h_samp_factor; + c->height_in_blocks = MCU_rows * c->v_samp_factor; + const uint64_t num_blocks = + static_cast(c->width_in_blocks) * c->height_in_blocks; + if (num_blocks > kBrunsliMaxNumBlocks) { + JXL_JPEG_DEBUG("Image too large."); + jpg->error = JPEGReadError::IMAGE_TOO_LARGE; + return false; + } + if (mode == JpegReadMode::kReadAll) { + c->coeffs.resize(num_blocks * kDCTBlockSize); + } + } + JXL_JPEG_VERIFY_MARKER_END(); + return true; +} + +// Reads the Start of Scan (SOS) marker segment and fills in *scan_info with the +// parsed data. +bool ProcessSOS(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + JXL_JPEG_VERIFY_LEN(3); + size_t marker_len = ReadUint16(data, pos); + size_t comps_in_scan = ReadUint8(data, pos); + JXL_JPEG_VERIFY_INPUT(comps_in_scan, 1, jpg->components.size(), + COMPS_IN_SCAN); + + JPEGScanInfo scan_info; + scan_info.num_components = comps_in_scan; + JXL_JPEG_VERIFY_LEN(2 * comps_in_scan); + std::vector ids_seen(256, false); + for (size_t i = 0; i < comps_in_scan; ++i) { + uint32_t id = ReadUint8(data, pos); + if (ids_seen[id]) { // (cf. section B.2.3, regarding CSj) + JXL_JPEG_DEBUG("Duplicate ID %d in SOS.", id); + jpg->error = JPEGReadError::DUPLICATE_COMPONENT_ID; + return false; + } + ids_seen[id] = true; + bool found_index = false; + for (size_t j = 0; j < jpg->components.size(); ++j) { + if (jpg->components[j].id == id) { + scan_info.components[i].comp_idx = j; + found_index = true; + } + } + if (!found_index) { + JXL_JPEG_DEBUG("SOS marker: Could not find component with id %d", id); + jpg->error = JPEGReadError::COMPONENT_NOT_FOUND; + return false; + } + int c = ReadUint8(data, pos); + int dc_tbl_idx = c >> 4; + int ac_tbl_idx = c & 0xf; + JXL_JPEG_VERIFY_INPUT(dc_tbl_idx, 0, 3, HUFFMAN_INDEX); + JXL_JPEG_VERIFY_INPUT(ac_tbl_idx, 0, 3, HUFFMAN_INDEX); + scan_info.components[i].dc_tbl_idx = dc_tbl_idx; + scan_info.components[i].ac_tbl_idx = ac_tbl_idx; + } + JXL_JPEG_VERIFY_LEN(3); + scan_info.Ss = ReadUint8(data, pos); + scan_info.Se = ReadUint8(data, pos); + JXL_JPEG_VERIFY_INPUT(static_cast(scan_info.Ss), 0, 63, START_OF_SCAN); + JXL_JPEG_VERIFY_INPUT(scan_info.Se, scan_info.Ss, 63, END_OF_SCAN); + int c = ReadUint8(data, pos); + scan_info.Ah = c >> 4; + scan_info.Al = c & 0xf; + if (scan_info.Ah != 0 && scan_info.Al != scan_info.Ah - 1) { + // section G.1.1.1.2 : Successive approximation control only improves + // by one bit at a time. But it's not always respected, so we just issue + // a warning. + JXL_WARNING("Invalid progressive parameters: Al=%d Ah=%d", scan_info.Al, + scan_info.Ah); + } + // Check that all the Huffman tables needed for this scan are defined. + for (size_t i = 0; i < comps_in_scan; ++i) { + bool found_dc_table = false; + bool found_ac_table = false; + for (size_t j = 0; j < jpg->huffman_code.size(); ++j) { + uint32_t slot_id = jpg->huffman_code[j].slot_id; + if (slot_id == scan_info.components[i].dc_tbl_idx) { + found_dc_table = true; + } else if (slot_id == scan_info.components[i].ac_tbl_idx + 16) { + found_ac_table = true; + } + } + if (scan_info.Ss == 0 && !found_dc_table) { + JXL_JPEG_DEBUG( + "SOS marker: Could not find DC Huffman table with index %d", + scan_info.components[i].dc_tbl_idx); + jpg->error = JPEGReadError::HUFFMAN_TABLE_NOT_FOUND; + return false; + } + if (scan_info.Se > 0 && !found_ac_table) { + JXL_JPEG_DEBUG( + "SOS marker: Could not find AC Huffman table with index %d", + scan_info.components[i].ac_tbl_idx); + jpg->error = JPEGReadError::HUFFMAN_TABLE_NOT_FOUND; + return false; + } + } + jpg->scan_info.push_back(scan_info); + JXL_JPEG_VERIFY_MARKER_END(); + return true; +} + +// Reads the Define Huffman Table (DHT) marker segment and fills in *jpg with +// the parsed data. Builds the Huffman decoding table in either dc_huff_lut or +// ac_huff_lut, depending on the type and solt_id of Huffman code being read. +bool ProcessDHT(const uint8_t* data, const size_t len, JpegReadMode mode, + std::vector* dc_huff_lut, + std::vector* ac_huff_lut, size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + JXL_JPEG_VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + if (marker_len == 2) { + JXL_JPEG_DEBUG("DHT marker: no Huffman table found"); + jpg->error = JPEGReadError::EMPTY_DHT; + return false; + } + while (*pos < start_pos + marker_len) { + JXL_JPEG_VERIFY_LEN(1 + kJpegHuffmanMaxBitLength); + JPEGHuffmanCode huff; + huff.slot_id = ReadUint8(data, pos); + int huffman_index = huff.slot_id; + int is_ac_table = (huff.slot_id & 0x10) != 0; + HuffmanTableEntry* huff_lut; + if (is_ac_table) { + huffman_index -= 0x10; + JXL_JPEG_VERIFY_INPUT(huffman_index, 0, 3, HUFFMAN_INDEX); + huff_lut = &(*ac_huff_lut)[huffman_index * kJpegHuffmanLutSize]; + } else { + JXL_JPEG_VERIFY_INPUT(huffman_index, 0, 3, HUFFMAN_INDEX); + huff_lut = &(*dc_huff_lut)[huffman_index * kJpegHuffmanLutSize]; + } + huff.counts[0] = 0; + int total_count = 0; + int space = 1 << kJpegHuffmanMaxBitLength; + int max_depth = 1; + for (size_t i = 1; i <= kJpegHuffmanMaxBitLength; ++i) { + int count = ReadUint8(data, pos); + if (count != 0) { + max_depth = i; + } + huff.counts[i] = count; + total_count += count; + space -= count * (1 << (kJpegHuffmanMaxBitLength - i)); + } + if (is_ac_table) { + JXL_JPEG_VERIFY_INPUT(total_count, 0, kJpegHuffmanAlphabetSize, + HUFFMAN_CODE); + } else { + JXL_JPEG_VERIFY_INPUT(total_count, 0, kJpegDCAlphabetSize, HUFFMAN_CODE); + } + JXL_JPEG_VERIFY_LEN(total_count); + std::vector values_seen(256, false); + for (int i = 0; i < total_count; ++i) { + int value = ReadUint8(data, pos); + if (!is_ac_table) { + JXL_JPEG_VERIFY_INPUT(value, 0, kJpegDCAlphabetSize - 1, HUFFMAN_CODE); + } + if (values_seen[value]) { + JXL_JPEG_DEBUG("Duplicate Huffman code value %d", value); + jpg->error = JPEGReadError::INVALID_HUFFMAN_CODE; + return false; + } + values_seen[value] = true; + huff.values[i] = value; + } + // Add an invalid symbol that will have the all 1 code. + ++huff.counts[max_depth]; + huff.values[total_count] = kJpegHuffmanAlphabetSize; + space -= (1 << (kJpegHuffmanMaxBitLength - max_depth)); + if (space < 0) { + JXL_JPEG_DEBUG("Invalid Huffman code lengths."); + jpg->error = JPEGReadError::INVALID_HUFFMAN_CODE; + return false; + } else if (space > 0 && huff_lut[0].value != 0xffff) { + // Re-initialize the values to an invalid symbol so that we can recognize + // it when reading the bit stream using a Huffman code with space > 0. + for (int i = 0; i < kJpegHuffmanLutSize; ++i) { + huff_lut[i].bits = 0; + huff_lut[i].value = 0xffff; + } + } + huff.is_last = (*pos == start_pos + marker_len); + if (mode == JpegReadMode::kReadAll) { + BuildJpegHuffmanTable(&huff.counts[0], &huff.values[0], huff_lut); + } + jpg->huffman_code.push_back(huff); + } + JXL_JPEG_VERIFY_MARKER_END(); + return true; +} + +// Reads the Define Quantization Table (DQT) marker segment and fills in *jpg +// with the parsed data. +bool ProcessDQT(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + JXL_JPEG_VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + if (marker_len == 2) { + JXL_JPEG_DEBUG("DQT marker: no quantization table found"); + jpg->error = JPEGReadError::EMPTY_DQT; + return false; + } + while (*pos < start_pos + marker_len && jpg->quant.size() < kMaxQuantTables) { + JXL_JPEG_VERIFY_LEN(1); + int quant_table_index = ReadUint8(data, pos); + int quant_table_precision = quant_table_index >> 4; + JXL_JPEG_VERIFY_INPUT(quant_table_precision, 0, 1, QUANT_TBL_PRECISION); + quant_table_index &= 0xf; + JXL_JPEG_VERIFY_INPUT(quant_table_index, 0, 3, QUANT_TBL_INDEX); + JXL_JPEG_VERIFY_LEN((quant_table_precision + 1) * kDCTBlockSize); + JPEGQuantTable table; + table.index = quant_table_index; + table.precision = quant_table_precision; + for (size_t i = 0; i < kDCTBlockSize; ++i) { + int quant_val = + quant_table_precision ? ReadUint16(data, pos) : ReadUint8(data, pos); + JXL_JPEG_VERIFY_INPUT(quant_val, 1, 65535, QUANT_VAL); + table.values[kJPEGNaturalOrder[i]] = quant_val; + } + table.is_last = (*pos == start_pos + marker_len); + jpg->quant.push_back(table); + } + JXL_JPEG_VERIFY_MARKER_END(); + return true; +} + +// Reads the DRI marker and saves the restart interval into *jpg. +bool ProcessDRI(const uint8_t* data, const size_t len, size_t* pos, + bool* found_dri, JPEGData* jpg) { + if (*found_dri) { + JXL_JPEG_DEBUG("Duplicate DRI marker."); + jpg->error = JPEGReadError::DUPLICATE_DRI; + return false; + } + *found_dri = true; + const size_t start_pos = *pos; + JXL_JPEG_VERIFY_LEN(4); + size_t marker_len = ReadUint16(data, pos); + int restart_interval = ReadUint16(data, pos); + jpg->restart_interval = restart_interval; + JXL_JPEG_VERIFY_MARKER_END(); + return true; +} + +// Saves the APP marker segment as a string to *jpg. +bool ProcessAPP(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + JXL_JPEG_VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + JXL_JPEG_VERIFY_INPUT(marker_len, 2, 65535, MARKER_LEN); + JXL_JPEG_VERIFY_LEN(marker_len - 2); + JXL_DASSERT(*pos >= 3); + // Save the marker type together with the app data. + const uint8_t* app_str_start = data + *pos - 3; + std::vector app_str(app_str_start, app_str_start + marker_len + 1); + *pos += marker_len - 2; + jpg->app_data.push_back(app_str); + return true; +} + +// Saves the COM marker segment as a string to *jpg. +bool ProcessCOM(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + JXL_JPEG_VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + JXL_JPEG_VERIFY_INPUT(marker_len, 2, 65535, MARKER_LEN); + JXL_JPEG_VERIFY_LEN(marker_len - 2); + const uint8_t* com_str_start = data + *pos - 3; + std::vector com_str(com_str_start, com_str_start + marker_len + 1); + *pos += marker_len - 2; + jpg->com_data.push_back(com_str); + return true; +} + +// Helper structure to read bits from the entropy coded data segment. +struct BitReaderState { + BitReaderState(const uint8_t* data, const size_t len, size_t pos) + : data_(data), len_(len) { + Reset(pos); + } + + void Reset(size_t pos) { + pos_ = pos; + val_ = 0; + bits_left_ = 0; + next_marker_pos_ = len_ - 2; + FillBitWindow(); + } + + // Returns the next byte and skips the 0xff/0x00 escape sequences. + uint8_t GetNextByte() { + if (pos_ >= next_marker_pos_) { + ++pos_; + return 0; + } + uint8_t c = data_[pos_++]; + if (c == 0xff) { + uint8_t escape = data_[pos_]; + if (escape == 0) { + ++pos_; + } else { + // 0xff was followed by a non-zero byte, which means that we found the + // start of the next marker segment. + next_marker_pos_ = pos_ - 1; + } + } + return c; + } + + void FillBitWindow() { + if (bits_left_ <= 16) { + while (bits_left_ <= 56) { + val_ <<= 8; + val_ |= (uint64_t)GetNextByte(); + bits_left_ += 8; + } + } + } + + int ReadBits(int nbits) { + FillBitWindow(); + uint64_t val = (val_ >> (bits_left_ - nbits)) & ((1ULL << nbits) - 1); + bits_left_ -= nbits; + return val; + } + + // Sets *pos to the next stream position where parsing should continue. + // Enqueue the padding bits seen (0 or 1). + // Returns false if there is inconsistent or invalid padding or the stream + // ended too early. + bool FinishStream(JPEGData* jpg, size_t* pos) { + int npadbits = bits_left_ & 7; + if (npadbits > 0) { + uint64_t padmask = (1ULL << npadbits) - 1; + uint64_t padbits = (val_ >> (bits_left_ - npadbits)) & padmask; + if (padbits != padmask) { + jpg->has_zero_padding_bit = true; + } + for (int i = npadbits - 1; i >= 0; --i) { + jpg->padding_bits.push_back((padbits >> i) & 1); + } + } + // Give back some bytes that we did not use. + int unused_bytes_left = bits_left_ >> 3; + while (unused_bytes_left-- > 0) { + --pos_; + // If we give back a 0 byte, we need to check if it was a 0xff/0x00 escape + // sequence, and if yes, we need to give back one more byte. + if (pos_ < next_marker_pos_ && data_[pos_] == 0 && + data_[pos_ - 1] == 0xff) { + --pos_; + } + } + if (pos_ > next_marker_pos_) { + // Data ran out before the scan was complete. + JXL_JPEG_DEBUG("Unexpected end of scan."); + return false; + } + *pos = pos_; + return true; + } + + const uint8_t* data_; + const size_t len_; + size_t pos_; + uint64_t val_; + int bits_left_; + size_t next_marker_pos_; +}; + +// Returns the next Huffman-coded symbol. +int ReadSymbol(const HuffmanTableEntry* table, BitReaderState* br) { + int nbits; + br->FillBitWindow(); + int val = (br->val_ >> (br->bits_left_ - 8)) & 0xff; + table += val; + nbits = table->bits - 8; + if (nbits > 0) { + br->bits_left_ -= 8; + table += table->value; + val = (br->val_ >> (br->bits_left_ - nbits)) & ((1 << nbits) - 1); + table += val; + } + br->bits_left_ -= table->bits; + return table->value; +} + +/** + * Returns the DC diff or AC value for extra bits value x and prefix code s. + * + * CCITT Rec. T.81 (1992 E) + * Table F.1 – Difference magnitude categories for DC coding + * SSSS | DIFF values + * ------+-------------------------- + * 0 | 0 + * 1 | –1, 1 + * 2 | –3, –2, 2, 3 + * 3 | –7..–4, 4..7 + * ......|.......................... + * 11 | –2047..–1024, 1024..2047 + * + * CCITT Rec. T.81 (1992 E) + * Table F.2 – Categories assigned to coefficient values + * [ Same as Table F.1, but does not include SSSS equal to 0 and 11] + * + * + * CCITT Rec. T.81 (1992 E) + * F.1.2.1.1 Structure of DC code table + * For each category,... additional bits... appended... to uniquely identify + * which difference... occurred... When DIFF is positive... SSSS... bits of DIFF + * are appended. When DIFF is negative... SSSS... bits of (DIFF – 1) are + * appended... Most significant bit... is 0 for negative differences and 1 for + * positive differences. + * + * In other words the upper half of extra bits range represents DIFF as is. + * The lower half represents the negative DIFFs with an offset. + */ +int HuffExtend(int x, int s) { + JXL_DASSERT(s >= 1); + int half = 1 << (s - 1); + if (x >= half) { + JXL_DASSERT(x < (1 << s)); + return x; + } else { + return x - (1 << s) + 1; + } +} + +// Decodes one 8x8 block of DCT coefficients from the bit stream. +bool DecodeDCTBlock(const HuffmanTableEntry* dc_huff, + const HuffmanTableEntry* ac_huff, int Ss, int Se, int Al, + int* eobrun, bool* reset_state, int* num_zero_runs, + BitReaderState* br, JPEGData* jpg, coeff_t* last_dc_coeff, + coeff_t* coeffs) { + // Nowadays multiplication is even faster than variable shift. + int Am = 1 << Al; + bool eobrun_allowed = Ss > 0; + if (Ss == 0) { + int s = ReadSymbol(dc_huff, br); + if (s >= kJpegDCAlphabetSize) { + JXL_JPEG_DEBUG("Invalid Huffman symbol %d for DC coefficient.", s); + jpg->error = JPEGReadError::INVALID_SYMBOL; + return false; + } + int diff = 0; + if (s > 0) { + int bits = br->ReadBits(s); + diff = HuffExtend(bits, s); + } + int coeff = diff + *last_dc_coeff; + const int dc_coeff = coeff * Am; + coeffs[0] = dc_coeff; + // TODO(eustas): is there a more elegant / explicit way to check this? + if (dc_coeff != coeffs[0]) { + JXL_JPEG_DEBUG("Invalid DC coefficient %d", dc_coeff); + jpg->error = JPEGReadError::NON_REPRESENTABLE_DC_COEFF; + return false; + } + *last_dc_coeff = coeff; + ++Ss; + } + if (Ss > Se) { + return true; + } + if (*eobrun > 0) { + --(*eobrun); + return true; + } + *num_zero_runs = 0; + for (int k = Ss; k <= Se; k++) { + int sr = ReadSymbol(ac_huff, br); + if (sr >= kJpegHuffmanAlphabetSize) { + JXL_JPEG_DEBUG("Invalid Huffman symbol %d for AC coefficient %d", sr, k); + jpg->error = JPEGReadError::INVALID_SYMBOL; + return false; + } + int r = sr >> 4; + int s = sr & 15; + if (s > 0) { + k += r; + if (k > Se) { + JXL_JPEG_DEBUG("Out-of-band coefficient %d band was %d-%d", k, Ss, Se); + jpg->error = JPEGReadError::OUT_OF_BAND_COEFF; + return false; + } + if (s + Al >= kJpegDCAlphabetSize) { + JXL_JPEG_DEBUG( + "Out of range AC coefficient value: s = %d Al = %d k = %d", s, Al, + k); + jpg->error = JPEGReadError::NON_REPRESENTABLE_AC_COEFF; + return false; + } + int bits = br->ReadBits(s); + int coeff = HuffExtend(bits, s); + coeffs[kJPEGNaturalOrder[k]] = coeff * Am; + *num_zero_runs = 0; + } else if (r == 15) { + k += 15; + ++(*num_zero_runs); + } else { + if (eobrun_allowed && k == Ss && *eobrun == 0) { + // We have two end-of-block runs right after each other, so we signal + // the jpeg encoder to force a state reset at this point. + *reset_state = true; + } + *eobrun = 1 << r; + if (r > 0) { + if (!eobrun_allowed) { + JXL_JPEG_DEBUG("End-of-block run crossing DC coeff."); + jpg->error = JPEGReadError::EOB_RUN_TOO_LONG; + return false; + } + *eobrun += br->ReadBits(r); + } + break; + } + } + --(*eobrun); + return true; +} + +bool RefineDCTBlock(const HuffmanTableEntry* ac_huff, int Ss, int Se, int Al, + int* eobrun, bool* reset_state, BitReaderState* br, + JPEGData* jpg, coeff_t* coeffs) { + // Nowadays multiplication is even faster than variable shift. + int Am = 1 << Al; + bool eobrun_allowed = Ss > 0; + if (Ss == 0) { + int s = br->ReadBits(1); + coeff_t dc_coeff = coeffs[0]; + dc_coeff |= s * Am; + coeffs[0] = dc_coeff; + ++Ss; + } + if (Ss > Se) { + return true; + } + int p1 = Am; + int m1 = -Am; + int k = Ss; + int r; + int s; + bool in_zero_run = false; + if (*eobrun <= 0) { + for (; k <= Se; k++) { + s = ReadSymbol(ac_huff, br); + if (s >= kJpegHuffmanAlphabetSize) { + JXL_JPEG_DEBUG("Invalid Huffman symbol %d for AC coefficient %d", s, k); + jpg->error = JPEGReadError::INVALID_SYMBOL; + return false; + } + r = s >> 4; + s &= 15; + if (s) { + if (s != 1) { + JXL_JPEG_DEBUG("Invalid Huffman symbol %d for AC coefficient %d", s, + k); + jpg->error = JPEGReadError::INVALID_SYMBOL; + return false; + } + s = br->ReadBits(1) ? p1 : m1; + in_zero_run = false; + } else { + if (r != 15) { + if (eobrun_allowed && k == Ss && *eobrun == 0) { + // We have two end-of-block runs right after each other, so we + // signal the jpeg encoder to force a state reset at this point. + *reset_state = true; + } + *eobrun = 1 << r; + if (r > 0) { + if (!eobrun_allowed) { + JXL_JPEG_DEBUG("End-of-block run crossing DC coeff."); + jpg->error = JPEGReadError::EOB_RUN_TOO_LONG; + return false; + } + *eobrun += br->ReadBits(r); + } + break; + } + in_zero_run = true; + } + do { + coeff_t thiscoef = coeffs[kJPEGNaturalOrder[k]]; + if (thiscoef != 0) { + if (br->ReadBits(1)) { + if ((thiscoef & p1) == 0) { + if (thiscoef >= 0) { + thiscoef += p1; + } else { + thiscoef += m1; + } + } + } + coeffs[kJPEGNaturalOrder[k]] = thiscoef; + } else { + if (--r < 0) { + break; + } + } + k++; + } while (k <= Se); + if (s) { + if (k > Se) { + JXL_JPEG_DEBUG("Out-of-band coefficient %d band was %d-%d", k, Ss, + Se); + jpg->error = JPEGReadError::OUT_OF_BAND_COEFF; + return false; + } + coeffs[kJPEGNaturalOrder[k]] = s; + } + } + } + if (in_zero_run) { + JXL_JPEG_DEBUG("Extra zero run before end-of-block."); + jpg->error = JPEGReadError::EXTRA_ZERO_RUN; + return false; + } + if (*eobrun > 0) { + for (; k <= Se; k++) { + coeff_t thiscoef = coeffs[kJPEGNaturalOrder[k]]; + if (thiscoef != 0) { + if (br->ReadBits(1)) { + if ((thiscoef & p1) == 0) { + if (thiscoef >= 0) { + thiscoef += p1; + } else { + thiscoef += m1; + } + } + } + coeffs[kJPEGNaturalOrder[k]] = thiscoef; + } + } + } + --(*eobrun); + return true; +} + +bool ProcessRestart(const uint8_t* data, const size_t len, + int* next_restart_marker, BitReaderState* br, + JPEGData* jpg) { + size_t pos = 0; + if (!br->FinishStream(jpg, &pos)) { + jpg->error = JPEGReadError::INVALID_SCAN; + return false; + } + int expected_marker = 0xd0 + *next_restart_marker; + JXL_JPEG_EXPECT_MARKER(); + int marker = data[pos + 1]; + if (marker != expected_marker) { + JXL_JPEG_DEBUG("Did not find expected restart marker %d actual %d", + expected_marker, marker); + jpg->error = JPEGReadError::WRONG_RESTART_MARKER; + return false; + } + br->Reset(pos + 2); + *next_restart_marker += 1; + *next_restart_marker &= 0x7; + return true; +} + +bool ProcessScan(const uint8_t* data, const size_t len, + const std::vector& dc_huff_lut, + const std::vector& ac_huff_lut, + uint16_t scan_progression[kMaxComponents][kDCTBlockSize], + bool is_progressive, size_t* pos, JPEGData* jpg) { + if (!ProcessSOS(data, len, pos, jpg)) { + return false; + } + JPEGScanInfo* scan_info = &jpg->scan_info.back(); + bool is_interleaved = (scan_info->num_components > 1); + int max_h_samp_factor = 1; + int max_v_samp_factor = 1; + for (size_t i = 0; i < jpg->components.size(); ++i) { + max_h_samp_factor = + std::max(max_h_samp_factor, jpg->components[i].h_samp_factor); + max_v_samp_factor = + std::max(max_v_samp_factor, jpg->components[i].v_samp_factor); + } + + int MCU_rows = DivCeil(jpg->height, max_v_samp_factor * 8); + int MCUs_per_row = DivCeil(jpg->width, max_h_samp_factor * 8); + if (!is_interleaved) { + const JPEGComponent& c = jpg->components[scan_info->components[0].comp_idx]; + MCUs_per_row = DivCeil(jpg->width * c.h_samp_factor, 8 * max_h_samp_factor); + MCU_rows = DivCeil(jpg->height * c.v_samp_factor, 8 * max_v_samp_factor); + } + coeff_t last_dc_coeff[kMaxComponents] = {0}; + BitReaderState br(data, len, *pos); + int restarts_to_go = jpg->restart_interval; + int next_restart_marker = 0; + int eobrun = -1; + int block_scan_index = 0; + const int Al = is_progressive ? scan_info->Al : 0; + const int Ah = is_progressive ? scan_info->Ah : 0; + const int Ss = is_progressive ? scan_info->Ss : 0; + const int Se = is_progressive ? scan_info->Se : 63; + const uint16_t scan_bitmask = Ah == 0 ? (0xffff << Al) : (1u << Al); + const uint16_t refinement_bitmask = (1 << Al) - 1; + for (size_t i = 0; i < scan_info->num_components; ++i) { + int comp_idx = scan_info->components[i].comp_idx; + for (int k = Ss; k <= Se; ++k) { + if (scan_progression[comp_idx][k] & scan_bitmask) { + JXL_JPEG_DEBUG( + "Overlapping scans: component=%d k=%d prev_mask: %u cur_mask %u", + comp_idx, k, scan_progression[i][k], scan_bitmask); + jpg->error = JPEGReadError::OVERLAPPING_SCANS; + return false; + } + if (scan_progression[comp_idx][k] & refinement_bitmask) { + JXL_JPEG_DEBUG( + "Invalid scan order, a more refined scan was already done: " + "component=%d k=%d prev_mask=%u cur_mask=%u", + comp_idx, k, scan_progression[i][k], scan_bitmask); + jpg->error = JPEGReadError::INVALID_SCAN_ORDER; + return false; + } + scan_progression[comp_idx][k] |= scan_bitmask; + } + } + if (Al > 10) { + JXL_JPEG_DEBUG("Scan parameter Al=%d is not supported.", Al); + jpg->error = JPEGReadError::NON_REPRESENTABLE_AC_COEFF; + return false; + } + for (int mcu_y = 0; mcu_y < MCU_rows; ++mcu_y) { + for (int mcu_x = 0; mcu_x < MCUs_per_row; ++mcu_x) { + // Handle the restart intervals. + if (jpg->restart_interval > 0) { + if (restarts_to_go == 0) { + if (ProcessRestart(data, len, &next_restart_marker, &br, jpg)) { + restarts_to_go = jpg->restart_interval; + memset(static_cast(last_dc_coeff), 0, sizeof(last_dc_coeff)); + if (eobrun > 0) { + JXL_JPEG_DEBUG("End-of-block run too long."); + jpg->error = JPEGReadError::EOB_RUN_TOO_LONG; + return false; + } + eobrun = -1; // fresh start + } else { + return false; + } + } + --restarts_to_go; + } + // Decode one MCU. + for (size_t i = 0; i < scan_info->num_components; ++i) { + JPEGComponentScanInfo* si = &scan_info->components[i]; + JPEGComponent* c = &jpg->components[si->comp_idx]; + const HuffmanTableEntry* dc_lut = + &dc_huff_lut[si->dc_tbl_idx * kJpegHuffmanLutSize]; + const HuffmanTableEntry* ac_lut = + &ac_huff_lut[si->ac_tbl_idx * kJpegHuffmanLutSize]; + int nblocks_y = is_interleaved ? c->v_samp_factor : 1; + int nblocks_x = is_interleaved ? c->h_samp_factor : 1; + for (int iy = 0; iy < nblocks_y; ++iy) { + for (int ix = 0; ix < nblocks_x; ++ix) { + int block_y = mcu_y * nblocks_y + iy; + int block_x = mcu_x * nblocks_x + ix; + int block_idx = block_y * c->width_in_blocks + block_x; + bool reset_state = false; + int num_zero_runs = 0; + coeff_t* coeffs = &c->coeffs[block_idx * kDCTBlockSize]; + if (Ah == 0) { + if (!DecodeDCTBlock(dc_lut, ac_lut, Ss, Se, Al, &eobrun, + &reset_state, &num_zero_runs, &br, jpg, + &last_dc_coeff[si->comp_idx], coeffs)) { + return false; + } + } else { + if (!RefineDCTBlock(ac_lut, Ss, Se, Al, &eobrun, &reset_state, + &br, jpg, coeffs)) { + return false; + } + } + if (reset_state) { + scan_info->reset_points.emplace_back(block_scan_index); + } + if (num_zero_runs > 0) { + JPEGScanInfo::ExtraZeroRunInfo info; + info.block_idx = block_scan_index; + info.num_extra_zero_runs = num_zero_runs; + scan_info->extra_zero_runs.push_back(info); + } + ++block_scan_index; + } + } + } + } + } + if (eobrun > 0) { + JXL_JPEG_DEBUG("End-of-block run too long."); + jpg->error = JPEGReadError::EOB_RUN_TOO_LONG; + return false; + } + if (!br.FinishStream(jpg, pos)) { + jpg->error = JPEGReadError::INVALID_SCAN; + return false; + } + if (*pos > len) { + JXL_JPEG_DEBUG("Unexpected end of file during scan. pos=%zu len=%zu", *pos, + len); + jpg->error = JPEGReadError::UNEXPECTED_EOF; + return false; + } + return true; +} + +// Changes the quant_idx field of the components to refer to the index of the +// quant table in the jpg->quant array. +bool FixupIndexes(JPEGData* jpg) { + for (size_t i = 0; i < jpg->components.size(); ++i) { + JPEGComponent* c = &jpg->components[i]; + bool found_index = false; + for (size_t j = 0; j < jpg->quant.size(); ++j) { + if (jpg->quant[j].index == c->quant_idx) { + c->quant_idx = j; + found_index = true; + break; + } + } + if (!found_index) { + JXL_JPEG_DEBUG("Quantization table with index %u not found", + c->quant_idx); + jpg->error = JPEGReadError::QUANT_TABLE_NOT_FOUND; + return false; + } + } + return true; +} + +size_t FindNextMarker(const uint8_t* data, const size_t len, size_t pos) { + // kIsValidMarker[i] == 1 means (0xc0 + i) is a valid marker. + static const uint8_t kIsValidMarker[] = { + 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + }; + size_t num_skipped = 0; + while (pos + 1 < len && (data[pos] != 0xff || data[pos + 1] < 0xc0 || + !kIsValidMarker[data[pos + 1] - 0xc0])) { + ++pos; + ++num_skipped; + } + return num_skipped; +} + +} // namespace + +bool ReadJpeg(const uint8_t* data, const size_t len, JpegReadMode mode, + JPEGData* jpg) { + size_t pos = 0; + // Check SOI marker. + JXL_JPEG_EXPECT_MARKER(); + int marker = data[pos + 1]; + pos += 2; + if (marker != 0xd8) { + JXL_JPEG_DEBUG("Did not find expected SOI marker, actual=%d", marker); + jpg->error = JPEGReadError::SOI_NOT_FOUND; + return false; + } + int lut_size = kMaxHuffmanTables * kJpegHuffmanLutSize; + std::vector dc_huff_lut(lut_size); + std::vector ac_huff_lut(lut_size); + bool found_sof = false; + bool found_dri = false; + uint16_t scan_progression[kMaxComponents][kDCTBlockSize] = {{0}}; + + jpg->padding_bits.resize(0); + bool is_progressive = false; // default + do { + // Read next marker. + size_t num_skipped = FindNextMarker(data, len, pos); + if (num_skipped > 0) { + // Add a fake marker to indicate arbitrary in-between-markers data. + jpg->marker_order.push_back(0xff); + jpg->inter_marker_data.emplace_back(data + pos, data + pos + num_skipped); + pos += num_skipped; + } + JXL_JPEG_EXPECT_MARKER(); + marker = data[pos + 1]; + pos += 2; + bool ok = true; + switch (marker) { + case 0xc0: + case 0xc1: + case 0xc2: + is_progressive = (marker == 0xc2); + ok = ProcessSOF(data, len, mode, &pos, jpg); + found_sof = true; + break; + case 0xc4: + ok = ProcessDHT(data, len, mode, &dc_huff_lut, &ac_huff_lut, &pos, jpg); + break; + case 0xd0: + case 0xd1: + case 0xd2: + case 0xd3: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xd7: + // RST markers do not have any data. + break; + case 0xd9: + // Found end marker. + break; + case 0xda: + if (mode == JpegReadMode::kReadAll) { + ok = ProcessScan(data, len, dc_huff_lut, ac_huff_lut, + scan_progression, is_progressive, &pos, jpg); + } + break; + case 0xdb: + ok = ProcessDQT(data, len, &pos, jpg); + break; + case 0xdd: + ok = ProcessDRI(data, len, &pos, &found_dri, jpg); + break; + case 0xe0: + case 0xe1: + case 0xe2: + case 0xe3: + case 0xe4: + case 0xe5: + case 0xe6: + case 0xe7: + case 0xe8: + case 0xe9: + case 0xea: + case 0xeb: + case 0xec: + case 0xed: + case 0xee: + case 0xef: + if (mode != JpegReadMode::kReadTables) { + ok = ProcessAPP(data, len, &pos, jpg); + } + break; + case 0xfe: + if (mode != JpegReadMode::kReadTables) { + ok = ProcessCOM(data, len, &pos, jpg); + } + break; + default: + JXL_JPEG_DEBUG("Unsupported marker: %d pos=%zu len=%zu", marker, pos, + len); + jpg->error = JPEGReadError::UNSUPPORTED_MARKER; + ok = false; + break; + } + if (!ok) { + return false; + } + jpg->marker_order.push_back(marker); + if (mode == JpegReadMode::kReadHeader && found_sof) { + break; + } + } while (marker != 0xd9); + + if (!found_sof) { + JXL_JPEG_DEBUG("Missing SOF marker."); + jpg->error = JPEGReadError::SOF_NOT_FOUND; + return false; + } + + // Supplemental checks. + if (mode == JpegReadMode::kReadAll) { + if (pos < len) { + jpg->tail_data = std::vector(data + pos, data + len); + } + if (!FixupIndexes(jpg)) { + return false; + } + if (jpg->huffman_code.empty()) { + // Section B.2.4.2: "If a table has never been defined for a particular + // destination, then when this destination is specified in a scan header, + // the results are unpredictable." + JXL_JPEG_DEBUG("Need at least one Huffman code table."); + jpg->error = JPEGReadError::HUFFMAN_TABLE_ERROR; + return false; + } + if (jpg->huffman_code.size() >= kMaxDHTMarkers) { + JXL_JPEG_DEBUG("Too many Huffman tables."); + jpg->error = JPEGReadError::HUFFMAN_TABLE_ERROR; + return false; + } + } + return true; +} + +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/enc_jpeg_data_reader.h b/lib/jxl/jpeg/enc_jpeg_data_reader.h new file mode 100644 index 0000000..3fad820 --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_data_reader.h @@ -0,0 +1,36 @@ +// 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. + +// Functions for reading a jpeg byte stream into a JPEGData object. + +#ifndef LIB_JXL_JPEG_ENC_JPEG_DATA_READER_H_ +#define LIB_JXL_JPEG_ENC_JPEG_DATA_READER_H_ + +#include +#include + +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { + +enum class JpegReadMode { + kReadHeader, // only basic headers + kReadTables, // headers and tables (quant, Huffman, ...) + kReadAll, // everything +}; + +// Parses the JPEG stream contained in data[*pos ... len) and fills in *jpg with +// the parsed information. +// If mode is kReadHeader, it fills in only the image dimensions in *jpg. +// Returns false if the data is not valid JPEG, or if it contains an unsupported +// JPEG feature. +bool ReadJpeg(const uint8_t* data, const size_t len, JpegReadMode mode, + JPEGData* jpg); + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_ENC_JPEG_DATA_READER_H_ diff --git a/lib/jxl/jpeg/enc_jpeg_huffman_decode.cc b/lib/jxl/jpeg/enc_jpeg_huffman_decode.cc new file mode 100644 index 0000000..38282e6 --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_huffman_decode.cc @@ -0,0 +1,103 @@ +// 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/jpeg/enc_jpeg_huffman_decode.h" + +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jxl { +namespace jpeg { + +// Returns the table width of the next 2nd level table, count is the histogram +// of bit lengths for the remaining symbols, len is the code length of the next +// processed symbol. +static inline int NextTableBitSize(const int* count, int len) { + int left = 1 << (len - kJpegHuffmanRootTableBits); + while (len < static_cast(kJpegHuffmanMaxBitLength)) { + left -= count[len]; + if (left <= 0) break; + ++len; + left <<= 1; + } + return len - kJpegHuffmanRootTableBits; +} + +void BuildJpegHuffmanTable(const uint32_t* count, const uint32_t* symbols, + HuffmanTableEntry* lut) { + HuffmanTableEntry code; // current table entry + HuffmanTableEntry* table; // next available space in table + int len; // current code length + int idx; // symbol index + int key; // prefix code + int reps; // number of replicate key values in current table + int low; // low bits for current root entry + int table_bits; // key length of current table + int table_size; // size of current table + + // Make a local copy of the input bit length histogram. + int tmp_count[kJpegHuffmanMaxBitLength + 1] = {0}; + int total_count = 0; + for (len = 1; len <= static_cast(kJpegHuffmanMaxBitLength); ++len) { + tmp_count[len] = count[len]; + total_count += tmp_count[len]; + } + + table = lut; + table_bits = kJpegHuffmanRootTableBits; + table_size = 1 << table_bits; + + // Special case code with only one value. + if (total_count == 1) { + code.bits = 0; + code.value = symbols[0]; + for (key = 0; key < table_size; ++key) { + table[key] = code; + } + return; + } + + // Fill in root table. + key = 0; + idx = 0; + for (len = 1; len <= kJpegHuffmanRootTableBits; ++len) { + for (; tmp_count[len] > 0; --tmp_count[len]) { + code.bits = len; + code.value = symbols[idx++]; + reps = 1 << (kJpegHuffmanRootTableBits - len); + while (reps--) { + table[key++] = code; + } + } + } + + // Fill in 2nd level tables and add pointers to root table. + table += table_size; + table_size = 0; + low = 0; + for (len = kJpegHuffmanRootTableBits + 1; + len <= static_cast(kJpegHuffmanMaxBitLength); ++len) { + for (; tmp_count[len] > 0; --tmp_count[len]) { + // Start a new sub-table if the previous one is full. + if (low >= table_size) { + table += table_size; + table_bits = NextTableBitSize(tmp_count, len); + table_size = 1 << table_bits; + low = 0; + lut[key].bits = table_bits + kJpegHuffmanRootTableBits; + lut[key].value = (table - lut) - key; + ++key; + } + code.bits = len - kJpegHuffmanRootTableBits; + code.value = symbols[idx++]; + reps = 1 << (table_bits - code.bits); + while (reps--) { + table[low++] = code; + } + } + } +} + +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/enc_jpeg_huffman_decode.h b/lib/jxl/jpeg/enc_jpeg_huffman_decode.h new file mode 100644 index 0000000..b8a60e4 --- /dev/null +++ b/lib/jxl/jpeg/enc_jpeg_huffman_decode.h @@ -0,0 +1,41 @@ +// 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. + +// Utility function for building a Huffman lookup table for the jpeg decoder. + +#ifndef LIB_JXL_JPEG_ENC_JPEG_HUFFMAN_DECODE_H_ +#define LIB_JXL_JPEG_ENC_JPEG_HUFFMAN_DECODE_H_ + +#include + +namespace jxl { +namespace jpeg { + +constexpr int kJpegHuffmanRootTableBits = 8; +// Maximum huffman lookup table size. +// According to zlib/examples/enough.c, 758 entries are always enough for +// an alphabet of 257 symbols (256 + 1 special symbol for the all 1s code) and +// max bit length 16 if the root table has 8 bits. +constexpr int kJpegHuffmanLutSize = 758; + +struct HuffmanTableEntry { + // Initialize the value to an invalid symbol so that we can recognize it + // when reading the bit stream using a Huffman code with space > 0. + HuffmanTableEntry() : bits(0), value(0xffff) {} + + uint8_t bits; // number of bits used for this symbol + uint16_t value; // symbol value or table offset +}; + +// Builds jpeg-style Huffman lookup table from the given symbols. +// The symbols are in order of increasing bit lengths. The number of symbols +// with bit length n is given in counts[n] for each n >= 1. +void BuildJpegHuffmanTable(const uint32_t* counts, const uint32_t* symbols, + HuffmanTableEntry* lut); + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_ENC_JPEG_HUFFMAN_DECODE_H_ diff --git a/lib/jxl/jpeg/jpeg_data.cc b/lib/jxl/jpeg/jpeg_data.cc new file mode 100644 index 0000000..42e5a49 --- /dev/null +++ b/lib/jxl/jpeg/jpeg_data.cc @@ -0,0 +1,448 @@ +// 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/jpeg/jpeg_data.h" + +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace jpeg { + +#if JPEGXL_ENABLE_TRANSCODE_JPEG + +namespace { +enum JPEGComponentType : uint32_t { + kGray = 0, + kYCbCr = 1, + kRGB = 2, + kCustom = 3, +}; + +struct JPEGInfo { + size_t num_app_markers = 0; + size_t num_com_markers = 0; + size_t num_scans = 0; + size_t num_intermarker = 0; + bool has_dri = false; +}; + +Status VisitMarker(uint8_t* marker, Visitor* visitor, JPEGInfo* info) { + uint32_t marker32 = *marker - 0xc0; + JXL_RETURN_IF_ERROR(visitor->Bits(6, 0x00, &marker32)); + *marker = marker32 + 0xc0; + if ((*marker & 0xf0) == 0xe0) { + info->num_app_markers++; + } + if (*marker == 0xfe) { + info->num_com_markers++; + } + if (*marker == 0xda) { + info->num_scans++; + } + // We use a fake 0xff marker to signal intermarker data. + if (*marker == 0xff) { + info->num_intermarker++; + } + if (*marker == 0xdd) { + info->has_dri = true; + } + return true; +} + +} // namespace + +Status JPEGData::VisitFields(Visitor* visitor) { + bool is_gray = components.size() == 1; + JXL_RETURN_IF_ERROR(visitor->Bool(false, &is_gray)); + if (visitor->IsReading()) { + components.resize(is_gray ? 1 : 3); + } + JPEGInfo info; + if (visitor->IsReading()) { + uint8_t marker = 0xc0; + do { + JXL_RETURN_IF_ERROR(VisitMarker(&marker, visitor, &info)); + marker_order.push_back(marker); + if (marker_order.size() > 16384) { + return JXL_FAILURE("Too many markers: %zu\n", marker_order.size()); + } + } while (marker != 0xd9); + } else { + if (marker_order.size() > 16384) { + return JXL_FAILURE("Too many markers: %zu\n", marker_order.size()); + } + for (size_t i = 0; i < marker_order.size(); i++) { + JXL_RETURN_IF_ERROR(VisitMarker(&marker_order[i], visitor, &info)); + } + if (!marker_order.empty()) { + // Last marker should always be EOI marker. + JXL_CHECK(marker_order.back() == 0xd9); + } + } + + // Size of the APP and COM markers. + if (visitor->IsReading()) { + app_data.resize(info.num_app_markers); + app_marker_type.resize(info.num_app_markers); + com_data.resize(info.num_com_markers); + scan_info.resize(info.num_scans); + } + JXL_ASSERT(app_data.size() == info.num_app_markers); + JXL_ASSERT(app_marker_type.size() == info.num_app_markers); + JXL_ASSERT(com_data.size() == info.num_com_markers); + JXL_ASSERT(scan_info.size() == info.num_scans); + for (size_t i = 0; i < app_data.size(); i++) { + auto& app = app_data[i]; + // Encodes up to 8 different values. + JXL_RETURN_IF_ERROR( + visitor->U32(Val(0), Val(1), BitsOffset(1, 2), BitsOffset(2, 4), 0, + reinterpret_cast(&app_marker_type[i]))); + if (app_marker_type[i] != AppMarkerType::kUnknown && + app_marker_type[i] != AppMarkerType::kICC && + app_marker_type[i] != AppMarkerType::kExif && + app_marker_type[i] != AppMarkerType::kXMP) { + return JXL_FAILURE("Unknown app marker type %u", + static_cast(app_marker_type[i])); + } + uint32_t len = app.size() - 1; + JXL_RETURN_IF_ERROR(visitor->Bits(16, 0, &len)); + if (visitor->IsReading()) app.resize(len + 1); + if (app.size() < 3) { + return JXL_FAILURE("Invalid marker size: %zu\n", app.size()); + } + } + for (auto& com : com_data) { + uint32_t len = com.size() - 1; + JXL_RETURN_IF_ERROR(visitor->Bits(16, 0, &len)); + if (visitor->IsReading()) com.resize(len + 1); + if (com.size() < 3) { + return JXL_FAILURE("Invalid marker size: %zu\n", com.size()); + } + } + + uint32_t num_quant_tables = quant.size(); + JXL_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), Val(4), 2, &num_quant_tables)); + if (num_quant_tables == 4) { + return JXL_FAILURE("Invalid number of quant tables"); + } + if (visitor->IsReading()) { + quant.resize(num_quant_tables); + } + for (size_t i = 0; i < num_quant_tables; i++) { + if (quant[i].precision > 1) { + return JXL_FAILURE( + "Quant tables with more than 16 bits are not supported"); + } + JXL_RETURN_IF_ERROR(visitor->Bits(1, 0, &quant[i].precision)); + JXL_RETURN_IF_ERROR(visitor->Bits(2, i, &quant[i].index)); + JXL_RETURN_IF_ERROR(visitor->Bool(true, &quant[i].is_last)); + } + + JPEGComponentType component_type = + components.size() == 1 && components[0].id == 1 + ? JPEGComponentType::kGray + : components.size() == 3 && components[0].id == 1 && + components[1].id == 2 && components[2].id == 3 + ? JPEGComponentType::kYCbCr + : components.size() == 3 && components[0].id == 'R' && + components[1].id == 'G' && components[2].id == 'B' + ? JPEGComponentType::kRGB + : JPEGComponentType::kCustom; + JXL_RETURN_IF_ERROR( + visitor->Bits(2, JPEGComponentType::kYCbCr, + reinterpret_cast(&component_type))); + uint32_t num_components; + if (component_type == JPEGComponentType::kGray) { + num_components = 1; + } else if (component_type != JPEGComponentType::kCustom) { + num_components = 3; + } else { + num_components = components.size(); + JXL_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), Val(4), 3, &num_components)); + if (num_components != 1 && num_components != 3) { + return JXL_FAILURE("Invalid number of components: %u", num_components); + } + } + if (visitor->IsReading()) { + components.resize(num_components); + } + if (component_type == JPEGComponentType::kCustom) { + for (size_t i = 0; i < components.size(); i++) { + JXL_RETURN_IF_ERROR(visitor->Bits(8, 0, &components[i].id)); + } + } else if (component_type == JPEGComponentType::kGray) { + components[0].id = 1; + } else if (component_type == JPEGComponentType::kRGB) { + components[0].id = 'R'; + components[1].id = 'G'; + components[2].id = 'B'; + } else { + components[0].id = 1; + components[1].id = 2; + components[2].id = 3; + } + size_t used_tables = 0; + for (size_t i = 0; i < components.size(); i++) { + JXL_RETURN_IF_ERROR(visitor->Bits(2, 0, &components[i].quant_idx)); + if (components[i].quant_idx >= quant.size()) { + return JXL_FAILURE("Invalid quant table for component %zu: %u\n", i, + components[i].quant_idx); + } + used_tables |= 1U << components[i].quant_idx; + } + if (used_tables + 1 != 1U << quant.size()) { + return JXL_FAILURE( + "Not all quant tables are used (%zu tables, %zx used table mask)", + quant.size(), used_tables); + } + + uint32_t num_huff = huffman_code.size(); + JXL_RETURN_IF_ERROR(visitor->U32(Val(4), BitsOffset(3, 2), BitsOffset(4, 10), + BitsOffset(6, 26), 4, &num_huff)); + if (visitor->IsReading()) { + huffman_code.resize(num_huff); + } + for (JPEGHuffmanCode& hc : huffman_code) { + bool is_ac = hc.slot_id >> 4; + uint32_t id = hc.slot_id & 0xF; + JXL_RETURN_IF_ERROR(visitor->Bool(false, &is_ac)); + JXL_RETURN_IF_ERROR(visitor->Bits(2, 0, &id)); + hc.slot_id = (static_cast(is_ac) << 4) | id; + JXL_RETURN_IF_ERROR(visitor->Bool(true, &hc.is_last)); + size_t num_symbols = 0; + for (size_t i = 0; i <= 16; i++) { + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), Val(1), BitsOffset(3, 2), + Bits(8), 0, &hc.counts[i])); + num_symbols += hc.counts[i]; + } + if (num_symbols < 1) { + // Actually, at least 2 symbols are required, since one of them is EOI. + return JXL_FAILURE("Empty Huffman table"); + } + if (num_symbols > hc.values.size()) { + return JXL_FAILURE("Huffman code too large (%zu)", num_symbols); + } + // Presence flags for 4 * 64 + 1 values. + uint64_t value_slots[5] = {}; + for (size_t i = 0; i < num_symbols; i++) { + // Goes up to 256, included. Might have the same symbol appear twice... + JXL_RETURN_IF_ERROR(visitor->U32(Bits(2), BitsOffset(2, 4), + BitsOffset(4, 8), BitsOffset(8, 1), 0, + &hc.values[i])); + value_slots[hc.values[i] >> 6] |= (uint64_t)1 << (hc.values[i] & 0x3F); + } + if (hc.values[num_symbols - 1] != kJpegHuffmanAlphabetSize) { + return JXL_FAILURE("Missing EOI symbol"); + } + // Last element, denoting EOI, have to be 1 after the loop. + JXL_ASSERT(value_slots[4] == 1); + size_t num_values = 1; + for (size_t i = 0; i < 4; ++i) num_values += hwy::PopCount(value_slots[i]); + if (num_values != num_symbols) { + return JXL_FAILURE("Duplicate Huffman symbols"); + } + if (!is_ac) { + bool only_dc = ((value_slots[0] >> kJpegDCAlphabetSize) | value_slots[1] | + value_slots[2] | value_slots[3]) == 0; + if (!only_dc) return JXL_FAILURE("Huffman symbols out of DC range"); + } + } + + for (auto& scan : scan_info) { + JXL_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), Val(4), 1, &scan.num_components)); + if (scan.num_components >= 4) { + return JXL_FAILURE("Invalid number of components in SOS marker"); + } + JXL_RETURN_IF_ERROR(visitor->Bits(6, 0, &scan.Ss)); + JXL_RETURN_IF_ERROR(visitor->Bits(6, 63, &scan.Se)); + JXL_RETURN_IF_ERROR(visitor->Bits(4, 0, &scan.Al)); + JXL_RETURN_IF_ERROR(visitor->Bits(4, 0, &scan.Ah)); + for (size_t i = 0; i < scan.num_components; i++) { + JXL_RETURN_IF_ERROR(visitor->Bits(2, 0, &scan.components[i].comp_idx)); + if (scan.components[i].comp_idx >= components.size()) { + return JXL_FAILURE("Invalid component idx in SOS marker"); + } + JXL_RETURN_IF_ERROR(visitor->Bits(2, 0, &scan.components[i].ac_tbl_idx)); + JXL_RETURN_IF_ERROR(visitor->Bits(2, 0, &scan.components[i].dc_tbl_idx)); + } + // TODO(veluca): actually set and use this value. + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), Val(1), Val(2), BitsOffset(3, 3), + kMaxNumPasses - 1, + &scan.last_needed_pass)); + } + + // From here on, this is data that is not strictly necessary to get a valid + // JPEG, but necessary for bit-exact JPEG reconstruction. + if (info.has_dri) { + JXL_RETURN_IF_ERROR(visitor->Bits(16, 0, &restart_interval)); + } + + uint64_t padding_spot_limit = scan_info.size(); + + for (auto& scan : scan_info) { + uint32_t num_reset_points = scan.reset_points.size(); + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(2, 1), BitsOffset(4, 4), + BitsOffset(16, 20), 0, &num_reset_points)); + if (visitor->IsReading()) { + scan.reset_points.resize(num_reset_points); + } + int last_block_idx = -1; + for (auto& block_idx : scan.reset_points) { + block_idx -= last_block_idx + 1; + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(3, 1), + BitsOffset(5, 9), BitsOffset(28, 41), 0, + &block_idx)); + block_idx += last_block_idx + 1; + if (static_cast(block_idx) < last_block_idx + 1) { + return JXL_FAILURE("Invalid block ID: %u, last block was %d", block_idx, + last_block_idx); + } + // TODO(eustas): better upper boundary could be given at this point; also + // it could be applied during reset_points reading. + if (block_idx > (1u << 30)) { + // At most 8K x 8K x num_channels blocks are expected. That is, + // typically, 1.5 * 2^27. 2^30 should be sufficient for any sane + // image. + return JXL_FAILURE("Invalid block ID: %u", block_idx); + } + last_block_idx = block_idx; + } + + uint32_t num_extra_zero_runs = scan.extra_zero_runs.size(); + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(2, 1), BitsOffset(4, 4), + BitsOffset(16, 20), 0, + &num_extra_zero_runs)); + if (visitor->IsReading()) { + scan.extra_zero_runs.resize(num_extra_zero_runs); + } + last_block_idx = -1; + for (size_t i = 0; i < scan.extra_zero_runs.size(); ++i) { + uint32_t& block_idx = scan.extra_zero_runs[i].block_idx; + JXL_RETURN_IF_ERROR(visitor->U32( + Val(1), BitsOffset(2, 2), BitsOffset(4, 5), BitsOffset(8, 20), 1, + &scan.extra_zero_runs[i].num_extra_zero_runs)); + block_idx -= last_block_idx + 1; + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(3, 1), + BitsOffset(5, 9), BitsOffset(28, 41), 0, + &block_idx)); + block_idx += last_block_idx + 1; + if (static_cast(block_idx) < last_block_idx + 1) { + return JXL_FAILURE("Invalid block ID: %u, last block was %d", block_idx, + last_block_idx); + } + if (block_idx > (1u << 30)) { + // At most 8K x 8K x num_channels blocks are expected. That is, + // typically, 1.5 * 2^27. 2^30 should be sufficient for any sane + // image. + return JXL_FAILURE("Invalid block ID: %u", block_idx); + } + last_block_idx = block_idx; + } + + if (restart_interval > 0) { + int MCUs_per_row = 0; + int MCU_rows = 0; + CalculateMcuSize(scan, &MCUs_per_row, &MCU_rows); + padding_spot_limit += DivCeil(MCU_rows * MCUs_per_row, restart_interval); + } + } + std::vector inter_marker_data_sizes; + inter_marker_data_sizes.reserve(info.num_intermarker); + for (size_t i = 0; i < info.num_intermarker; ++i) { + uint32_t len = visitor->IsReading() ? 0 : inter_marker_data[i].size(); + JXL_RETURN_IF_ERROR(visitor->Bits(16, 0, &len)); + if (visitor->IsReading()) inter_marker_data_sizes.emplace_back(len); + } + uint32_t tail_data_len = tail_data.size(); + JXL_RETURN_IF_ERROR(visitor->U32(Val(0), BitsOffset(8, 1), + BitsOffset(16, 257), BitsOffset(22, 65793), + 0, &tail_data_len)); + + JXL_RETURN_IF_ERROR(visitor->Bool(false, &has_zero_padding_bit)); + if (has_zero_padding_bit) { + uint32_t nbit = padding_bits.size(); + JXL_RETURN_IF_ERROR(visitor->Bits(24, 0, &nbit)); + if (nbit > 7 * padding_spot_limit) { + return JXL_FAILURE("Number of padding bits does not correspond to image"); + } + // TODO(eustas): check that that much bits of input are available. + if (visitor->IsReading()) { + padding_bits.resize(nbit); + } + // TODO(eustas): read in (8-64?) bit groups to reduce overhead. + for (uint8_t& bit : padding_bits) { + bool bbit = bit; + JXL_RETURN_IF_ERROR(visitor->Bool(false, &bbit)); + bit = bbit; + } + } + + // Apply postponed actions. + if (visitor->IsReading()) { + tail_data.resize(tail_data_len); + JXL_ASSERT(inter_marker_data_sizes.size() == info.num_intermarker); + inter_marker_data.reserve(info.num_intermarker); + for (size_t i = 0; i < info.num_intermarker; ++i) { + inter_marker_data.emplace_back(inter_marker_data_sizes[i]); + } + } + + return true; +} + +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +void JPEGData::CalculateMcuSize(const JPEGScanInfo& scan, int* MCUs_per_row, + int* MCU_rows) const { + const bool is_interleaved = (scan.num_components > 1); + const JPEGComponent& base_component = components[scan.components[0].comp_idx]; + // h_group / v_group act as numerators for converting number of blocks to + // number of MCU. In interleaved mode it is 1, so MCU is represented with + // max_*_samp_factor blocks. In non-interleaved mode we choose numerator to + // be the samping factor, consequently MCU is always represented with single + // block. + const int h_group = is_interleaved ? 1 : base_component.h_samp_factor; + const int v_group = is_interleaved ? 1 : base_component.v_samp_factor; + int max_h_samp_factor = 1; + int max_v_samp_factor = 1; + for (const auto& c : components) { + max_h_samp_factor = std::max(c.h_samp_factor, max_h_samp_factor); + max_v_samp_factor = std::max(c.v_samp_factor, max_v_samp_factor); + } + *MCUs_per_row = DivCeil(width * h_group, 8 * max_h_samp_factor); + *MCU_rows = DivCeil(height * v_group, 8 * max_v_samp_factor); +} + +#if JPEGXL_ENABLE_TRANSCODE_JPEG + +Status SetJPEGDataFromICC(const PaddedBytes& icc, jpeg::JPEGData* jpeg_data) { + size_t icc_pos = 0; + for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { + if (jpeg_data->app_marker_type[i] != jpeg::AppMarkerType::kICC) { + continue; + } + size_t len = jpeg_data->app_data[i].size() - 17; + if (icc_pos + len > icc.size()) { + return JXL_FAILURE( + "ICC length is less than APP markers: requested %zu more bytes, " + "%zu available", + len, icc.size() - icc_pos); + } + memcpy(&jpeg_data->app_data[i][17], icc.data() + icc_pos, len); + icc_pos += len; + } + if (icc_pos != icc.size() && icc_pos != 0) { + return JXL_FAILURE("ICC length is more than APP markers"); + } + return true; +} + +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +} // namespace jpeg +} // namespace jxl diff --git a/lib/jxl/jpeg/jpeg_data.h b/lib/jxl/jpeg/jpeg_data.h new file mode 100644 index 0000000..6b7cb02 --- /dev/null +++ b/lib/jxl/jpeg/jpeg_data.h @@ -0,0 +1,267 @@ +// 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. + +// Data structures that represent the non-pixel contents of a jpeg file. + +#ifndef LIB_JXL_JPEG_JPEG_DATA_H_ +#define LIB_JXL_JPEG_JPEG_DATA_H_ + +#include +#include + +#include +#include + +#include "lib/jxl/common.h" // JPEGXL_ENABLE_TRANSCODE_JPEG +#include "lib/jxl/fields.h" + +namespace jxl { +namespace jpeg { + +constexpr int kMaxComponents = 4; +constexpr int kMaxQuantTables = 4; +constexpr int kMaxHuffmanTables = 4; +constexpr size_t kJpegHuffmanMaxBitLength = 16; +constexpr int kJpegHuffmanAlphabetSize = 256; +constexpr int kJpegDCAlphabetSize = 12; +constexpr int kMaxDHTMarkers = 512; +constexpr int kMaxDimPixels = 65535; +constexpr uint8_t kApp1 = 0xE1; +constexpr uint8_t kApp2 = 0xE2; +const uint8_t kIccProfileTag[12] = "ICC_PROFILE"; +const uint8_t kExifTag[6] = "Exif\0"; +const uint8_t kXMPTag[29] = "http://ns.adobe.com/xap/1.0/"; + +/* clang-format off */ +constexpr uint32_t kJPEGNaturalOrder[80] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // extra entries for safety in decoder + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 +}; + +constexpr uint32_t kJPEGZigZagOrder[64] = { + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 +}; +/* clang-format on */ + +enum struct JPEGReadError { + OK = 0, + SOI_NOT_FOUND, + SOF_NOT_FOUND, + UNEXPECTED_EOF, + MARKER_BYTE_NOT_FOUND, + UNSUPPORTED_MARKER, + WRONG_MARKER_SIZE, + INVALID_PRECISION, + INVALID_WIDTH, + INVALID_HEIGHT, + INVALID_NUMCOMP, + INVALID_SAMP_FACTOR, + INVALID_START_OF_SCAN, + INVALID_END_OF_SCAN, + INVALID_SCAN_BIT_POSITION, + INVALID_COMPS_IN_SCAN, + INVALID_HUFFMAN_INDEX, + INVALID_QUANT_TBL_INDEX, + INVALID_QUANT_VAL, + INVALID_MARKER_LEN, + INVALID_SAMPLING_FACTORS, + INVALID_HUFFMAN_CODE, + INVALID_SYMBOL, + NON_REPRESENTABLE_DC_COEFF, + NON_REPRESENTABLE_AC_COEFF, + INVALID_SCAN, + OVERLAPPING_SCANS, + INVALID_SCAN_ORDER, + EXTRA_ZERO_RUN, + DUPLICATE_DRI, + DUPLICATE_SOF, + WRONG_RESTART_MARKER, + DUPLICATE_COMPONENT_ID, + COMPONENT_NOT_FOUND, + HUFFMAN_TABLE_NOT_FOUND, + HUFFMAN_TABLE_ERROR, + QUANT_TABLE_NOT_FOUND, + EMPTY_DHT, + EMPTY_DQT, + OUT_OF_BAND_COEFF, + EOB_RUN_TOO_LONG, + IMAGE_TOO_LARGE, + INVALID_QUANT_TBL_PRECISION, +}; + +// Quantization values for an 8x8 pixel block. +struct JPEGQuantTable { + std::array values; + uint32_t precision = 0; + // The index of this quantization table as it was parsed from the input JPEG. + // Each DQT marker segment contains an 'index' field, and we save this index + // here. Valid values are 0 to 3. + uint32_t index = 0; + // Set to true if this table is the last one within its marker segment. + bool is_last = true; +}; + +// Huffman code and decoding lookup table used for DC and AC coefficients. +struct JPEGHuffmanCode { + // Bit length histogram. + std::array counts = {}; + // Symbol values sorted by increasing bit lengths. + std::array values = {}; + // The index of the Huffman code in the current set of Huffman codes. For AC + // component Huffman codes, 0x10 is added to the index. + int slot_id = 0; + // Set to true if this Huffman code is the last one within its marker segment. + bool is_last = true; +}; + +// Huffman table indexes used for one component of one scan. +struct JPEGComponentScanInfo { + uint32_t comp_idx; + uint32_t dc_tbl_idx; + uint32_t ac_tbl_idx; +}; + +// Contains information that is used in one scan. +struct JPEGScanInfo { + // Parameters used for progressive scans (named the same way as in the spec): + // Ss : Start of spectral band in zig-zag sequence. + // Se : End of spectral band in zig-zag sequence. + // Ah : Successive approximation bit position, high. + // Al : Successive approximation bit position, low. + uint32_t Ss; + uint32_t Se; + uint32_t Ah; + uint32_t Al; + uint32_t num_components = 0; + std::array components; + // Last codestream pass that is needed to write this scan. + uint32_t last_needed_pass = 0; + + // Extra information required for bit-precise JPEG file reconstruction. + + // Set of block indexes where the JPEG encoder has to flush the end-of-block + // runs and refinement bits. + std::vector reset_points; + // The number of extra zero runs (Huffman symbol 0xf0) before the end of + // block (if nonzero), indexed by block index. + // All of these symbols can be omitted without changing the pixel values, but + // some jpeg encoders put these at the end of blocks. + typedef struct { + uint32_t block_idx; + uint32_t num_extra_zero_runs; + } ExtraZeroRunInfo; + std::vector extra_zero_runs; +}; + +typedef int16_t coeff_t; + +// Represents one component of a jpeg file. +struct JPEGComponent { + JPEGComponent() + : id(0), + h_samp_factor(1), + v_samp_factor(1), + quant_idx(0), + width_in_blocks(0), + height_in_blocks(0) {} + + // One-byte id of the component. + uint32_t id; + // Horizontal and vertical sampling factors. + // In interleaved mode, each minimal coded unit (MCU) has + // h_samp_factor x v_samp_factor DCT blocks from this component. + int h_samp_factor; + int v_samp_factor; + // The index of the quantization table used for this component. + uint32_t quant_idx; + // The dimensions of the component measured in 8x8 blocks. + uint32_t width_in_blocks; + uint32_t height_in_blocks; + // The DCT coefficients of this component, laid out block-by-block, divided + // through the quantization matrix values. + std::vector coeffs; +}; + +enum class AppMarkerType : uint32_t { + kUnknown = 0, + kICC = 1, + kExif = 2, + kXMP = 3, +}; + +// Represents a parsed jpeg file. +struct JPEGData : public Fields { + JPEGData() + : width(0), + height(0), + restart_interval(0), + error(JPEGReadError::OK), + has_zero_padding_bit(false) {} + + const char* Name() const override { return "JPEGData"; } +#if JPEGXL_ENABLE_TRANSCODE_JPEG + // Doesn't serialize everything - skips brotli-encoded data and what is + // already encoded in the codestream. + Status VisitFields(Visitor* visitor) override; +#else + Status VisitFields(Visitor* /* visitor */) override { + JXL_ABORT("JPEG transcoding support not enabled"); + } +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + + void CalculateMcuSize(const JPEGScanInfo& scan, int* MCUs_per_row, + int* MCU_rows) const; + + int width; + int height; + uint32_t restart_interval; + std::vector> app_data; + std::vector app_marker_type; + std::vector> com_data; + std::vector quant; + std::vector huffman_code; + std::vector components; + std::vector scan_info; + std::vector marker_order; + std::vector> inter_marker_data; + std::vector tail_data; + JPEGReadError error; + + // Extra information required for bit-precise JPEG file reconstruction. + + bool has_zero_padding_bit; + std::vector padding_bits; +}; + +#if JPEGXL_ENABLE_TRANSCODE_JPEG +// Set ICC profile in jpeg_data. +Status SetJPEGDataFromICC(const PaddedBytes& icc, jpeg::JPEGData* jpeg_data); +#else +static JXL_INLINE Status SetJPEGDataFromICC(const PaddedBytes& /* icc */, + jpeg::JPEGData* /* jpeg_data */) { + JXL_ABORT("JPEG transcoding support not enabled"); +} +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +} // namespace jpeg +} // namespace jxl + +#endif // LIB_JXL_JPEG_JPEG_DATA_H_ diff --git a/lib/jxl/jxl.syms b/lib/jxl/jxl.syms new file mode 100644 index 0000000..0f398d7 --- /dev/null +++ b/lib/jxl/jxl.syms @@ -0,0 +1,5 @@ +{ + extern "C" { + jpegxl_*; + }; +}; diff --git a/lib/jxl/jxl.version b/lib/jxl/jxl.version new file mode 100644 index 0000000..26b0e9e --- /dev/null +++ b/lib/jxl/jxl.version @@ -0,0 +1,17 @@ +JXL_0 { + global: + Jxl*; + + local: + # Hide all the std namespace symbols. std namespace is explicitly marked + # as visibility(default) and header-only functions or methods (such as those + # from templates) should be exposed in shared libraries as weak symbols but + # this is only needed when we expose those types in the shared library API + # in any way. We don't use C++ std types in the API and we also don't + # support exceptions in the library. + # See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022 for a discussion + # about this. + extern "C++" { + *std::*; + }; +}; diff --git a/lib/jxl/jxl_inspection.h b/lib/jxl/jxl_inspection.h new file mode 100644 index 0000000..0b70a58 --- /dev/null +++ b/lib/jxl/jxl_inspection.h @@ -0,0 +1,22 @@ +// 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_JXL_INSPECTION_H_ +#define LIB_JXL_JXL_INSPECTION_H_ + +#include + +#include "lib/jxl/image.h" + +namespace jxl { +// Type of the inspection-callback which, if enabled, will be called on various +// intermediate data during image processing, allowing inspection access. +// +// Returns false if processing can be stopped at that point, true otherwise. +// This is only advisory - it is always OK to just continue processing. +using InspectorImage3F = std::function; +} // namespace jxl + +#endif // LIB_JXL_JXL_INSPECTION_H_ diff --git a/lib/jxl/jxl_osx.syms b/lib/jxl/jxl_osx.syms new file mode 100644 index 0000000..96bc568 --- /dev/null +++ b/lib/jxl/jxl_osx.syms @@ -0,0 +1 @@ +_Jxl* diff --git a/lib/jxl/jxl_test.cc b/lib/jxl/jxl_test.cc new file mode 100644 index 0000000..5cff712 --- /dev/null +++ b/lib/jxl/jxl_test.cc @@ -0,0 +1,1679 @@ +// 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 +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/extras/codec_jpg.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/fake_parallel_runner_testonly.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/jpeg/enc_jpeg_data.h" +#include "lib/jxl/modular/options.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" +#include "tools/box/box.h" + +namespace jxl { +namespace { +using test::Roundtrip; + +#define JXL_TEST_NL 0 // Disabled in code + +void CreateImage1x1(CodecInOut* io) { + Image3F image(1, 1); + ZeroFillImage(&image); + io->metadata.m.SetUintSamples(8); + io->metadata.m.color_encoding = ColorEncoding::SRGB(); + io->SetFromImage(std::move(image), io->metadata.m.color_encoding); +} + +TEST(JxlTest, HeaderSize) { + CodecInOut io; + CreateImage1x1(&io); + + CompressParams cparams; + cparams.butteraugli_distance = 1.5; + DecompressParams dparams; + ThreadPool* pool = nullptr; + + { + CodecInOut io2; + AuxOut aux_out; + Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out); + EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 34u); + } + + { + CodecInOut io2; + io.metadata.m.SetAlphaBits(8); + ImageF alpha(1, 1); + alpha.Row(0)[0] = 1; + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + AuxOut aux_out; + Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out); + EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 57u); + } +} + +TEST(JxlTest, RoundtripSinglePixel) { + CodecInOut io; + CreateImage1x1(&io); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + ThreadPool* pool = nullptr; + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); +} + +// Changing serialized signature causes Decode to fail. +#ifndef JXL_CRASH_ON_ERROR +TEST(JxlTest, RoundtripMarker) { + CodecInOut io; + CreateImage1x1(&io); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + AuxOut* aux_out = nullptr; + ThreadPool* pool = nullptr; + + PassesEncoderState enc_state; + for (size_t i = 0; i < 2; ++i) { + PaddedBytes compressed; + EXPECT_TRUE( + EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + compressed[i] ^= 0xFF; + CodecInOut io2; + EXPECT_FALSE(DecodeFile(dparams, compressed, &io2, pool)); + } +} +#endif + +TEST(JxlTest, RoundtripTinyFast) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(32, 32); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.butteraugli_distance = 4.0f; + DecompressParams dparams; + + CodecInOut io2; + const size_t enc_bytes = Roundtrip(&io, cparams, dparams, pool, &io2); + printf("32x32 image size %zu bytes\n", enc_bytes); +} + +TEST(JxlTest, RoundtripSmallD1) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + CodecInOut io_out; + size_t compressed_size; + + { + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + EXPECT_LE(compressed_size, 1000u); + EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.5); + } + + { + // And then, with a lower intensity target than the default, the bitrate + // should be smaller. + CodecInOut io_dim; + io_dim.target_nits = 100; + ASSERT_TRUE(SetFromBytes(Span(orig), &io_dim, pool)); + io_dim.ShrinkTo(io_dim.xsize() / 8, io_dim.ysize() / 8); + EXPECT_LT(Roundtrip(&io_dim, cparams, dparams, pool, &io_out), + compressed_size); + EXPECT_LE(ButteraugliDistance(io_dim, io_out, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.5); + EXPECT_EQ(io_dim.metadata.m.IntensityTarget(), + io_out.metadata.m.IntensityTarget()); + } +} + +TEST(JxlTest, RoundtripOtherTransforms) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/64px/a2d1un_nkitzmiller_srgb8.png"); + std::unique_ptr io = jxl::make_unique(); + ASSERT_TRUE(SetFromBytes(Span(orig), io.get(), pool)); + + CompressParams cparams; + // Slow modes access linear image for adaptive quant search + cparams.speed_tier = SpeedTier::kKitten; + cparams.color_transform = ColorTransform::kNone; + cparams.butteraugli_distance = 5.0f; + DecompressParams dparams; + + std::unique_ptr io2 = jxl::make_unique(); + const size_t compressed_size = + Roundtrip(io.get(), cparams, dparams, pool, io2.get()); + EXPECT_LE(compressed_size, 23000u); + EXPECT_LE(ButteraugliDistance(*io, *io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 6); + + // Check the consistency when performing another roundtrip. + std::unique_ptr io3 = jxl::make_unique(); + const size_t compressed_size2 = + Roundtrip(io.get(), cparams, dparams, pool, io3.get()); + EXPECT_LE(compressed_size2, 23000u); + EXPECT_LE(ButteraugliDistance(*io, *io3, cparams.ba_params, + /*distmap=*/nullptr, pool), + 6); +} + +TEST(JxlTest, RoundtripResample2) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize(), io.ysize()); + CompressParams cparams; + cparams.resampling = 2; + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 17000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 12); +} +TEST(JxlTest, RoundtripResample2MT) { + ThreadPoolInternal pool(4); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + // image has to be large enough to have multiple groups after downsampling + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + CompressParams cparams; + cparams.resampling = 2; + DecompressParams dparams; + CodecInOut io2; + // TODO(veluca): Figure out why msan and release produce different + // file size. + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 64500u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool), +#if JXL_HIGH_PRECISION + 5.5); +#else + 13.5); +#endif +} + +// Roundtrip the image using a parallel runner that executes single-threaded but +// in random order. +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_cropped.jpg"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + // Image size is selected so that the block border needed is larger than the + // amount of pixels available on the next block. + io.ShrinkTo(513, 515); + + CompressParams cparams; + // Force epf so we end up needing a lot of border. + cparams.epf = 3; + + DecompressParams dparams; + CodecInOut io2; + Roundtrip(&io, cparams, dparams, &pool, &io2); + + EXPECT_GE(1.5, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, RoundtripResample4) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize(), io.ysize()); + CompressParams cparams; + cparams.resampling = 4; + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 6000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 28); +} + +TEST(JxlTest, RoundtripResample8) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize(), io.ysize()); + CompressParams cparams; + cparams.resampling = 8; + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2100u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 80); +} + +TEST(JxlTest, RoundtripUnalignedD2) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 12, io.ysize() / 7); + + CompressParams cparams; + cparams.butteraugli_distance = 2.0; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 700u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 3.2); +} + +#if JXL_TEST_NL + +TEST(JxlTest, RoundtripMultiGroupNL) { + ThreadPoolInternal pool(4); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + io.ShrinkTo(600, 1024); // partial X, full Y group + + CompressParams cparams; + DecompressParams dparams; + + cparams.fast_mode = true; + cparams.butteraugli_distance = 1.0f; + CodecInOut io2; + Roundtrip(&io, cparams, dparams, &pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 0.9f); + + cparams.butteraugli_distance = 2.0f; + CodecInOut io3; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 80000u); + EXPECT_LE(ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 1.5f); +} + +#endif + +TEST(JxlTest, RoundtripMultiGroup) { + ThreadPoolInternal pool(4); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + io.ShrinkTo(600, 1024); + + CompressParams cparams; + DecompressParams dparams; + + cparams.butteraugli_distance = 1.0f; + cparams.speed_tier = SpeedTier::kKitten; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 1.99f); + + cparams.butteraugli_distance = 2.0f; + CodecInOut io3; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 22100u); + EXPECT_LE(ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 3.0f); +} + +TEST(JxlTest, RoundtripLargeFast) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u); +} + +TEST(JxlTest, RoundtripDotsForceEpf) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.epf = 2; + cparams.dots = Override::kOn; + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u); +} + +// Checks for differing size/distance in two consecutive runs of distance 2, +// which involves additional processing including adaptive reconstruction. +// Failing this may be a sign of race conditions or invalid memory accesses. +TEST(JxlTest, RoundtripD2Consistent) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.butteraugli_distance = 2.0; + DecompressParams dparams; + + // Try each xsize mod kBlockDim to verify right border handling. + for (size_t xsize = 48; xsize > 40; --xsize) { + io.ShrinkTo(xsize, 15); + + CodecInOut io2; + const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + + CodecInOut io3; + const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + + // Exact same compressed size. + EXPECT_EQ(size2, size3); + + // Exact same distance. + const float dist2 = ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool); + const float dist3 = ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool); + EXPECT_EQ(dist2, dist3); + } +} + +// Same as above, but for full image, testing multiple groups. +TEST(JxlTest, RoundtripLargeConsistent) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.butteraugli_distance = 2.0; + DecompressParams dparams; + + // Try each xsize mod kBlockDim to verify right border handling. + CodecInOut io2; + const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + + CodecInOut io3; + const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + + // Exact same compressed size. + EXPECT_EQ(size2, size3); + + // Exact same distance. + const float dist2 = ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool); + const float dist3 = ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool); + EXPECT_EQ(dist2, dist3); +} + +#if JXL_TEST_NL + +TEST(JxlTest, RoundtripSmallNL) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1500u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.7); +} + +#endif + +TEST(JxlTest, RoundtripNoGaborishNoAR) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + CompressParams cparams; + cparams.gaborish = Override::kOff; + cparams.epf = 0; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 40000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.5); +} + +TEST(JxlTest, RoundtripSmallNoGaborish) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + CompressParams cparams; + cparams.gaborish = Override::kOff; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 900u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.7); +} + +TEST(JxlTest, RoundtripSmallPatchesAlpha) { + ThreadPool* pool = nullptr; + CodecInOut io; + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + Image3F black_with_small_lines(256, 256); + ImageF alpha(black_with_small_lines.xsize(), black_with_small_lines.ysize()); + ZeroFillImage(&black_with_small_lines); + // This pattern should be picked up by the patch detection heuristics. + for (size_t y = 0; y < black_with_small_lines.ysize(); y++) { + float* JXL_RESTRICT row = black_with_small_lines.PlaneRow(1, y); + for (size_t x = 0; x < black_with_small_lines.xsize(); x++) { + if (x % 4 == 0 && (y / 32) % 4 == 0) row[x] = 127.0f; + } + } + io.metadata.m.SetAlphaBits(8); + io.SetFromImage(std::move(black_with_small_lines), + ColorEncoding::LinearSRGB()); + FillImage(1.0f, &alpha); + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.butteraugli_distance = 0.1f; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 0.5f); +} + +TEST(JxlTest, RoundtripSmallPatches) { + ThreadPool* pool = nullptr; + CodecInOut io; + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + Image3F black_with_small_lines(256, 256); + ZeroFillImage(&black_with_small_lines); + // This pattern should be picked up by the patch detection heuristics. + for (size_t y = 0; y < black_with_small_lines.ysize(); y++) { + float* JXL_RESTRICT row = black_with_small_lines.PlaneRow(1, y); + for (size_t x = 0; x < black_with_small_lines.xsize(); x++) { + if (x % 4 == 0 && (y / 32) % 4 == 0) row[x] = 127.0f; + } + } + io.SetFromImage(std::move(black_with_small_lines), + ColorEncoding::LinearSRGB()); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.butteraugli_distance = 0.1f; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 0.5f); +} + +// Test header encoding of original bits per sample +TEST(JxlTest, RoundtripImageBundleOriginalBits) { + ThreadPool* pool = nullptr; + + // Image does not matter, only io.metadata.m and io2.metadata.m are tested. + Image3F image(1, 1); + ZeroFillImage(&image); + CodecInOut io; + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + io.SetFromImage(std::move(image), ColorEncoding::LinearSRGB()); + + CompressParams cparams; + DecompressParams dparams; + + // Test unsigned integers from 1 to 32 bits + for (uint32_t bit_depth = 1; bit_depth <= 32; bit_depth++) { + if (bit_depth == 32) { + // TODO(lode): allow testing 32, however the code below ends up in + // enc_modular which does not support 32. We only want to test the header + // encoding though, so try without modular. + break; + } + + io.metadata.m.SetUintSamples(bit_depth); + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + + EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_EQ(0u, io2.metadata.m.GetAlphaBits()); + } + + // Test various existing and non-existing floating point formats + for (uint32_t bit_depth = 8; bit_depth <= 32; bit_depth++) { + if (bit_depth != 32) { + // TODO: test other float types once they work + break; + } + + uint32_t exponent_bit_depth; + if (bit_depth < 10) { + exponent_bit_depth = 2; + } else if (bit_depth < 12) { + exponent_bit_depth = 3; + } else if (bit_depth < 16) { + exponent_bit_depth = 4; + } else if (bit_depth < 20) { + exponent_bit_depth = 5; + } else if (bit_depth < 24) { + exponent_bit_depth = 6; + } else if (bit_depth < 28) { + exponent_bit_depth = 7; + } else { + exponent_bit_depth = 8; + } + + io.metadata.m.bit_depth.bits_per_sample = bit_depth; + io.metadata.m.bit_depth.floating_point_sample = true; + io.metadata.m.bit_depth.exponent_bits_per_sample = exponent_bit_depth; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + + EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_TRUE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(exponent_bit_depth, + io2.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_EQ(0u, io2.metadata.m.GetAlphaBits()); + } +} + +TEST(JxlTest, RoundtripGrayscale) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + ASSERT_NE(io.xsize(), 0u); + io.ShrinkTo(128, 128); + EXPECT_TRUE(io.Main().IsGray()); + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + + { + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + PaddedBytes compressed; + EXPECT_TRUE( + EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(io2.Main().IsGray()); + + EXPECT_LE(compressed.size(), 7000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.7777777); + } + + // Test with larger butteraugli distance and other settings enabled so + // different jxl codepaths trigger. + { + CompressParams cparams; + cparams.butteraugli_distance = 8.0; + DecompressParams dparams; + + PaddedBytes compressed; + EXPECT_TRUE( + EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + EXPECT_TRUE(io2.Main().IsGray()); + + EXPECT_LE(compressed.size(), 1300u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 9.0); + } +} + +TEST(JxlTest, RoundtripAlpha) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + io.ShrinkTo(300, 300); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 10077u); + + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.4); +} + +TEST(JxlTest, RoundtripAlphaPremultiplied) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io, io_nopremul; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + ASSERT_TRUE(SetFromBytes(Span(orig), &io_nopremul, pool)); + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + io.ShrinkTo(300, 300); + io_nopremul.ShrinkTo(300, 300); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + io.PremultiplyAlpha(); + EXPECT_TRUE(io.Main().AlphaIsPremultiplied()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 10000u); + + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.4); + io2.Main().UnpremultiplyAlpha(); + EXPECT_LE(ButteraugliDistance(io_nopremul, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.8); +} + +TEST(JxlTest, RoundtripAlphaResampling) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + + CompressParams cparams; + cparams.resampling = 2; + cparams.ec_resampling = 2; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 15000u); + + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 6.0); +} + +TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + + CompressParams cparams; + cparams.ec_resampling = 2; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 31000u); + + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.5); +} + +TEST(JxlTest, RoundtripAlphaNonMultipleOf8) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + io.ShrinkTo(12, 12); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + DecompressParams dparams; + + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 200u); + + // TODO(robryk): Fix the following line in presence of different alpha_bits in + // the two contexts. + // EXPECT_TRUE(SamePixels(io.Main().alpha(), io2.Main().alpha())); + // TODO(robryk): Fix the distance estimate used in the encoder. + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 6.3); +} + +TEST(JxlTest, RoundtripAlpha16) { + ThreadPoolInternal pool(4); + + size_t xsize = 1200, ysize = 160; + Image3F color(xsize, ysize); + ImageF alpha(xsize, ysize); + // Generate 16-bit pattern that uses various colors and alpha values. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + color.PlaneRow(0, y)[x] = (y * 65535 / ysize) * (1.0f / 65535); + color.PlaneRow(1, y)[x] = (x * 65535 / xsize) * (1.0f / 65535); + color.PlaneRow(2, y)[x] = + ((y + x) * 65535 / (xsize + ysize)) * (1.0f / 65535); + alpha.Row(y)[x] = (x * 65535 / xsize) * (1.0f / 65535); + } + } + const bool is_gray = false; + CodecInOut io; + io.metadata.m.SetUintSamples(16); + io.metadata.m.SetAlphaBits(16); + io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); + io.SetFromImage(std::move(color), io.metadata.m.color_encoding); + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + + // The image is wider than 512 pixels to ensure multiple groups are tested. + + ASSERT_NE(io.xsize(), 0u); + ASSERT_TRUE(io.metadata.m.HasAlpha()); + ASSERT_TRUE(io.Main().HasAlpha()); + + CompressParams cparams; + cparams.butteraugli_distance = 0.5; + // Prevent the test to be too slow, does not affect alpha + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + io.metadata.m.SetUintSamples(16); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE( + EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, &pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, &pool)); + + EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); +} + +namespace { +CompressParams CParamsForLossless() { + CompressParams cparams; + cparams.modular_mode = true; + cparams.color_transform = jxl::ColorTransform::kNone; + cparams.quality_pair = {100, 100}; + cparams.options.predictor = {Predictor::Weighted}; + return cparams; +} +} // namespace + +TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli + // distance, then there is likely a floating point issue, that could be + // happening either in io or io2. The values of io are generated by + // external_image.cc, and those in io2 by the jxl decoder. If they use + // slightly different floating point operations (say, one casts int to float + // while other divides the int through 255.0f and later multiplies it by + // 255 again) they will get slightly different values. To fix, ensure both + // sides do the following formula for converting integer range 0-255 to + // floating point range 0.0f-255.0f: static_cast(i) + // without any further intermediate operations. + // Note that this precision issue is not a problem in practice if the values + // are equal when rounded to 8-bit int, but currently full exact precision is + // tested. + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathWP)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams = CParamsForLossless(); + cparams.speed_tier = SpeedTier::kFalcon; + cparams.options.skip_encoder_fast_path = true; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams = CParamsForLossless(); + cparams.speed_tier = SpeedTier::kThunder; + cparams.options.skip_encoder_fast_path = true; + cparams.options.predictor = {Predictor::Gradient}; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams = CParamsForLossless(); + cparams.speed_tier = SpeedTier::kLightning; + cparams.options.skip_encoder_fast_path = true; + cparams.options.predictor = {Predictor::Gradient}; + DecompressParams dparams; + + CodecInOut io2, io3; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); + cparams.options.skip_encoder_fast_path = false; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 3500000u); + EXPECT_EQ(0.0, ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8Falcon)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams = CParamsForLossless(); + cparams.speed_tier = SpeedTier::kFalcon; + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u); + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, RoundtripLossless8Alpha) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/tmshre_riaphotographs_alpha.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + EXPECT_EQ(8u, io.metadata.m.GetAlphaBits()); + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 350000u); + // If fails, see note about floating point in RoundtripLossless8. + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool)); + EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); + EXPECT_EQ(8u, io2.metadata.m.GetAlphaBits()); + EXPECT_EQ(8u, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample); +} + +TEST(JxlTest, RoundtripLossless16Alpha) { + ThreadPool* pool = nullptr; + + size_t xsize = 1200, ysize = 160; + Image3F color(xsize, ysize); + ImageF alpha(xsize, ysize); + // Generate 16-bit pattern that uses various colors and alpha values. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + color.PlaneRow(0, y)[x] = (y * 65535 / ysize) * (1.0f / 65535); + color.PlaneRow(1, y)[x] = (x * 65535 / xsize) * (1.0f / 65535); + color.PlaneRow(2, y)[x] = + ((y + x) * 65535 / (xsize + ysize)) * (1.0f / 65535); + alpha.Row(y)[x] = (x * 65535 / xsize) * (1.0f / 65535); + } + } + const bool is_gray = false; + CodecInOut io; + io.metadata.m.SetUintSamples(16); + io.metadata.m.SetAlphaBits(16); + io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); + io.SetFromImage(std::move(color), io.metadata.m.color_encoding); + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + + EXPECT_EQ(16u, io.metadata.m.GetAlphaBits()); + EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 7100u); + // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli + // distance, then there is likely a floating point issue, that could be + // happening either in io or io2. The values of io are generated by + // external_image.cc, and those in io2 by the jxl decoder. If they use + // slightly different floating point operations (say, one does "i / 257.0f" + // while the other does "i * (1.0f / 257)" they will get slightly different + // values. To fix, ensure both sides do the following formula for converting + // integer range 0-65535 to Image3F floating point range 0.0f-255.0f: + // "i * (1.0f / 257)". + // Note that this precision issue is not a problem in practice if the values + // are equal when rounded to 16-bit int, but currently full exact precision is + // tested. + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool)); + EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); + EXPECT_EQ(16u, io2.metadata.m.GetAlphaBits()); + EXPECT_EQ(16u, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample); +} + +TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) { + ThreadPool* pool = nullptr; + + size_t xsize = 128, ysize = 128; + Image3F color(xsize, ysize); + ImageF alpha(xsize, ysize); + // All 16-bit values, both color and alpha, of this image are below 64. + // This allows testing if a code path wrongly concludes it's an 8-bit instead + // of 16-bit image (or even 6-bit). + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + color.PlaneRow(0, y)[x] = (y * 64 / ysize) * (1.0f / 65535); + color.PlaneRow(1, y)[x] = (x * 64 / xsize) * (1.0f / 65535); + color.PlaneRow(2, y)[x] = + ((y + x) * 64 / (xsize + ysize)) * (1.0f / 65535); + alpha.Row(y)[x] = (64 * x / xsize) * (1.0f / 65535); + } + } + const bool is_gray = false; + CodecInOut io; + io.metadata.m.SetUintSamples(16); + io.metadata.m.SetAlphaBits(16); + io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray); + io.SetFromImage(std::move(color), io.metadata.m.color_encoding); + io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false); + + EXPECT_EQ(16u, io.metadata.m.GetAlphaBits()); + EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3100u); + EXPECT_EQ(16u, io2.metadata.m.GetAlphaBits()); + EXPECT_EQ(16u, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample); + // If fails, see note about floating point in RoundtripLossless8. + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool)); + EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha())); +} + +TEST(JxlTest, RoundtripYCbCr420) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + const PaddedBytes yuv420 = + ReadTestData("imagecompression.info/flower_foveon.png.ffmpeg.y4m"); + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(yuv420), &io2, pool)); + + CompressParams cparams = CParamsForLossless(); + cparams.speed_tier = SpeedTier::kThunder; + DecompressParams dparams; + + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE( + EncodeFile(cparams, &io2, &enc_state, &compressed, aux_out, pool)); + CodecInOut io3; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io3, pool)); + + EXPECT_LE(compressed.size(), 1320000u); + + // we're comparing an original PNG with a YCbCr 4:2:0 version + EXPECT_LE(ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.8); +} + +TEST(JxlTest, RoundtripDots) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + + CompressParams cparams; + cparams.dots = Override::kOn; + cparams.butteraugli_distance = 0.04; + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 400000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.2); +} + +TEST(JxlTest, RoundtripNoise) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + ASSERT_NE(io.xsize(), 0u); + + CompressParams cparams; + cparams.noise = Override::kOn; + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB()); + PassesEncoderState enc_state; + AuxOut* aux_out = nullptr; + PaddedBytes compressed; + EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, aux_out, pool)); + CodecInOut io2; + EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool)); + + EXPECT_LE(compressed.size(), 40000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.2); +} + +TEST(JxlTest, RoundtripLossless8Gray) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + + EXPECT_TRUE(io.Main().IsGray()); + EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample); + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 130000u); + // If fails, see note about floating point in RoundtripLossless8. + EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool)); + EXPECT_TRUE(io2.Main().IsGray()); + EXPECT_EQ(8u, io2.metadata.m.bit_depth.bits_per_sample); + EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample); + EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample); +} + +#if JPEGXL_ENABLE_GIF + +TEST(JxlTest, RoundtripAnimation) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = ReadTestData("jxl/traffic_light.gif"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + ASSERT_EQ(4u, io.frames.size()); + + CompressParams cparams; + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3000u); + + EXPECT_EQ(io2.frames.size(), io.frames.size()); + test::CoalesceGIFAnimationWithAlpha(&io); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), +#if JXL_HIGH_PRECISION + 1.55); +#else + 1.75); +#endif +} + +TEST(JxlTest, RoundtripLosslessAnimation) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = ReadTestData("jxl/traffic_light.gif"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + ASSERT_EQ(4u, io.frames.size()); + + CompressParams cparams = CParamsForLossless(); + DecompressParams dparams; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1200u); + + EXPECT_EQ(io2.frames.size(), io.frames.size()); + test::CoalesceGIFAnimationWithAlpha(&io); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 5e-4); +} + +#endif // JPEGXL_ENABLE_GIF + +#if JPEGXL_ENABLE_JPEG + +namespace { + +jxl::Status DecompressJxlToJPEGForTest( + const jpegxl::tools::JpegXlContainer& container, jxl::ThreadPool* pool, + jxl::PaddedBytes* output) { + output->clear(); + jxl::Span compressed(container.codestream, + container.codestream_size); + + JXL_RETURN_IF_ERROR(compressed.size() >= 2); + + // JXL case + // Decode to DCT when possible and generate a JPG file. + jxl::CodecInOut io; + jxl::DecompressParams params; + params.keep_dct = true; + if (!jpegxl::tools::DecodeJpegXlToJpeg(params, container, &io, pool)) { + return JXL_FAILURE("Failed to decode JXL to JPEG"); + } + io.jpeg_quality = 95; + if (!extras::EncodeImageJPGCoefficients(&io, output)) { + return JXL_FAILURE("Failed to generate JPEG"); + } + return true; +} + +} // namespace + +size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) { + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + EXPECT_TRUE(SetFromBytes(Span(jpeg_in), &io, pool)); + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + PassesEncoderState passes_enc_state; + PaddedBytes compressed, codestream; + + EXPECT_TRUE(EncodeFile(cparams, &io, &passes_enc_state, &codestream, + /*aux_out=*/nullptr, pool)); + jpegxl::tools::JpegXlContainer enc_container; + enc_container.codestream = codestream.data(); + enc_container.codestream_size = codestream.size(); + jpeg::JPEGData data_in = *io.Main().jpeg_data; + jxl::PaddedBytes jpeg_data; + EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data)); + enc_container.jpeg_reconstruction = jpeg_data.data(); + enc_container.jpeg_reconstruction_size = jpeg_data.size(); + EXPECT_TRUE(EncodeJpegXlContainerOneShot(enc_container, &compressed)); + + jpegxl::tools::JpegXlContainer container; + EXPECT_TRUE(DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(), + &container)); + PaddedBytes out; + EXPECT_TRUE(DecompressJxlToJPEGForTest(container, pool, &out)); + EXPECT_EQ(out.size(), jpeg_in.size()); + size_t failures = 0; + for (size_t i = 0; i < std::min(out.size(), jpeg_in.size()); i++) { + if (out[i] != jpeg_in[i]) { + EXPECT_EQ(out[i], jpeg_in[i]) + << "byte mismatch " << i << " " << out[i] << " != " << jpeg_in[i]; + if (++failures > 4) { + return compressed.size(); + } + } + } + return compressed.size(); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png.im_q85_444.jpg"); + // JPEG size is 326'916 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png.im_q85_444.jpg"); + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + DecompressParams dparams; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + + // TODO(eustas): investigate, why SJPEG and JpegRecompression pixels are + // different. + EXPECT_GE(1.8, ButteraugliDistance(io2, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png.im_q85_420.jpg"); + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + DecompressParams dparams; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + + EXPECT_GE(1.5, ButteraugliDistance(io2, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, + JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420EarlyFlush)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png.im_q85_420.jpg"); + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + DecompressParams dparams; + dparams.max_downsampling = 8; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + + EXPECT_GE(50, ButteraugliDistance(io2, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, + JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420Mul16)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon_cropped.jpg"); + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + DecompressParams dparams; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + + EXPECT_GE(1.5, ButteraugliDistance(io2, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, + JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels_asymmetric)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = ReadTestData( + "imagecompression.info/flower_foveon.png.im_q85_asymmetric.jpg"); + CodecInOut io; + io.dec_target = jxl::DecodeTarget::kQuantizedCoeffs; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CodecInOut io2; + ASSERT_TRUE(SetFromBytes(Span(orig), &io2, &pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kYCbCr; + + DecompressParams dparams; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + + EXPECT_GE(1.5, ButteraugliDistance(io2, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool)); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionGray)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("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"); + // JPEG size is 226'018 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 181050u); +} + +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"); + // JPEG size is 216'069 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u); +} + +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"); + // 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"); + // 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"); + // JPEG size is 262'249 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_asymmetric)) { + // 2x vertical downsample of one chroma channel, 2x horizontal downsample of + // the other. + ThreadPoolInternal pool(8); + const PaddedBytes orig = ReadTestData( + "imagecompression.info/flower_foveon.png.im_q85_asymmetric.jpg"); + // JPEG size is 262'249 bytes. + EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u); +} + +TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420Progr)) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = ReadTestData( + "imagecompression.info/flower_foveon.png.im_q85_420_progr.jpg"); + EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u); +} + +#endif // JPEGXL_ENABLE_JPEG + +TEST(JxlTest, RoundtripProgressive) { + ThreadPoolInternal pool(4); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + io.ShrinkTo(600, 1024); + + CompressParams cparams; + DecompressParams dparams; + + cparams.butteraugli_distance = 1.0f; + cparams.progressive_dc = true; + cparams.responsive = true; + cparams.progressive_mode = true; + CodecInOut io2; + EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 4.0f); +} + +TEST(JxlTest, RoundtripAnimationPatches) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = ReadTestData("jxl/animation_patches.gif"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + ASSERT_EQ(2u, io.frames.size()); + + CompressParams cparams; + cparams.patches = Override::kOn; + DecompressParams dparams; + CodecInOut io2; + // 40k with no patches, 27k with patch frames encoded multiple times. + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 24000u); + + EXPECT_EQ(io2.frames.size(), io.frames.size()); + // >10 with broken patches + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.0); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/lehmer_code.h b/lib/jxl/lehmer_code.h new file mode 100644 index 0000000..dd1d21c --- /dev/null +++ b/lib/jxl/lehmer_code.h @@ -0,0 +1,102 @@ +// 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_LEHMER_CODE_H_ +#define LIB_JXL_LEHMER_CODE_H_ + +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Permutation <=> factorial base representation (Lehmer code). + +using LehmerT = uint32_t; + +template +constexpr T ValueOfLowest1Bit(T t) { + return t & -t; +} + +// Computes the Lehmer (factorial basis) code of permutation, an array of n +// unique indices in [0..n), and stores it in code[0..len). N*logN time. +// temp must have n + 1 elements but need not be initialized. +template +void ComputeLehmerCode(const PermutationT* JXL_RESTRICT permutation, + uint32_t* JXL_RESTRICT temp, const size_t n, + LehmerT* JXL_RESTRICT code) { + for (size_t idx = 0; idx < n + 1; ++idx) temp[idx] = 0; + + for (size_t idx = 0; idx < n; ++idx) { + const PermutationT s = permutation[idx]; + + // Compute sum in Fenwick tree + uint32_t penalty = 0; + uint32_t i = s + 1; + while (i != 0) { + penalty += temp[i]; + i &= i - 1; // clear lowest bit + } + JXL_DASSERT(s >= penalty); + code[idx] = s - penalty; + i = s + 1; + // Add operation in Fenwick tree + while (i < n + 1) { + temp[i] += 1; + i += ValueOfLowest1Bit(i); + } + } +} + +// Decodes the Lehmer code in code[0..n) into permutation[0..n). +// temp must have 1 << CeilLog2(n) elements but need not be initialized. +template +void DecodeLehmerCode(const LehmerT* JXL_RESTRICT code, + uint32_t* JXL_RESTRICT temp, size_t n, + PermutationT* JXL_RESTRICT permutation) { + JXL_DASSERT(n != 0); + const size_t log2n = CeilLog2Nonzero(n); + const size_t padded_n = 1ull << log2n; + + for (size_t i = 0; i < padded_n; i++) { + const int32_t i1 = static_cast(i + 1); + temp[i] = static_cast(ValueOfLowest1Bit(i1)); + } + + for (size_t i = 0; i < n; i++) { + JXL_DASSERT(code[i] + i < n); + uint32_t rank = code[i] + 1; + + // Extract i-th unused element via implicit order-statistics tree. + size_t bit = padded_n; + size_t next = 0; + for (size_t i = 0; i <= log2n; i++) { + const size_t cand = next + bit; + JXL_DASSERT(cand >= 1); + bit >>= 1; + if (temp[cand - 1] < rank) { + next = cand; + rank -= temp[cand - 1]; + } + } + + permutation[i] = next; + + // Mark as used + next += 1; + while (next <= padded_n) { + temp[next - 1] -= 1; + next += ValueOfLowest1Bit(next); + } + } +} + +} // namespace jxl + +#endif // LIB_JXL_LEHMER_CODE_H_ diff --git a/lib/jxl/lehmer_code_test.cc b/lib/jxl/lehmer_code_test.cc new file mode 100644 index 0000000..ca9b577 --- /dev/null +++ b/lib/jxl/lehmer_code_test.cc @@ -0,0 +1,98 @@ +// 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/lehmer_code.h" + +#include +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/thread_pool_internal.h" + +namespace jxl { +namespace { + +template +struct WorkingSet { + explicit WorkingSet(size_t max_n) + : padded_n(1ull << CeilLog2Nonzero(max_n + 1)), + permutation(max_n), + temp(padded_n), + lehmer(max_n), + decoded(max_n) {} + + size_t padded_n; + std::vector permutation; + std::vector temp; + std::vector lehmer; + std::vector decoded; +}; + +template +void Roundtrip(size_t n, WorkingSet* ws) { + JXL_ASSERT(n != 0); + const size_t padded_n = 1ull << CeilLog2Nonzero(n); + + std::mt19937 rng(n * 65537 + 13); + + // Ensure indices fit into PermutationT + EXPECT_LE(n, 1ULL << (sizeof(PermutationT) * 8)); + + std::iota(ws->permutation.begin(), ws->permutation.begin() + n, 0); + + // For various random permutations: + for (size_t rep = 0; rep < 100; ++rep) { + std::shuffle(ws->permutation.begin(), ws->permutation.begin() + n, rng); + + // Must decode to the same permutation + ComputeLehmerCode(ws->permutation.data(), ws->temp.data(), n, + ws->lehmer.data()); + memset(ws->temp.data(), 0, padded_n * 4); + DecodeLehmerCode(ws->lehmer.data(), ws->temp.data(), n, ws->decoded.data()); + + for (size_t i = 0; i < n; ++i) { + EXPECT_EQ(ws->permutation[i], ws->decoded[i]); + } + } +} + +// Preallocates arrays and tests n = [begin, end). +template +void RoundtripSizeRange(ThreadPool* pool, uint32_t begin, uint32_t end) { + ASSERT_NE(0u, begin); // n = 0 not allowed. + std::vector> working_sets; + + RunOnPool( + pool, begin, end, + [&working_sets, end](size_t num_threads) { + for (size_t i = 0; i < num_threads; i++) { + working_sets.emplace_back(end - 1); + } + return true; + }, + [&working_sets](int n, int thread) { + Roundtrip(n, &working_sets[thread]); + }, + "lehmer test"); +} + +TEST(LehmerCodeTest, TestRoundtrips) { + ThreadPoolInternal pool(8); + + RoundtripSizeRange(&pool, 1, 1026); + + // Ensures PermutationT can fit > 16 bit values. + RoundtripSizeRange(&pool, 65536, 65540); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/libjxl.pc.in b/lib/jxl/libjxl.pc.in new file mode 100644 index 0000000..5dca2ac --- /dev/null +++ b/lib/jxl/libjxl.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: libjxl +Description: Loads and saves JPEG XL files +Version: @JPEGXL_LIBRARY_VERSION@ +Requires.private: @JPEGXL_LIBRARY_REQUIRES@ +Libs: -L${libdir} -ljxl +Libs.private: -lm +Cflags: -I${includedir} diff --git a/lib/jxl/linalg.cc b/lib/jxl/linalg.cc new file mode 100644 index 0000000..61d66dd --- /dev/null +++ b/lib/jxl/linalg.cc @@ -0,0 +1,235 @@ +// 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/linalg.h" + +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +void AssertSymmetric(const ImageD& A) { +#if JXL_ENABLE_ASSERT + JXL_ASSERT(A.xsize() == A.ysize()); + for (size_t i = 0; i < A.xsize(); ++i) { + for (size_t j = i + 1; j < A.xsize(); ++j) { + JXL_ASSERT(std::abs(A.Row(i)[j] - A.Row(j)[i]) < 1e-15); + } + } +#endif +} + +void Diagonalize2x2(const double a0, const double a1, const double b, double* c, + double* s) { + if (std::abs(b) < 1e-15) { + *c = 1.0; + *s = 0.0; + return; + } + double phi = std::atan2(2 * b, a1 - a0); + double theta = b > 0.0 ? 0.5 * phi : 0.5 * phi + Pi(1.0); + *c = std::cos(theta); + *s = std::sin(theta); +} + +void GivensRotation(const double x, const double y, double* c, double* s) { + if (y == 0.0) { + *c = x < 0.0 ? -1.0 : 1.0; + *s = 0.0; + } else { + const double h = hypot(x, y); + const double d = 1.0 / h; + *c = x * d; + *s = -y * d; + } +} + +void RotateMatrixCols(ImageD* const JXL_RESTRICT U, int i, int j, double c, + double s) { + JXL_ASSERT(U->xsize() == U->ysize()); + const size_t N = U->xsize(); + double* const JXL_RESTRICT u_i = U->Row(i); + double* const JXL_RESTRICT u_j = U->Row(j); + std::vector rot_i, rot_j; + rot_i.reserve(N); + rot_j.reserve(N); + for (size_t k = 0; k < N; ++k) { + rot_i.push_back(u_i[k] * c - u_j[k] * s); + rot_j.push_back(u_i[k] * s + u_j[k] * c); + } + for (size_t k = 0; k < N; ++k) { + u_i[k] = rot_i[k]; + u_j[k] = rot_j[k]; + } +} +void HouseholderReflector(const size_t N, const double* x, double* u) { + const double sigma = x[0] <= 0.0 ? 1.0 : -1.0; + u[0] = x[0] - sigma * std::sqrt(DotProduct(N, x, x)); + for (size_t k = 1; k < N; ++k) { + u[k] = x[k]; + } + double u_norm = 1.0 / std::sqrt(DotProduct(N, u, u)); + for (size_t k = 0; k < N; ++k) { + u[k] *= u_norm; + } +} + +void ConvertToTridiagonal(const ImageD& A, ImageD* const JXL_RESTRICT T, + ImageD* const JXL_RESTRICT U) { + AssertSymmetric(A); + const size_t N = A.xsize(); + *U = Identity(A.xsize()); + *T = CopyImage(A); + std::vector u_stack; + for (size_t k = 0; k + 2 < N; ++k) { + if (DotProduct(N - k - 2, &T->Row(k)[k + 2], &T->Row(k)[k + 2]) > 1e-15) { + ImageD u(N, 1); + ZeroFillImage(&u); + HouseholderReflector(N - k - 1, &T->Row(k)[k + 1], &u.Row(0)[k + 1]); + ImageD v = MatMul(*T, u); + double scale = DotProduct(u, v); + v = LinComb(2.0, v, -2.0 * scale, u); + SubtractFrom(MatMul(u, Transpose(v)), T); + SubtractFrom(MatMul(v, Transpose(u)), T); + u_stack.emplace_back(std::move(u)); + } + } + while (!u_stack.empty()) { + const ImageD& u = u_stack.back(); + ImageD v = MatMul(Transpose(*U), u); + SubtractFrom(ScaleImage(2.0, MatMul(u, Transpose(v))), U); + u_stack.pop_back(); + } +} + +double WilkinsonShift(const double a0, const double a1, const double b) { + const double d = 0.5 * (a0 - a1); + if (d == 0.0) { + return a1 - std::abs(b); + } + const double sign_d = d > 0.0 ? 1.0 : -1.0; + return a1 - b * b / (d + sign_d * hypotf(d, b)); +} + +void ImplicitQRStep(ImageD* const JXL_RESTRICT U, double* const JXL_RESTRICT a, + double* const JXL_RESTRICT b, int m0, int m1) { + JXL_ASSERT(m1 - m0 > 2); + double x = a[m0] - WilkinsonShift(a[m1 - 2], a[m1 - 1], b[m1 - 1]); + double y = b[m0 + 1]; + for (int k = m0; k < m1 - 1; ++k) { + double c, s; + GivensRotation(x, y, &c, &s); + const double w = c * x - s * y; + const double d = a[k] - a[k + 1]; + const double z = (2 * c * b[k + 1] + d * s) * s; + a[k] -= z; + a[k + 1] += z; + b[k + 1] = d * c * s + (c * c - s * s) * b[k + 1]; + x = b[k + 1]; + if (k > m0) { + b[k] = w; + } + if (k < m1 - 2) { + y = -s * b[k + 2]; + b[k + 2] *= c; + } + RotateMatrixCols(U, k, k + 1, c, s); + } +} + +void ScanInterval(const double* const JXL_RESTRICT a, + const double* const JXL_RESTRICT b, int istart, + const int iend, const double eps, + std::deque >* intervals) { + for (int k = istart; k < iend; ++k) { + if ((k + 1 == iend) || + std::abs(b[k + 1]) < eps * (std::abs(a[k]) + std::abs(a[k + 1]))) { + if (k > istart) { + intervals->push_back(std::make_pair(istart, k + 1)); + } + istart = k + 1; + } + } +} + +void ConvertToDiagonal(const ImageD& A, ImageD* const JXL_RESTRICT diag, + ImageD* const JXL_RESTRICT U) { + AssertSymmetric(A); + const size_t N = A.xsize(); + ImageD T; + ConvertToTridiagonal(A, &T, U); + // From now on, the algorithm keeps the transformed matrix tri-diagonal, + // so we only need to keep track of the diagonal and the off-diagonal entries. + std::vector a(N); + std::vector b(N); + for (size_t k = 0; k < N; ++k) { + a[k] = T.Row(k)[k]; + if (k > 0) b[k] = T.Row(k)[k - 1]; + } + // Run the symmetric tri-diagonal QR algorithm with implicit Wilkinson shift. + const double kEpsilon = 1e-14; + std::deque > intervals; + ScanInterval(&a[0], &b[0], 0, N, kEpsilon, &intervals); + while (!intervals.empty()) { + const int istart = intervals[0].first; + const int iend = intervals[0].second; + intervals.pop_front(); + if (iend == istart + 2) { + double& a0 = a[istart]; + double& a1 = a[istart + 1]; + double& b1 = b[istart + 1]; + double c, s; + Diagonalize2x2(a0, a1, b1, &c, &s); + const double d = a0 - a1; + const double z = (2 * c * b1 + d * s) * s; + a0 -= z; + a1 += z; + b1 = 0.0; + RotateMatrixCols(U, istart, istart + 1, c, s); + } else { + ImplicitQRStep(U, &a[0], &b[0], istart, iend); + ScanInterval(&a[0], &b[0], istart, iend, kEpsilon, &intervals); + } + } + *diag = ImageD(N, 1); + double* const JXL_RESTRICT diag_row = diag->Row(0); + for (size_t k = 0; k < N; ++k) { + diag_row[k] = a[k]; + } +} + +void ComputeQRFactorization(const ImageD& A, ImageD* const JXL_RESTRICT Q, + ImageD* const JXL_RESTRICT R) { + JXL_ASSERT(A.xsize() == A.ysize()); + const size_t N = A.xsize(); + *Q = Identity(N); + *R = CopyImage(A); + std::vector u_stack; + for (size_t k = 0; k + 1 < N; ++k) { + if (DotProduct(N - k - 1, &R->Row(k)[k + 1], &R->Row(k)[k + 1]) > 1e-15) { + ImageD u(N, 1); + FillImage(0.0, &u); + HouseholderReflector(N - k, &R->Row(k)[k], &u.Row(0)[k]); + ImageD v = MatMul(Transpose(u), *R); + SubtractFrom(ScaleImage(2.0, MatMul(u, v)), R); + u_stack.emplace_back(std::move(u)); + } + } + while (!u_stack.empty()) { + const ImageD& u = u_stack.back(); + ImageD v = MatMul(Transpose(u), *Q); + SubtractFrom(ScaleImage(2.0, MatMul(u, v)), Q); + u_stack.pop_back(); + } +} +} // namespace jxl diff --git a/lib/jxl/linalg.h b/lib/jxl/linalg.h new file mode 100644 index 0000000..7fbd943 --- /dev/null +++ b/lib/jxl/linalg.h @@ -0,0 +1,294 @@ +// 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_LINALG_H_ +#define LIB_JXL_LINALG_H_ + +// Linear algebra. + +#include + +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +using ImageD = Plane; + +template +inline T DotProduct(const size_t N, const T* const JXL_RESTRICT a, + const T* const JXL_RESTRICT b) { + T sum = 0.0; + for (size_t k = 0; k < N; ++k) { + sum += a[k] * b[k]; + } + return sum; +} + +template +inline T L2NormSquared(const size_t N, const T* const JXL_RESTRICT a) { + return DotProduct(N, a, a); +} + +template +inline T L1Norm(const size_t N, const T* const JXL_RESTRICT a) { + T sum = 0; + for (size_t k = 0; k < N; ++k) { + sum += a[k] >= 0 ? a[k] : -a[k]; + } + return sum; +} + +inline double DotProduct(const ImageD& a, const ImageD& b) { + JXL_ASSERT(a.ysize() == 1); + JXL_ASSERT(b.ysize() == 1); + JXL_ASSERT(a.xsize() == b.xsize()); + const double* const JXL_RESTRICT row_a = a.Row(0); + const double* const JXL_RESTRICT row_b = b.Row(0); + return DotProduct(a.xsize(), row_a, row_b); +} + +inline ImageD Transpose(const ImageD& A) { + ImageD out(A.ysize(), A.xsize()); + for (size_t x = 0; x < A.xsize(); ++x) { + double* const JXL_RESTRICT row_out = out.Row(x); + for (size_t y = 0; y < A.ysize(); ++y) { + row_out[y] = A.Row(y)[x]; + } + } + return out; +} + +template +Plane MatMul(const Plane& A, const Plane& B) { + JXL_ASSERT(A.ysize() == B.xsize()); + Plane out(A.xsize(), B.ysize()); + for (size_t y = 0; y < B.ysize(); ++y) { + const Tin2* const JXL_RESTRICT row_b = B.Row(y); + Tout* const JXL_RESTRICT row_out = out.Row(y); + for (size_t x = 0; x < A.xsize(); ++x) { + row_out[x] = 0.0; + for (size_t k = 0; k < B.xsize(); ++k) { + row_out[x] += A.Row(k)[x] * row_b[k]; + } + } + } + return out; +} + +template +ImageD MatMul(const Plane& A, const Plane& B) { + return MatMul(A, B); +} + +template +ImageI MatMulI(const Plane& A, const Plane& B) { + return MatMul(A, B); +} + +// Computes A = B * C, with sizes rows*cols: A=ha*wa, B=wa*wb, C=ha*wb +template +void MatMul(const T* a, const T* b, int ha, int wa, int wb, T* c) { + std::vector temp(wa); // Make better use of cache lines + for (int x = 0; x < wb; x++) { + for (int z = 0; z < wa; z++) { + temp[z] = b[z * wb + x]; + } + for (int y = 0; y < ha; y++) { + double e = 0; + for (int z = 0; z < wa; z++) { + e += a[y * wa + z] * temp[z]; + } + c[y * wb + x] = e; + } + } +} + +// Computes C = A + factor * B +template +void MatAdd(const T* a, const T* b, F factor, int h, int w, T* c) { + for (int i = 0; i < w * h; i++) { + c[i] = a[i] + b[i] * factor; + } +} + +template +inline Plane Identity(const size_t N) { + Plane out(N, N); + for (size_t i = 0; i < N; ++i) { + T* JXL_RESTRICT row = out.Row(i); + std::fill(row, row + N, 0); + row[i] = static_cast(1.0); + } + return out; +} + +inline ImageD Diagonal(const ImageD& d) { + JXL_ASSERT(d.ysize() == 1); + ImageD out(d.xsize(), d.xsize()); + const double* JXL_RESTRICT row_diag = d.Row(0); + for (size_t k = 0; k < d.xsize(); ++k) { + double* JXL_RESTRICT row_out = out.Row(k); + std::fill(row_out, row_out + d.xsize(), 0.0); + row_out[k] = row_diag[k]; + } + return out; +} + +// Computes c, s such that c^2 + s^2 = 1 and +// [c -s] [x] = [ * ] +// [s c] [y] [ 0 ] +void GivensRotation(double x, double y, double* c, double* s); + +// U = U * Givens(i, j, c, s) +void RotateMatrixCols(ImageD* JXL_RESTRICT U, int i, int j, double c, double s); + +// A is symmetric, U is orthogonal, T is tri-diagonal and +// A = U * T * Transpose(U). +void ConvertToTridiagonal(const ImageD& A, ImageD* JXL_RESTRICT T, + ImageD* JXL_RESTRICT U); + +// A is symmetric, U is orthogonal, and A = U * Diagonal(diag) * Transpose(U). +void ConvertToDiagonal(const ImageD& A, ImageD* JXL_RESTRICT diag, + ImageD* JXL_RESTRICT U); + +// A is square matrix, Q is orthogonal, R is upper triangular and A = Q * R; +void ComputeQRFactorization(const ImageD& A, ImageD* JXL_RESTRICT Q, + ImageD* JXL_RESTRICT R); + +// Inverts a 3x3 matrix in place +template +Status Inv3x3Matrix(T* matrix) { + // Intermediate computation is done in double precision. + double temp[9]; + temp[0] = static_cast(matrix[4]) * matrix[8] - + static_cast(matrix[5]) * matrix[7]; + temp[1] = static_cast(matrix[2]) * matrix[7] - + static_cast(matrix[1]) * matrix[8]; + temp[2] = static_cast(matrix[1]) * matrix[5] - + static_cast(matrix[2]) * matrix[4]; + temp[3] = static_cast(matrix[5]) * matrix[6] - + static_cast(matrix[3]) * matrix[8]; + temp[4] = static_cast(matrix[0]) * matrix[8] - + static_cast(matrix[2]) * matrix[6]; + temp[5] = static_cast(matrix[2]) * matrix[3] - + static_cast(matrix[0]) * matrix[5]; + temp[6] = static_cast(matrix[3]) * matrix[7] - + static_cast(matrix[4]) * matrix[6]; + temp[7] = static_cast(matrix[1]) * matrix[6] - + static_cast(matrix[0]) * matrix[7]; + temp[8] = static_cast(matrix[0]) * matrix[4] - + static_cast(matrix[1]) * matrix[3]; + double det = matrix[0] * temp[0] + matrix[1] * temp[3] + matrix[2] * temp[6]; + if (std::abs(det) < 1e-10) { + return JXL_FAILURE("Matrix determinant is too close to 0"); + } + double idet = 1.0 / det; + for (int i = 0; i < 9; i++) { + matrix[i] = temp[i] * idet; + } + return true; +} + +// Solves system of linear equations A * X = B using the conjugate gradient +// method. Matrix a must be a n*n, symmetric and positive definite. +// Vectors b and x must have n elements +template +void ConjugateGradient(const T* a, int n, const T* b, T* x) { + std::vector r(n); + MatMul(a, x, n, n, 1, r.data()); + MatAdd(b, r.data(), -1, n, 1, r.data()); + std::vector p = r; + T rr; + MatMul(r.data(), r.data(), 1, n, 1, &rr); // inner product + + if (rr == 0) return; // The initial values were already optimal + + for (int i = 0; i < n; i++) { + std::vector ap(n); + MatMul(a, p.data(), n, n, 1, ap.data()); + T alpha; + MatMul(r.data(), ap.data(), 1, n, 1, &alpha); + // Normally alpha couldn't be zero here but if numerical issues caused it, + // return assuming the solution is close. + if (alpha == 0) return; + alpha = rr / alpha; + MatAdd(x, p.data(), alpha, n, 1, x); + MatAdd(r.data(), ap.data(), -alpha, n, 1, r.data()); + + T rr2; + MatMul(r.data(), r.data(), 1, n, 1, &rr2); // inner product + if (rr2 < 1e-20) break; + + T beta = rr2 / rr; + MatAdd(r.data(), p.data(), beta, 1, n, p.data()); + rr = rr2; + } +} + +// Computes optimal coefficients r to approximate points p with linear +// combination of functions f. The matrix f has h rows and w columns, r has h +// values, p has w values. h is the amount of functions, w the amount of points. +// Uses the finite element method and minimizes mean square error. +template +void FEM(const T* f, int h, int w, const T* p, T* r) { + // Compute "Gramian" matrix G = F * F^T + // Speed up multiplication by using non-zero intervals in sparse F. + std::vector start(h); + std::vector end(h); + for (int y = 0; y < h; y++) { + start[y] = end[y] = 0; + for (int x = 0; x < w; x++) { + if (f[y * w + x] != 0) { + start[y] = x; + break; + } + } + for (int x = w - 1; x >= 0; x--) { + if (f[y * w + x] != 0) { + end[y] = x + 1; + break; + } + } + } + + std::vector g(h * h); + for (int y = 0; y < h; y++) { + for (int x = 0; x <= y; x++) { + T v = 0; + // Intersection of the two sparse intervals. + int s = std::max(start[x], start[y]); + int e = std::min(end[x], end[y]); + for (int z = s; z < e; z++) { + v += f[x * w + z] * f[y * w + z]; + } + // Symmetric, so two values output at once + g[y * h + x] = v; + g[x * h + y] = v; + } + } + + // B vector: sum of each column of F multiplied by corresponding p + std::vector b(h, 0); + for (int y = 0; y < h; y++) { + T v = 0; + for (int x = 0; x < w; x++) { + v += f[y * w + x] * p[x]; + } + b[y] = v; + } + + ConjugateGradient(g.data(), h, b.data(), r); +} + +} // namespace jxl + +#endif // LIB_JXL_LINALG_H_ diff --git a/lib/jxl/linalg_test.cc b/lib/jxl/linalg_test.cc new file mode 100644 index 0000000..0842f61 --- /dev/null +++ b/lib/jxl/linalg_test.cc @@ -0,0 +1,149 @@ +// 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/linalg.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +template +Plane RandomMatrix(const size_t xsize, const size_t ysize, Random& rng, + const T vmin, const T vmax) { + Plane A(xsize, ysize); + GeneratorRandom gen(&rng, vmin, vmax); + GenerateImage(gen, &A); + return A; +} + +template +Plane RandomSymmetricMatrix(const size_t N, Random& rng, const T vmin, + const T vmax) { + Plane A = RandomMatrix(N, N, rng, vmin, vmax); + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < i; ++j) { + A.Row(j)[i] = A.Row(i)[j]; + } + } + return A; +} +void VerifyMatrixEqual(const ImageD& A, const ImageD& B, const double eps) { + ASSERT_EQ(A.xsize(), B.xsize()); + ASSERT_EQ(A.ysize(), B.ysize()); + for (size_t y = 0; y < A.ysize(); ++y) { + for (size_t x = 0; x < A.xsize(); ++x) { + ASSERT_NEAR(A.Row(y)[x], B.Row(y)[x], eps); + } + } +} + +void VerifyOrthogonal(const ImageD& A, const double eps) { + VerifyMatrixEqual(Identity(A.xsize()), MatMul(Transpose(A), A), eps); +} + +void VerifyTridiagonal(const ImageD& T, const double eps) { + ASSERT_EQ(T.xsize(), T.ysize()); + for (size_t i = 0; i < T.xsize(); ++i) { + for (size_t j = i + 2; j < T.xsize(); ++j) { + ASSERT_NEAR(T.Row(i)[j], 0.0, eps); + ASSERT_NEAR(T.Row(j)[i], 0.0, eps); + } + } +} + +void VerifyUpperTriangular(const ImageD& R, const double eps) { + ASSERT_EQ(R.xsize(), R.ysize()); + for (size_t i = 0; i < R.xsize(); ++i) { + for (size_t j = i + 1; j < R.xsize(); ++j) { + ASSERT_NEAR(R.Row(i)[j], 0.0, eps); + } + } +} + +TEST(LinAlgTest, ConvertToTridiagonal) { + { + ImageD I = Identity(5); + ImageD T, U; + ConvertToTridiagonal(I, &T, &U); + VerifyMatrixEqual(I, T, 1e-15); + VerifyMatrixEqual(I, U, 1e-15); + } + { + ImageD A = Identity(5); + A.Row(0)[1] = A.Row(1)[0] = 2.0; + A.Row(0)[4] = A.Row(4)[0] = 3.0; + A.Row(2)[3] = A.Row(3)[2] = 2.0; + A.Row(3)[4] = A.Row(4)[3] = 2.0; + ImageD U, d; + ConvertToDiagonal(A, &d, &U); + VerifyOrthogonal(U, 1e-12); + VerifyMatrixEqual(A, MatMul(U, MatMul(Diagonal(d), Transpose(U))), 1e-12); + } + std::mt19937_64 rng; + for (int N = 2; N < 100; ++N) { + ImageD A = RandomSymmetricMatrix(N, rng, -1.0, 1.0); + ImageD T, U; + ConvertToTridiagonal(A, &T, &U); + VerifyOrthogonal(U, 1e-12); + VerifyTridiagonal(T, 1e-12); + VerifyMatrixEqual(A, MatMul(U, MatMul(T, Transpose(U))), 1e-12); + } +} + +TEST(LinAlgTest, ConvertToDiagonal) { + { + ImageD I = Identity(5); + ImageD U, d; + ConvertToDiagonal(I, &d, &U); + VerifyMatrixEqual(I, U, 1e-15); + for (int k = 0; k < 5; ++k) { + ASSERT_NEAR(d.Row(0)[k], 1.0, 1e-15); + } + } + { + ImageD A = Identity(5); + A.Row(0)[1] = A.Row(1)[0] = 2.0; + A.Row(2)[3] = A.Row(3)[2] = 2.0; + A.Row(3)[4] = A.Row(4)[3] = 2.0; + ImageD U, d; + ConvertToDiagonal(A, &d, &U); + VerifyOrthogonal(U, 1e-12); + VerifyMatrixEqual(A, MatMul(U, MatMul(Diagonal(d), Transpose(U))), 1e-12); + } + std::mt19937_64 rng; + for (int N = 2; N < 100; ++N) { + ImageD A = RandomSymmetricMatrix(N, rng, -1.0, 1.0); + ImageD U, d; + ConvertToDiagonal(A, &d, &U); + VerifyOrthogonal(U, 1e-12); + VerifyMatrixEqual(A, MatMul(U, MatMul(Diagonal(d), Transpose(U))), 1e-12); + } +} + +TEST(LinAlgTest, ComputeQRFactorization) { + { + ImageD I = Identity(5); + ImageD Q, R; + ComputeQRFactorization(I, &Q, &R); + VerifyMatrixEqual(I, Q, 1e-15); + VerifyMatrixEqual(I, R, 1e-15); + } + std::mt19937_64 rng; + for (int N = 2; N < 100; ++N) { + ImageD A = RandomMatrix(N, N, rng, -1.0, 1.0); + ImageD Q, R; + ComputeQRFactorization(A, &Q, &R); + VerifyOrthogonal(Q, 1e-12); + VerifyUpperTriangular(R, 1e-12); + VerifyMatrixEqual(A, MatMul(Q, R), 1e-12); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/loop_filter.cc b/lib/jxl/loop_filter.cc new file mode 100644 index 0000000..afa36a4 --- /dev/null +++ b/lib/jxl/loop_filter.cc @@ -0,0 +1,87 @@ +// 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/loop_filter.h" + +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/fields.h" + +namespace jxl { + +LoopFilter::LoopFilter() { Bundle::Init(this); } +Status LoopFilter::VisitFields(Visitor* JXL_RESTRICT visitor) { + // Must come before AllDefault. + + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(true, &gab)); + if (visitor->Conditional(gab)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &gab_custom)); + if (visitor->Conditional(gab_custom)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.104699568f, &gab_x_weight1)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.055680538f, &gab_x_weight2)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.104699568f, &gab_y_weight1)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.055680538f, &gab_y_weight2)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.104699568f, &gab_b_weight1)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(1.1 * 0.055680538f, &gab_b_weight2)); + } + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(2, 2, &epf_iters)); + if (visitor->Conditional(epf_iters > 0)) { + if (visitor->Conditional(!nonserialized_is_modular)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &epf_sharp_custom)); + if (visitor->Conditional(epf_sharp_custom)) { + for (size_t i = 0; i < kEpfSharpEntries; ++i) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16( + float(i) / float(kEpfSharpEntries - 1), &epf_sharp_lut[i])); + } + } + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &epf_weight_custom)); + if (visitor->Conditional(epf_weight_custom)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(40.0f, &epf_channel_scale[0])); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(5.0f, &epf_channel_scale[1])); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(3.5f, &epf_channel_scale[2])); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.45f, &epf_pass1_zeroflush)); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.6f, &epf_pass2_zeroflush)); + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &epf_sigma_custom)); + if (visitor->Conditional(epf_sigma_custom)) { + if (visitor->Conditional(!nonserialized_is_modular)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.46f, &epf_quant_mul)); + } + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(0.9f, &epf_pass0_sigma_scale)); + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(6.5f, &epf_pass2_sigma_scale)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->F16(0.6666666666666666f, &epf_border_sad_mul)); + } + if (visitor->Conditional(nonserialized_is_modular)) { + JXL_QUIET_RETURN_IF_ERROR(visitor->F16(1.0f, &epf_sigma_for_modular)); + if (epf_sigma_for_modular < 1e-8) { + return JXL_FAILURE("EPF: sigma for modular is too small"); + } + } + } + + JXL_QUIET_RETURN_IF_ERROR(visitor->BeginExtensions(&extensions)); + // Extensions: in chronological order of being added to the format. + return visitor->EndExtensions(); +} + +} // namespace jxl diff --git a/lib/jxl/loop_filter.h b/lib/jxl/loop_filter.h new file mode 100644 index 0000000..ffa68b5 --- /dev/null +++ b/lib/jxl/loop_filter.h @@ -0,0 +1,78 @@ +// 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_LOOP_FILTER_H_ +#define LIB_JXL_LOOP_FILTER_H_ + +// Parameters for loop filter(s), stored in each frame. + +#include +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +struct LoopFilter : public Fields { + LoopFilter(); + const char* Name() const override { return "LoopFilter"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + size_t Padding() const { + static const size_t padding_per_epf_iter[4] = {0, 2, 3, 6}; + return padding_per_epf_iter[epf_iters] + (gab ? 1 : 0); + } + + mutable bool all_default; + + // --- Gaborish convolution + bool gab; + + bool gab_custom; + float gab_x_weight1; + float gab_x_weight2; + float gab_y_weight1; + float gab_y_weight2; + float gab_b_weight1; + float gab_b_weight2; + + // --- Edge-preserving filter + + // Number of EPF stages to apply. 0 means EPF disabled. 1 applies only the + // first stage, 2 applies both stages and 3 applies the first stage twice and + // the second stage once. + uint32_t epf_iters; + + bool epf_sharp_custom; + enum { kEpfSharpEntries = 8 }; + float epf_sharp_lut[kEpfSharpEntries]; + + bool epf_weight_custom; // Custom weight params + float epf_channel_scale[3]; // Relative weight of each channel + float epf_pass1_zeroflush; // Minimum weight for first pass + float epf_pass2_zeroflush; // Minimum weight for second pass + + bool epf_sigma_custom; // Custom sigma parameters + float epf_quant_mul; // Sigma is ~ this * quant + float epf_pass0_sigma_scale; // Multiplier for sigma in pass 0 + float epf_pass2_sigma_scale; // Multiplier for sigma in the second pass + float epf_border_sad_mul; // (inverse) multiplier for sigma on borders + + float epf_sigma_for_modular; + + uint64_t extensions; + + bool nonserialized_is_modular = false; +}; + +} // namespace jxl + +#endif // LIB_JXL_LOOP_FILTER_H_ diff --git a/lib/jxl/luminance.cc b/lib/jxl/luminance.cc new file mode 100644 index 0000000..9eba4d4 --- /dev/null +++ b/lib/jxl/luminance.cc @@ -0,0 +1,31 @@ +// 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/luminance.h" + +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" + +namespace jxl { + +void SetIntensityTarget(CodecInOut* io) { + if (io->target_nits != 0) { + io->metadata.m.SetIntensityTarget(io->target_nits); + return; + } + if (io->metadata.m.color_encoding.tf.IsPQ()) { + // Peak luminance of PQ as defined by SMPTE ST 2084:2014. + io->metadata.m.SetIntensityTarget(10000); + } else if (io->metadata.m.color_encoding.tf.IsHLG()) { + // Nominal display peak luminance used as a reference by + // Rec. ITU-R BT.2100-2. + io->metadata.m.SetIntensityTarget(1000); + } else { + // SDR + io->metadata.m.SetIntensityTarget(kDefaultIntensityTarget); + } +} + +} // namespace jxl diff --git a/lib/jxl/luminance.h b/lib/jxl/luminance.h new file mode 100644 index 0000000..c6a9d9e --- /dev/null +++ b/lib/jxl/luminance.h @@ -0,0 +1,21 @@ +// 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_LUMINANCE_H_ +#define LIB_JXL_LUMINANCE_H_ + +namespace jxl { + +// Chooses a default intensity target based on the transfer function of the +// image, if known. For SDR images or images not known to be HDR, returns +// kDefaultIntensityTarget, for images known to have PQ or HLG transfer function +// returns a higher value. If the image metadata already has a non-zero +// intensity target, does nothing. +class CodecInOut; +void SetIntensityTarget(CodecInOut* io); + +} // namespace jxl + +#endif // LIB_JXL_LUMINANCE_H_ diff --git a/lib/jxl/memory_manager_internal.cc b/lib/jxl/memory_manager_internal.cc new file mode 100644 index 0000000..87727e7 --- /dev/null +++ b/lib/jxl/memory_manager_internal.cc @@ -0,0 +1,18 @@ +// 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/memory_manager_internal.h" + +#include + +namespace jxl { + +void* MemoryManagerDefaultAlloc(void* opaque, size_t size) { + return malloc(size); +} + +void MemoryManagerDefaultFree(void* opaque, void* address) { free(address); } + +} // namespace jxl diff --git a/lib/jxl/memory_manager_internal.h b/lib/jxl/memory_manager_internal.h new file mode 100644 index 0000000..b4a7890 --- /dev/null +++ b/lib/jxl/memory_manager_internal.h @@ -0,0 +1,101 @@ +// 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_MEMORY_MANAGER_INTERNAL_H_ +#define LIB_JXL_MEMORY_MANAGER_INTERNAL_H_ + +// Memory allocator with support for alignment + misalignment. + +#include +#include +#include +#include // memcpy + +#include +#include + +#include "jxl/memory_manager.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" + +namespace jxl { + +// Default alloc and free functions. +void* MemoryManagerDefaultAlloc(void* opaque, size_t size); +void MemoryManagerDefaultFree(void* opaque, void* address); + +// Initializes the memory manager instance with the passed one. The +// MemoryManager passed in |memory_manager| may be NULL or contain NULL +// functions which will be initialized with the default ones. If either alloc +// or free are NULL, then both must be NULL, otherwise this function returns an +// error. +static JXL_INLINE Status MemoryManagerInit( + JxlMemoryManager* self, const JxlMemoryManager* memory_manager) { + if (memory_manager) { + *self = *memory_manager; + } else { + memset(self, 0, sizeof(*self)); + } + if (!self->alloc != !self->free) { + return false; + } + if (!self->alloc) self->alloc = jxl::MemoryManagerDefaultAlloc; + if (!self->free) self->free = jxl::MemoryManagerDefaultFree; + + return true; +} + +static JXL_INLINE void* MemoryManagerAlloc( + const JxlMemoryManager* memory_manager, size_t size) { + return memory_manager->alloc(memory_manager->opaque, size); +} + +static JXL_INLINE void MemoryManagerFree(const JxlMemoryManager* memory_manager, + void* address) { + return memory_manager->free(memory_manager->opaque, address); +} + +// Helper class to be used as a deleter in a unique_ptr call. +class MemoryManagerDeleteHelper { + public: + explicit MemoryManagerDeleteHelper(const JxlMemoryManager* memory_manager) + : memory_manager_(memory_manager) {} + + // Delete and free the passed pointer using the memory_manager. + template + void operator()(T* address) const { + if (!address) { + return; + } + address->~T(); + return memory_manager_->free(memory_manager_->opaque, address); + } + + private: + const JxlMemoryManager* memory_manager_; +}; + +template +using MemoryManagerUniquePtr = std::unique_ptr; + +// Creates a new object T allocating it with the memory allocator into a +// unique_ptr. +template +JXL_INLINE MemoryManagerUniquePtr MemoryManagerMakeUnique( + const JxlMemoryManager* memory_manager, Args&&... args) { + T* mem = + static_cast(memory_manager->alloc(memory_manager->opaque, sizeof(T))); + if (!mem) { + // Allocation error case. + return MemoryManagerUniquePtr(nullptr, + MemoryManagerDeleteHelper(memory_manager)); + } + return MemoryManagerUniquePtr(new (mem) T(std::forward(args)...), + MemoryManagerDeleteHelper(memory_manager)); +} + +} // namespace jxl + +#endif // LIB_JXL_MEMORY_MANAGER_INTERNAL_H_ diff --git a/lib/jxl/modular/encoding/context_predict.h b/lib/jxl/modular/encoding/context_predict.h new file mode 100644 index 0000000..3304f92 --- /dev/null +++ b/lib/jxl/modular/encoding/context_predict.h @@ -0,0 +1,653 @@ +// 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_MODULAR_ENCODING_CONTEXT_PREDICT_H_ +#define LIB_JXL_MODULAR_ENCODING_CONTEXT_PREDICT_H_ + +#include +#include + +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +namespace weighted { +constexpr static size_t kNumPredictors = 4; +constexpr static int64_t kPredExtraBits = 3; +constexpr static int64_t kPredictionRound = ((1 << kPredExtraBits) >> 1) - 1; +constexpr static size_t kNumProperties = 1; + +struct Header : public Fields { + const char *Name() const override { return "WeightedPredictorHeader"; } + // TODO(janwas): move to cc file, avoid including fields.h. + Header() { Bundle::Init(this); } + + Status VisitFields(Visitor *JXL_RESTRICT visitor) override { + if (visitor->AllDefault(*this, &all_default)) { + // Overwrite all serialized fields, but not any nonserialized_*. + visitor->SetDefault(this); + return true; + } + auto visit_p = [visitor](pixel_type val, pixel_type *p) { + uint32_t up = *p; + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(5, val, &up)); + *p = up; + return Status(true); + }; + JXL_QUIET_RETURN_IF_ERROR(visit_p(16, &p1C)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(10, &p2C)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(7, &p3Ca)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(7, &p3Cb)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(7, &p3Cc)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(0, &p3Cd)); + JXL_QUIET_RETURN_IF_ERROR(visit_p(0, &p3Ce)); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(4, 0xd, &w[0])); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(4, 0xc, &w[1])); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(4, 0xc, &w[2])); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bits(4, 0xc, &w[3])); + return true; + } + + bool all_default; + pixel_type p1C = 0, p2C = 0, p3Ca = 0, p3Cb = 0, p3Cc = 0, p3Cd = 0, p3Ce = 0; + uint32_t w[kNumPredictors] = {}; +}; + +struct State { + pixel_type_w prediction[kNumPredictors] = {}; + pixel_type_w pred = 0; // *before* removing the added bits. + std::vector pred_errors[kNumPredictors]; + std::vector error; + Header header; + + // Allows to approximate division by a number from 1 to 64. + uint32_t divlookup[64]; + + constexpr static pixel_type_w AddBits(pixel_type_w x) { + return uint64_t(x) << kPredExtraBits; + } + + State(Header header, size_t xsize, size_t ysize) : header(header) { + // Extra margin to avoid out-of-bounds writes. + // All have space for two rows of data. + for (size_t i = 0; i < 4; i++) { + pred_errors[i].resize((xsize + 2) * 2); + } + error.resize((xsize + 2) * 2); + // Initialize division lookup table. + for (int i = 0; i < 64; i++) { + divlookup[i] = (1 << 24) / (i + 1); + } + } + + // Approximates 4+(maxweight<<24)/(x+1), avoiding division + JXL_INLINE uint32_t ErrorWeight(uint64_t x, uint32_t maxweight) const { + int shift = static_cast(FloorLog2Nonzero(x + 1)) - 5; + if (shift < 0) shift = 0; + return 4 + ((maxweight * divlookup[x >> shift]) >> shift); + } + + // Approximates the weighted average of the input values with the given + // weights, avoiding division. Weights must sum to at least 16. + JXL_INLINE pixel_type_w + WeightedAverage(const pixel_type_w *JXL_RESTRICT p, + std::array w) const { + uint32_t weight_sum = 0; + for (size_t i = 0; i < kNumPredictors; i++) { + weight_sum += w[i]; + } + JXL_DASSERT(weight_sum > 15); + uint32_t log_weight = FloorLog2Nonzero(weight_sum); // at least 4. + weight_sum = 0; + for (size_t i = 0; i < kNumPredictors; i++) { + w[i] >>= log_weight - 4; + weight_sum += w[i]; + } + // for rounding. + pixel_type_w sum = (weight_sum >> 1) - 1; + for (size_t i = 0; i < kNumPredictors; i++) { + sum += p[i] * w[i]; + } + return (sum * divlookup[weight_sum - 1]) >> 24; + } + + template + JXL_INLINE pixel_type_w Predict(size_t x, size_t y, size_t xsize, + pixel_type_w N, pixel_type_w W, + pixel_type_w NE, pixel_type_w NW, + pixel_type_w NN, Properties *properties, + size_t offset) { + size_t cur_row = y & 1 ? 0 : (xsize + 2); + size_t prev_row = y & 1 ? (xsize + 2) : 0; + size_t pos_N = prev_row + x; + size_t pos_NE = x < xsize - 1 ? pos_N + 1 : pos_N; + size_t pos_NW = x > 0 ? pos_N - 1 : pos_N; + std::array weights; + for (size_t i = 0; i < kNumPredictors; i++) { + // pred_errors[pos_N] also contains the error of pixel W. + // pred_errors[pos_NW] also contains the error of pixel WW. + weights[i] = pred_errors[i][pos_N] + pred_errors[i][pos_NE] + + pred_errors[i][pos_NW]; + weights[i] = ErrorWeight(weights[i], header.w[i]); + } + + N = AddBits(N); + W = AddBits(W); + NE = AddBits(NE); + NW = AddBits(NW); + NN = AddBits(NN); + + pixel_type_w teW = x == 0 ? 0 : error[cur_row + x - 1]; + pixel_type_w teN = error[pos_N]; + pixel_type_w teNW = error[pos_NW]; + pixel_type_w sumWN = teN + teW; + pixel_type_w teNE = error[pos_NE]; + + if (compute_properties) { + pixel_type_w p = teW; + if (std::abs(teN) > std::abs(p)) p = teN; + if (std::abs(teNW) > std::abs(p)) p = teNW; + if (std::abs(teNE) > std::abs(p)) p = teNE; + (*properties)[offset++] = p; + } + + prediction[0] = W + NE - N; + prediction[1] = N - (((sumWN + teNE) * header.p1C) >> 5); + prediction[2] = W - (((sumWN + teNW) * header.p2C) >> 5); + prediction[3] = + N - ((teNW * header.p3Ca + teN * header.p3Cb + teNE * header.p3Cc + + (NN - N) * header.p3Cd + (NW - W) * header.p3Ce) >> + 5); + + pred = WeightedAverage(prediction, weights); + + // If all three have the same sign, skip clamping. + if (((teN ^ teW) | (teN ^ teNW)) > 0) { + return (pred + kPredictionRound) >> kPredExtraBits; + } + + // Otherwise, clamp to min/max of neighbouring pixels (just W, NE, N). + pixel_type_w mx = std::max(W, std::max(NE, N)); + pixel_type_w mn = std::min(W, std::min(NE, N)); + pred = std::max(mn, std::min(mx, pred)); + return (pred + kPredictionRound) >> kPredExtraBits; + } + + JXL_INLINE void UpdateErrors(pixel_type_w val, size_t x, size_t y, + size_t xsize) { + size_t cur_row = y & 1 ? 0 : (xsize + 2); + size_t prev_row = y & 1 ? (xsize + 2) : 0; + val = AddBits(val); + error[cur_row + x] = pred - val; + for (size_t i = 0; i < kNumPredictors; i++) { + pixel_type_w err = + (std::abs(prediction[i] - val) + kPredictionRound) >> kPredExtraBits; + // For predicting in the next row. + pred_errors[i][cur_row + x] = err; + // Add the error on this pixel to the error on the NE pixel. This has the + // effect of adding the error on this pixel to the E and EE pixels. + pred_errors[i][prev_row + x + 1] += err; + } + } +}; + +// Encoder helper function to set the parameters to some presets. +inline void PredictorMode(int i, Header *header) { + switch (i) { + case 0: + // ~ lossless16 predictor + header->w[0] = 0xd; + header->w[1] = 0xc; + header->w[2] = 0xc; + header->w[3] = 0xc; + header->p1C = 16; + header->p2C = 10; + header->p3Ca = 7; + header->p3Cb = 7; + header->p3Cc = 7; + header->p3Cd = 0; + header->p3Ce = 0; + break; + case 1: + // ~ default lossless8 predictor + header->w[0] = 0xd; + header->w[1] = 0xc; + header->w[2] = 0xc; + header->w[3] = 0xb; + header->p1C = 8; + header->p2C = 8; + header->p3Ca = 4; + header->p3Cb = 0; + header->p3Cc = 3; + header->p3Cd = 23; + header->p3Ce = 2; + break; + case 2: + // ~ west lossless8 predictor + header->w[0] = 0xd; + header->w[1] = 0xc; + header->w[2] = 0xd; + header->w[3] = 0xc; + header->p1C = 10; + header->p2C = 9; + header->p3Ca = 7; + header->p3Cb = 0; + header->p3Cc = 0; + header->p3Cd = 16; + header->p3Ce = 9; + break; + case 3: + // ~ north lossless8 predictor + header->w[0] = 0xd; + header->w[1] = 0xd; + header->w[2] = 0xc; + header->w[3] = 0xc; + header->p1C = 16; + header->p2C = 8; + header->p3Ca = 0; + header->p3Cb = 16; + header->p3Cc = 0; + header->p3Cd = 23; + header->p3Ce = 0; + break; + case 4: + default: + // something else, because why not + header->w[0] = 0xd; + header->w[1] = 0xc; + header->w[2] = 0xc; + header->w[3] = 0xc; + header->p1C = 10; + header->p2C = 10; + header->p3Ca = 5; + header->p3Cb = 5; + header->p3Cc = 5; + header->p3Cd = 12; + header->p3Ce = 4; + break; + } +} +} // namespace weighted + +// Stores a node and its two children at the same time. This significantly +// reduces the number of branches needed during decoding. +struct FlatDecisionNode { + // Property + splitval of the top node. + int32_t property0; // -1 if leaf. + union { + PropertyVal splitval0; + Predictor predictor; + }; + uint32_t childID; // childID is ctx id if leaf. + // Property+splitval of the two child nodes. + union { + PropertyVal splitvals[2]; + int32_t multiplier; + }; + union { + int32_t properties[2]; + int64_t predictor_offset; + }; +}; +using FlatTree = std::vector; + +class MATreeLookup { + public: + explicit MATreeLookup(const FlatTree &tree) : nodes_(tree) {} + struct LookupResult { + uint32_t context; + Predictor predictor; + int64_t offset; + int32_t multiplier; + }; + LookupResult Lookup(const Properties &properties) const { + uint32_t pos = 0; + while (true) { + const FlatDecisionNode &node = nodes_[pos]; + if (node.property0 < 0) { + return {node.childID, node.predictor, node.predictor_offset, + node.multiplier}; + } + bool p0 = properties[node.property0] <= node.splitval0; + uint32_t off0 = properties[node.properties[0]] <= node.splitvals[0]; + uint32_t off1 = 2 | (properties[node.properties[1]] <= node.splitvals[1]); + pos = node.childID + (p0 ? off1 : off0); + } + } + + private: + const FlatTree &nodes_; +}; + +static constexpr size_t kExtraPropsPerChannel = 4; +static constexpr size_t kNumNonrefProperties = + kNumStaticProperties + 13 + weighted::kNumProperties; + +constexpr size_t kWPProp = kNumNonrefProperties - weighted::kNumProperties; +constexpr size_t kGradientProp = 9; + +// Clamps gradient to the min/max of n, w (and l, implicitly). +static JXL_INLINE int32_t ClampedGradient(const int32_t n, const int32_t w, + const int32_t l) { + const int32_t m = std::min(n, w); + const int32_t M = std::max(n, w); + // The end result of this operation doesn't overflow or underflow if the + // result is between m and M, but the intermediate value may overflow, so we + // do the intermediate operations in uint32_t and check later if we had an + // overflow or underflow condition comparing m, M and l directly. + // grad = M + m - l = n + w - l + const int32_t grad = + static_cast(static_cast(n) + static_cast(w) - + static_cast(l)); + // We use two sets of ternary operators to force the evaluation of them in + // any case, allowing the compiler to avoid branches and use cmovl/cmovg in + // x86. + const int32_t grad_clamp_M = (l < m) ? M : grad; + return (l > M) ? m : grad_clamp_M; +} + +inline pixel_type_w Select(pixel_type_w a, pixel_type_w b, pixel_type_w c) { + pixel_type_w p = a + b - c; + pixel_type_w pa = std::abs(p - a); + pixel_type_w pb = std::abs(p - b); + return pa < pb ? a : b; +} + +inline void PrecomputeReferences(const Channel &ch, size_t y, + const Image &image, uint32_t i, + Channel *references) { + ZeroFillImage(&references->plane); + uint32_t offset = 0; + size_t num_extra_props = references->w; + intptr_t onerow = references->plane.PixelsPerRow(); + for (int32_t j = static_cast(i) - 1; + j >= 0 && offset < num_extra_props; j--) { + if (image.channel[j].w != image.channel[i].w || + image.channel[j].h != image.channel[i].h) { + continue; + } + if (image.channel[j].hshift != image.channel[i].hshift) continue; + if (image.channel[j].vshift != image.channel[i].vshift) continue; + pixel_type *JXL_RESTRICT rp = references->Row(0) + offset; + const pixel_type *JXL_RESTRICT rpp = image.channel[j].Row(y); + const pixel_type *JXL_RESTRICT rpprev = image.channel[j].Row(y ? y - 1 : 0); + for (size_t x = 0; x < ch.w; x++, rp += onerow) { + pixel_type_w v = rpp[x]; + rp[0] = std::abs(v); + rp[1] = v; + pixel_type_w vleft = (x ? rpp[x - 1] : 0); + pixel_type_w vtop = (y ? rpprev[x] : vleft); + pixel_type_w vtopleft = (x && y ? rpprev[x - 1] : vleft); + pixel_type_w vpredicted = ClampedGradient(vleft, vtop, vtopleft); + rp[2] = std::abs(v - vpredicted); + rp[3] = v - vpredicted; + } + + offset += kExtraPropsPerChannel; + } +} + +struct PredictionResult { + int context = 0; + pixel_type_w guess = 0; + Predictor predictor; + int32_t multiplier; +}; + +inline std::string PropertyName(size_t i) { + static_assert(kNumNonrefProperties == 16, "Update this function"); + switch (i) { + case 0: + return "c"; + case 1: + return "g"; + case 2: + return "y"; + case 3: + return "x"; + case 4: + return "|N|"; + case 5: + return "|W|"; + case 6: + return "N"; + case 7: + return "W"; + case 8: + return "W-WW-NW+NWW"; + case 9: + return "W+N-NW"; + case 10: + return "W-NW"; + case 11: + return "NW-N"; + case 12: + return "N-NE"; + case 13: + return "N-NN"; + case 14: + return "W-WW"; + case 15: + return "WGH"; + default: + return "ch[" + ToString(15 - (int)i) + "]"; + } +} + +inline void InitPropsRow( + Properties *p, + const std::array &static_props, + const int y) { + for (size_t i = 0; i < kNumStaticProperties; i++) { + (*p)[i] = static_props[i]; + } + (*p)[2] = y; + (*p)[9] = 0; // local gradient. +} + +namespace detail { +enum PredictorMode { + kUseTree = 1, + kUseWP = 2, + kForceComputeProperties = 4, + kAllPredictions = 8, +}; + +JXL_INLINE pixel_type_w PredictOne(Predictor p, pixel_type_w left, + pixel_type_w top, pixel_type_w toptop, + pixel_type_w topleft, pixel_type_w topright, + pixel_type_w leftleft, + pixel_type_w toprightright, + pixel_type_w wp_pred) { + switch (p) { + case Predictor::Zero: + return pixel_type_w{0}; + case Predictor::Left: + return left; + case Predictor::Top: + return top; + case Predictor::Select: + return Select(left, top, topleft); + case Predictor::Weighted: + return wp_pred; + case Predictor::Gradient: + return pixel_type_w{ClampedGradient(left, top, topleft)}; + case Predictor::TopLeft: + return topleft; + case Predictor::TopRight: + return topright; + case Predictor::LeftLeft: + return leftleft; + case Predictor::Average0: + return (left + top) / 2; + case Predictor::Average1: + return (left + topleft) / 2; + case Predictor::Average2: + return (topleft + top) / 2; + case Predictor::Average3: + return (top + topright) / 2; + case Predictor::Average4: + return (6 * top - 2 * toptop + 7 * left + 1 * leftleft + + 1 * toprightright + 3 * topright + 8) / + 16; + default: + return pixel_type_w{0}; + } +} + +template +inline PredictionResult Predict( + Properties *p, size_t w, const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const size_t x, const size_t y, Predictor predictor, + const MATreeLookup *lookup, const Channel *references, + weighted::State *wp_state, pixel_type_w *predictions) { + // We start in position 3 because of 2 static properties + y. + 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); + + if (compute_properties) { + // location + (*p)[offset++] = x; + // neighbors + (*p)[offset++] = std::abs(top); + (*p)[offset++] = std::abs(left); + (*p)[offset++] = top; + (*p)[offset++] = left; + + // local gradient + (*p)[offset] = left - (*p)[offset + 1]; + offset++; + // local gradient + (*p)[offset++] = left + top - topleft; + + // FFV1 context properties + (*p)[offset++] = left - topleft; + (*p)[offset++] = topleft - top; + (*p)[offset++] = top - topright; + (*p)[offset++] = top - toptop; + (*p)[offset++] = left - leftleft; + } + + pixel_type_w wp_pred = 0; + if (mode & kUseWP) { + wp_pred = wp_state->Predict( + x, y, w, top, left, topright, topleft, toptop, p, offset); + } + if (compute_properties) { + offset += weighted::kNumProperties; + // Extra properties. + const pixel_type *JXL_RESTRICT rp = references->Row(x); + for (size_t i = 0; i < references->w; i++) { + (*p)[offset++] = rp[i]; + } + } + PredictionResult result; + if (mode & kUseTree) { + MATreeLookup::LookupResult lr = lookup->Lookup(*p); + result.context = lr.context; + result.guess = lr.offset; + result.multiplier = lr.multiplier; + predictor = lr.predictor; + } + if (mode & kAllPredictions) { + for (size_t i = 0; i < kNumModularPredictors; i++) { + predictions[i] = PredictOne((Predictor)i, left, top, toptop, topleft, + topright, leftleft, toprightright, wp_pred); + } + } + result.guess += PredictOne(predictor, left, top, toptop, topleft, topright, + leftleft, toprightright, wp_pred); + result.predictor = predictor; + + return result; +} +} // namespace detail + +inline PredictionResult PredictNoTreeNoWP(size_t w, + const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, + const int y, Predictor predictor) { + return detail::Predict( + /*p=*/nullptr, w, pp, onerow, x, y, predictor, /*lookup=*/nullptr, + /*references=*/nullptr, /*wp_state=*/nullptr, /*predictions=*/nullptr); +} + +inline PredictionResult PredictNoTreeWP(size_t w, + const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, + const int y, Predictor predictor, + weighted::State *wp_state) { + return detail::Predict( + /*p=*/nullptr, w, pp, onerow, x, y, predictor, /*lookup=*/nullptr, + /*references=*/nullptr, wp_state, /*predictions=*/nullptr); +} + +inline PredictionResult PredictTreeNoWP(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( + 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, + const intptr_t onerow, const int x, + const int y, + const MATreeLookup &tree_lookup, + const Channel &references, + weighted::State *wp_state) { + return detail::Predict( + p, w, pp, onerow, x, y, Predictor::Zero, &tree_lookup, &references, + wp_state, /*predictions=*/nullptr); +} + +inline PredictionResult PredictLearn(Properties *p, size_t w, + const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, + const int y, Predictor predictor, + const Channel &references, + weighted::State *wp_state) { + return detail::Predict( + p, w, pp, onerow, x, y, predictor, /*lookup=*/nullptr, &references, + wp_state, /*predictions=*/nullptr); +} + +inline void PredictLearnAll(Properties *p, size_t w, + const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, const int y, + const Channel &references, + weighted::State *wp_state, + pixel_type_w *predictions) { + detail::Predict( + p, w, pp, onerow, x, y, Predictor::Zero, + /*lookup=*/nullptr, &references, wp_state, predictions); +} + +inline void PredictAllNoWP(size_t w, const pixel_type *JXL_RESTRICT pp, + const intptr_t onerow, const int x, const int y, + pixel_type_w *predictions) { + detail::Predict( + /*p=*/nullptr, w, pp, onerow, x, y, Predictor::Zero, + /*lookup=*/nullptr, + /*references=*/nullptr, /*wp_state=*/nullptr, predictions); +} +} // namespace jxl + +#endif // LIB_JXL_MODULAR_ENCODING_CONTEXT_PREDICT_H_ diff --git a/lib/jxl/modular/encoding/dec_ma.cc b/lib/jxl/modular/encoding/dec_ma.cc new file mode 100644 index 0000000..e12eb51 --- /dev/null +++ b/lib/jxl/modular/encoding/dec_ma.cc @@ -0,0 +1,107 @@ +// 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/modular/encoding/dec_ma.h" + +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/modular/encoding/ma_common.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +namespace { + +Status ValidateTree( + const Tree &tree, + const std::vector> &prop_bounds, + size_t root) { + if (tree[root].property == -1) return true; + size_t p = tree[root].property; + int val = tree[root].splitval; + if (prop_bounds[p].first > val) return JXL_FAILURE("Invalid tree"); + // Splitting at max value makes no sense: left range will be exactly same + // as parent, right range will be invalid (min > max). + if (prop_bounds[p].second <= val) return JXL_FAILURE("Invalid tree"); + auto new_bounds = prop_bounds; + new_bounds[p].first = val + 1; + JXL_RETURN_IF_ERROR(ValidateTree(tree, new_bounds, tree[root].lchild)); + new_bounds[p] = prop_bounds[p]; + new_bounds[p].second = val; + return ValidateTree(tree, new_bounds, tree[root].rchild); +} + +Status DecodeTree(BitReader *br, ANSSymbolReader *reader, + const std::vector &context_map, Tree *tree, + size_t tree_size_limit) { + size_t leaf_id = 0; + size_t to_decode = 1; + tree->clear(); + while (to_decode > 0) { + JXL_RETURN_IF_ERROR(br->AllReadsWithinBounds()); + if (tree->size() > tree_size_limit) { + return JXL_FAILURE("Tree is too large"); + } + to_decode--; + int property = static_cast(reader->ReadHybridUint(kPropertyContext, br, + context_map)) - + 1; + if (property < -1 || property >= 256) { + return JXL_FAILURE("Invalid tree property value"); + } + if (property == -1) { + size_t predictor = + reader->ReadHybridUint(kPredictorContext, br, context_map); + if (predictor >= kNumModularPredictors) { + return JXL_FAILURE("Invalid predictor"); + } + int64_t predictor_offset = + UnpackSigned(reader->ReadHybridUint(kOffsetContext, br, context_map)); + uint32_t mul_log = + reader->ReadHybridUint(kMultiplierLogContext, br, context_map); + if (mul_log >= 31) { + return JXL_FAILURE("Invalid multiplier logarithm"); + } + uint32_t mul_bits = + reader->ReadHybridUint(kMultiplierBitsContext, br, context_map); + if (mul_bits + 1 >= 1u << (31u - mul_log)) { + return JXL_FAILURE("Invalid multiplier"); + } + uint32_t multiplier = (mul_bits + 1U) << mul_log; + tree->emplace_back(-1, 0, leaf_id++, 0, static_cast(predictor), + predictor_offset, multiplier); + continue; + } + int splitval = + UnpackSigned(reader->ReadHybridUint(kSplitValContext, br, context_map)); + tree->emplace_back(property, splitval, tree->size() + to_decode + 1, + tree->size() + to_decode + 2, Predictor::Zero, 0, 1); + to_decode += 2; + } + std::vector> prop_bounds; + prop_bounds.resize(256, {std::numeric_limits::min(), + std::numeric_limits::max()}); + return ValidateTree(*tree, prop_bounds, 0); +} +} // namespace + +Status DecodeTree(BitReader *br, Tree *tree, size_t tree_size_limit) { + std::vector tree_context_map; + ANSCode tree_code; + JXL_RETURN_IF_ERROR( + DecodeHistograms(br, kNumTreeContexts, &tree_code, &tree_context_map)); + // TODO(eustas): investigate more infinite tree cases. + if (tree_code.degenerate_symbols[tree_context_map[kPropertyContext]] > 0) { + return JXL_FAILURE("Infinite tree"); + } + ANSSymbolReader reader(&tree_code, br); + JXL_RETURN_IF_ERROR(DecodeTree(br, &reader, tree_context_map, tree, + std::min(tree_size_limit, kMaxTreeSize))); + if (!reader.CheckANSFinalState()) { + return JXL_FAILURE("ANS decode final state failed"); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/encoding/dec_ma.h b/lib/jxl/modular/encoding/dec_ma.h new file mode 100644 index 0000000..a910c4d --- /dev/null +++ b/lib/jxl/modular/encoding/dec_ma.h @@ -0,0 +1,66 @@ +// 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_MODULAR_ENCODING_DEC_MA_H_ +#define LIB_JXL_MODULAR_ENCODING_DEC_MA_H_ + +#include +#include + +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +// inner nodes +struct PropertyDecisionNode { + PropertyVal splitval; + int16_t property; // -1: leaf node, lchild points to leaf node + uint32_t lchild; + uint32_t rchild; + Predictor predictor; + int64_t predictor_offset; + uint32_t multiplier; + + PropertyDecisionNode(int p, int split_val, int lchild, int rchild, + Predictor predictor, int64_t predictor_offset, + uint32_t multiplier) + : splitval(split_val), + property(p), + lchild(lchild), + rchild(rchild), + predictor(predictor), + predictor_offset(predictor_offset), + multiplier(multiplier) {} + PropertyDecisionNode() + : splitval(0), + property(-1), + lchild(0), + rchild(0), + predictor(Predictor::Zero), + predictor_offset(0), + multiplier(1) {} + static PropertyDecisionNode Leaf(Predictor predictor, int64_t offset = 0, + uint32_t multiplier = 1) { + return PropertyDecisionNode(-1, 0, 0, 0, predictor, offset, multiplier); + } + static PropertyDecisionNode Split(int p, int split_val, int lchild, + int rchild = -1) { + if (rchild == -1) rchild = lchild + 1; + return PropertyDecisionNode(p, split_val, lchild, rchild, Predictor::Zero, + 0, 1); + } +}; + +using Tree = std::vector; + +Status DecodeTree(BitReader *br, Tree *tree, size_t tree_size_limit); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_ENCODING_DEC_MA_H_ diff --git a/lib/jxl/modular/encoding/enc_encoding.cc b/lib/jxl/modular/encoding/enc_encoding.cc new file mode 100644 index 0000000..bf3dd4b --- /dev/null +++ b/lib/jxl/modular/encoding/enc_encoding.cc @@ -0,0 +1,549 @@ +// 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/base/os_macros.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/enc_ma.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/encoding/ma_common.h" +#include "lib/jxl/modular/options.h" +#include "lib/jxl/modular/transform/transform.h" +#include "lib/jxl/toc.h" + +#if JXL_OS_IOS +#define JXL_ENABLE_DOT 0 +#else +#define JXL_ENABLE_DOT 1 // iOS lacks C89 system() +#endif + +namespace jxl { + +namespace { +// Plot tree (if enabled) and predictor usage map. +constexpr bool kWantDebug = false; +} // namespace + +void GatherTreeData(const Image &image, pixel_type chan, size_t group_id, + const weighted::Header &wp_header, + const ModularOptions &options, TreeSamples &tree_samples, + size_t *total_pixels) { + const Channel &channel = image.channel[chan]; + + JXL_DEBUG_V(7, "Learning %zux%zu channel %d", channel.w, channel.h, chan); + + std::array static_props = { + {chan, (int)group_id}}; + Properties properties(kNumNonrefProperties + + kExtraPropsPerChannel * options.max_properties); + double pixel_fraction = std::min(1.0f, options.nb_repeats); + // a fraction of 0 is used to disable learning entirely. + if (pixel_fraction > 0) { + pixel_fraction = std::max(pixel_fraction, + std::min(1.0, 1024.0 / (channel.w * channel.h))); + } + uint64_t threshold = + (std::numeric_limits::max() >> 32) * pixel_fraction; + uint64_t s[2] = {0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull}; + // Xorshift128+ adapted from xorshift128+-inl.h + auto use_sample = [&]() { + auto s1 = s[0]; + const auto s0 = s[1]; + const auto bits = s1 + s0; // b, c + s[0] = s0; + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + s[1] = s1; + return (bits >> 32) <= threshold; + }; + + const intptr_t onerow = channel.plane.PixelsPerRow(); + Channel references(properties.size() - kNumNonrefProperties, channel.w); + weighted::State wp_state(wp_header, channel.w, channel.h); + tree_samples.PrepareForSamples(pixel_fraction * channel.h * channel.w + 64); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT p = channel.Row(y); + PrecomputeReferences(channel, y, image, chan, &references); + InitPropsRow(&properties, static_props, y); + // TODO(veluca): avoid computing WP if we don't use its property or + // predictions. + for (size_t x = 0; x < channel.w; x++) { + pixel_type_w pred[kNumModularPredictors]; + if (tree_samples.NumPredictors() != 1) { + PredictLearnAll(&properties, channel.w, p + x, onerow, x, y, references, + &wp_state, pred); + } else { + pred[static_cast(tree_samples.PredictorFromIndex(0))] = + PredictLearn(&properties, channel.w, p + x, onerow, x, y, + tree_samples.PredictorFromIndex(0), references, + &wp_state) + .guess; + } + (*total_pixels)++; + if (use_sample()) { + tree_samples.AddSample(p[x], properties, pred); + } + wp_state.UpdateErrors(p[x], x, y, channel.w); + } + } +} + +Tree LearnTree(TreeSamples &&tree_samples, size_t total_pixels, + const ModularOptions &options, + const std::vector &multiplier_info = {}, + StaticPropRange static_prop_range = {}) { + for (size_t i = 0; i < kNumStaticProperties; i++) { + if (static_prop_range[i][1] == 0) { + static_prop_range[i][1] = std::numeric_limits::max(); + } + } + if (!tree_samples.HasSamples()) { + Tree tree; + tree.emplace_back(); + tree.back().predictor = tree_samples.PredictorFromIndex(0); + tree.back().property = -1; + tree.back().predictor_offset = 0; + tree.back().multiplier = 1; + return tree; + } + float pixel_fraction = tree_samples.NumSamples() * 1.0f / total_pixels; + float required_cost = pixel_fraction * 0.9 + 0.1; + tree_samples.AllSamplesDone(); + Tree tree; + ComputeBestTree(tree_samples, + options.splitting_heuristics_node_threshold * required_cost, + multiplier_info, static_prop_range, + options.fast_decode_multiplier, &tree); + return tree; +} + +constexpr bool kPrintTree = false; + +void PrintTree(const Tree &tree, const std::string &path) { + if (!kPrintTree) return; + FILE *f = fopen((path + ".dot").c_str(), "w"); + fprintf(f, "graph{\n"); + for (size_t cur = 0; cur < tree.size(); cur++) { + if (tree[cur].property < 0) { + fprintf(f, "n%05zu [label=\"%s%+" PRId64 " (x%u)\"];\n", cur, + PredictorName(tree[cur].predictor), tree[cur].predictor_offset, + tree[cur].multiplier); + } else { + fprintf(f, "n%05zu [label=\"%s>%d\"];\n", cur, + PropertyName(tree[cur].property).c_str(), tree[cur].splitval); + fprintf(f, "n%05zu -- n%05d;\n", cur, tree[cur].lchild); + fprintf(f, "n%05zu -- n%05d;\n", cur, tree[cur].rchild); + } + } + fprintf(f, "}\n"); + fclose(f); +#if JXL_ENABLE_DOT + JXL_ASSERT( + system(("dot " + path + ".dot -T svg -o " + path + ".svg").c_str()) == 0); +#endif +} + +Status EncodeModularChannelMAANS(const Image &image, pixel_type chan, + const weighted::Header &wp_header, + const Tree &global_tree, + std::vector *tokens, AuxOut *aux_out, + size_t group_id, bool skip_encoder_fast_path) { + const Channel &channel = image.channel[chan]; + + JXL_ASSERT(channel.w != 0 && channel.h != 0); + + Image3F predictor_img; + if (kWantDebug) predictor_img = Image3F(channel.w, channel.h); + + JXL_DEBUG_V(6, + "Encoding %zux%zu channel %d, " + "(shift=%i,%i)", + channel.w, channel.h, chan, channel.hshift, channel.vshift); + + std::array static_props = { + {chan, (int)group_id}}; + bool use_wp, is_wp_only; + bool is_gradient_only; + size_t num_props; + FlatTree tree = FilterTree(global_tree, static_props, &num_props, &use_wp, + &is_wp_only, &is_gradient_only); + Properties properties(num_props); + MATreeLookup tree_lookup(tree); + JXL_DEBUG_V(3, "Encoding using a MA tree with %zu nodes", tree.size()); + + // Check if this tree is a WP-only tree with a small enough property value + // range. + // Initialized to avoid clang-tidy complaining. + uint16_t context_lookup[2 * kPropRangeFast] = {}; + int8_t offsets[2 * kPropRangeFast] = {}; + if (is_wp_only) { + is_wp_only = TreeToLookupTable(tree, context_lookup, offsets); + } + if (is_gradient_only) { + is_gradient_only = TreeToLookupTable(tree, context_lookup, offsets); + } + + tokens->reserve(tokens->size() + channel.w * channel.h); + if (is_wp_only && !skip_encoder_fast_path) { + for (size_t c = 0; c < 3; c++) { + FillImage(static_cast(PredictorColor(Predictor::Weighted)[c]), + &predictor_img.Plane(c)); + } + const intptr_t onerow = channel.plane.PixelsPerRow(); + weighted::State wp_state(wp_header, channel.w, channel.h); + Properties properties(1); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + size_t offset = 0; + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + pixel_type_w topright = + (x + 1 < channel.w && y ? *(r + x + 1 - onerow) : top); + pixel_type_w toptop = (y > 1 ? *(r + x - onerow - onerow) : top); + int32_t guess = wp_state.Predict( + x, y, channel.w, top, left, topright, topleft, toptop, &properties, + offset); + uint32_t pos = + kPropRangeFast + std::min(std::max(-kPropRangeFast, properties[0]), + kPropRangeFast - 1); + uint32_t ctx_id = context_lookup[pos]; + int32_t residual = r[x] - guess - offsets[pos]; + tokens->emplace_back(ctx_id, PackSigned(residual)); + wp_state.UpdateErrors(r[x], x, y, channel.w); + } + } + } else if (tree.size() == 1 && tree[0].predictor == Predictor::Gradient && + tree[0].multiplier == 1 && tree[0].predictor_offset == 0 && + !skip_encoder_fast_path) { + for (size_t c = 0; c < 3; c++) { + FillImage(static_cast(PredictorColor(Predictor::Gradient)[c]), + &predictor_img.Plane(c)); + } + const intptr_t onerow = channel.plane.PixelsPerRow(); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + int32_t guess = ClampedGradient(top, left, topleft); + int32_t residual = r[x] - guess; + tokens->emplace_back(tree[0].childID, PackSigned(residual)); + } + } + } else if (is_gradient_only && !skip_encoder_fast_path) { + for (size_t c = 0; c < 3; c++) { + FillImage(static_cast(PredictorColor(Predictor::Gradient)[c]), + &predictor_img.Plane(c)); + } + const intptr_t onerow = channel.plane.PixelsPerRow(); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + int32_t guess = ClampedGradient(top, left, topleft); + uint32_t pos = + kPropRangeFast + + std::min( + std::max(-kPropRangeFast, top + left - topleft), + kPropRangeFast - 1); + uint32_t ctx_id = context_lookup[pos]; + int32_t residual = r[x] - guess - offsets[pos]; + tokens->emplace_back(ctx_id, PackSigned(residual)); + } + } + } else if (tree.size() == 1 && tree[0].predictor == Predictor::Zero && + tree[0].multiplier == 1 && tree[0].predictor_offset == 0 && + !skip_encoder_fast_path) { + for (size_t c = 0; c < 3; c++) { + FillImage(static_cast(PredictorColor(Predictor::Zero)[c]), + &predictor_img.Plane(c)); + } + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT p = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + tokens->emplace_back(tree[0].childID, PackSigned(p[x])); + } + } + } else if (tree.size() == 1 && tree[0].predictor != Predictor::Weighted && + (tree[0].multiplier & (tree[0].multiplier - 1)) == 0 && + tree[0].predictor_offset == 0 && !skip_encoder_fast_path) { + // multiplier is a power of 2. + for (size_t c = 0; c < 3; c++) { + FillImage(static_cast(PredictorColor(tree[0].predictor)[c]), + &predictor_img.Plane(c)); + } + uint32_t mul_shift = FloorLog2Nonzero((uint32_t)tree[0].multiplier); + const intptr_t onerow = channel.plane.PixelsPerRow(); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT r = channel.Row(y); + for (size_t x = 0; x < channel.w; x++) { + PredictionResult pred = PredictNoTreeNoWP(channel.w, r + x, onerow, x, + y, tree[0].predictor); + pixel_type_w residual = r[x] - pred.guess; + JXL_DASSERT((residual >> mul_shift) * tree[0].multiplier == residual); + tokens->emplace_back(tree[0].childID, + PackSigned(residual >> mul_shift)); + } + } + + } else if (!use_wp && !skip_encoder_fast_path) { + const intptr_t onerow = channel.plane.PixelsPerRow(); + Channel references(properties.size() - kNumNonrefProperties, channel.w); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT p = channel.Row(y); + PrecomputeReferences(channel, y, image, chan, &references); + float *pred_img_row[3]; + if (kWantDebug) { + for (size_t c = 0; c < 3; c++) { + pred_img_row[c] = predictor_img.PlaneRow(c, y); + } + } + 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); + if (kWantDebug) { + for (size_t i = 0; i < 3; i++) { + pred_img_row[i][x] = PredictorColor(res.predictor)[i]; + } + } + pixel_type_w residual = p[x] - res.guess; + JXL_ASSERT(residual % res.multiplier == 0); + tokens->emplace_back(res.context, + PackSigned(residual / res.multiplier)); + } + } + } else { + const intptr_t onerow = channel.plane.PixelsPerRow(); + Channel references(properties.size() - kNumNonrefProperties, channel.w); + weighted::State wp_state(wp_header, channel.w, channel.h); + for (size_t y = 0; y < channel.h; y++) { + const pixel_type *JXL_RESTRICT p = channel.Row(y); + PrecomputeReferences(channel, y, image, chan, &references); + float *pred_img_row[3]; + if (kWantDebug) { + for (size_t c = 0; c < 3; c++) { + pred_img_row[c] = predictor_img.PlaneRow(c, y); + } + } + InitPropsRow(&properties, static_props, y); + for (size_t x = 0; x < channel.w; x++) { + PredictionResult res = + PredictTreeWP(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references, &wp_state); + if (kWantDebug) { + for (size_t i = 0; i < 3; i++) { + pred_img_row[i][x] = PredictorColor(res.predictor)[i]; + } + } + pixel_type_w residual = p[x] - res.guess; + JXL_ASSERT(residual % res.multiplier == 0); + tokens->emplace_back(res.context, + PackSigned(residual / res.multiplier)); + wp_state.UpdateErrors(p[x], x, y, channel.w); + } + } + } + if (kWantDebug && WantDebugOutput(aux_out)) { + aux_out->DumpImage( + ("pred_" + ToString(group_id) + "_" + ToString(chan)).c_str(), + predictor_img); + } + return true; +} + +Status ModularEncode(const Image &image, const ModularOptions &options, + BitWriter *writer, AuxOut *aux_out, size_t layer, + size_t group_id, TreeSamples *tree_samples, + size_t *total_pixels, const Tree *tree, + GroupHeader *header, std::vector *tokens, + size_t *width) { + if (image.error) return JXL_FAILURE("Invalid image"); + size_t nb_channels = image.channel.size(); + JXL_DEBUG_V(2, "Encoding %zu-channel, %i-bit, %zux%zu image.", nb_channels, + image.bitdepth, image.w, image.h); + + if (nb_channels < 1) { + return true; // is there any use for a zero-channel image? + } + + // encode transforms + GroupHeader header_storage; + if (header == nullptr) header = &header_storage; + Bundle::Init(header); + if (options.predictor == Predictor::Weighted) { + weighted::PredictorMode(options.wp_mode, &header->wp_header); + } + header->transforms = image.transform; + // This doesn't actually work + if (tree != nullptr) { + header->use_global_tree = true; + } + if (tree_samples == nullptr && tree == nullptr) { + JXL_RETURN_IF_ERROR(Bundle::Write(*header, writer, layer, aux_out)); + } + + TreeSamples tree_samples_storage; + size_t total_pixels_storage = 0; + if (!total_pixels) total_pixels = &total_pixels_storage; + // If there's no tree, compute one (or gather data to). + if (tree == nullptr) { + bool gather_data = tree_samples != nullptr; + if (tree_samples == nullptr) { + JXL_RETURN_IF_ERROR(tree_samples_storage.SetPredictor( + options.predictor, options.wp_tree_mode)); + JXL_RETURN_IF_ERROR(tree_samples_storage.SetProperties( + options.splitting_heuristics_properties, options.wp_tree_mode)); + std::vector pixel_samples; + std::vector diff_samples; + std::vector group_pixel_count; + std::vector channel_pixel_count; + CollectPixelSamples(image, options, 0, group_pixel_count, + channel_pixel_count, pixel_samples, diff_samples); + std::vector dummy_multiplier_info; + StaticPropRange range; + tree_samples_storage.PreQuantizeProperties( + range, dummy_multiplier_info, group_pixel_count, channel_pixel_count, + pixel_samples, diff_samples, options.max_property_values); + } + for (size_t i = 0; i < nb_channels; i++) { + if (!image.channel[i].w || !image.channel[i].h) { + continue; // skip empty channels + } + if (i >= image.nb_meta_channels && + (image.channel[i].w > options.max_chan_size || + image.channel[i].h > options.max_chan_size)) { + break; + } + GatherTreeData(image, i, group_id, header->wp_header, options, + gather_data ? *tree_samples : tree_samples_storage, + total_pixels); + } + if (gather_data) return true; + } + + JXL_ASSERT((tree == nullptr) == (tokens == nullptr)); + + Tree tree_storage; + std::vector> tokens_storage(1); + // Compute tree. + if (tree == nullptr) { + EntropyEncodingData code; + std::vector context_map; + + std::vector> tree_tokens(1); + tree_storage = + LearnTree(std::move(tree_samples_storage), *total_pixels, options); + tree = &tree_storage; + tokens = &tokens_storage[0]; + + Tree decoded_tree; + TokenizeTree(*tree, &tree_tokens[0], &decoded_tree); + JXL_ASSERT(tree->size() == decoded_tree.size()); + tree_storage = std::move(decoded_tree); + + if (kWantDebug && WantDebugOutput(aux_out)) { + PrintTree(*tree, aux_out->debug_prefix + "/tree_" + ToString(group_id)); + } + // Write tree + BuildAndEncodeHistograms(HistogramParams(), kNumTreeContexts, tree_tokens, + &code, &context_map, writer, kLayerModularTree, + aux_out); + WriteTokens(tree_tokens[0], code, context_map, writer, kLayerModularTree, + aux_out); + } + + size_t image_width = 0; + for (size_t i = 0; i < nb_channels; i++) { + if (!image.channel[i].w || !image.channel[i].h) { + continue; // skip empty channels + } + if (i >= image.nb_meta_channels && + (image.channel[i].w > options.max_chan_size || + image.channel[i].h > options.max_chan_size)) { + break; + } + if (image.channel[i].w > image_width) image_width = image.channel[i].w; + if (options.zero_tokens) { + tokens->resize(tokens->size() + image.channel[i].w * image.channel[i].h, + {0, 0}); + } else { + JXL_RETURN_IF_ERROR(EncodeModularChannelMAANS( + image, i, header->wp_header, *tree, tokens, aux_out, group_id, + options.skip_encoder_fast_path)); + } + } + + // Write data if not using a global tree/ANS stream. + if (!header->use_global_tree) { + EntropyEncodingData code; + std::vector context_map; + HistogramParams histo_params; + histo_params.image_widths.push_back(image_width); + BuildAndEncodeHistograms(histo_params, (tree->size() + 1) / 2, + tokens_storage, &code, &context_map, writer, layer, + aux_out); + WriteTokens(tokens_storage[0], code, context_map, writer, layer, aux_out); + } else { + *width = image_width; + } + return true; +} + +Status ModularGenericCompress(Image &image, const ModularOptions &opts, + BitWriter *writer, AuxOut *aux_out, size_t layer, + size_t group_id, TreeSamples *tree_samples, + size_t *total_pixels, const Tree *tree, + GroupHeader *header, std::vector *tokens, + size_t *width) { + if (image.w == 0 || image.h == 0) return true; + ModularOptions options = opts; // Make a copy to modify it. + + if (options.predictor == static_cast(-1)) { + options.predictor = Predictor::Gradient; + } + + size_t bits = writer ? writer->BitsWritten() : 0; + JXL_RETURN_IF_ERROR(ModularEncode(image, options, writer, aux_out, layer, + group_id, tree_samples, total_pixels, tree, + header, tokens, width)); + bits = writer ? writer->BitsWritten() - bits : 0; + if (writer) { + JXL_DEBUG_V( + 4, + "Modular-encoded a %zux%zu bitdepth=%i nbchans=%zu image in %zu bytes", + image.w, image.h, image.bitdepth, image.channel.size(), bits / 8); + } + (void)bits; + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/encoding/enc_encoding.h b/lib/jxl/modular/encoding/enc_encoding.h new file mode 100644 index 0000000..9c083e9 --- /dev/null +++ b/lib/jxl/modular/encoding/enc_encoding.h @@ -0,0 +1,49 @@ +// 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_MODULAR_ENCODING_ENC_ENCODING_H_ +#define LIB_JXL_MODULAR_ENCODING_ENC_ENCODING_H_ + +#include +#include + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/image.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/enc_ma.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/options.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +void PrintTree(const Tree &tree, const std::string &path); +Tree LearnTree(TreeSamples &&tree_samples, size_t total_pixels, + const ModularOptions &options, + const std::vector &multiplier_info = {}, + StaticPropRange static_prop_range = {}); + +// TODO(veluca): make cleaner interfaces. + +Status ModularGenericCompress( + Image &image, const ModularOptions &opts, BitWriter *writer, + AuxOut *aux_out = nullptr, size_t layer = 0, size_t group_id = 0, + // For gathering data for producing a global tree. + TreeSamples *tree_samples = nullptr, size_t *total_pixels = nullptr, + // For encoding with global tree. + const Tree *tree = nullptr, GroupHeader *header = nullptr, + std::vector *tokens = nullptr, size_t *widths = nullptr); +} // namespace jxl + +#endif // LIB_JXL_MODULAR_ENCODING_ENC_ENCODING_H_ diff --git a/lib/jxl/modular/encoding/enc_ma.cc b/lib/jxl/modular/encoding/enc_ma.cc new file mode 100644 index 0000000..0e2eaac --- /dev/null +++ b/lib/jxl/modular/encoding/enc_ma.cc @@ -0,0 +1,1043 @@ +// 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/modular/encoding/enc_ma.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "lib/jxl/modular/encoding/ma_common.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/modular/encoding/enc_ma.cc" +#include +#include + +#ifndef LIB_JXL_ENC_MODULAR_ENCODING_MA_ +#define LIB_JXL_ENC_MODULAR_ENCODING_MA_ +namespace { +struct Rng { + uint64_t s[2]; + explicit Rng(size_t seed) + : s{0x94D049BB133111EBull, 0xBF58476D1CE4E5B9ull + seed} {} + // Xorshift128+ adapted from xorshift128+-inl.h + uint64_t operator()() { + uint64_t s1 = s[0]; + const uint64_t s0 = s[1]; + const uint64_t bits = s1 + s0; // b, c + s[0] = s0; + s1 ^= s1 << 23; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + s[1] = s1; + return bits; + } + static constexpr uint64_t max() { return ~0ULL; } + static constexpr uint64_t min() { return 0; } +}; +} // namespace +#endif + +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/fast_math-inl.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/options.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +const HWY_FULL(float) df; +const HWY_FULL(int32_t) di; +size_t Padded(size_t x) { return RoundUpTo(x, Lanes(df)); } + +float EstimateBits(const int32_t *counts, int32_t *rounded_counts, + size_t num_symbols) { + // Try to approximate the effect of rounding up nonzero probabilities. + int32_t total = std::accumulate(counts, counts + num_symbols, 0); + const auto min = Set(di, (total + ANS_TAB_SIZE - 1) >> ANS_LOG_TAB_SIZE); + const auto zero_i = Zero(di); + for (size_t i = 0; i < num_symbols; i += Lanes(df)) { + auto counts_v = LoadU(di, &counts[i]); + counts_v = IfThenElse(counts_v == zero_i, zero_i, + IfThenElse(counts_v < min, min, counts_v)); + StoreU(counts_v, di, &rounded_counts[i]); + } + // Compute entropy of the "rounded" probabilities. + const auto zero = Zero(df); + const size_t total_scalar = + std::accumulate(rounded_counts, rounded_counts + num_symbols, 0); + const auto inv_total = Set(df, 1.0f / total_scalar); + auto bits_lanes = Zero(df); + auto total_v = Set(di, total_scalar); + for (size_t i = 0; i < num_symbols; i += Lanes(df)) { + const auto counts_v = ConvertTo(df, LoadU(di, &counts[i])); + const auto round_counts_v = LoadU(di, &rounded_counts[i]); + const auto probs = ConvertTo(df, round_counts_v) * inv_total; + const auto nbps = IfThenElse(round_counts_v == total_v, BitCast(di, zero), + BitCast(di, FastLog2f(df, probs))); + bits_lanes -= + IfThenElse(counts_v == zero, zero, counts_v * BitCast(df, nbps)); + } + return GetLane(SumOfLanes(bits_lanes)); +} + +void MakeSplitNode(size_t pos, int property, int splitval, Predictor lpred, + int64_t loff, Predictor rpred, int64_t roff, Tree *tree) { + // Note that the tree splits on *strictly greater*. + (*tree)[pos].lchild = tree->size(); + (*tree)[pos].rchild = tree->size() + 1; + (*tree)[pos].splitval = splitval; + (*tree)[pos].property = property; + tree->emplace_back(); + tree->back().property = -1; + tree->back().predictor = rpred; + tree->back().predictor_offset = roff; + tree->back().multiplier = 1; + tree->emplace_back(); + tree->back().property = -1; + tree->back().predictor = lpred; + tree->back().predictor_offset = loff; + tree->back().multiplier = 1; +} + +enum class IntersectionType { kNone, kPartial, kInside }; +IntersectionType BoxIntersects(StaticPropRange needle, StaticPropRange haystack, + uint32_t &partial_axis, uint32_t &partial_val) { + bool partial = false; + for (size_t i = 0; i < kNumStaticProperties; i++) { + if (haystack[i][0] >= needle[i][1]) { + return IntersectionType::kNone; + } + if (haystack[i][1] <= needle[i][0]) { + return IntersectionType::kNone; + } + if (haystack[i][0] <= needle[i][0] && haystack[i][1] >= needle[i][1]) { + continue; + } + partial = true; + partial_axis = i; + if (haystack[i][0] > needle[i][0] && haystack[i][0] < needle[i][1]) { + partial_val = haystack[i][0] - 1; + } else { + JXL_DASSERT(haystack[i][1] > needle[i][0] && + haystack[i][1] < needle[i][1]); + partial_val = haystack[i][1] - 1; + } + } + return partial ? IntersectionType::kPartial : IntersectionType::kInside; +} + +void SplitTreeSamples(TreeSamples &tree_samples, size_t begin, size_t pos, + size_t end, size_t prop) { + auto cmp = [&](size_t a, size_t b) { + return int32_t(tree_samples.Property(prop, a)) - + int32_t(tree_samples.Property(prop, b)); + }; + Rng rng(0); + while (end > begin + 1) { + { + JXL_ASSERT(end > begin); // silence clang-tidy. + size_t pivot = rng() % (end - begin) + begin; + tree_samples.Swap(begin, pivot); + } + size_t pivot_begin = begin; + size_t pivot_end = pivot_begin + 1; + for (size_t i = begin + 1; i < end; i++) { + JXL_DASSERT(i >= pivot_end); + JXL_DASSERT(pivot_end > pivot_begin); + int32_t cmp_result = cmp(i, pivot_begin); + if (cmp_result < 0) { // i < pivot, move pivot forward and put i before + // the pivot. + tree_samples.ThreeShuffle(pivot_begin, pivot_end, i); + pivot_begin++; + pivot_end++; + } else if (cmp_result == 0) { + tree_samples.Swap(pivot_end, i); + pivot_end++; + } + } + JXL_DASSERT(pivot_begin >= begin); + JXL_DASSERT(pivot_end > pivot_begin); + JXL_DASSERT(pivot_end <= end); + for (size_t i = begin; i < pivot_begin; i++) { + JXL_DASSERT(cmp(i, pivot_begin) < 0); + } + for (size_t i = pivot_end; i < end; i++) { + JXL_DASSERT(cmp(i, pivot_begin) > 0); + } + for (size_t i = pivot_begin; i < pivot_end; i++) { + JXL_DASSERT(cmp(i, pivot_begin) == 0); + } + // We now have that [begin, pivot_begin) is < pivot, [pivot_begin, + // pivot_end) is = pivot, and [pivot_end, end) is > pivot. + // If pos falls in the first or the last interval, we continue in that + // interval; otherwise, we are done. + if (pivot_begin > pos) { + end = pivot_begin; + } else if (pivot_end < pos) { + begin = pivot_end; + } else { + break; + } + } +} + +void FindBestSplit(TreeSamples &tree_samples, float threshold, + const std::vector &mul_info, + StaticPropRange initial_static_prop_range, + float fast_decode_multiplier, Tree *tree) { + struct NodeInfo { + size_t pos; + size_t begin; + size_t end; + uint64_t used_properties; + StaticPropRange static_prop_range; + }; + std::vector nodes; + nodes.push_back(NodeInfo{0, 0, tree_samples.NumDistinctSamples(), 0, + initial_static_prop_range}); + + size_t num_predictors = tree_samples.NumPredictors(); + size_t num_properties = tree_samples.NumProperties(); + + // TODO(veluca): consider parallelizing the search (processing multiple nodes + // at a time). + while (!nodes.empty()) { + size_t pos = nodes.back().pos; + size_t begin = nodes.back().begin; + size_t end = nodes.back().end; + uint64_t used_properties = nodes.back().used_properties; + StaticPropRange static_prop_range = nodes.back().static_prop_range; + nodes.pop_back(); + if (begin == end) continue; + + struct SplitInfo { + size_t prop = 0; + uint32_t val = 0; + size_t pos = 0; + float lcost = std::numeric_limits::max(); + float rcost = std::numeric_limits::max(); + Predictor lpred = Predictor::Zero; + Predictor rpred = Predictor::Zero; + float Cost() { return lcost + rcost; } + }; + + SplitInfo best_split_static_constant; + SplitInfo best_split_static; + SplitInfo best_split_nonstatic; + SplitInfo best_split_nowp; + + JXL_DASSERT(begin <= end); + JXL_DASSERT(end <= tree_samples.NumDistinctSamples()); + + // Compute the maximum token in the range. + size_t max_symbols = 0; + for (size_t pred = 0; pred < num_predictors; pred++) { + for (size_t i = begin; i < end; i++) { + uint32_t tok = tree_samples.Token(pred, i); + max_symbols = max_symbols > tok + 1 ? max_symbols : tok + 1; + } + } + max_symbols = Padded(max_symbols); + std::vector rounded_counts(max_symbols); + std::vector counts(max_symbols * num_predictors); + std::vector tot_extra_bits(num_predictors); + for (size_t pred = 0; pred < num_predictors; pred++) { + for (size_t i = begin; i < end; i++) { + counts[pred * max_symbols + tree_samples.Token(pred, i)] += + tree_samples.Count(i); + tot_extra_bits[pred] += + tree_samples.NBits(pred, i) * tree_samples.Count(i); + } + } + + float base_bits; + { + size_t pred = tree_samples.PredictorIndex((*tree)[pos].predictor); + base_bits = EstimateBits(counts.data() + pred * max_symbols, + rounded_counts.data(), max_symbols) + + tot_extra_bits[pred]; + } + + SplitInfo *best = &best_split_nonstatic; + + SplitInfo forced_split; + // The multiplier ranges cut halfway through the current ranges of static + // properties. We do this even if the current node is not a leaf, to + // minimize the number of nodes in the resulting tree. + for (size_t i = 0; i < mul_info.size(); i++) { + uint32_t axis, val; + IntersectionType t = + BoxIntersects(static_prop_range, mul_info[i].range, axis, val); + if (t == IntersectionType::kNone) continue; + if (t == IntersectionType::kInside) { + (*tree)[pos].multiplier = mul_info[i].multiplier; + break; + } + if (t == IntersectionType::kPartial) { + forced_split.val = tree_samples.QuantizeProperty(axis, val); + forced_split.prop = axis; + forced_split.lcost = forced_split.rcost = base_bits / 2 - threshold; + forced_split.lpred = forced_split.rpred = (*tree)[pos].predictor; + best = &forced_split; + best->pos = begin; + JXL_ASSERT(best->prop == tree_samples.PropertyFromIndex(best->prop)); + for (size_t x = begin; x < end; x++) { + if (tree_samples.Property(best->prop, x) <= best->val) { + best->pos++; + } + } + break; + } + } + + if (best != &forced_split) { + std::vector prop_value_used_count; + std::vector count_increase; + std::vector extra_bits_increase; + // For each property, compute which of its values are used, and what + // tokens correspond to those usages. Then, iterate through the values, + // and compute the entropy of each side of the split (of the form `prop > + // threshold`). Finally, find the split that minimizes the cost. + struct CostInfo { + float cost = std::numeric_limits::max(); + float extra_cost = 0; + float Cost() const { return cost + extra_cost; } + Predictor pred; // will be uninitialized in some cases, but never used. + }; + std::vector costs_l; + std::vector costs_r; + + std::vector counts_above(max_symbols); + std::vector counts_below(max_symbols); + + // The lower the threshold, the higher the expected noisiness of the + // estimate. Thus, discourage changing predictors. + float change_pred_penalty = 800.0f / (100.0f + threshold); + for (size_t prop = 0; prop < num_properties && base_bits > threshold; + prop++) { + costs_l.clear(); + costs_r.clear(); + size_t prop_size = tree_samples.NumPropertyValues(prop); + if (extra_bits_increase.size() < prop_size) { + count_increase.resize(prop_size * max_symbols); + extra_bits_increase.resize(prop_size); + } + // Clear prop_value_used_count (which cannot be cleared "on the go") + prop_value_used_count.clear(); + prop_value_used_count.resize(prop_size); + + size_t first_used = prop_size; + size_t last_used = 0; + + // TODO(veluca): consider finding multiple splits along a single + // property at the same time, possibly with a bottom-up approach. + for (size_t i = begin; i < end; i++) { + size_t p = tree_samples.Property(prop, i); + prop_value_used_count[p]++; + last_used = std::max(last_used, p); + first_used = std::min(first_used, p); + } + costs_l.resize(last_used - first_used); + costs_r.resize(last_used - first_used); + // For all predictors, compute the right and left costs of each split. + for (size_t pred = 0; pred < num_predictors; pred++) { + // Compute cost and histogram increments for each property value. + for (size_t i = begin; i < end; i++) { + size_t p = tree_samples.Property(prop, i); + size_t cnt = tree_samples.Count(i); + size_t sym = tree_samples.Token(pred, i); + count_increase[p * max_symbols + sym] += cnt; + extra_bits_increase[p] += tree_samples.NBits(pred, i) * cnt; + } + memcpy(counts_above.data(), counts.data() + pred * max_symbols, + max_symbols * sizeof counts_above[0]); + memset(counts_below.data(), 0, max_symbols * sizeof counts_below[0]); + size_t extra_bits_below = 0; + // Exclude last used: this ensures neither counts_above nor + // counts_below is empty. + for (size_t i = first_used; i < last_used; i++) { + if (!prop_value_used_count[i]) continue; + extra_bits_below += extra_bits_increase[i]; + // The increase for this property value has been used, and will not + // be used again: clear it. Also below. + extra_bits_increase[i] = 0; + for (size_t sym = 0; sym < max_symbols; sym++) { + counts_above[sym] -= count_increase[i * max_symbols + sym]; + counts_below[sym] += count_increase[i * max_symbols + sym]; + count_increase[i * max_symbols + sym] = 0; + } + float rcost = EstimateBits(counts_above.data(), + rounded_counts.data(), max_symbols) + + tot_extra_bits[pred] - extra_bits_below; + float lcost = EstimateBits(counts_below.data(), + rounded_counts.data(), max_symbols) + + extra_bits_below; + JXL_DASSERT(extra_bits_below <= tot_extra_bits[pred]); + float penalty = 0; + // Never discourage moving away from the Weighted predictor. + if (tree_samples.PredictorFromIndex(pred) != + (*tree)[pos].predictor && + (*tree)[pos].predictor != Predictor::Weighted) { + penalty = change_pred_penalty; + } + // If everything else is equal, disfavour Weighted (slower) and + // favour Zero (faster if it's the only predictor used in a + // group+channel combination) + if (tree_samples.PredictorFromIndex(pred) == Predictor::Weighted) { + penalty += 1e-8; + } + if (tree_samples.PredictorFromIndex(pred) == Predictor::Zero) { + penalty -= 1e-8; + } + if (rcost + penalty < costs_r[i - first_used].Cost()) { + costs_r[i - first_used].cost = rcost; + costs_r[i - first_used].extra_cost = penalty; + costs_r[i - first_used].pred = + tree_samples.PredictorFromIndex(pred); + } + if (lcost + penalty < costs_l[i - first_used].Cost()) { + costs_l[i - first_used].cost = lcost; + costs_l[i - first_used].extra_cost = penalty; + costs_l[i - first_used].pred = + tree_samples.PredictorFromIndex(pred); + } + } + } + // Iterate through the possible splits and find the one with minimum sum + // of costs of the two sides. + size_t split = begin; + for (size_t i = first_used; i < last_used; i++) { + if (!prop_value_used_count[i]) continue; + split += prop_value_used_count[i]; + float rcost = costs_r[i - first_used].cost; + float lcost = costs_l[i - first_used].cost; + // WP was not used + we would use the WP property or predictor + bool adds_wp = + (tree_samples.PropertyFromIndex(prop) == kWPProp && + (used_properties & (1LU << prop)) == 0) || + ((costs_l[i - first_used].pred == Predictor::Weighted || + costs_r[i - first_used].pred == Predictor::Weighted) && + (*tree)[pos].predictor != Predictor::Weighted); + bool zero_entropy_side = rcost == 0 || lcost == 0; + + SplitInfo &best = + prop < kNumStaticProperties + ? (zero_entropy_side ? best_split_static_constant + : best_split_static) + : (adds_wp ? best_split_nonstatic : best_split_nowp); + if (lcost + rcost < best.Cost()) { + best.prop = prop; + best.val = i; + best.pos = split; + best.lcost = lcost; + best.lpred = costs_l[i - first_used].pred; + best.rcost = rcost; + best.rpred = costs_r[i - first_used].pred; + } + } + // Clear extra_bits_increase and cost_increase for last_used. + extra_bits_increase[last_used] = 0; + for (size_t sym = 0; sym < max_symbols; sym++) { + count_increase[last_used * max_symbols + sym] = 0; + } + } + + // Try to avoid introducing WP. + if (best_split_nowp.Cost() + threshold < base_bits && + best_split_nowp.Cost() <= fast_decode_multiplier * best->Cost()) { + best = &best_split_nowp; + } + // Split along static props if possible and not significantly more + // expensive. + if (best_split_static.Cost() + threshold < base_bits && + best_split_static.Cost() <= fast_decode_multiplier * best->Cost()) { + best = &best_split_static; + } + // Split along static props to create constant nodes if possible. + if (best_split_static_constant.Cost() + threshold < base_bits) { + best = &best_split_static_constant; + } + } + + if (best->Cost() + threshold < base_bits) { + uint32_t p = tree_samples.PropertyFromIndex(best->prop); + pixel_type dequant = + tree_samples.UnquantizeProperty(best->prop, best->val); + // Split node and try to split children. + MakeSplitNode(pos, p, dequant, best->lpred, 0, best->rpred, 0, tree); + // "Sort" according to winning property + SplitTreeSamples(tree_samples, begin, best->pos, end, best->prop); + if (p >= kNumStaticProperties) { + used_properties |= 1 << best->prop; + } + auto new_sp_range = static_prop_range; + if (p < kNumStaticProperties) { + JXL_ASSERT(static_cast(dequant + 1) <= new_sp_range[p][1]); + new_sp_range[p][1] = dequant + 1; + JXL_ASSERT(new_sp_range[p][0] < new_sp_range[p][1]); + } + nodes.push_back(NodeInfo{(*tree)[pos].rchild, begin, best->pos, + used_properties, new_sp_range}); + new_sp_range = static_prop_range; + if (p < kNumStaticProperties) { + JXL_ASSERT(new_sp_range[p][0] <= static_cast(dequant + 1)); + new_sp_range[p][0] = dequant + 1; + JXL_ASSERT(new_sp_range[p][0] < new_sp_range[p][1]); + } + nodes.push_back(NodeInfo{(*tree)[pos].lchild, best->pos, end, + used_properties, new_sp_range}); + } + } +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +HWY_EXPORT(FindBestSplit); // Local function. + +void ComputeBestTree(TreeSamples &tree_samples, float threshold, + const std::vector &mul_info, + StaticPropRange static_prop_range, + float fast_decode_multiplier, Tree *tree) { + // TODO(veluca): take into account that different contexts can have different + // uint configs. + // + // Initialize tree. + tree->emplace_back(); + tree->back().property = -1; + tree->back().predictor = tree_samples.PredictorFromIndex(0); + tree->back().predictor_offset = 0; + tree->back().multiplier = 1; + JXL_ASSERT(tree_samples.NumProperties() < 64); + + JXL_ASSERT(tree_samples.NumDistinctSamples() <= + std::numeric_limits::max()); + HWY_DYNAMIC_DISPATCH(FindBestSplit) + (tree_samples, threshold, mul_info, static_prop_range, fast_decode_multiplier, + tree); +} + +constexpr int TreeSamples::kPropertyRange; +constexpr uint32_t TreeSamples::kDedupEntryUnused; + +Status TreeSamples::SetPredictor(Predictor predictor, + ModularOptions::TreeMode wp_tree_mode) { + if (wp_tree_mode == ModularOptions::TreeMode::kWPOnly) { + predictors = {Predictor::Weighted}; + residuals.resize(1); + return true; + } + if (wp_tree_mode == ModularOptions::TreeMode::kNoWP && + predictor == Predictor::Weighted) { + return JXL_FAILURE("Invalid predictor settings"); + } + if (predictor == Predictor::Variable) { + for (size_t i = 0; i < kNumModularPredictors; i++) { + predictors.push_back(static_cast(i)); + } + std::swap(predictors[0], predictors[static_cast(Predictor::Weighted)]); + std::swap(predictors[1], predictors[static_cast(Predictor::Gradient)]); + } else if (predictor == Predictor::Best) { + predictors = {Predictor::Weighted, Predictor::Gradient}; + } else { + predictors = {predictor}; + } + if (wp_tree_mode == ModularOptions::TreeMode::kNoWP) { + auto wp_it = + std::find(predictors.begin(), predictors.end(), Predictor::Weighted); + if (wp_it != predictors.end()) { + predictors.erase(wp_it); + } + } + residuals.resize(predictors.size()); + return true; +} + +Status TreeSamples::SetProperties(const std::vector &properties, + ModularOptions::TreeMode wp_tree_mode) { + props_to_use = properties; + if (wp_tree_mode == ModularOptions::TreeMode::kWPOnly) { + props_to_use = {static_cast(kWPProp)}; + } + if (wp_tree_mode == ModularOptions::TreeMode::kGradientOnly) { + props_to_use = {static_cast(kGradientProp)}; + } + if (wp_tree_mode == ModularOptions::TreeMode::kNoWP) { + auto it = std::find(props_to_use.begin(), props_to_use.end(), kWPProp); + if (it != props_to_use.end()) { + props_to_use.erase(it); + } + } + if (props_to_use.empty()) { + return JXL_FAILURE("Invalid property set configuration"); + } + props.resize(props_to_use.size()); + return true; +} + +void TreeSamples::InitTable(size_t size) { + JXL_DASSERT((size & (size - 1)) == 0); + if (dedup_table_.size() == size) return; + dedup_table_.resize(size, kDedupEntryUnused); + for (size_t i = 0; i < NumDistinctSamples(); i++) { + if (sample_counts[i] != std::numeric_limits::max()) { + AddToTable(i); + } + } +} + +bool TreeSamples::AddToTableAndMerge(size_t a) { + size_t pos1 = Hash1(a); + size_t pos2 = Hash2(a); + if (dedup_table_[pos1] != kDedupEntryUnused && + IsSameSample(a, dedup_table_[pos1])) { + JXL_DASSERT(sample_counts[a] == 1); + sample_counts[dedup_table_[pos1]]++; + // Remove from hash table samples that are saturated. + if (sample_counts[dedup_table_[pos1]] == + std::numeric_limits::max()) { + dedup_table_[pos1] = kDedupEntryUnused; + } + return true; + } + if (dedup_table_[pos2] != kDedupEntryUnused && + IsSameSample(a, dedup_table_[pos2])) { + JXL_DASSERT(sample_counts[a] == 1); + sample_counts[dedup_table_[pos2]]++; + // Remove from hash table samples that are saturated. + if (sample_counts[dedup_table_[pos2]] == + std::numeric_limits::max()) { + dedup_table_[pos2] = kDedupEntryUnused; + } + return true; + } + AddToTable(a); + return false; +} + +void TreeSamples::AddToTable(size_t a) { + size_t pos1 = Hash1(a); + size_t pos2 = Hash2(a); + if (dedup_table_[pos1] == kDedupEntryUnused) { + dedup_table_[pos1] = a; + } else if (dedup_table_[pos2] == kDedupEntryUnused) { + dedup_table_[pos2] = a; + } +} + +void TreeSamples::PrepareForSamples(size_t num_samples) { + for (auto &res : residuals) { + res.reserve(res.size() + num_samples); + } + for (auto &p : props) { + p.reserve(p.size() + num_samples); + } + size_t total_num_samples = num_samples + sample_counts.size(); + size_t next_pow2 = 1LLU << CeilLog2Nonzero(total_num_samples * 3 / 2); + InitTable(next_pow2); +} + +size_t TreeSamples::Hash1(size_t a) const { + constexpr uint64_t constant = 0x1e35a7bd; + uint64_t h = constant; + for (const auto &r : residuals) { + h = h * constant + r[a].tok; + h = h * constant + r[a].nbits; + } + for (const auto &p : props) { + h = h * constant + p[a]; + } + return (h >> 16) & (dedup_table_.size() - 1); +} +size_t TreeSamples::Hash2(size_t a) const { + constexpr uint64_t constant = 0x1e35a7bd1e35a7bd; + uint64_t h = constant; + for (const auto &p : props) { + h = h * constant ^ p[a]; + } + for (const auto &r : residuals) { + h = h * constant ^ r[a].tok; + h = h * constant ^ r[a].nbits; + } + return (h >> 16) & (dedup_table_.size() - 1); +} + +bool TreeSamples::IsSameSample(size_t a, size_t b) const { + bool ret = true; + for (const auto &r : residuals) { + if (r[a].tok != r[b].tok) { + ret = false; + } + if (r[a].nbits != r[b].nbits) { + ret = false; + } + } + for (const auto &p : props) { + if (p[a] != p[b]) { + ret = false; + } + } + return ret; +} + +void TreeSamples::AddSample(pixel_type_w pixel, const Properties &properties, + const pixel_type_w *predictions) { + for (size_t i = 0; i < predictors.size(); i++) { + pixel_type v = pixel - predictions[static_cast(predictors[i])]; + uint32_t tok, nbits, bits; + HybridUintConfig(4, 1, 2).Encode(PackSigned(v), &tok, &nbits, &bits); + JXL_DASSERT(tok < 256); + JXL_DASSERT(nbits < 256); + residuals[i].emplace_back( + ResidualToken{static_cast(tok), static_cast(nbits)}); + } + for (size_t i = 0; i < props_to_use.size(); i++) { + props[i].push_back(QuantizeProperty(i, properties[props_to_use[i]])); + } + sample_counts.push_back(1); + num_samples++; + if (AddToTableAndMerge(sample_counts.size() - 1)) { + for (auto &r : residuals) r.pop_back(); + for (auto &p : props) p.pop_back(); + sample_counts.pop_back(); + } +} + +void TreeSamples::Swap(size_t a, size_t b) { + if (a == b) return; + for (auto &r : residuals) { + std::swap(r[a], r[b]); + } + for (auto &p : props) { + std::swap(p[a], p[b]); + } + std::swap(sample_counts[a], sample_counts[b]); +} + +void TreeSamples::ThreeShuffle(size_t a, size_t b, size_t c) { + if (b == c) return Swap(a, b); + for (auto &r : residuals) { + auto tmp = r[a]; + r[a] = r[c]; + r[c] = r[b]; + r[b] = tmp; + } + for (auto &p : props) { + auto tmp = p[a]; + p[a] = p[c]; + p[c] = p[b]; + p[b] = tmp; + } + auto tmp = sample_counts[a]; + sample_counts[a] = sample_counts[c]; + sample_counts[c] = sample_counts[b]; + sample_counts[b] = tmp; +} + +namespace { +std::vector QuantizeHistogram(const std::vector &histogram, + size_t num_chunks) { + if (histogram.empty()) return {}; + // TODO(veluca): selecting distinct quantiles is likely not the best + // way to go about this. + std::vector thresholds; + size_t sum = std::accumulate(histogram.begin(), histogram.end(), 0LU); + size_t cumsum = 0; + size_t threshold = 0; + for (size_t i = 0; i + 1 < histogram.size(); i++) { + cumsum += histogram[i]; + if (cumsum > (threshold + 1) * sum / num_chunks) { + thresholds.push_back(i); + while (cumsum >= (threshold + 1) * sum / num_chunks) threshold++; + } + } + return thresholds; +} + +std::vector QuantizeSamples(const std::vector &samples, + size_t num_chunks) { + if (samples.empty()) return {}; + int min = *std::min_element(samples.begin(), samples.end()); + constexpr int kRange = 512; + min = std::min(std::max(min, -kRange), kRange); + std::vector counts(2 * kRange + 1); + for (int s : samples) { + uint32_t sample_offset = std::min(std::max(s, -kRange), kRange) - min; + counts[sample_offset]++; + } + std::vector thresholds = QuantizeHistogram(counts, num_chunks); + for (auto &v : thresholds) v += min; + return thresholds; +} +} // namespace + +void TreeSamples::PreQuantizeProperties( + const StaticPropRange &range, + const std::vector &multiplier_info, + const std::vector &group_pixel_count, + const std::vector &channel_pixel_count, + std::vector &pixel_samples, + std::vector &diff_samples, size_t max_property_values) { + // If we have forced splits because of multipliers, choose channel and group + // thresholds accordingly. + std::vector group_multiplier_thresholds; + std::vector channel_multiplier_thresholds; + for (const auto &v : multiplier_info) { + if (v.range[0][0] != range[0][0]) { + channel_multiplier_thresholds.push_back(v.range[0][0] - 1); + } + if (v.range[0][1] != range[0][1]) { + channel_multiplier_thresholds.push_back(v.range[0][1] - 1); + } + if (v.range[1][0] != range[1][0]) { + group_multiplier_thresholds.push_back(v.range[1][0] - 1); + } + if (v.range[1][1] != range[1][1]) { + group_multiplier_thresholds.push_back(v.range[1][1] - 1); + } + } + std::sort(channel_multiplier_thresholds.begin(), + channel_multiplier_thresholds.end()); + channel_multiplier_thresholds.resize( + std::unique(channel_multiplier_thresholds.begin(), + channel_multiplier_thresholds.end()) - + channel_multiplier_thresholds.begin()); + std::sort(group_multiplier_thresholds.begin(), + group_multiplier_thresholds.end()); + group_multiplier_thresholds.resize( + std::unique(group_multiplier_thresholds.begin(), + group_multiplier_thresholds.end()) - + group_multiplier_thresholds.begin()); + + compact_properties.resize(props_to_use.size()); + auto quantize_channel = [&]() { + if (!channel_multiplier_thresholds.empty()) { + return channel_multiplier_thresholds; + } + return QuantizeHistogram(channel_pixel_count, max_property_values); + }; + auto quantize_group_id = [&]() { + if (!group_multiplier_thresholds.empty()) { + return group_multiplier_thresholds; + } + return QuantizeHistogram(group_pixel_count, max_property_values); + }; + auto quantize_coordinate = [&]() { + std::vector quantized; + quantized.reserve(max_property_values - 1); + for (size_t i = 0; i + 1 < max_property_values; i++) { + quantized.push_back((i + 1) * 256 / max_property_values - 1); + } + return quantized; + }; + std::vector abs_pixel_thr; + std::vector pixel_thr; + auto quantize_pixel_property = [&]() { + if (pixel_thr.empty()) { + pixel_thr = QuantizeSamples(pixel_samples, max_property_values); + } + return pixel_thr; + }; + auto quantize_abs_pixel_property = [&]() { + if (abs_pixel_thr.empty()) { + quantize_pixel_property(); // Compute the non-abs thresholds. + for (auto &v : pixel_samples) v = std::abs(v); + abs_pixel_thr = QuantizeSamples(pixel_samples, max_property_values); + } + return abs_pixel_thr; + }; + std::vector abs_diff_thr; + std::vector diff_thr; + auto quantize_diff_property = [&]() { + if (diff_thr.empty()) { + diff_thr = QuantizeSamples(diff_samples, max_property_values); + } + return diff_thr; + }; + auto quantize_abs_diff_property = [&]() { + if (abs_diff_thr.empty()) { + quantize_diff_property(); // Compute the non-abs thresholds. + for (auto &v : diff_samples) v = std::abs(v); + abs_diff_thr = QuantizeSamples(diff_samples, max_property_values); + } + return abs_diff_thr; + }; + auto quantize_wp = [&]() { + if (max_property_values < 32) { + return std::vector{-127, -63, -31, -15, -7, -3, -1, 0, + 1, 3, 7, 15, 31, 63, 127}; + } + if (max_property_values < 64) { + return std::vector{-255, -191, -127, -95, -63, -47, -31, -23, + -15, -11, -7, -5, -3, -1, 0, 1, + 3, 5, 7, 11, 15, 23, 31, 47, + 63, 95, 127, 191, 255}; + } + return std::vector{ + -255, -223, -191, -159, -127, -111, -95, -79, -63, -55, -47, + -39, -31, -27, -23, -19, -15, -13, -11, -9, -7, -6, + -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, + 6, 7, 9, 11, 13, 15, 19, 23, 27, 31, 39, + 47, 55, 63, 79, 95, 111, 127, 159, 191, 223, 255}; + }; + + property_mapping.resize(props_to_use.size()); + for (size_t i = 0; i < props_to_use.size(); i++) { + if (props_to_use[i] == 0) { + compact_properties[i] = quantize_channel(); + } else if (props_to_use[i] == 1) { + compact_properties[i] = quantize_group_id(); + } else if (props_to_use[i] == 2 || props_to_use[i] == 3) { + compact_properties[i] = quantize_coordinate(); + } else if (props_to_use[i] == 6 || props_to_use[i] == 7 || + props_to_use[i] == 8 || + (props_to_use[i] >= kNumNonrefProperties && + (props_to_use[i] - kNumNonrefProperties) % 4 == 1)) { + compact_properties[i] = quantize_pixel_property(); + } else if (props_to_use[i] == 4 || props_to_use[i] == 5 || + (props_to_use[i] >= kNumNonrefProperties && + (props_to_use[i] - kNumNonrefProperties) % 4 == 0)) { + compact_properties[i] = quantize_abs_pixel_property(); + } else if (props_to_use[i] >= kNumNonrefProperties && + (props_to_use[i] - kNumNonrefProperties) % 4 == 2) { + compact_properties[i] = quantize_abs_diff_property(); + } else if (props_to_use[i] == kWPProp) { + compact_properties[i] = quantize_wp(); + } else { + compact_properties[i] = quantize_diff_property(); + } + property_mapping[i].resize(kPropertyRange * 2 + 1); + size_t mapped = 0; + for (size_t j = 0; j < property_mapping[i].size(); j++) { + while (mapped < compact_properties[i].size() && + static_cast(j) - kPropertyRange > + compact_properties[i][mapped]) { + mapped++; + } + // property_mapping[i] of a value V is `mapped` if + // compact_properties[i][mapped] <= j and + // compact_properties[i][mapped-1] > j + // This is because the decision node in the tree splits on (property) > j, + // hence everything that is not > of a threshold should be clustered + // together. + property_mapping[i][j] = mapped; + } + } +} + +void CollectPixelSamples(const Image &image, const ModularOptions &options, + size_t group_id, + std::vector &group_pixel_count, + std::vector &channel_pixel_count, + std::vector &pixel_samples, + std::vector &diff_samples) { + if (options.nb_repeats == 0) return; + if (group_pixel_count.size() <= group_id) { + group_pixel_count.resize(group_id + 1); + } + if (channel_pixel_count.size() < image.channel.size()) { + channel_pixel_count.resize(image.channel.size()); + } + Rng rng(group_id); + // Sample 10% of the final number of samples for property quantization. + float fraction = options.nb_repeats * 0.1; + std::geometric_distribution dist(fraction); + size_t total_pixels = 0; + std::vector channel_ids; + for (size_t i = 0; i < image.channel.size(); i++) { + if (image.channel[i].w <= 1 || image.channel[i].h == 0) { + continue; // skip empty or width-1 channels. + } + if (i >= image.nb_meta_channels && + (image.channel[i].w > options.max_chan_size || + image.channel[i].h > options.max_chan_size)) { + break; + } + channel_ids.push_back(i); + group_pixel_count[group_id] += image.channel[i].w * image.channel[i].h; + channel_pixel_count[i] += image.channel[i].w * image.channel[i].h; + total_pixels += image.channel[i].w * image.channel[i].h; + } + if (channel_ids.empty()) return; + pixel_samples.reserve(pixel_samples.size() + fraction * total_pixels); + diff_samples.reserve(diff_samples.size() + fraction * total_pixels); + size_t i = 0; + size_t y = 0; + size_t x = 0; + auto advance = [&](size_t amount) { + x += amount; + // Detect row overflow (rare). + while (x >= image.channel[channel_ids[i]].w) { + x -= image.channel[channel_ids[i]].w; + y++; + // Detect end-of-channel (even rarer). + if (y == image.channel[channel_ids[i]].h) { + i++; + y = 0; + if (i >= channel_ids.size()) { + return; + } + } + } + }; + advance(dist(rng)); + for (; i < channel_ids.size(); advance(dist(rng) + 1)) { + const pixel_type *row = image.channel[channel_ids[i]].Row(y); + pixel_samples.push_back(row[x]); + size_t xp = x == 0 ? 1 : x - 1; + diff_samples.push_back(row[x] - row[xp]); + } +} + +// TODO(veluca): very simple encoding scheme. This should be improved. +void TokenizeTree(const Tree &tree, std::vector *tokens, + Tree *decoder_tree) { + JXL_ASSERT(tree.size() <= kMaxTreeSize); + std::queue q; + q.push(0); + size_t leaf_id = 0; + decoder_tree->clear(); + while (!q.empty()) { + int cur = q.front(); + q.pop(); + JXL_ASSERT(tree[cur].property >= -1); + tokens->emplace_back(kPropertyContext, tree[cur].property + 1); + if (tree[cur].property == -1) { + tokens->emplace_back(kPredictorContext, + static_cast(tree[cur].predictor)); + tokens->emplace_back(kOffsetContext, + PackSigned(tree[cur].predictor_offset)); + uint32_t mul_log = Num0BitsBelowLS1Bit_Nonzero(tree[cur].multiplier); + uint32_t mul_bits = (tree[cur].multiplier >> mul_log) - 1; + tokens->emplace_back(kMultiplierLogContext, mul_log); + tokens->emplace_back(kMultiplierBitsContext, mul_bits); + JXL_ASSERT(tree[cur].predictor < Predictor::Best); + decoder_tree->emplace_back(-1, 0, leaf_id++, 0, tree[cur].predictor, + tree[cur].predictor_offset, + tree[cur].multiplier); + continue; + } + decoder_tree->emplace_back(tree[cur].property, tree[cur].splitval, + decoder_tree->size() + q.size() + 1, + decoder_tree->size() + q.size() + 2, + Predictor::Zero, 0, 1); + q.push(tree[cur].lchild); + q.push(tree[cur].rchild); + tokens->emplace_back(kSplitValContext, PackSigned(tree[cur].splitval)); + } +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/modular/encoding/enc_ma.h b/lib/jxl/modular/encoding/enc_ma.h new file mode 100644 index 0000000..d0a90cc --- /dev/null +++ b/lib/jxl/modular/encoding/enc_ma.h @@ -0,0 +1,157 @@ +// 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_MODULAR_ENCODING_ENC_MA_H_ +#define LIB_JXL_MODULAR_ENCODING_ENC_MA_H_ + +#include + +#include "lib/jxl/enc_ans.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/modular/encoding/dec_ma.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +// Struct to collect all the data needed to build a tree. +struct TreeSamples { + bool HasSamples() const { + return !residuals.empty() && !residuals[0].empty(); + } + size_t NumDistinctSamples() const { return sample_counts.size(); } + size_t NumSamples() const { return num_samples; } + // Set the predictor to use. Must be called before adding any samples. + Status SetPredictor(Predictor predictor, + ModularOptions::TreeMode wp_tree_mode); + // Set the properties to use. Must be called before adding any samples. + Status SetProperties(const std::vector &properties, + ModularOptions::TreeMode wp_tree_mode); + + size_t Token(size_t pred, size_t i) const { return residuals[pred][i].tok; } + size_t NBits(size_t pred, size_t i) const { return residuals[pred][i].nbits; } + size_t Count(size_t i) const { return sample_counts[i]; } + size_t PredictorIndex(Predictor predictor) const { + const auto predictor_elem = + std::find(predictors.begin(), predictors.end(), predictor); + JXL_DASSERT(predictor_elem != predictors.end()); + return predictor_elem - predictors.begin(); + } + size_t PropertyIndex(size_t property) const { + const auto property_elem = + std::find(props_to_use.begin(), props_to_use.end(), property); + JXL_DASSERT(property_elem != props_to_use.end()); + return property_elem - props_to_use.begin(); + } + size_t NumPropertyValues(size_t property_index) const { + return compact_properties[property_index].size() + 1; + } + // Returns the *quantized* property value. + size_t Property(size_t property_index, size_t i) const { + return props[property_index][i]; + } + int UnquantizeProperty(size_t property_index, uint32_t quant) const { + JXL_ASSERT(quant < compact_properties[property_index].size()); + return compact_properties[property_index][quant]; + } + + Predictor PredictorFromIndex(size_t index) const { + JXL_DASSERT(index < predictors.size()); + return predictors[index]; + } + size_t PropertyFromIndex(size_t index) const { + JXL_DASSERT(index < props_to_use.size()); + return props_to_use[index]; + } + size_t NumPredictors() const { return predictors.size(); } + size_t NumProperties() const { return props_to_use.size(); } + + // Preallocate data for a given number of samples. MUST be called before + // adding any sample. + void PrepareForSamples(size_t num_samples); + // Add a sample. + void AddSample(pixel_type_w pixel, const Properties &properties, + const pixel_type_w *predictions); + // Pre-cluster property values. + void PreQuantizeProperties( + const StaticPropRange &range, + const std::vector &multiplier_info, + const std::vector &group_pixel_count, + const std::vector &channel_pixel_count, + std::vector &pixel_samples, + std::vector &diff_samples, size_t max_property_values); + + void AllSamplesDone() { dedup_table_ = std::vector(); } + + uint32_t QuantizeProperty(uint32_t prop, pixel_type v) const { + v = std::min(std::max(v, -kPropertyRange), kPropertyRange) + kPropertyRange; + return property_mapping[prop][v]; + } + + // Swaps samples in position a and b. Does nothing if a == b. + void Swap(size_t a, size_t b); + + // Cycles samples: a -> b -> c -> a. We assume a <= b <= c, so that we can + // just call Swap(a, b) if b==c. + void ThreeShuffle(size_t a, size_t b, size_t c); + + private: + // TODO(veluca): as the total number of properties and predictors are known + // before adding any samples, it might be better to interleave predictors, + // properties and counts in a single vector to improve locality. + // A first attempt at doing this actually results in much slower encoding, + // possibly because of the more complex addressing. + struct ResidualToken { + uint8_t tok; + uint8_t nbits; + }; + // Residual information: token and number of extra bits, per predictor. + std::vector> residuals; + // Number of occurrences of each sample. + std::vector sample_counts; + // Property values, quantized to at most 256 distinct values. + std::vector> props; + // Decompactification info for `props`. + std::vector> compact_properties; + // List of properties to use. + std::vector props_to_use; + // List of predictors to use. + std::vector predictors; + // Mapping property value -> quantized property value. + static constexpr int kPropertyRange = 511; + std::vector> property_mapping; + // Number of samples seen. + size_t num_samples = 0; + // Table for deduplication. + static constexpr uint32_t kDedupEntryUnused{static_cast(-1)}; + std::vector dedup_table_; + + // Functions for sample deduplication. + bool IsSameSample(size_t a, size_t b) const; + size_t Hash1(size_t a) const; + size_t Hash2(size_t a) const; + void InitTable(size_t size); + // Returns true if `a` was already present in the table. + bool AddToTableAndMerge(size_t a); + void AddToTable(size_t a); +}; + +void TokenizeTree(const Tree &tree, std::vector *tokens, + Tree *decoder_tree); + +void CollectPixelSamples(const Image &image, const ModularOptions &options, + size_t group_id, + std::vector &group_pixel_count, + std::vector &channel_pixel_count, + std::vector &pixel_samples, + std::vector &diff_samples); + +void ComputeBestTree(TreeSamples &tree_samples, float threshold, + const std::vector &mul_info, + StaticPropRange static_prop_range, + float fast_decode_multiplier, Tree *tree); + +} // namespace jxl +#endif // LIB_JXL_MODULAR_ENCODING_ENC_MA_H_ diff --git a/lib/jxl/modular/encoding/encoding.cc b/lib/jxl/modular/encoding/encoding.cc new file mode 100644 index 0000000..db42bd8 --- /dev/null +++ b/lib/jxl/modular/encoding/encoding.cc @@ -0,0 +1,530 @@ +// 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/modular/encoding/encoding.h" + +#include +#include + +#include + +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +// Removes all nodes that use a static property (i.e. channel or group ID) from +// the tree and collapses each node on even levels with its two children to +// produce a flatter tree. Also computes whether the resulting tree requires +// using the weighted predictor. +FlatTree FilterTree(const Tree &global_tree, + std::array &static_props, + size_t *num_props, bool *use_wp, bool *wp_only, + bool *gradient_only) { + *num_props = 0; + bool has_wp = false; + bool has_non_wp = false; + *gradient_only = true; + const auto mark_property = [&](int32_t p) { + if (p == kWPProp) { + has_wp = true; + } else if (p >= kNumStaticProperties) { + has_non_wp = true; + } + if (p >= kNumStaticProperties && p != kGradientProp) { + *gradient_only = false; + } + }; + FlatTree output; + std::queue nodes; + nodes.push(0); + // Produces a trimmed and flattened tree by doing a BFS visit of the original + // tree, ignoring branches that are known to be false and proceeding two + // levels at a time to collapse nodes in a flatter tree; if an inner parent + // node has a leaf as a child, the leaf is duplicated and an implicit fake + // node is added. This allows to reduce the number of branches when traversing + // the resulting flat tree. + while (!nodes.empty()) { + size_t cur = nodes.front(); + nodes.pop(); + // Skip nodes that we can decide now, by jumping directly to their children. + while (global_tree[cur].property < kNumStaticProperties && + global_tree[cur].property != -1) { + if (static_props[global_tree[cur].property] > global_tree[cur].splitval) { + cur = global_tree[cur].lchild; + } else { + cur = global_tree[cur].rchild; + } + } + FlatDecisionNode flat; + if (global_tree[cur].property == -1) { + flat.property0 = -1; + flat.childID = global_tree[cur].lchild; + flat.predictor = global_tree[cur].predictor; + flat.predictor_offset = global_tree[cur].predictor_offset; + flat.multiplier = global_tree[cur].multiplier; + *gradient_only &= flat.predictor == Predictor::Gradient; + has_wp |= flat.predictor == Predictor::Weighted; + has_non_wp |= flat.predictor != Predictor::Weighted; + output.push_back(flat); + continue; + } + flat.childID = output.size() + nodes.size() + 1; + + flat.property0 = global_tree[cur].property; + *num_props = std::max(flat.property0 + 1, *num_props); + flat.splitval0 = global_tree[cur].splitval; + + for (size_t i = 0; i < 2; i++) { + size_t cur_child = + i == 0 ? global_tree[cur].lchild : global_tree[cur].rchild; + // Skip nodes that we can decide now. + while (global_tree[cur_child].property < kNumStaticProperties && + global_tree[cur_child].property != -1) { + if (static_props[global_tree[cur_child].property] > + global_tree[cur_child].splitval) { + cur_child = global_tree[cur_child].lchild; + } else { + cur_child = global_tree[cur_child].rchild; + } + } + // We ended up in a leaf, add a dummy decision and two copies of the leaf. + if (global_tree[cur_child].property == -1) { + flat.properties[i] = 0; + flat.splitvals[i] = 0; + nodes.push(cur_child); + nodes.push(cur_child); + } else { + flat.properties[i] = global_tree[cur_child].property; + flat.splitvals[i] = global_tree[cur_child].splitval; + nodes.push(global_tree[cur_child].lchild); + nodes.push(global_tree[cur_child].rchild); + *num_props = std::max(flat.properties[i] + 1, *num_props); + } + } + + for (size_t j = 0; j < 2; j++) mark_property(flat.properties[j]); + mark_property(flat.property0); + output.push_back(flat); + } + if (*num_props > kNumNonrefProperties) { + *num_props = + DivCeil(*num_props - kNumNonrefProperties, kExtraPropsPerChannel) * + kExtraPropsPerChannel + + kNumNonrefProperties; + } else { + *num_props = kNumNonrefProperties; + } + *use_wp = has_wp; + *wp_only = has_wp && !has_non_wp; + + return output; +} + +Status DecodeModularChannelMAANS(BitReader *br, ANSSymbolReader *reader, + const std::vector &context_map, + const Tree &global_tree, + const weighted::Header &wp_header, + pixel_type chan, size_t group_id, + Image *image) { + Channel &channel = image->channel[chan]; + + std::array static_props = { + {chan, (int)group_id}}; + // TODO(veluca): filter the tree according to static_props. + + // zero pixel channel? could happen + if (channel.w == 0 || channel.h == 0) return true; + + bool tree_has_wp_prop_or_pred = false; + bool is_wp_only = false; + bool is_gradient_only = false; + size_t num_props; + FlatTree tree = + FilterTree(global_tree, static_props, &num_props, + &tree_has_wp_prop_or_pred, &is_wp_only, &is_gradient_only); + + // From here on, tree lookup returns a *clustered* context ID. + // This avoids an extra memory lookup after tree traversal. + for (size_t i = 0; i < tree.size(); i++) { + if (tree[i].property0 == -1) { + tree[i].childID = context_map[tree[i].childID]; + } + } + + JXL_DEBUG_V(3, "Decoded MA tree with %zu nodes", tree.size()); + + // MAANS decode + const auto make_pixel = [](uint64_t v, pixel_type multiplier, + pixel_type_w offset) -> pixel_type { + JXL_DASSERT((v & 0xFFFFFFFF) == v); + pixel_type_w val = UnpackSigned(v); + // if it overflows, it overflows, and we have a problem anyway + return val * multiplier + offset; + }; + + if (tree.size() == 1) { + // special optimized case: no meta-adaptation, so no need + // to compute properties. + Predictor predictor = tree[0].predictor; + int64_t offset = tree[0].predictor_offset; + int32_t multiplier = tree[0].multiplier; + size_t ctx_id = tree[0].childID; + if (predictor == Predictor::Zero) { + uint32_t value; + if (reader->IsSingleValueAndAdvance(ctx_id, &value, + channel.w * channel.h)) { + // Special-case: histogram has a single symbol, with no extra bits, and + // we use ANS mode. + JXL_DEBUG_V(8, "Fastest track."); + pixel_type v = make_pixel(value, multiplier, offset); + for (size_t y = 0; y < channel.h; y++) { + 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); + } + } + } + } else if (predictor == Predictor::Gradient && offset == 0 && + multiplier == 1) { + JXL_DEBUG_V(8, "Gradient very fast track."); + const intptr_t onerow = channel.plane.PixelsPerRow(); + 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++) { + pixel_type left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type top = (y ? *(r + x - onerow) : left); + pixel_type topleft = (x && y ? *(r + x - 1 - onerow) : left); + pixel_type guess = ClampedGradient(top, left, topleft); + uint64_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = make_pixel(v, 1, guess); + } + } + } else if (predictor != Predictor::Weighted) { + // special optimized case: no wp + JXL_DEBUG_V(8, "Quite fast track."); + const intptr_t onerow = channel.plane.PixelsPerRow(); + 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++) { + PredictionResult pred = + PredictNoTreeNoWP(channel.w, r + x, onerow, x, y, predictor); + pixel_type_w g = pred.guess + offset; + uint64_t v = reader->ReadHybridUintClustered(ctx_id, br); + // NOTE: pred.multiplier is unset. + r[x] = make_pixel(v, multiplier, g); + } + } + } else { + JXL_DEBUG_V(8, "Somewhat fast track."); + const intptr_t onerow = channel.plane.PixelsPerRow(); + weighted::State wp_state(wp_header, channel.w, channel.h); + 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++) { + pixel_type_w g = PredictNoTreeWP(channel.w, r + x, onerow, x, y, + predictor, &wp_state) + .guess + + offset; + uint64_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = make_pixel(v, multiplier, g); + wp_state.UpdateErrors(r[x], x, y, channel.w); + } + } + } + return true; + } + + // Check if this tree is a WP-only tree with a small enough property value + // range. + // Initialized to avoid clang-tidy complaining. + uint8_t context_lookup[2 * kPropRangeFast] = {}; + int8_t multipliers[2 * kPropRangeFast] = {}; + int8_t offsets[2 * kPropRangeFast] = {}; + if (is_wp_only) { + is_wp_only = TreeToLookupTable(tree, context_lookup, offsets, multipliers); + } + if (is_gradient_only) { + is_gradient_only = + TreeToLookupTable(tree, context_lookup, offsets, multipliers); + } + + if (is_gradient_only) { + JXL_DEBUG_V(8, "Gradient fast track."); + const intptr_t onerow = channel.plane.PixelsPerRow(); + 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++) { + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + int32_t guess = ClampedGradient(top, left, topleft); + uint32_t pos = + kPropRangeFast + + std::min( + std::max(-kPropRangeFast, top + left - topleft), + kPropRangeFast - 1); + uint32_t ctx_id = context_lookup[pos]; + uint64_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = make_pixel(v, multipliers[pos], + static_cast(offsets[pos]) + guess); + } + } + } else if (is_wp_only) { + JXL_DEBUG_V(8, "WP fast track."); + const intptr_t onerow = channel.plane.PixelsPerRow(); + weighted::State wp_state(wp_header, channel.w, channel.h); + Properties properties(1); + 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++) { + size_t offset = 0; + pixel_type_w left = (x ? r[x - 1] : y ? *(r + x - onerow) : 0); + pixel_type_w top = (y ? *(r + x - onerow) : left); + pixel_type_w topleft = (x && y ? *(r + x - 1 - onerow) : left); + pixel_type_w topright = + (x + 1 < channel.w && y ? *(r + x + 1 - onerow) : top); + pixel_type_w toptop = (y > 1 ? *(r + x - onerow - onerow) : top); + int32_t guess = wp_state.Predict( + x, y, channel.w, top, left, topright, topleft, toptop, &properties, + offset); + uint32_t pos = + kPropRangeFast + std::min(std::max(-kPropRangeFast, properties[0]), + kPropRangeFast - 1); + uint32_t ctx_id = context_lookup[pos]; + uint64_t v = reader->ReadHybridUintClustered(ctx_id, br); + r[x] = make_pixel(v, multipliers[pos], + static_cast(offsets[pos]) + guess); + wp_state.UpdateErrors(r[x], x, y, channel.w); + } + } + } else if (!tree_has_wp_prop_or_pred) { + // special optimized case: the weighted predictor and its properties are not + // used, so no need to compute weights and properties. + JXL_DEBUG_V(8, "Slow track."); + MATreeLookup tree_lookup(tree); + Properties properties = Properties(num_props); + const intptr_t onerow = channel.plane.PixelsPerRow(); + Channel references(properties.size() - kNumNonrefProperties, channel.w); + for (size_t y = 0; y < channel.h; y++) { + 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); + } + } + } else { + JXL_DEBUG_V(8, "Slowest track."); + MATreeLookup tree_lookup(tree); + Properties properties = Properties(num_props); + const intptr_t onerow = channel.plane.PixelsPerRow(); + Channel references(properties.size() - kNumNonrefProperties, channel.w); + weighted::State wp_state(wp_header, channel.w, channel.h); + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT p = channel.Row(y); + InitPropsRow(&properties, static_props, y); + PrecomputeReferences(channel, y, *image, chan, &references); + for (size_t x = 0; x < channel.w; x++) { + PredictionResult res = + PredictTreeWP(&properties, channel.w, p + x, onerow, x, y, + tree_lookup, references, &wp_state); + uint64_t v = reader->ReadHybridUintClustered(res.context, br); + p[x] = make_pixel(v, res.multiplier, res.guess); + wp_state.UpdateErrors(p[x], x, y, channel.w); + } + } + } + return true; +} + +GroupHeader::GroupHeader() { Bundle::Init(this); } + +Status ValidateChannelDimensions(const Image &image, + const ModularOptions &options) { + size_t nb_channels = image.channel.size(); + for (bool is_dc : {true, false}) { + size_t group_dim = options.group_dim * (is_dc ? kBlockDim : 1); + size_t c = image.nb_meta_channels; + for (; c < nb_channels; c++) { + const Channel &ch = image.channel[c]; + if (ch.w > options.group_dim || ch.h > options.group_dim) break; + } + for (; c < nb_channels; c++) { + const Channel &ch = image.channel[c]; + if (ch.w == 0 || ch.h == 0) continue; // skip empty + bool is_dc_channel = std::min(ch.hshift, ch.vshift) >= 3; + if (is_dc_channel != is_dc) continue; + size_t tile_dim = group_dim >> std::max(ch.hshift, ch.vshift); + if (tile_dim == 0) { + return JXL_FAILURE("Inconsistent transforms"); + } + } + } + return true; +} + +Status ModularDecode(BitReader *br, Image &image, GroupHeader &header, + size_t group_id, ModularOptions *options, + const Tree *global_tree, const ANSCode *global_code, + const std::vector *global_ctx_map, + bool allow_truncated_group) { + if (image.channel.empty()) return true; + + // decode transforms + JXL_RETURN_IF_ERROR(Bundle::Read(br, &header)); + JXL_DEBUG_V(3, "Image data underwent %zu transformations: ", + header.transforms.size()); + image.transform = header.transforms; + for (Transform &transform : image.transform) { + JXL_RETURN_IF_ERROR(transform.MetaApply(image)); + } + if (image.error) { + return JXL_FAILURE("Corrupt file. Aborting."); + } + if (br->AllReadsWithinBounds()) { + // Only check if the transforms list is complete. + JXL_RETURN_IF_ERROR(ValidateChannelDimensions(image, *options)); + } + + size_t nb_channels = image.channel.size(); + + size_t num_chans = 0; + size_t distance_multiplier = 0; + for (size_t i = 0; i < nb_channels; i++) { + Channel &channel = image.channel[i]; + if (!channel.w || !channel.h) { + continue; // skip empty channels + } + if (i >= image.nb_meta_channels && (channel.w > options->max_chan_size || + channel.h > options->max_chan_size)) { + break; + } + if (channel.w > distance_multiplier) { + distance_multiplier = channel.w; + } + num_chans++; + } + if (num_chans == 0) return true; + + // Read tree. + Tree tree_storage; + std::vector context_map_storage; + ANSCode code_storage; + const Tree *tree = &tree_storage; + const ANSCode *code = &code_storage; + const std::vector *context_map = &context_map_storage; + if (!header.use_global_tree) { + size_t max_tree_size = 1024; + for (size_t i = 0; i < nb_channels; i++) { + Channel &channel = image.channel[i]; + if (!channel.w || !channel.h) { + continue; // skip empty channels + } + if (i >= image.nb_meta_channels && (channel.w > options->max_chan_size || + channel.h > options->max_chan_size)) { + break; + } + size_t pixels = channel.w * channel.h; + if (pixels / channel.w != channel.h) { + return JXL_FAILURE("Tree size overflow"); + } + max_tree_size += pixels; + if (max_tree_size < pixels) return JXL_FAILURE("Tree size overflow"); + } + + JXL_RETURN_IF_ERROR(DecodeTree(br, &tree_storage, max_tree_size)); + JXL_RETURN_IF_ERROR(DecodeHistograms(br, (tree_storage.size() + 1) / 2, + &code_storage, &context_map_storage)); + } else { + if (!global_tree || !global_code || !global_ctx_map || + global_tree->empty()) { + return JXL_FAILURE("No global tree available but one was requested"); + } + tree = global_tree; + code = global_code; + context_map = global_ctx_map; + } + + // Read channels + ANSSymbolReader reader(code, br, distance_multiplier); + for (size_t i = 0; i < nb_channels; i++) { + Channel &channel = image.channel[i]; + if (!channel.w || !channel.h) { + continue; // skip empty channels + } + if (i >= image.nb_meta_channels && (channel.w > options->max_chan_size || + channel.h > options->max_chan_size)) { + break; + } + JXL_RETURN_IF_ERROR(DecodeModularChannelMAANS(br, &reader, *context_map, + *tree, header.wp_header, i, + group_id, &image)); + // Truncated group. + if (!br->AllReadsWithinBounds()) { + if (!allow_truncated_group) return JXL_FAILURE("Truncated input"); + ZeroFillImage(&channel.plane); + while (++i < nb_channels) ZeroFillImage(&image.channel[i].plane); + return Status(StatusCode::kNotEnoughBytes); + } + } + if (!reader.CheckANSFinalState()) { + return JXL_FAILURE("ANS decode final state failed"); + } + return true; +} + +Status ModularGenericDecompress(BitReader *br, Image &image, + GroupHeader *header, size_t group_id, + ModularOptions *options, int undo_transforms, + const Tree *tree, const ANSCode *code, + const std::vector *ctx_map, + bool allow_truncated_group) { +#ifdef JXL_ENABLE_ASSERT + std::vector> req_sizes(image.channel.size()); + for (size_t c = 0; c < req_sizes.size(); c++) { + req_sizes[c] = {image.channel[c].w, image.channel[c].h}; + } +#endif + GroupHeader local_header; + if (header == nullptr) header = &local_header; + auto dec_status = ModularDecode(br, image, *header, group_id, options, tree, + code, ctx_map, allow_truncated_group); + if (!allow_truncated_group) JXL_RETURN_IF_ERROR(dec_status); + if (dec_status.IsFatalError()) return dec_status; + image.undo_transforms(header->wp_header, undo_transforms); + if (image.error) return JXL_FAILURE("Corrupt file. Aborting."); + size_t bit_pos = br->TotalBitsConsumed(); + JXL_DEBUG_V(4, "Modular-decoded a %zux%zu nbchans=%zu image from %zu bytes", + image.w, image.h, image.channel.size(), + (br->TotalBitsConsumed() - bit_pos) / 8); + (void)bit_pos; +#ifdef JXL_ENABLE_ASSERT + // Check that after applying all transforms we are back to the requested image + // sizes, otherwise there's a programming error with the transformations. + if (undo_transforms == -1 || undo_transforms == 0) { + JXL_ASSERT(image.channel.size() == req_sizes.size()); + for (size_t c = 0; c < req_sizes.size(); c++) { + JXL_ASSERT(req_sizes[c].first == image.channel[c].w); + JXL_ASSERT(req_sizes[c].second == image.channel[c].h); + } + } +#endif + return dec_status; +} + +} // namespace jxl diff --git a/lib/jxl/modular/encoding/encoding.h b/lib/jxl/modular/encoding/encoding.h new file mode 100644 index 0000000..8a20876 --- /dev/null +++ b/lib/jxl/modular/encoding/encoding.h @@ -0,0 +1,140 @@ +// 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_MODULAR_ENCODING_ENCODING_H_ +#define LIB_JXL_MODULAR_ENCODING_ENCODING_H_ + +#include +#include + +#include + +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/image.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/encoding/dec_ma.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/options.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +// Valid range of properties for using lookup tables instead of trees. +constexpr int32_t kPropRangeFast = 512; + +struct GroupHeader : public Fields { + GroupHeader(); + + const char *Name() const override { return "GroupHeader"; } + + Status VisitFields(Visitor *JXL_RESTRICT visitor) override { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &use_global_tree)); + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&wp_header)); + uint32_t num_transforms = static_cast(transforms.size()); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(0), Val(1), BitsOffset(4, 2), + BitsOffset(8, 18), 0, + &num_transforms)); + if (visitor->IsReading()) transforms.resize(num_transforms); + for (size_t i = 0; i < num_transforms; i++) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&transforms[i])); + } + return true; + } + + bool use_global_tree; + weighted::Header wp_header; + + std::vector transforms; +}; + +FlatTree FilterTree(const Tree &global_tree, + std::array &static_props, + size_t *num_props, bool *use_wp, bool *wp_only, + bool *gradient_only); + +template +bool TreeToLookupTable(const FlatTree &tree, + T context_lookup[2 * kPropRangeFast], + int8_t offsets[2 * kPropRangeFast], + int8_t multipliers[2 * kPropRangeFast] = nullptr) { + struct TreeRange { + // Begin *excluded*, end *included*. This works best with > vs <= decision + // nodes. + int begin, end; + size_t pos; + }; + std::vector ranges; + ranges.push_back(TreeRange{-kPropRangeFast - 1, kPropRangeFast - 1, 0}); + while (!ranges.empty()) { + TreeRange cur = ranges.back(); + ranges.pop_back(); + if (cur.begin < -kPropRangeFast - 1 || cur.begin >= kPropRangeFast - 1 || + cur.end > kPropRangeFast - 1) { + // Tree is outside the allowed range, exit. + return false; + } + auto &node = tree[cur.pos]; + // Leaf. + if (node.property0 == -1) { + if (node.predictor_offset < std::numeric_limits::min() || + node.predictor_offset > std::numeric_limits::max()) { + return false; + } + if (node.multiplier < std::numeric_limits::min() || + node.multiplier > std::numeric_limits::max()) { + return false; + } + if (multipliers == nullptr && node.multiplier != 1) { + return false; + } + for (int i = cur.begin + 1; i < cur.end + 1; i++) { + context_lookup[i + kPropRangeFast] = node.childID; + if (multipliers) multipliers[i + kPropRangeFast] = node.multiplier; + offsets[i + kPropRangeFast] = node.predictor_offset; + } + continue; + } + // > side of top node. + if (node.properties[0] >= kNumStaticProperties) { + ranges.push_back(TreeRange({node.splitvals[0], cur.end, node.childID})); + ranges.push_back( + TreeRange({node.splitval0, node.splitvals[0], node.childID + 1})); + } else { + ranges.push_back(TreeRange({node.splitval0, cur.end, node.childID})); + } + // <= side + if (node.properties[1] >= kNumStaticProperties) { + ranges.push_back( + TreeRange({node.splitvals[1], node.splitval0, node.childID + 2})); + ranges.push_back( + TreeRange({cur.begin, node.splitvals[1], node.childID + 3})); + } else { + ranges.push_back( + TreeRange({cur.begin, node.splitval0, node.childID + 2})); + } + } + return true; +} +// TODO(veluca): make cleaner interfaces. + +Status ValidateChannelDimensions(const Image &image, + const ModularOptions &options); + +// undo_transforms == N > 0: undo all transforms except the first N +// (e.g. to represent YCbCr420 losslessly) +// undo_transforms == 0: undo all transforms +// undo_transforms == -1: undo all transforms but don't clamp to range +// undo_transforms == -2: don't undo any transform +Status ModularGenericDecompress(BitReader *br, Image &image, + GroupHeader *header, size_t group_id, + ModularOptions *options, + int undo_transforms = -1, + const Tree *tree = nullptr, + const ANSCode *code = nullptr, + const std::vector *ctx_map = nullptr, + bool allow_truncated_group = false); +} // namespace jxl + +#endif // LIB_JXL_MODULAR_ENCODING_ENCODING_H_ diff --git a/lib/jxl/modular/encoding/ma_common.h b/lib/jxl/modular/encoding/ma_common.h new file mode 100644 index 0000000..71b7847 --- /dev/null +++ b/lib/jxl/modular/encoding/ma_common.h @@ -0,0 +1,28 @@ +// 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_MODULAR_ENCODING_MA_COMMON_H_ +#define LIB_JXL_MODULAR_ENCODING_MA_COMMON_H_ + +#include + +namespace jxl { + +enum MATreeContext : size_t { + kSplitValContext = 0, + kPropertyContext = 1, + kPredictorContext = 2, + kOffsetContext = 3, + kMultiplierLogContext = 4, + kMultiplierBitsContext = 5, + + kNumTreeContexts = 6, +}; + +static constexpr size_t kMaxTreeSize = 1 << 22; + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_ENCODING_MA_COMMON_H_ diff --git a/lib/jxl/modular/modular_image.cc b/lib/jxl/modular/modular_image.cc new file mode 100644 index 0000000..6c26b96 --- /dev/null +++ b/lib/jxl/modular/modular_image.cc @@ -0,0 +1,62 @@ +// 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/modular/modular_image.h" + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +void Image::undo_transforms(const weighted::Header &wp_header, int keep, + jxl::ThreadPool *pool) { + if (keep == -2) return; + while ((int)transform.size() > keep && transform.size() > 0) { + Transform t = transform.back(); + JXL_DEBUG_V(4, "Undoing transform %s", t.Name()); + Status result = t.Inverse(*this, wp_header, pool); + if (result == false) { + JXL_NOTIFY_ERROR("Error while undoing transform %s.", t.Name()); + error = true; + return; + } + JXL_DEBUG_V(8, "Undoing transform %s: done", t.Name()); + transform.pop_back(); + } + if (!keep && bitdepth < 32) { + // clamp the values to the valid range (lossy compression can produce values + // outside the range) + pixel_type maxval = (1u << bitdepth) - 1; + for (size_t i = 0; i < channel.size(); i++) { + for (size_t y = 0; y < channel[i].h; y++) { + pixel_type *JXL_RESTRICT p = channel[i].plane.Row(y); + for (size_t x = 0; x < channel[i].w; x++, p++) { + *p = Clamp1(*p, 0, maxval); + } + } + } + } +} + +Image::Image(size_t iw, size_t ih, int bd, int nb_chans) + : w(iw), h(ih), bitdepth(bd), nb_meta_channels(0), error(false) { + for (int i = 0; i < nb_chans; i++) channel.emplace_back(Channel(iw, ih)); +} + +Image::Image() : w(0), h(0), bitdepth(8), nb_meta_channels(0), error(true) {} + +Image &Image::operator=(Image &&other) noexcept { + w = other.w; + h = other.h; + bitdepth = other.bitdepth; + nb_meta_channels = other.nb_meta_channels; + error = other.error; + channel = std::move(other.channel); + transform = std::move(other.transform); + return *this; +} + +} // namespace jxl diff --git a/lib/jxl/modular/modular_image.h b/lib/jxl/modular/modular_image.h new file mode 100644 index 0000000..c418ba4 --- /dev/null +++ b/lib/jxl/modular/modular_image.h @@ -0,0 +1,107 @@ +// 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_MODULAR_MODULAR_IMAGE_H_ +#define LIB_JXL_MODULAR_MODULAR_IMAGE_H_ + +#include +#include +#include +#include + +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" + +namespace jxl { + +typedef int32_t pixel_type; // can use int16_t if it's only for 8-bit images. + // Need some wiggle room for YCoCg / Squeeze etc + +typedef int64_t pixel_type_w; + +namespace weighted { +struct Header; +} + +class Channel { + public: + jxl::Plane plane; + size_t w, h; + int hshift, vshift; // w ~= image.w >> hshift; h ~= image.h >> vshift + Channel(size_t iw, size_t ih, int hsh = 0, int vsh = 0) + : plane(iw, ih), w(iw), h(ih), hshift(hsh), vshift(vsh) {} + + Channel(const Channel& other) = delete; + Channel& operator=(const Channel& other) = delete; + + // Move assignment + Channel& operator=(Channel&& other) noexcept { + w = other.w; + h = other.h; + hshift = other.hshift; + vshift = other.vshift; + plane = std::move(other.plane); + return *this; + } + + // Move constructor + Channel(Channel&& other) noexcept = default; + + void shrink() { + if (plane.xsize() == w && plane.ysize() == h) return; + jxl::Plane resizedplane(w, h); + plane = std::move(resizedplane); + } + void shrink(int nw, int nh) { + w = nw; + h = nh; + shrink(); + } + + JXL_INLINE pixel_type* Row(const size_t y) { return plane.Row(y); } + JXL_INLINE const pixel_type* Row(const size_t y) const { + return plane.Row(y); + } +}; + +class Transform; + +class Image { + public: + // image data, transforms can dramatically change the number of channels and + // their semantics + std::vector channel; + // transforms that have been applied (and that have to be undone) + std::vector transform; + + // image dimensions (channels may have different dimensions due to transforms) + size_t w, h; + int bitdepth; + size_t nb_meta_channels; // first few channels might contain palette(s) + bool error; // true if a fatal error occurred, false otherwise + + Image(size_t iw, size_t ih, int bitdepth, int nb_chans); + Image(); + + Image(const Image& other) = delete; + Image& operator=(const Image& other) = delete; + + Image& operator=(Image&& other) noexcept; + Image(Image&& other) noexcept = default; + + // undo all except the first 'keep' transforms + void undo_transforms(const weighted::Header& wp_header, int keep = 0, + jxl::ThreadPool* pool = nullptr); +}; + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_MODULAR_IMAGE_H_ diff --git a/lib/jxl/modular/options.h b/lib/jxl/modular/options.h new file mode 100644 index 0000000..9b92d5f --- /dev/null +++ b/lib/jxl/modular/options.h @@ -0,0 +1,172 @@ +// 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_MODULAR_OPTIONS_H_ +#define LIB_JXL_MODULAR_OPTIONS_H_ + +#include + +#include +#include + +namespace jxl { + +using PropertyVal = int32_t; +using Properties = std::vector; + +enum class Predictor : uint32_t { + Zero = 0, + Left = 1, + Top = 2, + Average0 = 3, + Select = 4, + Gradient = 5, + Weighted = 6, + TopRight = 7, + TopLeft = 8, + LeftLeft = 9, + Average1 = 10, + Average2 = 11, + Average3 = 12, + Average4 = 13, + // The following predictors are encoder-only. + Best = 14, // Best of Gradient and Weighted + Variable = + 15, // Find the best decision tree for predictors/predictor per row +}; + +inline const char* PredictorName(Predictor p) { + switch (p) { + case Predictor::Zero: + return "Zero"; + case Predictor::Left: + return "Left"; + case Predictor::Top: + return "Top"; + case Predictor::Average0: + return "Avg0"; + case Predictor::Average1: + return "Avg1"; + case Predictor::Average2: + return "Avg2"; + case Predictor::Average3: + return "Avg3"; + case Predictor::Average4: + return "Avg4"; + case Predictor::Select: + return "Sel"; + case Predictor::Gradient: + return "Grd"; + case Predictor::Weighted: + return "Wgh"; + case Predictor::TopLeft: + return "TopL"; + case Predictor::TopRight: + return "TopR"; + case Predictor::LeftLeft: + return "LL"; + default: + return "INVALID"; + }; +} + +inline std::array PredictorColor(Predictor p) { + switch (p) { + case Predictor::Zero: + return {{0, 0, 0}}; + case Predictor::Left: + return {{255, 0, 0}}; + case Predictor::Top: + return {{0, 255, 0}}; + case Predictor::Average0: + return {{0, 0, 255}}; + case Predictor::Average4: + return {{192, 128, 128}}; + case Predictor::Select: + return {{255, 255, 0}}; + case Predictor::Gradient: + return {{255, 0, 255}}; + case Predictor::Weighted: + return {{0, 255, 255}}; + // TODO + default: + return {{255, 255, 255}}; + }; +} + +constexpr size_t kNumModularPredictors = static_cast(Predictor::Best); + +static constexpr ssize_t kNumStaticProperties = 2; // channel, group_id. + +using StaticPropRange = + std::array, kNumStaticProperties>; + +struct ModularMultiplierInfo { + StaticPropRange range; + uint32_t multiplier; +}; + +struct ModularOptions { + /// Used in both encode and decode: + + // Stop encoding/decoding when reaching a (non-meta) channel that has a + // dimension bigger than max_chan_size. + size_t max_chan_size = 0xFFFFFF; + + // Used during decoding for validation of transforms (sqeeezing) scheme. + size_t group_dim = 0x1FFFFFFF; + + /// Encode options: + // Fraction of pixels to look at to learn a MA tree + // Number of iterations to do to learn a MA tree + // (if zero there is no MA context model) + float nb_repeats = .5f; + + // Maximum number of (previous channel) properties to use in the MA trees + int max_properties = 0; // no previous channels + + // Alternative heuristic tweaks. + // Properties default to channel, group, weighted, gradient residual, W-NW, + // NW-N, N-NE, N-NN + std::vector splitting_heuristics_properties = {0, 1, 15, 9, + 10, 11, 12, 13}; + float splitting_heuristics_node_threshold = 96; + size_t max_property_values = 32; + + // Predictor to use for each channel. + Predictor predictor = static_cast(-1); + + int wp_mode = 0; + + float fast_decode_multiplier = 1.01f; + + // Forces the encoder to produce a tree that is compatible with the WP-only + // decode path (or with the no-wp path, or the gradient-only path). + enum class TreeMode { kGradientOnly, kWPOnly, kNoWP, kDefault }; + TreeMode wp_tree_mode = TreeMode::kDefault; + + // Skip fast paths in the encoder. + bool skip_encoder_fast_path = false; + + // Kind of tree to use. + // TODO(veluca): add tree kinds for JPEG recompression with CfL enabled, + // general AC metadata, different DC qualities, and others. + enum class TreeKind { + kLearn, + kJpegTranscodeACMeta, + kFalconACMeta, + kACMeta, + kWPFixedDC, + kGradientFixedDC, + }; + TreeKind tree_kind = TreeKind::kLearn; + + // Ignore the image and just pretend all tokens are zeroes + bool zero_tokens = false; +}; + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_OPTIONS_H_ diff --git a/lib/jxl/modular/transform/enc_palette.cc b/lib/jxl/modular/transform/enc_palette.cc new file mode 100644 index 0000000..3bf478c --- /dev/null +++ b/lib/jxl/modular/transform/enc_palette.cc @@ -0,0 +1,562 @@ +// 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/modular/transform/enc_palette.h" + +#include +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/enc_transform.h" +#include "lib/jxl/modular/transform/palette.h" + +namespace jxl { + +namespace palette_internal { + +static constexpr bool kEncodeToHighQualityImplicitPalette = true; + +// Inclusive. +static constexpr int kMinImplicitPaletteIndex = -(2 * 72 - 1); + +float ColorDistance(const std::vector &JXL_RESTRICT a, + const std::vector &JXL_RESTRICT b) { + JXL_ASSERT(a.size() == b.size()); + float distance = 0; + float ave3 = 0; + if (a.size() >= 3) { + ave3 = (a[0] + b[0] + a[1] + b[1] + a[2] + b[2]) * (1.21f / 3.0f); + } + float sum_a = 0, sum_b = 0; + for (size_t c = 0; c < a.size(); ++c) { + const float difference = + static_cast(a[c]) - static_cast(b[c]); + float weight = c == 0 ? 3 : c == 1 ? 5 : 2; + if (c < 3 && (a[c] + b[c] >= ave3)) { + const float add_w[3] = { + 1.15, + 1.15, + 1.12, + }; + weight += add_w[c]; + if (c == 2 && ((a[2] + b[2]) < 1.22 * ave3)) { + weight -= 0.5; + } + } + distance += difference * difference * weight * weight; + const int sum_weight = c == 0 ? 3 : c == 1 ? 5 : 1; + sum_a += a[c] * sum_weight; + sum_b += b[c] * sum_weight; + } + distance *= 4; + float sum_difference = sum_a - sum_b; + distance += sum_difference * sum_difference; + return distance; +} + +static int QuantizeColorToImplicitPaletteIndex( + const std::vector &color, const int palette_size, + const int bit_depth, bool high_quality) { + int index = 0; + if (high_quality) { + int multiplier = 1; + for (size_t c = 0; c < color.size(); c++) { + int quantized = ((kLargeCube - 1) * color[c] + (1 << (bit_depth - 1))) / + ((1 << bit_depth) - 1); + JXL_ASSERT((quantized % kLargeCube) == quantized); + index += quantized * multiplier; + multiplier *= kLargeCube; + } + return index + palette_size + kLargeCubeOffset; + } else { + int multiplier = 1; + for (size_t c = 0; c < color.size(); c++) { + int value = color[c]; + value -= 1 << (std::max(0, bit_depth - 3)); + value = std::max(0, value); + int quantized = ((kLargeCube - 1) * value + (1 << (bit_depth - 1))) / + ((1 << bit_depth) - 1); + JXL_ASSERT((quantized % kLargeCube) == quantized); + if (quantized > kSmallCube - 1) { + quantized = kSmallCube - 1; + } + index += quantized * multiplier; + multiplier *= kSmallCube; + } + return index + palette_size; + } +} + +} // namespace palette_internal + +int RoundInt(int value, int div) { // symmetric rounding around 0 + if (value < 0) return -RoundInt(-value, div); + return (value + div / 2) / div; +} + +struct PaletteIterationData { + static constexpr int kMaxDeltas = 128; + bool final_run = false; + std::vector deltas[3]; + std::vector delta_distances; + std::vector frequent_deltas[3]; + + // Populates `frequent_deltas` with items from `deltas` based on frequencies + // and color distances. + void FindFrequentColorDeltas(int num_pixels) { + using pixel_type_3d = std::array; + std::map delta_frequency_map; + // Store frequency weighted by delta distance from quantized value. + for (size_t i = 0; i < deltas[0].size(); ++i) { + pixel_type_3d delta = { + {RoundInt(deltas[0][i], 3), RoundInt(deltas[1][i], 3), + RoundInt(deltas[2][i], 3)}}; // a basic form of clustering + if (delta[0] == 0 && delta[1] == 0 && delta[2] == 0) continue; + delta_frequency_map[delta] += sqrt(sqrt(delta_distances[i])); + } + + // Weigh frequencies by magnitude and normalize. + for (auto &delta_frequency : delta_frequency_map) { + std::vector current_delta = {delta_frequency.first[0], + delta_frequency.first[1], + delta_frequency.first[2]}; + float delta_distance = + sqrt(palette_internal::ColorDistance({0, 0, 0}, current_delta)) + 1; + delta_frequency.second *= delta_distance / num_pixels; + } + + // Sort by weighted frequency. + using pixel_type_3d_frequency = std::pair; + std::vector sorted_delta_frequency_map( + delta_frequency_map.begin(), delta_frequency_map.end()); + std::sort( + sorted_delta_frequency_map.begin(), sorted_delta_frequency_map.end(), + [](const pixel_type_3d_frequency &a, const pixel_type_3d_frequency &b) { + return a.second > b.second; + }); + + // Store the top deltas. + for (auto &delta_frequency : sorted_delta_frequency_map) { + if (frequent_deltas[0].size() >= kMaxDeltas) break; + // Number obtained by optimizing on jyrki31 corpus: + if (delta_frequency.second < 17) break; + for (int c = 0; c < 3; ++c) { + frequent_deltas[c].push_back(delta_frequency.first[c] * 3); + } + } + } +}; + +Status FwdPaletteIteration(Image &input, uint32_t begin_c, uint32_t end_c, + uint32_t &nb_colors, uint32_t &nb_deltas, + bool ordered, bool lossy, Predictor &predictor, + const weighted::Header &wp_header, + PaletteIterationData &palette_iteration_data) { + JXL_QUIET_RETURN_IF_ERROR(CheckEqualChannels(input, begin_c, end_c)); + JXL_ASSERT(begin_c >= input.nb_meta_channels); + uint32_t nb = end_c - begin_c + 1; + + size_t w = input.channel[begin_c].w; + size_t h = input.channel[begin_c].h; + + if (!lossy && nb == 1) { + // Channel palette special case + if (nb_colors == 0) return false; + std::vector lookup; + pixel_type minval, maxval; + compute_minmax(input.channel[begin_c], &minval, &maxval); + size_t lookup_table_size = + static_cast(maxval) - static_cast(minval) + 1; + if (lookup_table_size > palette_internal::kMaxPaletteLookupTableSize) { + return false; // too large lookup table + } + lookup.resize(lookup_table_size, 0); + pixel_type idx = 0; + for (size_t y = 0; y < h; y++) { + const pixel_type *p = input.channel[begin_c].Row(y); + for (size_t x = 0; x < w; x++) { + if (lookup[p[x] - minval] == 0) { + lookup[p[x] - minval] = 1; + idx++; + if (idx > (int)nb_colors) return false; + } + } + } + JXL_DEBUG_V(6, "Channel %i uses only %i colors.", begin_c, idx); + Channel pch(idx, 1); + pch.hshift = -1; + nb_colors = idx; + idx = 0; + pixel_type *JXL_RESTRICT p_palette = pch.Row(0); + for (size_t i = 0; i < lookup_table_size; i++) { + if (lookup[i]) { + p_palette[idx] = i + minval; + lookup[i] = idx; + idx++; + } + } + for (size_t y = 0; y < h; y++) { + pixel_type *p = input.channel[begin_c].Row(y); + for (size_t x = 0; x < w; x++) p[x] = lookup[p[x] - minval]; + } + predictor = Predictor::Zero; + input.nb_meta_channels++; + input.channel.insert(input.channel.begin(), std::move(pch)); + return true; + } + + Image quantized_input; + if (lossy) { + quantized_input = Image(w, h, input.bitdepth, nb); + for (size_t c = 0; c < nb; c++) { + CopyImageTo(input.channel[begin_c + c].plane, + &quantized_input.channel[c].plane); + } + } + + JXL_DEBUG_V( + 7, "Trying to represent channels %i-%i using at most a %i-color palette.", + begin_c, end_c, nb_colors); + nb_deltas = 0; + bool delta_used = false; + std::set> + candidate_palette; // ordered lexicographically + std::vector> candidate_palette_imageorder; + std::vector color(nb); + std::vector color_with_error(nb); + std::vector p_in(nb); + + if (lossy) { + palette_iteration_data.FindFrequentColorDeltas(w * h); + nb_deltas = palette_iteration_data.frequent_deltas[0].size(); + + // Count color frequency for colors that make a cross. + std::map, size_t> color_freq_map; + for (size_t y = 1; y + 1 < h; y++) { + for (uint32_t c = 0; c < nb; c++) { + p_in[c] = input.channel[begin_c + c].Row(y); + } + for (size_t x = 1; x + 1 < w; x++) { + for (uint32_t c = 0; c < nb; c++) { + color[c] = p_in[c][x]; + } + int offsets[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + bool makes_cross = true; + for (int i = 0; i < 4 && makes_cross; ++i) { + int dx = offsets[i][0]; + int dy = offsets[i][1]; + for (uint32_t c = 0; c < nb && makes_cross; c++) { + if (input.channel[begin_c + c].Row(y + dy)[x + dx] != color[c]) { + makes_cross = false; + } + } + } + if (makes_cross) color_freq_map[color] += 1; + } + } + // Add colors satisfying frequency condition to the palette. + constexpr float kImageFraction = 0.01f; + size_t color_frequency_lower_bound = 5 + input.h * input.w * kImageFraction; + for (const auto &color_freq : color_freq_map) { + if (color_freq.second > color_frequency_lower_bound) { + candidate_palette.insert(color_freq.first); + candidate_palette_imageorder.push_back(color_freq.first); + } + } + } + + for (size_t y = 0; y < h; y++) { + for (uint32_t c = 0; c < nb; c++) { + p_in[c] = input.channel[begin_c + c].Row(y); + } + for (size_t x = 0; x < w; x++) { + if (lossy && candidate_palette.size() >= nb_colors) break; + for (uint32_t c = 0; c < nb; c++) { + color[c] = p_in[c][x]; + } + const bool new_color = candidate_palette.insert(color).second; + if (new_color) { + candidate_palette_imageorder.push_back(color); + } + if (candidate_palette.size() > nb_colors) { + return false; // too many colors + } + } + } + + nb_colors = nb_deltas + candidate_palette.size(); + JXL_DEBUG_V(6, "Channels %i-%i can be represented using a %i-color palette.", + begin_c, end_c, nb_colors); + + Channel pch(nb_colors, nb); + pch.hshift = -1; + pixel_type *JXL_RESTRICT p_palette = pch.Row(0); + intptr_t onerow = pch.plane.PixelsPerRow(); + intptr_t onerow_image = input.channel[begin_c].plane.PixelsPerRow(); + const int bit_depth = input.bitdepth; + + if (lossy) { + for (uint32_t i = 0; i < nb_deltas; i++) { + for (size_t c = 0; c < 3; c++) { + p_palette[c * onerow + i] = + palette_iteration_data.frequent_deltas[c][i]; + } + } + } + + int x = 0; + if (ordered) { + JXL_DEBUG_V(7, "Palette of %i colors, using lexicographic order", + nb_colors); + for (auto pcol : candidate_palette) { + JXL_DEBUG_V(9, " Color %i : ", x); + for (size_t i = 0; i < nb; i++) { + p_palette[nb_deltas + i * onerow + x] = pcol[i]; + } + for (size_t i = 0; i < nb; i++) { + JXL_DEBUG_V(9, "%i ", pcol[i]); + } + x++; + } + } else { + JXL_DEBUG_V(7, "Palette of %i colors, using image order", nb_colors); + for (auto pcol : candidate_palette_imageorder) { + JXL_DEBUG_V(9, " Color %i : ", x); + for (size_t i = 0; i < nb; i++) + p_palette[nb_deltas + i * onerow + x] = pcol[i]; + for (size_t i = 0; i < nb; i++) JXL_DEBUG_V(9, "%i ", pcol[i]); + x++; + } + } + std::vector wp_states; + for (size_t c = 0; c < nb; c++) { + wp_states.emplace_back(wp_header, w, h); + } + std::vector p_quant(nb); + // Three rows of error for dithering: y to y + 2. + // Each row has two pixels of padding in the ends, which is + // beneficial for both precision and encoding speed. + std::vector> error_row[3]; + if (lossy) { + for (int i = 0; i < 3; ++i) { + error_row[i].resize(nb); + for (size_t c = 0; c < nb; ++c) { + error_row[i][c].resize(w + 4); + } + } + } + for (size_t y = 0; y < h; y++) { + for (size_t c = 0; c < nb; c++) { + p_in[c] = input.channel[begin_c + c].Row(y); + if (lossy) p_quant[c] = quantized_input.channel[c].Row(y); + } + pixel_type *JXL_RESTRICT p = input.channel[begin_c].Row(y); + for (size_t x = 0; x < w; x++) { + int index; + if (!lossy) { + for (size_t c = 0; c < nb; c++) color[c] = p_in[c][x]; + // Exact search. + for (index = 0; static_cast(index) < nb_colors; index++) { + bool found = true; + for (size_t c = 0; c < nb; c++) { + if (color[c] != p_palette[c * onerow + index]) { + found = false; + break; + } + } + if (found) break; + } + if (index < static_cast(nb_deltas)) { + delta_used = true; + } + } else { + for (size_t c = 0; c < nb; c++) { + color_with_error[c] = p_in[c][x] + palette_iteration_data.final_run * + error_row[0][c][x + 2]; + color[c] = Clamp1(lroundf(color_with_error[c]), 0l, + (1l << input.bitdepth) - 1); + } + float best_distance = std::numeric_limits::infinity(); + int best_index = 0; + bool best_is_delta = false; + std::vector best_val(nb, 0); + std::vector ideal_residual(nb, 0); + std::vector quantized_val(nb); + std::vector predictions(nb); + for (size_t c = 0; c < nb; ++c) { + predictions[c] = PredictNoTreeWP(w, p_quant[c] + x, onerow_image, x, + y, predictor, &wp_states[c]) + .guess; + } + const auto TryIndex = [&](const int index) { + for (size_t c = 0; c < nb; c++) { + quantized_val[c] = palette_internal::GetPaletteValue( + p_palette, index, /*c=*/c, + /*palette_size=*/nb_colors, + /*onerow=*/onerow, /*bit_depth=*/bit_depth); + if (index < static_cast(nb_deltas)) { + quantized_val[c] += predictions[c]; + } + } + const float color_distance = + 32 * + palette_internal::ColorDistance(color_with_error, quantized_val); + float index_penalty = 0; + if (index == -1) { + index_penalty = -124; + } else if (index < static_cast(nb_colors)) { + index_penalty = 2 * std::abs(index); + } else if (index < static_cast(nb_colors) + + palette_internal::kLargeCubeOffset) { + index_penalty = 70; + } else { + index_penalty = 256; + } + index_penalty *= 1LL << std::max(2 * (bit_depth - 8), 0); + const float distance = color_distance + index_penalty; + if (distance < best_distance) { + best_distance = distance; + best_index = index; + best_is_delta = index < static_cast(nb_deltas); + best_val.swap(quantized_val); + for (size_t c = 0; c < nb; ++c) { + ideal_residual[c] = color_with_error[c] - predictions[c]; + } + } + }; + for (index = palette_internal::kMinImplicitPaletteIndex; + index < static_cast(nb_colors); index++) { + TryIndex(index); + } + TryIndex(palette_internal::QuantizeColorToImplicitPaletteIndex( + color, nb_colors, bit_depth, + /*high_quality=*/false)); + if (palette_internal::kEncodeToHighQualityImplicitPalette) { + TryIndex(palette_internal::QuantizeColorToImplicitPaletteIndex( + color, nb_colors, bit_depth, + /*high_quality=*/true)); + } + index = best_index; + delta_used |= best_is_delta; + if (!palette_iteration_data.final_run) { + for (size_t c = 0; c < 3; ++c) { + palette_iteration_data.deltas[c].push_back(ideal_residual[c]); + } + palette_iteration_data.delta_distances.push_back(best_distance); + } + + for (size_t c = 0; c < nb; ++c) { + wp_states[c].UpdateErrors(best_val[c], x, y, w); + p_quant[c][x] = best_val[c]; + } + float len_error = 0; + for (size_t c = 0; c < nb; ++c) { + float local_error = color_with_error[c] - best_val[c]; + len_error += local_error * local_error; + } + len_error = sqrt(len_error); + float modulate = 1.0; + int len_limit = 38 << std::max(0, bit_depth - 8); + if (len_error > len_limit) { + modulate *= len_limit / len_error; + } + for (size_t c = 0; c < nb; ++c) { + float local_error = (color_with_error[c] - best_val[c]); + float total_error = 0.65 * local_error; + + // If the neighboring pixels have some error in the opposite + // direction of total_error, cancel some or all of it out before + // spreading among them. + constexpr int offsets[12][2] = {{1, 2}, {0, 3}, {0, 4}, {1, 1}, + {1, 3}, {2, 2}, {1, 0}, {1, 4}, + {2, 1}, {2, 3}, {2, 0}, {2, 4}}; + float total_available = 0; + int n = 0; + for (int i = 0; i < 11; ++i) { + const int row = offsets[i][0]; + const int col = offsets[i][1]; + if (std::signbit(error_row[row][c][x + col]) != + std::signbit(total_error)) { + total_available += error_row[row][c][x + col]; + n++; + } + } + float weight = + std::abs(total_error) / (std::abs(total_available) + 1e-3); + weight = std::min(weight, 1.0f); + for (int i = 0; i < 11; ++i) { + const int row = offsets[i][0]; + const int col = offsets[i][1]; + if (std::signbit(error_row[row][c][x + col]) != + std::signbit(total_error)) { + total_error += weight * error_row[row][c][x + col]; + error_row[row][c][x + col] *= (1 - weight); + } + } + total_error *= modulate; + const float remaining_error = (1.0f / 14.) * total_error; + error_row[0][c][x + 3] += 2 * remaining_error; + error_row[0][c][x + 4] += remaining_error; + error_row[1][c][x + 0] += remaining_error; + for (int i = 0; i < 5; ++i) { + error_row[1][c][x + i] += remaining_error; + error_row[2][c][x + i] += remaining_error; + } + } + } + if (palette_iteration_data.final_run) p[x] = index; + } + if (lossy) { + for (size_t c = 0; c < nb; ++c) { + error_row[0][c].swap(error_row[1][c]); + error_row[1][c].swap(error_row[2][c]); + std::fill(error_row[2][c].begin(), error_row[2][c].end(), 0.f); + } + } + } + if (!delta_used) { + predictor = Predictor::Zero; + } + if (palette_iteration_data.final_run) { + input.nb_meta_channels++; + input.channel.erase(input.channel.begin() + begin_c + 1, + input.channel.begin() + end_c + 1); + input.channel.insert(input.channel.begin(), std::move(pch)); + } + nb_colors -= nb_deltas; + return true; +} + +Status FwdPalette(Image &input, uint32_t begin_c, uint32_t end_c, + uint32_t &nb_colors, uint32_t &nb_deltas, bool ordered, + 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; + bool status; + // TODO(iulia,jyrki): also handle 16-bit case + if ((lossy || nb != 1) && + input.bitdepth == 8) { // if no channel palette special case + status = FwdPaletteIteration(input, begin_c, end_c, nb_colors, nb_deltas, + ordered, lossy, predictor, wp_header, + palette_iteration_data); + } + palette_iteration_data.final_run = true; + nb_colors = nb_colors_orig; + nb_deltas = nb_deltas_orig; + status = + FwdPaletteIteration(input, begin_c, end_c, nb_colors, nb_deltas, ordered, + lossy, predictor, wp_header, palette_iteration_data); + return status; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/enc_palette.h b/lib/jxl/modular/transform/enc_palette.h new file mode 100644 index 0000000..0f3d668 --- /dev/null +++ b/lib/jxl/modular/transform/enc_palette.h @@ -0,0 +1,22 @@ +// 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_MODULAR_TRANSFORM_ENC_PALETTE_H_ +#define LIB_JXL_MODULAR_TRANSFORM_ENC_PALETTE_H_ + +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +Status FwdPalette(Image &input, uint32_t begin_c, uint32_t end_c, + uint32_t &nb_colors, uint32_t &nb_deltas, bool ordered, + bool lossy, Predictor &predictor, + const weighted::Header &wp_header); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_ENC_PALETTE_H_ diff --git a/lib/jxl/modular/transform/enc_rct.cc b/lib/jxl/modular/transform/enc_rct.cc new file mode 100644 index 0000000..81ba7e6 --- /dev/null +++ b/lib/jxl/modular/transform/enc_rct.cc @@ -0,0 +1,71 @@ +// 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/modular/transform/enc_rct.h" + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" // CheckEqualChannels + +namespace jxl { + +Status FwdRCT(Image& input, size_t begin_c, size_t rct_type) { + JXL_RETURN_IF_ERROR(CheckEqualChannels(input, begin_c, begin_c + 2)); + if (rct_type == 0) { // noop + return false; + } + // Permutation: 0=RGB, 1=GBR, 2=BRG, 3=RBG, 4=GRB, 5=BGR + int permutation = rct_type / 7; + // 0-5 values have the low bit corresponding to Third and the high bits + // corresponding to Second. 6 corresponds to YCoCg. + // + // Second: 0=nop, 1=SubtractFirst, 2=SubtractAvgFirstThird + // + // Third: 0=nop, 1=SubtractFirst + int custom = rct_type % 7; + size_t m = begin_c; + size_t w = input.channel[m + 0].w; + size_t h = input.channel[m + 0].h; + int second = (custom % 7) >> 1; + int third = (custom % 7) & 1; + for (size_t y = 0; y < h; y++) { + const pixel_type* in0 = input.channel[m + (permutation % 3)].Row(y); + const pixel_type* in1 = + input.channel[m + ((permutation + 1 + permutation / 3) % 3)].Row(y); + const pixel_type* in2 = + input.channel[m + ((permutation + 2 - permutation / 3) % 3)].Row(y); + pixel_type* out0 = input.channel[m].Row(y); + pixel_type* out1 = input.channel[m + 1].Row(y); + pixel_type* out2 = input.channel[m + 2].Row(y); + for (size_t x = 0; x < w; x++) { + if (custom == 6) { + pixel_type R = in0[x]; + pixel_type G = in1[x]; + pixel_type B = in2[x]; + out1[x] = R - B; + pixel_type tmp = B + (out1[x] >> 1); + out2[x] = G - tmp; + out0[x] = tmp + (out2[x] >> 1); + } else { + pixel_type First = in0[x]; + pixel_type Second = in1[x]; + pixel_type Third = in2[x]; + if (second == 1) { + Second = Second - First; + } else if (second == 2) { + Second = Second - ((First + Third) >> 1); + } + if (third) Third = Third - First; + out0[x] = First; + out1[x] = Second; + out2[x] = Third; + } + } + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/enc_rct.h b/lib/jxl/modular/transform/enc_rct.h new file mode 100644 index 0000000..8a41239 --- /dev/null +++ b/lib/jxl/modular/transform/enc_rct.h @@ -0,0 +1,17 @@ +// 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_MODULAR_TRANSFORM_ENC_RCT_H_ +#define LIB_JXL_MODULAR_TRANSFORM_ENC_RCT_H_ + +#include "lib/jxl/modular/modular_image.h" + +namespace jxl { + +Status FwdRCT(Image &input, size_t begin_c, size_t rct_type); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_ENC_RCT_H_ diff --git a/lib/jxl/modular/transform/enc_squeeze.cc b/lib/jxl/modular/transform/enc_squeeze.cc new file mode 100644 index 0000000..7a3219e --- /dev/null +++ b/lib/jxl/modular/transform/enc_squeeze.cc @@ -0,0 +1,140 @@ +// 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/modular/transform/enc_squeeze.h" + +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/squeeze.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +void FwdHSqueeze(Image &input, int c, int rc) { + const Channel &chin = input.channel[c]; + + JXL_DEBUG_V(4, "Doing horizontal squeeze of channel %i to new channel %i", c, + rc); + + Channel chout((chin.w + 1) / 2, chin.h, chin.hshift + 1, chin.vshift); + Channel chout_residual(chin.w - chout.w, chout.h, chin.hshift + 1, + chin.vshift); + + for (size_t y = 0; y < chout.h; y++) { + const pixel_type *JXL_RESTRICT p_in = chin.Row(y); + pixel_type *JXL_RESTRICT p_out = chout.Row(y); + pixel_type *JXL_RESTRICT p_res = chout_residual.Row(y); + for (size_t x = 0; x < chout_residual.w; x++) { + pixel_type A = p_in[x * 2]; + pixel_type B = p_in[x * 2 + 1]; + pixel_type avg = (A + B + (A > B)) >> 1; + p_out[x] = avg; + + pixel_type diff = A - B; + + pixel_type next_avg = avg; + if (x + 1 < chout_residual.w) { + next_avg = (p_in[x * 2 + 2] + p_in[x * 2 + 3] + + (p_in[x * 2 + 2] > p_in[x * 2 + 3])) >> + 1; // which will be chout.value(y,x+1) + } else if (chin.w & 1) + next_avg = p_in[x * 2 + 2]; + pixel_type left = (x > 0 ? p_in[x * 2 - 1] : avg); + pixel_type tendency = SmoothTendency(left, avg, next_avg); + + p_res[x] = diff - tendency; + } + if (chin.w & 1) { + int x = chout.w - 1; + p_out[x] = p_in[x * 2]; + } + } + input.channel[c] = std::move(chout); + input.channel.insert(input.channel.begin() + rc, std::move(chout_residual)); +} + +void FwdVSqueeze(Image &input, int c, int rc) { + const Channel &chin = input.channel[c]; + + JXL_DEBUG_V(4, "Doing vertical squeeze of channel %i to new channel %i", c, + rc); + + Channel chout(chin.w, (chin.h + 1) / 2, chin.hshift, chin.vshift + 1); + Channel chout_residual(chin.w, chin.h - chout.h, chin.hshift, + chin.vshift + 1); + intptr_t onerow_in = chin.plane.PixelsPerRow(); + for (size_t y = 0; y < chout_residual.h; y++) { + const pixel_type *JXL_RESTRICT p_in = chin.Row(y * 2); + pixel_type *JXL_RESTRICT p_out = chout.Row(y); + pixel_type *JXL_RESTRICT p_res = chout_residual.Row(y); + for (size_t x = 0; x < chout.w; x++) { + pixel_type A = p_in[x]; + pixel_type B = p_in[x + onerow_in]; + pixel_type avg = (A + B + (A > B)) >> 1; + p_out[x] = avg; + + pixel_type diff = A - B; + + pixel_type next_avg = avg; + if (y + 1 < chout_residual.h) { + next_avg = (p_in[x + 2 * onerow_in] + p_in[x + 3 * onerow_in] + + (p_in[x + 2 * onerow_in] > p_in[x + 3 * onerow_in])) >> + 1; // which will be chout.value(y+1,x) + } else if (chin.h & 1) { + next_avg = p_in[x + 2 * onerow_in]; + } + pixel_type top = + (y > 0 ? p_in[static_cast(x) - onerow_in] : avg); + pixel_type tendency = SmoothTendency(top, avg, next_avg); + + p_res[x] = diff - tendency; + } + } + if (chin.h & 1) { + size_t y = chout.h - 1; + const pixel_type *p_in = chin.Row(y * 2); + pixel_type *p_out = chout.Row(y); + for (size_t x = 0; x < chout.w; x++) { + p_out[x] = p_in[x]; + } + } + input.channel[c] = std::move(chout); + input.channel.insert(input.channel.begin() + rc, std::move(chout_residual)); +} + +Status FwdSqueeze(Image &input, std::vector parameters, + ThreadPool *pool) { + if (parameters.empty()) { + DefaultSqueezeParameters(¶meters, input); + } + + for (size_t i = 0; i < parameters.size(); 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(); + } + for (uint32_t c = beginc; c <= endc; c++) { + if (horizontal) { + FwdHSqueeze(input, c, offset + c - beginc); + } else { + FwdVSqueeze(input, c, offset + c - beginc); + } + } + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/enc_squeeze.h b/lib/jxl/modular/transform/enc_squeeze.h new file mode 100644 index 0000000..39b0010 --- /dev/null +++ b/lib/jxl/modular/transform/enc_squeeze.h @@ -0,0 +1,20 @@ +// 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_MODULAR_TRANSFORM_ENC_SQUEEZE_H_ +#define LIB_JXL_MODULAR_TRANSFORM_ENC_SQUEEZE_H_ + +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +Status FwdSqueeze(Image &input, std::vector parameters, + ThreadPool *pool); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_ENC_SQUEEZE_H_ diff --git a/lib/jxl/modular/transform/enc_transform.cc b/lib/jxl/modular/transform/enc_transform.cc new file mode 100644 index 0000000..b1d5d42 --- /dev/null +++ b/lib/jxl/modular/transform/enc_transform.cc @@ -0,0 +1,46 @@ +// 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/modular/transform/enc_transform.h" + +#include "lib/jxl/modular/transform/enc_palette.h" +#include "lib/jxl/modular/transform/enc_rct.h" +#include "lib/jxl/modular/transform/enc_squeeze.h" + +namespace jxl { + +Status TransformForward(Transform &t, Image &input, + const weighted::Header &wp_header, ThreadPool *pool) { + switch (t.id) { + case TransformId::kRCT: + return FwdRCT(input, t.begin_c, t.rct_type); + case TransformId::kSqueeze: + return FwdSqueeze(input, t.squeezes, pool); + case TransformId::kPalette: + return FwdPalette(input, t.begin_c, t.begin_c + t.num_c - 1, t.nb_colors, + t.nb_deltas, t.ordered_palette, t.lossy_palette, + t.predictor, wp_header); + default: + return JXL_FAILURE("Unknown transformation (ID=%u)", + static_cast(t.id)); + } +} + +void compute_minmax(const Channel &ch, pixel_type *min, pixel_type *max) { + pixel_type realmin = std::numeric_limits::max(); + pixel_type realmax = std::numeric_limits::min(); + for (size_t y = 0; y < ch.h; y++) { + const pixel_type *JXL_RESTRICT p = ch.Row(y); + for (size_t x = 0; x < ch.w; x++) { + if (p[x] < realmin) realmin = p[x]; + if (p[x] > realmax) realmax = p[x]; + } + } + + if (min) *min = realmin; + if (max) *max = realmax; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/enc_transform.h b/lib/jxl/modular/transform/enc_transform.h new file mode 100644 index 0000000..07659e1 --- /dev/null +++ b/lib/jxl/modular/transform/enc_transform.h @@ -0,0 +1,22 @@ +// 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_MODULAR_TRANSFORM_ENC_TRANSFORM_H_ +#define LIB_JXL_MODULAR_TRANSFORM_ENC_TRANSFORM_H_ + +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +Status TransformForward(Transform &t, Image &input, + const weighted::Header &wp_header, ThreadPool *pool); + +void compute_minmax(const Channel &ch, pixel_type *min, pixel_type *max); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_ENC_TRANSFORM_H_ diff --git a/lib/jxl/modular/transform/palette.h b/lib/jxl/modular/transform/palette.h new file mode 100644 index 0000000..c644ab8 --- /dev/null +++ b/lib/jxl/modular/transform/palette.h @@ -0,0 +1,319 @@ +// 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_MODULAR_TRANSFORM_PALETTE_H_ +#define LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" // CheckEqualChannels + +namespace jxl { + +namespace palette_internal { + +static constexpr int kMaxPaletteLookupTableSize = 1 << 16; + +static constexpr int kRgbChannels = 3; + +// 5x5x5 color cube for the larger cube. +static constexpr int kLargeCube = 5; + +// Smaller interleaved color cube to fill the holes of the larger cube. +static constexpr int kSmallCube = 4; +static constexpr int kSmallCubeBits = 2; +// kSmallCube ** 3 +static constexpr int kLargeCubeOffset = kSmallCube * kSmallCube * kSmallCube; + +static inline pixel_type Scale(uint64_t value, uint64_t bit_depth, + uint64_t denom) { + // return (value * ((static_cast(1) << bit_depth) - 1)) / denom; + // We only call this function with kSmallCube or kLargeCube - 1 as denom, + // allowing us to avoid a division here. + JXL_ASSERT(denom == 4); + return (value * ((static_cast(1) << bit_depth) - 1)) >> 2; +} + +// The purpose of this function is solely to extend the interpretation of +// palette indices to implicit values. If index < nb_deltas, indicating that the +// result is a delta palette entry, it is the responsibility of the caller to +// treat it as such. +static pixel_type GetPaletteValue(const pixel_type *const palette, int index, + const size_t c, const int palette_size, + const int onerow, const int bit_depth) { + if (index < 0) { + static constexpr std::array, 72> kDeltaPalette = { + { + {{0, 0, 0}}, {{4, 4, 4}}, {{11, 0, 0}}, + {{0, 0, -13}}, {{0, -12, 0}}, {{-10, -10, -10}}, + {{-18, -18, -18}}, {{-27, -27, -27}}, {{-18, -18, 0}}, + {{0, 0, -32}}, {{-32, 0, 0}}, {{-37, -37, -37}}, + {{0, -32, -32}}, {{24, 24, 45}}, {{50, 50, 50}}, + {{-45, -24, -24}}, {{-24, -45, -45}}, {{0, -24, -24}}, + {{-34, -34, 0}}, {{-24, 0, -24}}, {{-45, -45, -24}}, + {{64, 64, 64}}, {{-32, 0, -32}}, {{0, -32, 0}}, + {{-32, 0, 32}}, {{-24, -45, -24}}, {{45, 24, 45}}, + {{24, -24, -45}}, {{-45, -24, 24}}, {{80, 80, 80}}, + {{64, 0, 0}}, {{0, 0, -64}}, {{0, -64, -64}}, + {{-24, -24, 45}}, {{96, 96, 96}}, {{64, 64, 0}}, + {{45, -24, -24}}, {{34, -34, 0}}, {{112, 112, 112}}, + {{24, -45, -45}}, {{45, 45, -24}}, {{0, -32, 32}}, + {{24, -24, 45}}, {{0, 96, 96}}, {{45, -24, 24}}, + {{24, -45, -24}}, {{-24, -45, 24}}, {{0, -64, 0}}, + {{96, 0, 0}}, {{128, 128, 128}}, {{64, 0, 64}}, + {{144, 144, 144}}, {{96, 96, 0}}, {{-36, -36, 36}}, + {{45, -24, -45}}, {{45, -45, -24}}, {{0, 0, -96}}, + {{0, 128, 128}}, {{0, 96, 0}}, {{45, 24, -45}}, + {{-128, 0, 0}}, {{24, -45, 24}}, {{-45, 24, -45}}, + {{64, 0, -64}}, {{64, -64, -64}}, {{96, 0, 96}}, + {{45, -45, 24}}, {{24, 45, -45}}, {{64, 64, -64}}, + {{128, 128, 0}}, {{0, 0, -128}}, {{-24, 45, -45}}, + }}; + if (c >= kRgbChannels) { + return 0; + } + // Do not open the brackets, otherwise INT32_MIN negation could overflow. + index = -(index + 1); + index %= 1 + 2 * (kDeltaPalette.size() - 1); + static constexpr int kMultiplier[] = {-1, 1}; + pixel_type result = + kDeltaPalette[((index + 1) >> 1)][c] * kMultiplier[index & 1]; + if (bit_depth > 8) { + result *= static_cast(1) << (bit_depth - 8); + } + return result; + } else if (palette_size <= index && index < palette_size + kLargeCubeOffset) { + if (c >= kRgbChannels) return 0; + index -= palette_size; + index >>= c * kSmallCubeBits; + return Scale(index % kSmallCube, bit_depth, kSmallCube) + + (1 << (std::max(0, bit_depth - 3))); + } else if (palette_size + kLargeCubeOffset <= index) { + if (c >= kRgbChannels) return 0; + index -= palette_size + kLargeCubeOffset; + // TODO(eustas): should we take care of ambiguity created by + // index >= kLargeCube ** 3 ? + switch (c) { + case 0: + break; + case 1: + index /= kLargeCube; + break; + case 2: + index /= kLargeCube * kLargeCube; + break; + } + return Scale(index % kLargeCube, bit_depth, kLargeCube - 1); + } + return palette[c * onerow + static_cast(index)]; +} + +} // namespace palette_internal + +static Status InvPalette(Image &input, uint32_t begin_c, uint32_t nb_colors, + uint32_t nb_deltas, Predictor predictor, + const weighted::Header &wp_header, ThreadPool *pool) { + if (input.nb_meta_channels < 1) { + return JXL_FAILURE("Error: Palette transform without palette."); + } + std::atomic num_errors{0}; + int nb = input.channel[0].h; + uint32_t c0 = begin_c + 1; + if (c0 >= input.channel.size()) { + return JXL_FAILURE("Channel is out of range."); + } + size_t w = input.channel[c0].w; + size_t h = input.channel[c0].h; + if (nb < 1) return JXL_FAILURE("Corrupted transforms"); + for (int i = 1; i < nb; i++) { + input.channel.insert( + input.channel.begin() + c0 + 1, + Channel(w, h, input.channel[c0].hshift, input.channel[c0].vshift)); + } + const Channel &palette = input.channel[0]; + const pixel_type *JXL_RESTRICT p_palette = input.channel[0].Row(0); + intptr_t onerow = input.channel[0].plane.PixelsPerRow(); + intptr_t onerow_image = input.channel[c0].plane.PixelsPerRow(); + const int bit_depth = input.bitdepth; + + if (w == 0) { + // Nothing to do. + // Avoid touching "empty" channels with non-zero height. + } else if (nb_deltas == 0 && predictor == Predictor::Zero) { + if (nb == 1) { + RunOnPool( + pool, 0, h, ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + pixel_type *p = input.channel[c0].Row(y); + for (size_t x = 0; x < w; x++) { + const int index = Clamp1(p[x], 0, (pixel_type)palette.w - 1); + p[x] = palette_internal::GetPaletteValue( + p_palette, index, /*c=*/0, + /*palette_size=*/palette.w, + /*onerow=*/onerow, /*bit_depth=*/bit_depth); + } + }, + "UndoChannelPalette"); + } else { + RunOnPool( + pool, 0, h, ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + std::vector p_out(nb); + const pixel_type *p_index = input.channel[c0].Row(y); + for (int c = 0; c < nb; c++) + p_out[c] = input.channel[c0 + c].Row(y); + for (size_t x = 0; x < w; x++) { + const int index = p_index[x]; + for (int c = 0; c < nb; c++) { + p_out[c][x] = palette_internal::GetPaletteValue( + p_palette, index, /*c=*/c, + /*palette_size=*/palette.w, + /*onerow=*/onerow, /*bit_depth=*/bit_depth); + } + } + }, + "UndoPalette"); + } + } else { + // Parallelized per channel. + ImageI indices = CopyImage(input.channel[c0].plane); + if (predictor == Predictor::Weighted) { + RunOnPool( + pool, 0, nb, ThreadPool::SkipInit(), + [&](size_t c, size_t _) { + Channel &channel = input.channel[c0 + c]; + weighted::State wp_state(wp_header, channel.w, channel.h); + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT p = channel.Row(y); + const pixel_type *JXL_RESTRICT idx = indices.Row(y); + for (size_t x = 0; x < channel.w; x++) { + int index = idx[x]; + pixel_type_w val = 0; + const pixel_type palette_entry = + palette_internal::GetPaletteValue( + p_palette, index, /*c=*/c, + /*palette_size=*/palette.w, /*onerow=*/onerow, + /*bit_depth=*/bit_depth); + if (index < static_cast(nb_deltas)) { + PredictionResult pred = + PredictNoTreeWP(channel.w, p + x, onerow_image, x, y, + predictor, &wp_state); + val = pred.guess + palette_entry; + } else { + val = palette_entry; + } + p[x] = val; + wp_state.UpdateErrors(p[x], x, y, channel.w); + } + } + }, + "UndoDeltaPaletteWP"); + } else if (predictor == Predictor::Gradient) { + // Gradient is the most common predictor for now. This special case gives + // about 20% extra speed. + RunOnPool( + pool, 0, nb, ThreadPool::SkipInit(), + [&](size_t c, size_t _) { + Channel &channel = input.channel[c0 + c]; + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT p = channel.Row(y); + const pixel_type *JXL_RESTRICT idx = indices.Row(y); + for (size_t x = 0; x < channel.w; x++) { + int index = idx[x]; + pixel_type val = 0; + const pixel_type palette_entry = + palette_internal::GetPaletteValue( + p_palette, index, /*c=*/c, + /*palette_size=*/palette.w, + /*onerow=*/onerow, /*bit_depth=*/bit_depth); + if (index < static_cast(nb_deltas)) { + pixel_type left = + x ? p[x - 1] : (y ? *(p + x - onerow_image) : 0); + pixel_type top = y ? *(p + x - onerow_image) : left; + pixel_type topleft = + x && y ? *(p + x - 1 - onerow_image) : left; + val = PixelAdd(ClampedGradient(left, top, topleft), + palette_entry); + } else { + val = palette_entry; + } + p[x] = val; + } + } + }, + "UndoDeltaPaletteGradient"); + } else { + RunOnPool( + pool, 0, nb, ThreadPool::SkipInit(), + [&](size_t c, size_t _) { + Channel &channel = input.channel[c0 + c]; + for (size_t y = 0; y < channel.h; y++) { + pixel_type *JXL_RESTRICT p = channel.Row(y); + const pixel_type *JXL_RESTRICT idx = indices.Row(y); + for (size_t x = 0; x < channel.w; x++) { + int index = idx[x]; + pixel_type_w val = 0; + const pixel_type palette_entry = + palette_internal::GetPaletteValue( + p_palette, index, /*c=*/c, + /*palette_size=*/palette.w, + /*onerow=*/onerow, /*bit_depth=*/bit_depth); + if (index < static_cast(nb_deltas)) { + PredictionResult pred = PredictNoTreeNoWP( + channel.w, p + x, onerow_image, x, y, predictor); + val = pred.guess + palette_entry; + } else { + val = palette_entry; + } + p[x] = val; + } + } + }, + "UndoDeltaPaletteNoWP"); + } + } + if (c0 >= input.nb_meta_channels) { + // Palette was done on normal channels + input.nb_meta_channels--; + } else { + // Palette was done on metachannels + JXL_ASSERT(static_cast(input.nb_meta_channels) >= 2 - nb); + input.nb_meta_channels -= 2 - nb; + JXL_ASSERT(begin_c + nb - 1 < input.nb_meta_channels); + } + input.channel.erase(input.channel.begin(), input.channel.begin() + 1); + return num_errors.load(std::memory_order_relaxed) == 0; +} + +static Status MetaPalette(Image &input, uint32_t begin_c, uint32_t end_c, + uint32_t nb_colors, uint32_t nb_deltas, bool lossy) { + JXL_RETURN_IF_ERROR(CheckEqualChannels(input, begin_c, end_c)); + + size_t nb = end_c - begin_c + 1; + if (begin_c >= input.nb_meta_channels) { + // Palette was done on normal channels + input.nb_meta_channels++; + } else { + // Palette was done on metachannels + JXL_ASSERT(end_c < input.nb_meta_channels); + // we remove nb-1 metachannels and add one + input.nb_meta_channels += 2 - nb; + } + input.channel.erase(input.channel.begin() + begin_c + 1, + input.channel.begin() + end_c + 1); + Channel pch(nb_colors + nb_deltas, nb); + pch.hshift = -1; + input.channel.insert(input.channel.begin(), std::move(pch)); + return true; +} + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_PALETTE_H_ diff --git a/lib/jxl/modular/transform/rct.h b/lib/jxl/modular/transform/rct.h new file mode 100644 index 0000000..6af36a0 --- /dev/null +++ b/lib/jxl/modular/transform/rct.h @@ -0,0 +1,107 @@ +// 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_MODULAR_TRANSFORM_RCT_H_ +#define LIB_JXL_MODULAR_TRANSFORM_RCT_H_ + +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" // CheckEqualChannels + +namespace jxl { + +template +void InvRCTRow(const pixel_type* in0, const pixel_type* in1, + const pixel_type* in2, pixel_type* out0, pixel_type* out1, + pixel_type* out2, size_t w) { + static_assert(transform_type >= 0 && transform_type < 7, + "Invalid transform type"); + int second = transform_type >> 1; + int third = transform_type & 1; + for (size_t x = 0; x < w; x++) { + if (transform_type == 6) { + pixel_type Y = in0[x]; + pixel_type Co = in1[x]; + pixel_type Cg = in2[x]; + pixel_type tmp = PixelAdd(Y, -(Cg >> 1)); + pixel_type G = PixelAdd(Cg, tmp); + pixel_type B = PixelAdd(tmp, -(Co >> 1)); + pixel_type R = PixelAdd(B, Co); + out0[x] = R; + out1[x] = G; + out2[x] = B; + } else { + pixel_type First = in0[x]; + pixel_type Second = in1[x]; + pixel_type Third = in2[x]; + if (third) Third = PixelAdd(Third, First); + if (second == 1) { + Second = PixelAdd(Second, First); + } else if (second == 2) { + Second = PixelAdd(Second, (PixelAdd(First, Third) >> 1)); + } + out0[x] = First; + out1[x] = Second; + out2[x] = Third; + } + } +} + +Status InvRCT(Image& input, size_t begin_c, size_t rct_type, ThreadPool* pool) { + JXL_RETURN_IF_ERROR(CheckEqualChannels(input, begin_c, begin_c + 2)); + size_t m = begin_c; + Channel& c0 = input.channel[m + 0]; + size_t w = c0.w; + size_t h = c0.h; + if (rct_type == 0) { // noop + return true; + } + // Permutation: 0=RGB, 1=GBR, 2=BRG, 3=RBG, 4=GRB, 5=BGR + int permutation = rct_type / 7; + JXL_CHECK(permutation < 6); + // 0-5 values have the low bit corresponding to Third and the high bits + // corresponding to Second. 6 corresponds to YCoCg. + // + // Second: 0=nop, 1=SubtractFirst, 2=SubtractAvgFirstThird + // + // Third: 0=nop, 1=SubtractFirst + int custom = rct_type % 7; + // Special case: permute-only. Swap channels around. + if (custom == 0) { + Channel ch0 = std::move(input.channel[m]); + Channel ch1 = std::move(input.channel[m + 1]); + Channel ch2 = std::move(input.channel[m + 2]); + input.channel[m + (permutation % 3)] = std::move(ch0); + input.channel[m + ((permutation + 1 + permutation / 3) % 3)] = + std::move(ch1); + input.channel[m + ((permutation + 2 - permutation / 3) % 3)] = + std::move(ch2); + return true; + } + constexpr decltype(&InvRCTRow<0>) inv_rct_row[] = { + InvRCTRow<0>, InvRCTRow<1>, InvRCTRow<2>, InvRCTRow<3>, + InvRCTRow<4>, InvRCTRow<5>, InvRCTRow<6>}; + RunOnPool( + pool, 0, h, ThreadPool::SkipInit(), + [&](const int task, const int thread) { + const size_t y = task; + const pixel_type* in0 = input.channel[m].Row(y); + const pixel_type* in1 = input.channel[m + 1].Row(y); + const pixel_type* in2 = input.channel[m + 2].Row(y); + pixel_type* out0 = input.channel[m + (permutation % 3)].Row(y); + pixel_type* out1 = + input.channel[m + ((permutation + 1 + permutation / 3) % 3)].Row(y); + pixel_type* out2 = + input.channel[m + ((permutation + 2 - permutation / 3) % 3)].Row(y); + inv_rct_row[custom](in0, in1, in2, out0, out1, out2, w); + }, + "InvRCT"); + return true; +} + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_RCT_H_ diff --git a/lib/jxl/modular/transform/squeeze.cc b/lib/jxl/modular/transform/squeeze.cc new file mode 100644 index 0000000..3edbfc9 --- /dev/null +++ b/lib/jxl/modular/transform/squeeze.cc @@ -0,0 +1,329 @@ +// 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/modular/transform/squeeze.h" + +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" + +namespace jxl { + +void 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]; + 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)); + JXL_ASSERT(chin.h == chin_residual.h); + + if (chin_residual.w == 0) { + // Short-circuit: output channel has same dimensions as input. + input.channel[c].hshift--; + return; + } + + // Note: chin.w >= chin_residual.w and at most 1 different. + Channel chout(chin.w + chin_residual.w, chin.h, chin.hshift - 1, chin.vshift); + JXL_DEBUG_V(4, + "Undoing horizontal squeeze of channel %i using residuals in " + "channel %i (going from width %zu to %zu)", + c, rc, chin.w, chout.w); + + if (chin_residual.h == 0) { + // Short-circuit: channel with no pixels. + input.channel[c] = std::move(chout); + return; + } + + RunOnPool( + pool, 0, chin.h, ThreadPool::SkipInit(), + [&](const int task, const int 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); + + // 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 (chout.w & 1) p_out[chout.w - 1] = p_avg[chin.w - 1]; + }, + "InvHorizontalSqueeze"); + input.channel[c] = std::move(chout); +} + +void InvVSqueeze(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]; + const Channel &chin_residual = input.channel[rc]; + // These must be valid since we ran MetaApply already. + JXL_ASSERT(chin.h == DivCeil(chin.h + chin_residual.h, 2)); + JXL_ASSERT(chin.w == chin_residual.w); + + if (chin_residual.h == 0) { + // Short-circuit: output channel has same dimensions as input. + input.channel[c].vshift--; + return; + } + + // Note: chin.h >= chin_residual.h and at most 1 different. + Channel chout(chin.w, chin.h + chin_residual.h, chin.hshift, chin.vshift - 1); + JXL_DEBUG_V( + 4, + "Undoing vertical squeeze of channel %i using residuals in channel " + "%i (going from height %zu to %zu)", + c, rc, chin.h, chout.h); + + if (chin_residual.w == 0) { + // Short-circuit: channel with no pixels. + input.channel[c] = std::move(chout); + return; + } + + intptr_t onerow_in = chin.plane.PixelsPerRow(); + intptr_t onerow_out = chout.plane.PixelsPerRow(); + constexpr int kColsPerThread = 64; + RunOnPool( + pool, 0, DivCeil(chin.w, kColsPerThread), ThreadPool::SkipInit(), + [&](const int task, const int 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(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"); + + if (chout.h & 1) { + size_t y = chin.h - 1; + const pixel_type *p_avg = chin.Row(y); + pixel_type *p_out = chout.Row(y << 1); + for (size_t x = 0; x < chin.w; x++) { + p_out[x] = p_avg[x]; + } + } + input.channel[c] = std::move(chout); +} + +void DefaultSqueezeParameters(std::vector *parameters, + const Image &image) { + int nb_channels = image.channel.size() - image.nb_meta_channels; + + parameters->clear(); + size_t w = image.channel[image.nb_meta_channels].w; + size_t h = image.channel[image.nb_meta_channels].h; + JXL_DEBUG_V(7, "Default squeeze parameters for %zux%zu image: ", w, h); + + // do horizontal first on wide images; vertical first on tall images + bool wide = (w > h); + + if (nb_channels > 2 && image.channel[image.nb_meta_channels + 1].w == w && + image.channel[image.nb_meta_channels + 1].h == h) { + // assume channels 1 and 2 are chroma, and can be squeezed first for 4:2:0 + // previews + JXL_DEBUG_V(7, "(4:2:0 chroma), %zux%zu image", w, h); + SqueezeParams params; + // horizontal chroma squeeze + params.horizontal = true; + params.in_place = false; + params.begin_c = image.nb_meta_channels + 1; + params.num_c = 2; + parameters->push_back(params); + params.horizontal = false; + // vertical chroma squeeze + parameters->push_back(params); + } + SqueezeParams params; + params.begin_c = image.nb_meta_channels; + params.num_c = nb_channels; + params.in_place = true; + + if (!wide) { + if (h > JXL_MAX_FIRST_PREVIEW_SIZE) { + params.horizontal = false; + parameters->push_back(params); + h = (h + 1) / 2; + JXL_DEBUG_V(7, "Vertical (%zux%zu), ", w, h); + } + } + while (w > JXL_MAX_FIRST_PREVIEW_SIZE || h > JXL_MAX_FIRST_PREVIEW_SIZE) { + if (w > JXL_MAX_FIRST_PREVIEW_SIZE) { + params.horizontal = true; + parameters->push_back(params); + w = (w + 1) / 2; + JXL_DEBUG_V(7, "Horizontal (%zux%zu), ", w, h); + } + if (h > JXL_MAX_FIRST_PREVIEW_SIZE) { + params.horizontal = false; + parameters->push_back(params); + h = (h + 1) / 2; + JXL_DEBUG_V(7, "Vertical (%zux%zu), ", w, h); + } + } + JXL_DEBUG_V(7, "that's it"); +} + +Status CheckMetaSqueezeParams(const SqueezeParams ¶meter, + int num_channels) { + int c1 = parameter.begin_c; + int c2 = parameter.begin_c + parameter.num_c - 1; + if (c1 < 0 || c1 >= num_channels || c2 < 0 || c2 >= num_channels || c2 < c1) { + return JXL_FAILURE("Invalid channel range"); + } + return true; +} + +Status MetaSqueeze(Image &image, std::vector *parameters) { + if (parameters->empty()) { + DefaultSqueezeParameters(parameters, image); + } + + for (size_t i = 0; i < parameters->size(); i++) { + JXL_RETURN_IF_ERROR( + CheckMetaSqueezeParams((*parameters)[i], image.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 (beginc < image.nb_meta_channels) { + if (endc >= image.nb_meta_channels) { + return JXL_FAILURE("Invalid squeeze: mix of meta and nonmeta channels"); + } + if (!in_place) + return JXL_FAILURE( + "Invalid squeeze: meta channels require in-place residuals"); + image.nb_meta_channels += (*parameters)[i].num_c; + } + if (in_place) { + offset = endc + 1; + } else { + offset = image.channel.size(); + } + for (uint32_t c = beginc; c <= endc; c++) { + if (image.channel[c].hshift > 30 || image.channel[c].vshift > 30) { + return JXL_FAILURE("Too many squeezes: shift > 30"); + } + size_t w = image.channel[c].w; + size_t h = image.channel[c].h; + if (horizontal) { + image.channel[c].w = (w + 1) / 2; + image.channel[c].hshift++; + w = w - (w + 1) / 2; + } else { + image.channel[c].h = (h + 1) / 2; + image.channel[c].vshift++; + h = h - (h + 1) / 2; + } + image.channel[c].shrink(); + Channel dummy(w, h); + dummy.hshift = image.channel[c].hshift; + dummy.vshift = image.channel[c].vshift; + + image.channel.insert(image.channel.begin() + offset + (c - beginc), + std::move(dummy)); + } + } + return true; +} + +Status InvSqueeze(Image &input, std::vector parameters, + ThreadPool *pool) { + if (parameters.empty()) { + DefaultSqueezeParameters(¶meters, input); + } + + 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) { + InvHSqueeze(input, c, rc, pool); + } else { + InvVSqueeze(input, c, rc, pool); + } + } + input.channel.erase(input.channel.begin() + offset, + input.channel.begin() + offset + (endc - beginc + 1)); + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/squeeze.h b/lib/jxl/modular/transform/squeeze.h new file mode 100644 index 0000000..a2d3afd --- /dev/null +++ b/lib/jxl/modular/transform/squeeze.h @@ -0,0 +1,94 @@ +// 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_MODULAR_TRANSFORM_SQUEEZE_H_ +#define LIB_JXL_MODULAR_TRANSFORM_SQUEEZE_H_ + +// Haar-like transform: halves the resolution in one direction +// A B -> (A+B)>>1 in one channel (average) -> same range as +// original channel +// A-B - tendency in a new channel ('residual' needed to make +// the transform reversible) +// -> theoretically range could be 2.5 +// times larger (2 times without the +// 'tendency'), but there should be lots +// of zeroes +// Repeated application (alternating horizontal and vertical squeezes) results +// in downscaling +// +// The default coefficient ordering is low-frequency to high-frequency, as in +// M. Antonini, M. Barlaud, P. Mathieu and I. Daubechies, "Image coding using +// wavelet transform", IEEE Transactions on Image Processing, vol. 1, no. 2, pp. +// 205-220, April 1992, doi: 10.1109/83.136597. + +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/common.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/transform.h" + +#define JXL_MAX_FIRST_PREVIEW_SIZE 8 + +namespace jxl { + +/* + int avg=(A+B)>>1; + int diff=(A-B); + int rA=(diff+(avg<<1)+(diff&1))>>1; + int rB=rA-diff; + +*/ +// |A B|C D|E F| +// p a n p=avg(A,B), a=avg(C,D), n=avg(E,F) +// +// Goal: estimate C-D (avoiding ringing artifacts) +// (ensuring that in smooth areas, a zero residual corresponds to a smooth +// gradient) + +// best estimate for C: (B + 2*a)/3 +// best estimate for D: (n + 3*a)/4 +// best estimate for C-D: 4*B - 3*n - a /12 + +// avoid ringing by 1) only doing this if B <= a <= n or B >= a >= n +// (otherwise, this is not a smooth area and we cannot really estimate C-D) +// 2) making sure that B <= C <= D <= n or B >= C >= D >= n + +inline pixel_type_w SmoothTendency(pixel_type_w B, pixel_type_w a, + pixel_type_w n) { + pixel_type_w diff = 0; + if (B >= a && a >= n) { + diff = (4 * B - 3 * n - a + 6) / 12; + // 2C = a<<1 + diff - diff&1 <= 2B so diff - diff&1 <= 2B - 2a + // 2D = a<<1 - diff - diff&1 >= 2n so diff + diff&1 <= 2a - 2n + if (diff - (diff & 1) > 2 * (B - a)) diff = 2 * (B - a) + 1; + if (diff + (diff & 1) > 2 * (a - n)) diff = 2 * (a - n); + } else if (B <= a && a <= n) { + diff = (4 * B - 3 * n - a - 6) / 12; + // 2C = a<<1 + diff + diff&1 >= 2B so diff + diff&1 >= 2B - 2a + // 2D = a<<1 - diff + diff&1 <= 2n so diff - diff&1 >= 2a - 2n + if (diff + (diff & 1) < 2 * (B - a)) diff = 2 * (B - a) - 1; + if (diff - (diff & 1) < 2 * (a - n)) diff = 2 * (a - n); + } + 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 *parameters, + const Image &image); + +Status CheckMetaSqueezeParams(const SqueezeParams ¶meter, int num_channels); + +Status MetaSqueeze(Image &image, std::vector *parameters); + +Status InvSqueeze(Image &input, std::vector parameters, + ThreadPool *pool); + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_SQUEEZE_H_ diff --git a/lib/jxl/modular/transform/transform.cc b/lib/jxl/modular/transform/transform.cc new file mode 100644 index 0000000..55960f9 --- /dev/null +++ b/lib/jxl/modular/transform/transform.cc @@ -0,0 +1,98 @@ +// 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/modular/transform/transform.h" + +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/modular_image.h" +#include "lib/jxl/modular/transform/palette.h" +#include "lib/jxl/modular/transform/rct.h" +#include "lib/jxl/modular/transform/squeeze.h" + +namespace jxl { + +SqueezeParams::SqueezeParams() { Bundle::Init(this); } +Transform::Transform(TransformId id) { + Bundle::Init(this); + this->id = id; +} + +Status Transform::Inverse(Image &input, const weighted::Header &wp_header, + ThreadPool *pool) { + JXL_DEBUG_V(6, "Input channels (%zu, %zu meta): ", input.channel.size(), + input.nb_meta_channels); + switch (id) { + case TransformId::kRCT: + return InvRCT(input, begin_c, rct_type, pool); + case TransformId::kSqueeze: + return InvSqueeze(input, squeezes, pool); + case TransformId::kPalette: + return InvPalette(input, begin_c, nb_colors, nb_deltas, predictor, + wp_header, pool); + default: + return JXL_FAILURE("Unknown transformation (ID=%u)", + static_cast(id)); + } +} + +Status Transform::MetaApply(Image &input) { + JXL_DEBUG_V(6, "Input channels (%zu, %zu meta): ", input.channel.size(), + input.nb_meta_channels); + switch (id) { + case TransformId::kRCT: + JXL_DEBUG_V(2, "Transform: kRCT, rct_type=%" PRIu32, rct_type); + return CheckEqualChannels(input, begin_c, begin_c + 2); + case TransformId::kSqueeze: + JXL_DEBUG_V(2, "Transform: kSqueeze:"); +#if JXL_DEBUG_V_LEVEL >= 2 + { + auto squeezes_copy = squeezes; + if (squeezes_copy.empty()) { + DefaultSqueezeParameters(&squeezes_copy, input); + } + for (const auto ¶ms : squeezes_copy) { + JXL_DEBUG_V( + 2, + " squeeze params: horizontal=%d, in_place=%d, begin_c=%" PRIu32 + ", num_c=%" PRIu32, + params.horizontal, params.in_place, params.begin_c, params.num_c); + } + } +#endif + return MetaSqueeze(input, &squeezes); + case TransformId::kPalette: + JXL_DEBUG_V(2, + "Transform: kPalette, begin_c=%" PRIu32 ", num_c=%" PRIu32 + ", nb_colors=%" PRIu32 ", nb_deltas=%" PRIu32, + begin_c, num_c, nb_colors, nb_deltas); + return MetaPalette(input, begin_c, begin_c + num_c - 1, nb_colors, + nb_deltas, lossy_palette); + default: + return JXL_FAILURE("Unknown transformation (ID=%u)", + static_cast(id)); + } +} + +Status CheckEqualChannels(const Image &image, uint32_t c1, uint32_t c2) { + if (c1 > image.channel.size() || c2 >= image.channel.size() || c2 < c1) { + return JXL_FAILURE( + "Invalid channel range: %u..%u (there are only %zu channels)", c1, c2, + image.channel.size()); + } + if (c1 < image.nb_meta_channels && c2 >= image.nb_meta_channels) { + return JXL_FAILURE("Invalid: transforming mix of meta and nonmeta"); + } + const auto &ch1 = image.channel[c1]; + for (size_t c = c1 + 1; c <= c2; c++) { + const auto &ch2 = image.channel[c]; + if (ch1.w != ch2.w || ch1.h != ch2.h || ch1.hshift != ch2.hshift || + ch1.vshift != ch2.vshift) { + return false; + } + } + return true; +} + +} // namespace jxl diff --git a/lib/jxl/modular/transform/transform.h b/lib/jxl/modular/transform/transform.h new file mode 100644 index 0000000..0562d2f --- /dev/null +++ b/lib/jxl/modular/transform/transform.h @@ -0,0 +1,148 @@ +// 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_MODULAR_TRANSFORM_TRANSFORM_H_ +#define LIB_JXL_MODULAR_TRANSFORM_TRANSFORM_H_ + +#include +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/modular/encoding/context_predict.h" +#include "lib/jxl/modular/options.h" + +namespace jxl { + +enum class TransformId : uint32_t { + // G, R-G, B-G and variants (including YCoCg). + kRCT = 0, + + // Color palette. Parameters are: [begin_c] [end_c] [nb_colors] + kPalette = 1, + + // Squeezing (Haar-style) + kSqueeze = 2, + + // Invalid for now. + kInvalid = 3, +}; + +struct SqueezeParams : public Fields { + const char *Name() const override { return "SqueezeParams"; } + bool horizontal; + bool in_place; + uint32_t begin_c; + uint32_t num_c; + SqueezeParams(); + Status VisitFields(Visitor *JXL_RESTRICT visitor) override { + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &horizontal)); + JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &in_place)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Bits(3), BitsOffset(6, 8), + BitsOffset(10, 72), + BitsOffset(13, 1096), 0, &begin_c)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(2), Val(3), BitsOffset(4, 4), 2, &num_c)); + return true; + } +}; + +class Transform : public Fields { + public: + TransformId id; + // for Palette and RCT. + uint32_t begin_c; + // for RCT. 42 possible values starting from 0. + uint32_t rct_type; + // Only for Palette and NearLossless. + uint32_t num_c; + // Only for Palette. + uint32_t nb_colors; + uint32_t nb_deltas; + // for Squeeze. Default squeeze if empty. + std::vector squeezes; + // for NearLossless, not serialized. + int max_delta_error; + // Serialized for Palette. + Predictor predictor; + // for Palette, not serialized. + bool ordered_palette = true; + bool lossy_palette = false; + + explicit Transform(TransformId id); + // default constructor for bundles. + Transform() : Transform(TransformId::kInvalid) {} + + Status VisitFields(Visitor *JXL_RESTRICT visitor) override { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + Val((uint32_t)TransformId::kRCT), Val((uint32_t)TransformId::kPalette), + Val((uint32_t)TransformId::kSqueeze), + Val((uint32_t)TransformId::kInvalid), (uint32_t)TransformId::kRCT, + reinterpret_cast(&id))); + if (id == TransformId::kInvalid) { + return JXL_FAILURE("Invalid transform ID"); + } + if (visitor->Conditional(id == TransformId::kRCT || + id == TransformId::kPalette)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Bits(3), BitsOffset(6, 8), BitsOffset(10, 72), + BitsOffset(13, 1096), 0, &begin_c)); + } + if (visitor->Conditional(id == TransformId::kRCT)) { + // 0-41, default YCoCg. + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(6), Bits(2), BitsOffset(4, 2), + BitsOffset(6, 10), 6, &rct_type)); + if (rct_type >= 42) { + return JXL_FAILURE("Invalid transform RCT type"); + } + } + if (visitor->Conditional(id == TransformId::kPalette)) { + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(1), Val(3), Val(4), BitsOffset(13, 1), 3, &num_c)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + BitsOffset(8, 0), BitsOffset(10, 256), BitsOffset(12, 1280), + BitsOffset(16, 5376), 256, &nb_colors)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), BitsOffset(8, 1), BitsOffset(10, 257), + BitsOffset(16, 1281), 0, &nb_deltas)); + JXL_QUIET_RETURN_IF_ERROR( + visitor->Bits(4, (uint32_t)Predictor::Zero, + reinterpret_cast(&predictor))); + if (predictor >= Predictor::Best) { + return JXL_FAILURE("Invalid predictor"); + } + } + + if (visitor->Conditional(id == TransformId::kSqueeze)) { + uint32_t num_squeezes = static_cast(squeezes.size()); + JXL_QUIET_RETURN_IF_ERROR( + visitor->U32(Val(0), BitsOffset(4, 1), BitsOffset(6, 9), + BitsOffset(8, 41), 0, &num_squeezes)); + if (visitor->IsReading()) squeezes.resize(num_squeezes); + for (size_t i = 0; i < num_squeezes; i++) { + JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&squeezes[i])); + } + } + return true; + } + + const char *Name() const override { return "Transform"; } + + Status Inverse(Image &input, const weighted::Header &wp_header, + ThreadPool *pool = nullptr); + Status MetaApply(Image &input); +}; + +Status CheckEqualChannels(const Image &image, uint32_t c1, uint32_t c2); + +static inline pixel_type PixelAdd(pixel_type a, pixel_type b) { + return static_cast(static_cast(a) + + static_cast(b)); +} + +} // namespace jxl + +#endif // LIB_JXL_MODULAR_TRANSFORM_TRANSFORM_H_ diff --git a/lib/jxl/modular_test.cc b/lib/jxl/modular_test.cc new file mode 100644 index 0000000..e4b7041 --- /dev/null +++ b/lib/jxl/modular_test.cc @@ -0,0 +1,171 @@ +// 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 +#include + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/modular/encoding/enc_encoding.h" +#include "lib/jxl/modular/encoding/encoding.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { +using test::Roundtrip; + +void TestLosslessGroups(size_t group_size_shift) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CompressParams cparams; + cparams.modular_mode = true; + cparams.modular_group_size_shift = group_size_shift; + cparams.color_transform = jxl::ColorTransform::kNone; + DecompressParams dparams; + + CodecInOut io_out; + size_t compressed_size; + + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 4, io.ysize() / 4); + + compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + EXPECT_LE(compressed_size, 280000u); + EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, + /*distmap=*/nullptr, pool), + 0.0); +} + +TEST(ModularTest, RoundtripLosslessGroups128) { TestLosslessGroups(0); } + +TEST(ModularTest, JXL_TSAN_SLOW_TEST(RoundtripLosslessGroups512)) { + TestLosslessGroups(2); +} + +TEST(ModularTest, JXL_TSAN_SLOW_TEST(RoundtripLosslessGroups1024)) { + TestLosslessGroups(3); +} + +TEST(ModularTest, RoundtripLossy) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CompressParams cparams; + cparams.modular_mode = true; + cparams.quality_pair = {80.0f, 80.0f}; + DecompressParams dparams; + + CodecInOut io_out; + size_t compressed_size; + + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + EXPECT_LE(compressed_size, 40000u); + cparams.ba_params.intensity_target = 80.0f; + EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, + /*distmap=*/nullptr, pool), + 3.0); +} + +TEST(ModularTest, RoundtripLossy16) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("raw.pixls/DJI-FC6310-16bit_709_v4_krita.png"); + CompressParams cparams; + cparams.modular_mode = true; + cparams.quality_pair = {80.0f, 80.0f}; + DecompressParams dparams; + + CodecInOut io_out; + size_t compressed_size; + + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + JXL_CHECK(io.TransformTo(ColorEncoding::SRGB(), pool)); + io.metadata.m.color_encoding = ColorEncoding::SRGB(); + + compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out); + EXPECT_LE(compressed_size, 400u); + cparams.ba_params.intensity_target = 80.0f; + EXPECT_LE(ButteraugliDistance(io, io_out, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.5); +} + +TEST(ModularTest, RoundtripExtraProperties) { + constexpr size_t kSize = 250; + Image image(kSize, kSize, /*bitdepth=*/8, 3); + ModularOptions options; + options.max_properties = 4; + options.predictor = Predictor::Zero; + std::mt19937 rng(0); + std::uniform_int_distribution<> dist(0, 8); + for (size_t y = 0; y < kSize; y++) { + for (size_t x = 0; x < kSize; x++) { + image.channel[0].plane.Row(y)[x] = image.channel[2].plane.Row(y)[x] = + dist(rng); + } + } + ZeroFillImage(&image.channel[1].plane); + BitWriter writer; + ASSERT_TRUE(ModularGenericCompress(image, options, &writer)); + writer.ZeroPadToByte(); + Image decoded(kSize, kSize, /*bitdepth=*/8, image.channel.size()); + for (size_t i = 0; i < image.channel.size(); i++) { + const Channel& ch = image.channel[i]; + decoded.channel[i] = Channel(ch.w, ch.h, ch.hshift, ch.vshift); + } + Status status = true; + { + BitReader reader(writer.GetSpan()); + BitReaderScopedCloser closer(&reader, &status); + ASSERT_TRUE(ModularGenericDecompress(&reader, decoded, /*header=*/nullptr, + /*group_id=*/0, &options)); + } + ASSERT_TRUE(status); + ASSERT_EQ(image.channel.size(), decoded.channel.size()); + for (size_t c = 0; c < image.channel.size(); c++) { + for (size_t y = 0; y < image.channel[c].plane.ysize(); y++) { + for (size_t x = 0; x < image.channel[c].plane.xsize(); x++) { + EXPECT_EQ(image.channel[c].plane.Row(y)[x], + decoded.channel[c].plane.Row(y)[x]) + << "c = " << c << ", x = " << x << ", y = " << y; + } + } + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/noise.h b/lib/jxl/noise.h new file mode 100644 index 0000000..329b325 --- /dev/null +++ b/lib/jxl/noise.h @@ -0,0 +1,60 @@ +// 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_NOISE_H_ +#define LIB_JXL_NOISE_H_ + +// Noise parameters shared by encoder/decoder. + +#include + +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +const float kNoisePrecision = 1 << 10; + +struct NoiseParams { + // LUT index is an intensity of pixel / mean intensity of patch + static constexpr size_t kNumNoisePoints = 8; + float lut[kNumNoisePoints]; + + void Clear() { + for (float& i : lut) i = 0; + } + bool HasAny() const { + for (float i : lut) { + if (std::abs(i) > 1e-3f) return true; + } + return false; + } +}; + +static inline std::pair IndexAndFrac(float x) { + constexpr size_t kScaleNumerator = NoiseParams::kNumNoisePoints - 2; + // TODO: instead of 1, this should be a proper Y range. + constexpr float kScale = kScaleNumerator / 1; + float scaled_x = std::max(0.f, x * kScale); + float floor_x; + float frac_x = std::modf(scaled_x, &floor_x); + if (JXL_UNLIKELY(scaled_x >= kScaleNumerator)) { + floor_x = kScaleNumerator - 1; + frac_x = 1; + } + return std::make_pair(static_cast(static_cast(floor_x)), frac_x); +} + +struct NoiseLevel { + float noise_level; + float intensity; +}; + +} // namespace jxl + +#endif // LIB_JXL_NOISE_H_ diff --git a/lib/jxl/noise_distributions.h b/lib/jxl/noise_distributions.h new file mode 100644 index 0000000..65a61cc --- /dev/null +++ b/lib/jxl/noise_distributions.h @@ -0,0 +1,138 @@ +// 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_NOISE_DISTRIBUTIONS_H_ +#define LIB_JXL_NOISE_DISTRIBUTIONS_H_ + +// Noise distributions for testing partial_derivatives and robust_statistics. + +#include +#include + +#include // distributions +#include + +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// Unmodified input +struct NoiseNone { + std::string Name() const { return "None"; } + + template + float operator()(const float in, Random* rng) const { + return in; + } +}; + +// Salt+pepper +class NoiseImpulse { + public: + explicit NoiseImpulse(const uint32_t threshold) : threshold_(threshold) {} + std::string Name() const { return "Impulse" + ToString(threshold_); } + + // Sets pixels to 0 if rand < threshold or 1 if rand > ~threshold. + template + float operator()(const float in, Random* rng) const { + const uint32_t rand = (*rng)(); + float out = 0.0f; + if (rand > ~threshold_) { + out = 1.0f; + } + if (rand > threshold_) { + out = in; + } + return out; + } + + private: + const uint32_t threshold_; +}; + +class NoiseUniform { + public: + NoiseUniform(const float min, const float max_exclusive) + : dist_(min, max_exclusive) {} + std::string Name() const { return "Uniform" + ToString(dist_.b()); } + + template + float operator()(const float in, Random* rng) const { + return in + dist_(*rng); + } + + private: + mutable std::uniform_real_distribution dist_; +}; + +// Additive, zero-mean Gaussian. +class NoiseGaussian { + public: + explicit NoiseGaussian(const float stddev) : dist_(0.0f, stddev) {} + std::string Name() const { return "Gaussian" + ToString(dist_.stddev()); } + + template + float operator()(const float in, Random* rng) const { + return in + dist_(*rng); + } + + private: + mutable std::normal_distribution dist_; +}; + +// Integer noise is scaled by 1E-3. +class NoisePoisson { + public: + explicit NoisePoisson(const double mean) : dist_(mean) {} + std::string Name() const { return "Poisson" + ToString(dist_.mean()); } + + template + float operator()(const float in, Random* rng) const { + return in + dist_(*rng) * 1E-3f; + } + + private: + mutable std::poisson_distribution dist_; +}; + +// Returns the result of applying the randomized "noise" function to each pixel. +template +ImageF AddNoise(const ImageF& in, const NoiseType& noise, Random* rng) { + const size_t xsize = in.xsize(); + const size_t ysize = in.ysize(); + ImageF out(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* JXL_RESTRICT in_row = in.ConstRow(y); + float* JXL_RESTRICT out_row = out.Row(y); + for (size_t x = 0; x < xsize; ++x) { + out_row[x] = noise(in_row[x], rng); + } + } + return out; +} + +template +Image3F AddNoise(const Image3F& in, const NoiseType& noise, Random* rng) { + const size_t xsize = in.xsize(); + const size_t ysize = in.ysize(); + Image3F out(xsize, ysize); + // noise_estimator_test requires this loop order. + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + const float* JXL_RESTRICT in_row = in.ConstPlaneRow(c, y); + float* JXL_RESTRICT out_row = out.PlaneRow(c, y); + + for (size_t x = 0; x < xsize; ++x) { + out_row[x] = noise(in_row[x], rng); + } + } + } + return out; +} + +} // namespace jxl + +#endif // LIB_JXL_NOISE_DISTRIBUTIONS_H_ diff --git a/lib/jxl/opsin_image_test.cc b/lib/jxl/opsin_image_test.cc new file mode 100644 index 0000000..d79c7cf --- /dev/null +++ b/lib/jxl/opsin_image_test.cc @@ -0,0 +1,127 @@ +// 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 + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/linalg.h" +#include "lib/jxl/opsin_params.h" + +namespace jxl { +namespace { + +class OpsinImageTargetTest : public hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(OpsinImageTargetTest); + +TEST_P(OpsinImageTargetTest, MaxCubeRootError) { TestCubeRoot(); } + +// Convert a single linear sRGB color to xyb, using the exact image conversion +// procedure that jpeg xl uses. +void LinearSrgbToOpsin(float rgb_r, float rgb_g, float rgb_b, + float* JXL_RESTRICT xyb_x, float* JXL_RESTRICT xyb_y, + float* JXL_RESTRICT xyb_b) { + Image3F linear(1, 1); + linear.PlaneRow(0, 0)[0] = rgb_r; + linear.PlaneRow(1, 0)[0] = rgb_g; + linear.PlaneRow(2, 0)[0] = rgb_b; + + ImageMetadata metadata; + metadata.SetFloat32Samples(); + metadata.color_encoding = ColorEncoding::LinearSRGB(); + ImageBundle ib(&metadata); + ib.SetFromImage(std::move(linear), metadata.color_encoding); + Image3F opsin(1, 1); + (void)ToXYB(ib, /*pool=*/nullptr, &opsin); + + *xyb_x = opsin.PlaneRow(0, 0)[0]; + *xyb_y = opsin.PlaneRow(1, 0)[0]; + *xyb_b = opsin.PlaneRow(2, 0)[0]; +} + +// Convert a single XYB color to linear sRGB, using the exact image conversion +// procedure that jpeg xl uses. +void OpsinToLinearSrgb(float xyb_x, float xyb_y, float xyb_b, + float* JXL_RESTRICT rgb_r, float* JXL_RESTRICT rgb_g, + float* JXL_RESTRICT rgb_b) { + Image3F opsin(1, 1); + opsin.PlaneRow(0, 0)[0] = xyb_x; + opsin.PlaneRow(1, 0)[0] = xyb_y; + opsin.PlaneRow(2, 0)[0] = xyb_b; + Image3F linear(1, 1); + OpsinParams opsin_params; + opsin_params.Init(/*intensity_target=*/255.0f); + OpsinToLinear(opsin, Rect(opsin), nullptr, &linear, opsin_params); + *rgb_r = linear.PlaneRow(0, 0)[0]; + *rgb_g = linear.PlaneRow(1, 0)[0]; + *rgb_b = linear.PlaneRow(2, 0)[0]; +} + +void OpsinRoundtripTestRGB(float r, float g, float b) { + float xyb_x, xyb_y, xyb_b; + LinearSrgbToOpsin(r, g, b, &xyb_x, &xyb_y, &xyb_b); + float r2, g2, b2; + OpsinToLinearSrgb(xyb_x, xyb_y, xyb_b, &r2, &g2, &b2); + EXPECT_NEAR(r, r2, 1e-3); + EXPECT_NEAR(g, g2, 1e-3); + EXPECT_NEAR(b, b2, 1e-3); +} + +TEST(OpsinImageTest, VerifyOpsinAbsorbanceInverseMatrix) { + float matrix[9]; // writable copy + for (int i = 0; i < 9; i++) { + matrix[i] = GetOpsinAbsorbanceInverseMatrix()[i]; + } + EXPECT_TRUE(Inv3x3Matrix(matrix)); + for (int i = 0; i < 9; i++) { + EXPECT_NEAR(matrix[i], kOpsinAbsorbanceMatrix[i], 1e-6); + } +} + +TEST(OpsinImageTest, OpsinRoundtrip) { + OpsinRoundtripTestRGB(0, 0, 0); + OpsinRoundtripTestRGB(1. / 255, 1. / 255, 1. / 255); + OpsinRoundtripTestRGB(128. / 255, 128. / 255, 128. / 255); + OpsinRoundtripTestRGB(1, 1, 1); + + OpsinRoundtripTestRGB(0, 0, 1. / 255); + OpsinRoundtripTestRGB(0, 0, 128. / 255); + OpsinRoundtripTestRGB(0, 0, 1); + + OpsinRoundtripTestRGB(0, 1. / 255, 0); + OpsinRoundtripTestRGB(0, 128. / 255, 0); + OpsinRoundtripTestRGB(0, 1, 0); + + OpsinRoundtripTestRGB(1. / 255, 0, 0); + OpsinRoundtripTestRGB(128. / 255, 0, 0); + OpsinRoundtripTestRGB(1, 0, 0); +} + +TEST(OpsinImageTest, VerifyZero) { + // Test that black color (zero energy) is 0,0,0 in xyb. + float x, y, b; + LinearSrgbToOpsin(0, 0, 0, &x, &y, &b); + EXPECT_NEAR(0, x, 1e-9); + EXPECT_NEAR(0, y, 1e-7); + EXPECT_NEAR(0, b, 1e-7); +} + +TEST(OpsinImageTest, VerifyGray) { + // Test that grayscale colors have a fixed y/b ratio and x==0. + for (size_t i = 1; i < 255; i++) { + float x, y, b; + LinearSrgbToOpsin(i / 255., i / 255., i / 255., &x, &y, &b); + EXPECT_NEAR(0, x, 1e-6); + EXPECT_NEAR(kYToBRatio, b / y, 3e-5); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/opsin_inverse_test.cc b/lib/jxl/opsin_inverse_test.cc new file mode 100644 index 0000000..b7c1964 --- /dev/null +++ b/lib/jxl/opsin_inverse_test.cc @@ -0,0 +1,55 @@ +// 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 "gtest/gtest.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/color_management.h" +#include "lib/jxl/dec_xyb.h" +#include "lib/jxl/enc_xyb.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +TEST(OpsinInverseTest, LinearInverseInverts) { + Image3F linear(128, 128); + RandomFillImage(&linear, 1.0f); + + CodecInOut io; + io.metadata.m.SetFloat32Samples(); + io.metadata.m.color_encoding = ColorEncoding::LinearSRGB(); + io.SetFromImage(CopyImage(linear), io.metadata.m.color_encoding); + ThreadPool* null_pool = nullptr; + Image3F opsin(io.xsize(), io.ysize()); + (void)ToXYB(io.Main(), null_pool, &opsin); + + OpsinParams opsin_params; + opsin_params.Init(/*intensity_target=*/255.0f); + OpsinToLinearInplace(&opsin, /*pool=*/nullptr, opsin_params); + + VerifyRelativeError(linear, opsin, 3E-3, 2E-4); +} + +TEST(OpsinInverseTest, YcbCrInverts) { + Image3F rgb(128, 128); + RandomFillImage(&rgb, 1.0f); + + ThreadPool* null_pool = nullptr; + Image3F ycbcr(rgb.xsize(), rgb.ysize()); + RgbToYcbcr(rgb.Plane(0), rgb.Plane(1), rgb.Plane(2), &ycbcr.Plane(1), + &ycbcr.Plane(0), &ycbcr.Plane(2), null_pool); + + Image3F rgb2(rgb.xsize(), rgb.ysize()); + YcbcrToRgb(ycbcr, &rgb2, Rect(rgb)); + + VerifyRelativeError(rgb, rgb2, 4E-5, 4E-7); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/opsin_params.cc b/lib/jxl/opsin_params.cc new file mode 100644 index 0000000..f80a18a --- /dev/null +++ b/lib/jxl/opsin_params.cc @@ -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. + +#include "lib/jxl/opsin_params.h" + +#include + +#include "lib/jxl/linalg.h" + +namespace jxl { + +#define INVERSE_OPSIN_FROM_SPEC 1 + +const float* GetOpsinAbsorbanceInverseMatrix() { +#if INVERSE_OPSIN_FROM_SPEC + return DefaultInverseOpsinAbsorbanceMatrix(); +#else // INVERSE_OPSIN_FROM_SPEC + // Compute the inverse opsin matrix from the forward matrix. Less precise + // than taking the values from the specification, but must be used if the + // forward transform is changed and the spec will require updating. + static const float* const kInverse = [] { + static float inverse[9]; + for (int i = 0; i < 9; i++) { + inverse[i] = kOpsinAbsorbanceMatrix[i]; + } + Inv3x3Matrix(inverse); + return inverse; + }(); + return kInverse; +#endif // INVERSE_OPSIN_FROM_SPEC +} + +void InitSIMDInverseMatrix(const float* JXL_RESTRICT inverse, + float* JXL_RESTRICT simd_inverse, + float intensity_target) { + for (size_t i = 0; i < 9; ++i) { + simd_inverse[4 * i] = simd_inverse[4 * i + 1] = simd_inverse[4 * i + 2] = + simd_inverse[4 * i + 3] = inverse[i] * (255.0f / intensity_target); + } +} + +} // namespace jxl diff --git a/lib/jxl/opsin_params.h b/lib/jxl/opsin_params.h new file mode 100644 index 0000000..e8e2e43 --- /dev/null +++ b/lib/jxl/opsin_params.h @@ -0,0 +1,74 @@ +// 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_OPSIN_PARAMS_H_ +#define LIB_JXL_OPSIN_PARAMS_H_ + +// Constants that define the XYB color space. + +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" + +namespace jxl { + +// Parameters for opsin absorbance. +static const float kM02 = 0.078f; +static const float kM00 = 0.30f; +static const float kM01 = 1.0f - kM02 - kM00; + +static const float kM12 = 0.078f; +static const float kM10 = 0.23f; +static const float kM11 = 1.0f - kM12 - kM10; + +static const float kM20 = 0.24342268924547819f; +static const float kM21 = 0.20476744424496821f; +static const float kM22 = 1.0f - kM20 - kM21; + +static const float kBScale = 1.0f; +static const float kYToBRatio = 1.0f; // works better with 0.50017729543783418 +static const float kBToYRatio = 1.0f / kYToBRatio; + +static const float kB0 = 0.0037930732552754493f; +static const float kB1 = kB0; +static const float kB2 = kB0; + +// Opsin absorbance matrix is now frozen. +static const float kOpsinAbsorbanceMatrix[9] = { + kM00, kM01, kM02, kM10, kM11, kM12, kM20, kM21, kM22, +}; + +// Must be the inverse matrix of kOpsinAbsorbanceMatrix and match the spec. +static inline const float* DefaultInverseOpsinAbsorbanceMatrix() { + static float kDefaultInverseOpsinAbsorbanceMatrix[9] = { + 11.031566901960783f, -9.866943921568629f, -0.16462299647058826f, + -3.254147380392157f, 4.418770392156863f, -0.16462299647058826f, + -3.6588512862745097f, 2.7129230470588235f, 1.9459282392156863f}; + return kDefaultInverseOpsinAbsorbanceMatrix; +} + +// Returns 3x3 row-major matrix inverse of kOpsinAbsorbanceMatrix. +// opsin_image_test verifies this is actually the inverse. +const float* GetOpsinAbsorbanceInverseMatrix(); + +void InitSIMDInverseMatrix(const float* JXL_RESTRICT inverse, + float* JXL_RESTRICT simd_inverse, + float intensity_target); + +static const float kOpsinAbsorbanceBias[3] = { + kB0, + kB1, + kB2, +}; + +static const float kNegOpsinAbsorbanceBiasRGB[4] = { + -kOpsinAbsorbanceBias[0], -kOpsinAbsorbanceBias[1], + -kOpsinAbsorbanceBias[2], 1.0f}; + +} // namespace jxl + +#endif // LIB_JXL_OPSIN_PARAMS_H_ diff --git a/lib/jxl/optimize.cc b/lib/jxl/optimize.cc new file mode 100644 index 0000000..0816596 --- /dev/null +++ b/lib/jxl/optimize.cc @@ -0,0 +1,163 @@ +// 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/optimize.h" + +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { + +namespace optimize { + +namespace { + +// simplex vector must be sorted by first element of its elements +std::vector Midpoint(const std::vector>& simplex) { + JXL_CHECK(!simplex.empty()); + JXL_CHECK(simplex.size() == simplex[0].size()); + int dim = simplex.size() - 1; + std::vector result(dim + 1, 0); + for (int i = 0; i < dim; i++) { + for (int k = 0; k < dim; k++) { + result[i + 1] += simplex[k][i + 1]; + } + result[i + 1] /= dim; + } + return result; +} + +// first element ignored +std::vector Subtract(const std::vector& a, + const std::vector& b) { + JXL_CHECK(a.size() == b.size()); + std::vector result(a.size()); + result[0] = 0; + for (size_t i = 1; i < result.size(); i++) { + result[i] = a[i] - b[i]; + } + return result; +} + +// first element ignored +std::vector Add(const std::vector& a, + const std::vector& b) { + JXL_CHECK(a.size() == b.size()); + std::vector result(a.size()); + result[0] = 0; + for (size_t i = 1; i < result.size(); i++) { + result[i] = a[i] + b[i]; + } + return result; +} + +// first element ignored +std::vector Average(const std::vector& a, + const std::vector& b) { + JXL_CHECK(a.size() == b.size()); + std::vector result(a.size()); + result[0] = 0; + for (size_t i = 1; i < result.size(); i++) { + result[i] = 0.5 * (a[i] + b[i]); + } + return result; +} + +// vec: [0] will contain the objective function, [1:] will +// contain the vector position for the objective function. +// fun: the function evaluates the value. +void Eval(std::vector* vec, + const std::function&)>& fun) { + std::vector args(vec->begin() + 1, vec->end()); + (*vec)[0] = fun(args); +} + +void Sort(std::vector>* simplex) { + std::sort(simplex->begin(), simplex->end()); +} + +// Main iteration step of Nelder-Mead like optimization. +void Reflect(std::vector>* simplex, + const std::function&)>& fun) { + Sort(simplex); + const std::vector& last = simplex->back(); + std::vector mid = Midpoint(*simplex); + std::vector diff = Subtract(mid, last); + std::vector mirrored = Add(mid, diff); + Eval(&mirrored, fun); + if (mirrored[0] > (*simplex)[simplex->size() - 2][0]) { + // Still the worst, shrink towards the best. + std::vector shrinking = Average(simplex->back(), (*simplex)[0]); + Eval(&shrinking, fun); + simplex->back() = shrinking; + } else if (mirrored[0] < (*simplex)[0][0]) { + // new best + std::vector even_further = Add(mirrored, diff); + Eval(&even_further, fun); + if (even_further[0] < mirrored[0]) { + mirrored = even_further; + } + simplex->back() = mirrored; + } else { + // not a best, not a worst point + simplex->back() = mirrored; + } +} + +// Initialize the simplex at origin. +std::vector> InitialSimplex( + int dim, double amount, const std::vector& init, + const std::function&)>& fun) { + std::vector best(1 + dim, 0); + std::copy(init.begin(), init.end(), best.begin() + 1); + Eval(&best, fun); + std::vector> result{best}; + for (int i = 0; i < dim; i++) { + best = result[0]; + best[i + 1] += amount; + Eval(&best, fun); + result.push_back(best); + Sort(&result); + } + return result; +} + +// For comparing the same with the python tool +/*void RunSimplexExternal( + int dim, double amount, int max_iterations, + const std::function&))>& fun) { + vector vars; + for (int i = 0; i < dim; i++) { + vars.push_back(atof(getenv(StrCat("VAR", i).c_str()))); + } + double result = fun(vars); + std::cout << "Result=" << result; +}*/ + +} // namespace + +std::vector RunSimplex( + int dim, double amount, int max_iterations, const std::vector& init, + const std::function&)>& fun) { + std::vector> simplex = + InitialSimplex(dim, amount, init, fun); + for (int i = 0; i < max_iterations; i++) { + Sort(&simplex); + Reflect(&simplex, fun); + } + return simplex[0]; +} + +std::vector RunSimplex( + int dim, double amount, int max_iterations, + const std::function&)>& fun) { + std::vector init(dim, 0.0); + return RunSimplex(dim, amount, max_iterations, init, fun); +} + +} // namespace optimize + +} // namespace jxl diff --git a/lib/jxl/optimize.h b/lib/jxl/optimize.h new file mode 100644 index 0000000..0a60198 --- /dev/null +++ b/lib/jxl/optimize.h @@ -0,0 +1,218 @@ +// 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. + +// Utility functions for optimizing multi-dimensional nonlinear functions. + +#ifndef LIB_JXL_OPTIMIZE_H_ +#define LIB_JXL_OPTIMIZE_H_ + +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace optimize { + +// An array type of numeric values that supports math operations with operator-, +// operator+, etc. +template +class Array { + public: + Array() = default; + explicit Array(T v) { + for (size_t i = 0; i < N; i++) v_[i] = v; + } + + size_t size() const { return N; } + + T& operator[](size_t index) { + JXL_DASSERT(index < N); + return v_[index]; + } + T operator[](size_t index) const { + JXL_DASSERT(index < N); + return v_[index]; + } + + private: + // The values used by this Array. + T v_[N]; +}; + +template +Array operator+(const Array& x, const Array& y) { + Array z; + for (size_t i = 0; i < N; ++i) { + z[i] = x[i] + y[i]; + } + return z; +} + +template +Array operator-(const Array& x, const Array& y) { + Array z; + for (size_t i = 0; i < N; ++i) { + z[i] = x[i] - y[i]; + } + return z; +} + +template +Array operator*(T v, const Array& x) { + Array y; + for (size_t i = 0; i < N; ++i) { + y[i] = v * x[i]; + } + return y; +} + +template +T operator*(const Array& x, const Array& y) { + T r = 0.0; + for (size_t i = 0; i < N; ++i) { + r += x[i] * y[i]; + } + return r; +} + +// Runs Nelder-Mead like optimization. Runs for max_iterations times, +// fun gets called with a vector of size dim as argument, and returns the score +// based on those parameters (lower is better). Returns a vector of dim+1 +// dimensions, where the first value is the optimal value of the function and +// the rest is the argmin value. Use init to pass an initial guess or where +// the optimal value is. +// +// Usage example: +// +// RunSimplex(2, 0.1, 100, [](const vector& v) { +// return (v[0] - 5) * (v[0] - 5) + (v[1] - 7) * (v[1] - 7); +// }); +// +// Returns (0.0, 5, 7) +std::vector RunSimplex( + int dim, double amount, int max_iterations, + const std::function&)>& fun); +std::vector RunSimplex( + int dim, double amount, int max_iterations, const std::vector& init, + const std::function&)>& fun); + +// Implementation of the Scaled Conjugate Gradient method described in the +// following paper: +// Moller, M. "A Scaled Conjugate Gradient Algorithm for Fast Supervised +// Learning", Neural Networks, Vol. 6. pp. 525-533, 1993 +// http://sci2s.ugr.es/keel/pdf/algorithm/articulo/moller1990.pdf +// +// The Function template parameter is a class that has the following method: +// +// // Returns the value of the function at point w and sets *df to be the +// // negative gradient vector of the function at point w. +// double Compute(const optimize::Array& w, +// optimize::Array* df) const; +// +// Returns a vector w, such that |df(w)| < grad_norm_threshold. +template +Array OptimizeWithScaledConjugateGradientMethod( + const Function& f, const Array& w0, const T grad_norm_threshold, + size_t max_iters) { + const size_t n = w0.size(); + const T rsq_threshold = grad_norm_threshold * grad_norm_threshold; + const T sigma0 = static_cast(0.0001); + const T l_min = static_cast(1.0e-15); + const T l_max = static_cast(1.0e15); + + Array w = w0; + Array wp; + Array r; + Array rt; + Array e; + Array p; + T psq; + T fp; + T D; + T d; + T m; + T a; + T b; + T s; + T t; + + T fw = f.Compute(w, &r); + T rsq = r * r; + e = r; + p = r; + T l = static_cast(1.0); + bool success = true; + size_t n_success = 0; + size_t k = 0; + + while (k++ < max_iters) { + if (success) { + m = -(p * r); + if (m >= 0) { + p = r; + m = -(p * r); + } + psq = p * p; + s = sigma0 / std::sqrt(psq); + f.Compute(w + (s * p), &rt); + t = (p * (r - rt)) / s; + } + + d = t + l * psq; + if (d <= 0) { + d = l * psq; + l = l - t / psq; + } + + a = -m / d; + wp = w + a * p; + fp = f.Compute(wp, &rt); + + D = 2.0 * (fp - fw) / (a * m); + if (D >= 0.0) { + success = true; + n_success++; + w = wp; + } else { + success = false; + } + + if (success) { + e = r; + r = rt; + rsq = r * r; + fw = fp; + if (rsq <= rsq_threshold) { + break; + } + } + + if (D < 0.25) { + l = std::min(4.0 * l, l_max); + } else if (D > 0.75) { + l = std::max(0.25 * l, l_min); + } + + if ((n_success % n) == 0) { + p = r; + l = 1.0; + } else if (success) { + b = ((e - r) * r) / m; + p = b * p + r; + } + } + + return w; +} + +} // namespace optimize +} // namespace jxl + +#endif // LIB_JXL_OPTIMIZE_H_ diff --git a/lib/jxl/optimize_test.cc b/lib/jxl/optimize_test.cc new file mode 100644 index 0000000..c606a03 --- /dev/null +++ b/lib/jxl/optimize_test.cc @@ -0,0 +1,109 @@ +// 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/optimize.h" + +#include + +#include "gtest/gtest.h" + +namespace jxl { +namespace optimize { +namespace { + +// The maximum number of iterations for the test. +static const size_t kMaxTestIter = 100000; + +// F(w) = (w - w_min)^2. +struct SimpleQuadraticFunction { + typedef Array ArrayType; + explicit SimpleQuadraticFunction(const ArrayType& w0) : w_min(w0) {} + + double Compute(const ArrayType& w, ArrayType* df) const { + ArrayType dw = w - w_min; + *df = -2.0 * dw; + return dw * dw; + } + + ArrayType w_min; +}; + +// F(alpha, beta, gamma| x,y) = \sum_i(y_i - (alpha x_i ^ gamma + beta))^2. +struct PowerFunction { + explicit PowerFunction(const std::vector& x0, + const std::vector& y0) + : x(x0), y(y0) {} + + typedef Array ArrayType; + double Compute(const ArrayType& w, ArrayType* df) const { + double loss_function = 0; + (*df)[0] = 0; + (*df)[1] = 0; + (*df)[2] = 0; + for (size_t ind = 0; ind < y.size(); ++ind) { + if (x[ind] != 0) { + double l_f = y[ind] - (w[0] * pow(x[ind], w[1]) + w[2]); + (*df)[0] += 2.0 * l_f * pow(x[ind], w[1]); + (*df)[1] += 2.0 * l_f * w[0] * pow(x[ind], w[1]) * log(x[ind]); + (*df)[2] += 2.0 * l_f * 1; + loss_function += l_f * l_f; + } + } + return loss_function; + } + + std::vector x; + std::vector y; +}; + +TEST(OptimizeTest, SimpleQuadraticFunction) { + SimpleQuadraticFunction::ArrayType w_min; + w_min[0] = 1.0; + w_min[1] = 2.0; + SimpleQuadraticFunction f(w_min); + SimpleQuadraticFunction::ArrayType w(0.); + static const double kPrecision = 1e-8; + w = optimize::OptimizeWithScaledConjugateGradientMethod(f, w, kPrecision, + kMaxTestIter); + EXPECT_NEAR(w[0], 1.0, kPrecision); + EXPECT_NEAR(w[1], 2.0, kPrecision); +} + +TEST(OptimizeTest, PowerFunction) { + std::vector x(10); + std::vector y(10); + for (int ind = 0; ind < 10; ++ind) { + x[ind] = 1. * ind; + y[ind] = 2. * pow(x[ind], 3) + 5.; + } + PowerFunction f(x, y); + PowerFunction::ArrayType w(0.); + + static const double kPrecision = 0.01; + w = optimize::OptimizeWithScaledConjugateGradientMethod(f, w, kPrecision, + kMaxTestIter); + EXPECT_NEAR(w[0], 2.0, kPrecision); + EXPECT_NEAR(w[1], 3.0, kPrecision); + EXPECT_NEAR(w[2], 5.0, kPrecision); +} + +TEST(OptimizeTest, SimplexOptTest) { + auto f = [](const std::vector& x) -> double { + double t1 = x[0] - 1.0; + double t2 = x[1] + 1.5; + return 2.0 + t1 * t1 + t2 * t2; + }; + auto opt = RunSimplex(2, 0.01, 100, f); + EXPECT_EQ(opt.size(), 3u); + + static const double kPrecision = 0.01; + EXPECT_NEAR(opt[0], 2.0, kPrecision); + EXPECT_NEAR(opt[1], 1.0, kPrecision); + EXPECT_NEAR(opt[2], -1.5, kPrecision); +} + +} // namespace +} // namespace optimize +} // namespace jxl diff --git a/lib/jxl/padded_bytes_test.cc b/lib/jxl/padded_bytes_test.cc new file mode 100644 index 0000000..d8005e4 --- /dev/null +++ b/lib/jxl/padded_bytes_test.cc @@ -0,0 +1,126 @@ +// 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/padded_bytes.h" + +#include // iota +#include + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(PaddedBytesTest, TestNonEmptyFirstByteZero) { + PaddedBytes pb(1); + EXPECT_EQ(0, pb[0]); + // Even after resizing.. + pb.resize(20); + EXPECT_EQ(0, pb[0]); + // And reserving. + pb.reserve(200); + EXPECT_EQ(0, pb[0]); +} + +TEST(PaddedBytesTest, TestEmptyFirstByteZero) { + PaddedBytes pb(0); + // After resizing - new zero is written despite there being nothing to copy. + pb.resize(20); + EXPECT_EQ(0, pb[0]); +} + +TEST(PaddedBytesTest, TestFillWithoutReserve) { + PaddedBytes pb; + for (size_t i = 0; i < 170u; ++i) { + pb.push_back(i); + } + EXPECT_EQ(170u, pb.size()); + EXPECT_GE(pb.capacity(), 170u); +} + +TEST(PaddedBytesTest, TestFillWithExactReserve) { + PaddedBytes pb; + pb.reserve(170); + for (size_t i = 0; i < 170u; ++i) { + pb.push_back(i); + } + EXPECT_EQ(170u, pb.size()); + EXPECT_EQ(pb.capacity(), 170u); +} + +TEST(PaddedBytesTest, TestFillWithMoreReserve) { + PaddedBytes pb; + pb.reserve(171); + for (size_t i = 0; i < 170u; ++i) { + pb.push_back(i); + } + EXPECT_EQ(170u, pb.size()); + EXPECT_GT(pb.capacity(), 170u); +} + +// Can assign() a subset of the valid data. +TEST(PaddedBytesTest, TestAssignFromWithin) { + PaddedBytes pb; + pb.reserve(256); + for (size_t i = 0; i < 256; ++i) { + pb.push_back(i); + } + pb.assign(pb.data() + 64, pb.data() + 192); + EXPECT_EQ(128u, pb.size()); + for (size_t i = 0; i < 128; ++i) { + EXPECT_EQ(i + 64, pb[i]); + } +} + +// Can assign() a range with both valid and previously-allocated data. +TEST(PaddedBytesTest, TestAssignReclaim) { + PaddedBytes pb; + pb.reserve(256); + for (size_t i = 0; i < 256; ++i) { + pb.push_back(i); + } + + const uint8_t* mem = pb.data(); + pb.resize(200); + // Just shrank without reallocating + EXPECT_EQ(mem, pb.data()); + EXPECT_EQ(256u, pb.capacity()); + + // Reclaim part of initial allocation + pb.assign(pb.data() + 100, pb.data() + 240); + EXPECT_EQ(140u, pb.size()); + + for (size_t i = 0; i < 140; ++i) { + EXPECT_EQ(i + 100, pb[i]); + } +} + +// Can assign() smaller and larger ranges outside the current allocation. +TEST(PaddedBytesTest, TestAssignOutside) { + PaddedBytes pb; + pb.resize(400); + std::iota(pb.begin(), pb.end(), 1); + + std::vector small(64); + std::iota(small.begin(), small.end(), 500); + + pb.assign(small.data(), small.data() + small.size()); + EXPECT_EQ(64u, pb.size()); + for (size_t i = 0; i < 64; ++i) { + EXPECT_EQ((i + 500) & 0xFF, pb[i]); + } + + std::vector large(1000); + std::iota(large.begin(), large.end(), 600); + + pb.assign(large.data(), large.data() + large.size()); + EXPECT_EQ(1000u, pb.size()); + for (size_t i = 0; i < 1000; ++i) { + EXPECT_EQ((i + 600) & 0xFF, pb[i]); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/passes_state.cc b/lib/jxl/passes_state.cc new file mode 100644 index 0000000..a0cc198 --- /dev/null +++ b/lib/jxl/passes_state.cc @@ -0,0 +1,68 @@ +// 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/passes_state.h" + +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/common.h" + +namespace jxl { + +Status InitializePassesSharedState(const FrameHeader& frame_header, + PassesSharedState* JXL_RESTRICT shared, + bool encoder) { + JXL_ASSERT(frame_header.nonserialized_metadata != nullptr); + shared->frame_header = frame_header; + shared->metadata = frame_header.nonserialized_metadata; + shared->frame_dim = frame_header.ToFrameDimensions(); + shared->image_features.patches.SetPassesSharedState(shared); + + const FrameDimensions& frame_dim = shared->frame_dim; + + shared->ac_strategy = + AcStrategyImage(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + shared->raw_quant_field = + ImageI(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + shared->epf_sharpness = + ImageB(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + shared->cmap = ColorCorrelationMap(frame_dim.xsize, frame_dim.ysize); + + // In the decoder, we allocate coeff orders afterwards, when we know how many + // we will actually need. + shared->coeff_order_size = kCoeffOrderMaxSize; + if (encoder && + shared->coeff_orders.size() < + frame_header.passes.num_passes * kCoeffOrderMaxSize && + frame_header.encoding == FrameEncoding::kVarDCT) { + shared->coeff_orders.resize(frame_header.passes.num_passes * + kCoeffOrderMaxSize); + } + + shared->quant_dc = ImageB(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + if (!(frame_header.flags & FrameHeader::kUseDcFrame) || encoder) { + shared->dc_storage = + Image3F(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + } else { + if (frame_header.dc_level == 4) { + return JXL_FAILURE("Invalid DC level for kUseDcFrame: %u", + frame_header.dc_level); + } + shared->dc = &shared->dc_frames[frame_header.dc_level]; + if (shared->dc->xsize() == 0) { + return JXL_FAILURE( + "kUseDcFrame specified for dc_level %u, but no frame was decoded " + "with level %u", + frame_header.dc_level, frame_header.dc_level + 1); + } + ZeroFillImage(&shared->quant_dc); + } + + shared->dc_storage = Image3F(frame_dim.xsize_blocks, frame_dim.ysize_blocks); + + return true; +} + +} // namespace jxl diff --git a/lib/jxl/passes_state.h b/lib/jxl/passes_state.h new file mode 100644 index 0000000..069d7ac --- /dev/null +++ b/lib/jxl/passes_state.h @@ -0,0 +1,138 @@ +// 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_PASSES_STATE_H_ +#define LIB_JXL_PASSES_STATE_H_ + +#include "lib/jxl/ac_context.h" +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/loop_filter.h" +#include "lib/jxl/noise.h" +#include "lib/jxl/quant_weights.h" +#include "lib/jxl/quantizer.h" +#include "lib/jxl/splines.h" + +// Structures that hold the (en/de)coder state for a JPEG XL kVarDCT +// (en/de)coder. + +namespace jxl { + +struct ImageFeatures { + NoiseParams noise_params; + PatchDictionary patches; + Splines splines; +}; + +// State common to both encoder and decoder. +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +struct PassesSharedState { + PassesSharedState() : frame_header(nullptr) {} + + // Headers and metadata. + const CodecMetadata* metadata; + FrameHeader frame_header; + + FrameDimensions frame_dim; + + // Control fields and parameters. + AcStrategyImage ac_strategy; + + // Dequant matrices + quantizer. + DequantMatrices matrices; + Quantizer quantizer{&matrices}; + ImageI raw_quant_field; + + // Per-block side information for EPF detail preservation. + ImageB epf_sharpness; + + ColorCorrelationMap cmap; + + ImageFeatures image_features; + + // Memory area for storing coefficient orders. + // `coeff_order_size` is the size used by *one* set of coefficient orders (at + // most kMaxCoeffOrderSize). A set of coefficient orders is present for each + // pass. + size_t coeff_order_size = 0; + std::vector coeff_orders; + + // Decoder-side DC and quantized DC. + ImageB quant_dc; + Image3F dc_storage; + const Image3F* JXL_RESTRICT dc = &dc_storage; + + BlockCtxMap block_ctx_map; + + Image3F dc_frames[4]; + + struct { + ImageBundle storage; + // Can either point to `storage`, if this is a frame that is not stored in + // the CodecInOut, or can point to an existing ImageBundle. + // TODO(veluca): pointing to ImageBundles in CodecInOut is not possible for + // now, as they are stored in a vector and thus may be moved. Fix this. + ImageBundle* JXL_RESTRICT frame = &storage; + // ImageBundle doesn't yet have a simple way to state it is in XYB. + bool ib_is_in_xyb = false; + } reference_frames[4] = {}; + + // Number of pre-clustered set of histograms (with the same ctx map), per + // pass. Encoded as num_histograms_ - 1. + size_t num_histograms = 0; + + bool IsGrayscale() const { return metadata->m.color_encoding.IsGray(); } + + Rect GroupRect(size_t group_index) const { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + const Rect rect(gx * frame_dim.group_dim, gy * frame_dim.group_dim, + frame_dim.group_dim, frame_dim.group_dim, frame_dim.xsize, + frame_dim.ysize); + return rect; + } + + Rect PaddedGroupRect(size_t group_index) const { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + const Rect rect(gx * frame_dim.group_dim, gy * frame_dim.group_dim, + frame_dim.group_dim, frame_dim.group_dim, + frame_dim.xsize_padded, frame_dim.ysize_padded); + return rect; + } + + Rect BlockGroupRect(size_t group_index) const { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + const Rect rect(gx * (frame_dim.group_dim >> 3), + gy * (frame_dim.group_dim >> 3), frame_dim.group_dim >> 3, + frame_dim.group_dim >> 3, frame_dim.xsize_blocks, + frame_dim.ysize_blocks); + return rect; + } + + Rect DCGroupRect(size_t group_index) const { + const size_t gx = group_index % frame_dim.xsize_dc_groups; + const size_t gy = group_index / frame_dim.xsize_dc_groups; + const Rect rect(gx * frame_dim.group_dim, gy * frame_dim.group_dim, + frame_dim.group_dim, frame_dim.group_dim, + frame_dim.xsize_blocks, frame_dim.ysize_blocks); + return rect; + } +}; + +// Initialized the state information that is shared between encoder and decoder. +Status InitializePassesSharedState(const FrameHeader& frame_header, + PassesSharedState* JXL_RESTRICT shared, + bool encoder = false); + +} // namespace jxl + +#endif // LIB_JXL_PASSES_STATE_H_ diff --git a/lib/jxl/passes_test.cc b/lib/jxl/passes_test.cc new file mode 100644 index 0000000..451075e --- /dev/null +++ b/lib/jxl/passes_test.cc @@ -0,0 +1,389 @@ +// 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 + +#include +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { +using test::Roundtrip; + +TEST(PassesTest, RoundtripSmallPasses) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + CompressParams cparams; + cparams.butteraugli_distance = 1.0; + cparams.progressive_mode = true; + DecompressParams dparams; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.5); +} + +TEST(PassesTest, RoundtripUnalignedPasses) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 12, io.ysize() / 7); + + CompressParams cparams; + cparams.butteraugli_distance = 2.0; + cparams.progressive_mode = true; + DecompressParams dparams; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 3.2); +} + +TEST(PassesTest, RoundtripMultiGroupPasses) { + ThreadPoolInternal pool(4); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + io.ShrinkTo(600, 1024); // partial X, full Y group + + CompressParams cparams; + DecompressParams dparams; + + cparams.butteraugli_distance = 1.0f; + cparams.progressive_mode = true; + CodecInOut io2; + Roundtrip(&io, cparams, dparams, &pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 1.99f); + + cparams.butteraugli_distance = 2.0f; + CodecInOut io3; + Roundtrip(&io, cparams, dparams, &pool, &io3); + EXPECT_LE(ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool), + 3.0f); +} + +TEST(PassesTest, RoundtripLargeFastPasses) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_mode = true; + DecompressParams dparams; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, &pool, &io2); +} + +// Checks for differing size/distance in two consecutive runs of distance 2, +// which involves additional processing including adaptive reconstruction. +// Failing this may be a sign of race conditions or invalid memory accesses. +TEST(PassesTest, RoundtripProgressiveConsistent) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_mode = true; + cparams.butteraugli_distance = 2.0; + DecompressParams dparams; + + // Try each xsize mod kBlockDim to verify right border handling. + for (size_t xsize = 48; xsize > 40; --xsize) { + io.ShrinkTo(xsize, 15); + + CodecInOut io2; + const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2); + + CodecInOut io3; + const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3); + + // Exact same compressed size. + EXPECT_EQ(size2, size3); + + // Exact same distance. + const float dist2 = ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, &pool); + const float dist3 = ButteraugliDistance(io, io3, cparams.ba_params, + /*distmap=*/nullptr, &pool); + EXPECT_EQ(dist2, dist3); + } +} + +TEST(PassesTest, AllDownsampleFeasible) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + PaddedBytes compressed; + AuxOut aux; + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_mode = true; + cparams.butteraugli_distance = 1.0; + PassesEncoderState enc_state; + ASSERT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, &aux, &pool)); + + EXPECT_LE(compressed.size(), 240000u); + float target_butteraugli[9] = {}; + target_butteraugli[1] = 2.5f; + target_butteraugli[2] = 16.0f; + target_butteraugli[4] = 20.0f; + target_butteraugli[8] = 80.0f; + + // The default progressive encoding scheme should make all these downsampling + // factors achievable. + // TODO(veluca): re-enable downsampling 16. + std::vector downsamplings = {1, 2, 4, 8}; //, 16}; + + auto check = [&](uint32_t task, uint32_t /* thread */) -> void { + const size_t downsampling = downsamplings[task]; + DecompressParams dparams; + dparams.max_downsampling = downsampling; + CodecInOut output; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + EXPECT_EQ(output.xsize(), io.xsize()) << "downsampling = " << downsampling; + EXPECT_EQ(output.ysize(), io.ysize()) << "downsampling = " << downsampling; + EXPECT_LE(ButteraugliDistance(io, output, cparams.ba_params, + /*distmap=*/nullptr, nullptr), + target_butteraugli[downsampling]) + << "downsampling: " << downsampling; + }; + pool.Run(0, downsamplings.size(), ThreadPool::SkipInit(), check); +} + +TEST(PassesTest, AllDownsampleFeasibleQProgressive) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + PaddedBytes compressed; + AuxOut aux; + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.qprogressive_mode = true; + cparams.butteraugli_distance = 1.0; + PassesEncoderState enc_state; + ASSERT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, &aux, &pool)); + + EXPECT_LE(compressed.size(), 220000u); + + float target_butteraugli[9] = {}; + target_butteraugli[1] = 3.0f; + target_butteraugli[2] = 6.0f; + target_butteraugli[4] = 10.0f; + target_butteraugli[8] = 80.0f; + + // The default progressive encoding scheme should make all these downsampling + // factors achievable. + std::vector downsamplings = {1, 2, 4, 8}; + + auto check = [&](uint32_t task, uint32_t /* thread */) -> void { + const size_t downsampling = downsamplings[task]; + DecompressParams dparams; + dparams.max_downsampling = downsampling; + CodecInOut output; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + EXPECT_EQ(output.xsize(), io.xsize()) << "downsampling = " << downsampling; + EXPECT_EQ(output.ysize(), io.ysize()) << "downsampling = " << downsampling; + EXPECT_LE(ButteraugliDistance(io, output, cparams.ba_params, + /*distmap=*/nullptr, nullptr), + target_butteraugli[downsampling]) + << "downsampling: " << downsampling; + }; + pool.Run(0, downsamplings.size(), ThreadPool::SkipInit(), check); +} + +TEST(PassesTest, ProgressiveDownsample2DegradesCorrectlyGrayscale) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("wesaturate/500px/cvo9xd_keong_macan_grayscale.png"); + CodecInOut io_orig; + ASSERT_TRUE(SetFromBytes(Span(orig), &io_orig, &pool)); + Rect rect(0, 0, io_orig.xsize(), 128); + // need 2 DC groups for the DC frame to actually be progressive. + Image3F large(4242, rect.ysize()); + ZeroFillImage(&large); + CopyImageTo(rect, *io_orig.Main().color(), rect, &large); + CodecInOut io; + io.metadata = io_orig.metadata; + io.SetFromImage(std::move(large), io_orig.Main().c_current()); + + PaddedBytes compressed; + AuxOut aux; + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_dc = 1; + cparams.responsive = true; + cparams.qprogressive_mode = true; + cparams.butteraugli_distance = 1.0; + PassesEncoderState enc_state; + ASSERT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, &aux, &pool)); + + EXPECT_LE(compressed.size(), 10000u); + + DecompressParams dparams; + dparams.max_downsampling = 1; + CodecInOut output; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + + dparams.max_downsampling = 2; + CodecInOut output_d2; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output_d2, nullptr)); + + // 0 if reading all the passes, ~15 if skipping the 8x pass. + float butteraugli_distance_down2_full = + ButteraugliDistance(output, output_d2, cparams.ba_params, + /*distmap=*/nullptr, nullptr); + + EXPECT_LE(butteraugli_distance_down2_full, 3.2f); + EXPECT_GE(butteraugli_distance_down2_full, 1.0f); +} + +TEST(PassesTest, ProgressiveDownsample2DegradesCorrectly) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io_orig; + ASSERT_TRUE(SetFromBytes(Span(orig), &io_orig, &pool)); + Rect rect(0, 0, io_orig.xsize(), 128); + // need 2 DC groups for the DC frame to actually be progressive. + Image3F large(4242, rect.ysize()); + ZeroFillImage(&large); + CopyImageTo(rect, *io_orig.Main().color(), rect, &large); + CodecInOut io; + io.SetFromImage(std::move(large), io_orig.Main().c_current()); + + PaddedBytes compressed; + AuxOut aux; + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_dc = 1; + cparams.responsive = true; + cparams.qprogressive_mode = true; + cparams.butteraugli_distance = 1.0; + PassesEncoderState enc_state; + ASSERT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, &aux, &pool)); + + EXPECT_LE(compressed.size(), 220000u); + + DecompressParams dparams; + dparams.max_downsampling = 1; + CodecInOut output; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output, nullptr)); + + dparams.max_downsampling = 2; + CodecInOut output_d2; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output_d2, nullptr)); + + // 0 if reading all the passes, ~15 if skipping the 8x pass. + float butteraugli_distance_down2_full = + ButteraugliDistance(output, output_d2, cparams.ba_params, + /*distmap=*/nullptr, nullptr); + + EXPECT_LE(butteraugli_distance_down2_full, 3.0f); + EXPECT_GE(butteraugli_distance_down2_full, 1.0f); +} + +TEST(PassesTest, NonProgressiveDCImage) { + ThreadPoolInternal pool(8); + const PaddedBytes orig = + ReadTestData("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + PaddedBytes compressed; + AuxOut aux; + + CompressParams cparams; + cparams.speed_tier = SpeedTier::kSquirrel; + cparams.progressive_mode = false; + cparams.butteraugli_distance = 2.0; + PassesEncoderState enc_state; + ASSERT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, &aux, &pool)); + + // Even in non-progressive mode, it should be possible to return a DC-only + // image. + DecompressParams dparams; + dparams.max_downsampling = 100; + CodecInOut output; + ASSERT_TRUE(DecodeFile(dparams, compressed, &output, &pool)); + EXPECT_EQ(output.xsize(), io.xsize()); + EXPECT_EQ(output.ysize(), io.ysize()); +} + +TEST(PassesTest, RoundtripSmallNoGaborishPasses) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + CompressParams cparams; + cparams.gaborish = Override::kOff; + cparams.butteraugli_distance = 1.0; + cparams.progressive_mode = true; + DecompressParams dparams; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.7); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/patch_dictionary_internal.h b/lib/jxl/patch_dictionary_internal.h new file mode 100644 index 0000000..e4172f6 --- /dev/null +++ b/lib/jxl/patch_dictionary_internal.h @@ -0,0 +1,31 @@ +// 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_PATCH_DICTIONARY_INTERNAL_H_ +#define LIB_JXL_PATCH_DICTIONARY_INTERNAL_H_ + +#include "lib/jxl/dec_patch_dictionary.h" +#include "lib/jxl/passes_state.h" // for PassesSharedState + +namespace jxl { + +// Context numbers as specified in Section C.4.5, Listing C.2: +enum Contexts { + kNumRefPatchContext = 0, + kReferenceFrameContext = 1, + kPatchSizeContext = 2, + kPatchReferencePositionContext = 3, + kPatchPositionContext = 4, + kPatchBlendModeContext = 5, + kPatchOffsetContext = 6, + kPatchCountContext = 7, + kPatchAlphaChannelContext = 8, + kPatchClampContext = 9, + kNumPatchDictionaryContexts +}; + +} // namespace jxl + +#endif // LIB_JXL_PATCH_DICTIONARY_INTERNAL_H_ diff --git a/lib/jxl/patch_dictionary_test.cc b/lib/jxl/patch_dictionary_test.cc new file mode 100644 index 0000000..3ae064e --- /dev/null +++ b/lib/jxl/patch_dictionary_test.cc @@ -0,0 +1,58 @@ +// 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 "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { + +using ::jxl::test::Roundtrip; + +TEST(PatchDictionaryTest, GrayscaleModular) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = ReadTestData("jxl/grayscale_patches.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + CompressParams cparams; + cparams.color_transform = jxl::ColorTransform::kNone; + cparams.modular_mode = true; + cparams.patches = jxl::Override::kOn; + DecompressParams dparams; + + CodecInOut io2; + // Without patches: ~25k + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 8000u); + VerifyRelativeError(*io.Main().color(), *io2.Main().color(), 1e-7f, 0); +} + +TEST(PatchDictionaryTest, GrayscaleVarDCT) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = ReadTestData("jxl/grayscale_patches.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + + CompressParams cparams; + cparams.patches = jxl::Override::kOn; + DecompressParams dparams; + + CodecInOut io2; + // Without patches: ~47k + EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 14000u); + // Without patches: ~1.2 + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 1.1); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/preview_test.cc b/lib/jxl/preview_test.cc new file mode 100644 index 0000000..aff70b1 --- /dev/null +++ b/lib/jxl/preview_test.cc @@ -0,0 +1,83 @@ +// 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 + +#include + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/override.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/headers.h" +#include "lib/jxl/image_bundle.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { +using test::Roundtrip; + +TEST(PreviewTest, RoundtripGivenPreview) { + ThreadPool* pool = nullptr; + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromBytes(Span(orig), &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + // Same as main image + io.preview_frame = io.Main().Copy(); + const size_t preview_xsize = 15; + const size_t preview_ysize = 27; + io.preview_frame.ShrinkTo(preview_xsize, preview_ysize); + io.metadata.m.have_preview = true; + ASSERT_TRUE(io.metadata.m.preview_size.Set(io.preview_frame.xsize(), + io.preview_frame.ysize())); + + CompressParams cparams; + cparams.butteraugli_distance = 2.0; + cparams.speed_tier = SpeedTier::kSquirrel; + DecompressParams dparams; + + dparams.preview = Override::kOff; + + CodecInOut io2; + Roundtrip(&io, cparams, dparams, pool, &io2); + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.5); + EXPECT_EQ(0u, io2.preview_frame.xsize()); + + dparams.preview = Override::kOn; + + CodecInOut io3; + Roundtrip(&io, cparams, dparams, pool, &io3); + EXPECT_EQ(preview_xsize, io3.metadata.m.preview_size.xsize()); + EXPECT_EQ(preview_ysize, io3.metadata.m.preview_size.ysize()); + EXPECT_EQ(preview_xsize, io3.preview_frame.xsize()); + EXPECT_EQ(preview_ysize, io3.preview_frame.ysize()); + + EXPECT_LE(ButteraugliDistance(io.preview_frame, io3.preview_frame, + cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.5); + EXPECT_LE(ButteraugliDistance(io.Main(), io3.Main(), cparams.ba_params, + /*distmap=*/nullptr, pool), + 2.5); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/progressive_split.cc b/lib/jxl/progressive_split.cc new file mode 100644 index 0000000..d0a16b9 --- /dev/null +++ b/lib/jxl/progressive_split.cc @@ -0,0 +1,128 @@ +// 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/progressive_split.h" + +#include + +#include +#include + +#include "lib/jxl/common.h" +#include "lib/jxl/image.h" + +namespace jxl { + +bool ProgressiveSplitter::SuperblockIsSalient(size_t row_start, + size_t col_start, size_t num_rows, + size_t num_cols) const { + if (saliency_map_ == nullptr || saliency_map_->xsize() == 0 || + saliency_threshold_ == 0.0) { + // If we do not have a saliency-map, or the threshold says to include + // every block, we straightaway classify the superblock as 'salient'. + return true; + } + const size_t row_end = std::min(saliency_map_->ysize(), row_start + num_rows); + const size_t col_end = std::min(saliency_map_->xsize(), col_start + num_cols); + for (size_t num_row = row_start; num_row < row_end; num_row++) { + const float* JXL_RESTRICT map_row = saliency_map_->ConstRow(num_row); + for (size_t num_col = col_start; num_col < col_end; num_col++) { + if (map_row[num_col] >= saliency_threshold_) { + // One of the blocks covered by this superblock is above the saliency + // threshold. + return true; + } + } + } + // We did not see any block above the saliency threshold. + return false; +} + +template +void ProgressiveSplitter::SplitACCoefficients( + const T* JXL_RESTRICT block, size_t size, const AcStrategy& acs, size_t bx, + size_t by, size_t offset, T* JXL_RESTRICT output[kMaxNumPasses][3]) { + auto shift_right_round0 = [&](T v, int shift) { + T one_if_negative = static_cast(v) >> 31; + T add = (one_if_negative << shift) - one_if_negative; + return (v + add) >> shift; + }; + // Early quit for the simple case of only one pass. + if (mode_.num_passes == 1) { + for (size_t c = 0; c < 3; c++) { + memcpy(output[0][c] + offset, block + c * size, sizeof(T) * size); + } + return; + } + size_t ncoeffs_all_done_from_earlier_passes = 1; + size_t previous_pass_salient_only = false; + + int previous_pass_shift = 0; + for (size_t num_pass = 0; num_pass < mode_.num_passes; num_pass++) { // pass + // Zero out output block. + for (size_t c = 0; c < 3; c++) { + memset(output[num_pass][c] + offset, 0, size * sizeof(T)); + } + const bool current_pass_salient_only = mode_.passes[num_pass].salient_only; + const int pass_shift = mode_.passes[num_pass].shift; + size_t frame_ncoeffs = mode_.passes[num_pass].num_coefficients; + for (size_t c = 0; c < 3; c++) { // color-channel + size_t xsize = acs.covered_blocks_x(); + size_t ysize = acs.covered_blocks_y(); + CoefficientLayout(&ysize, &xsize); + if (current_pass_salient_only || previous_pass_salient_only) { + // Current or previous pass is salient-only. + const bool superblock_is_salient = + SuperblockIsSalient(by, bx, ysize, xsize); + if (current_pass_salient_only != superblock_is_salient) { + // Current pass is salient-only, but block is not salient, + // OR last pass was salient-only, and block is salient + // (hence was already included in last pass). + continue; + } + } + for (size_t y = 0; y < ysize * frame_ncoeffs; y++) { // superblk-y + for (size_t x = 0; x < xsize * frame_ncoeffs; x++) { // superblk-x + size_t pos = y * xsize * kBlockDim + x; + if (x < xsize * ncoeffs_all_done_from_earlier_passes && + y < ysize * ncoeffs_all_done_from_earlier_passes) { + // This coefficient was already included in an earlier pass, + // which included a genuinely smaller set of coefficients + // (= is not about saliency-splitting). + continue; + } + T v = block[c * size + pos]; + // Previous pass discarded some bits: do not encode them again. + if (previous_pass_shift != 0) { + T previous_v = shift_right_round0(v, previous_pass_shift) * + (1 << previous_pass_shift); + v -= previous_v; + } + output[num_pass][c][offset + pos] = shift_right_round0(v, pass_shift); + } // superblk-x + } // superblk-y + } // color-channel + if (!current_pass_salient_only) { + // We just finished a non-salient pass. + // Hence, we are now guaranteed to have included all coeffs up to + // frame_ncoeffs in every block, unless the current pass is shifted. + if (mode_.passes[num_pass].shift == 0) { + ncoeffs_all_done_from_earlier_passes = frame_ncoeffs; + } + } + previous_pass_salient_only = current_pass_salient_only; + previous_pass_shift = mode_.passes[num_pass].shift; + } // num_pass +} + +template void ProgressiveSplitter::SplitACCoefficients( + const int32_t* JXL_RESTRICT, size_t, const AcStrategy&, size_t, size_t, + size_t, int32_t* JXL_RESTRICT[kMaxNumPasses][3]); + +template void ProgressiveSplitter::SplitACCoefficients( + const int16_t* JXL_RESTRICT, size_t, const AcStrategy&, size_t, size_t, + size_t, int16_t* JXL_RESTRICT[kMaxNumPasses][3]); + +} // namespace jxl diff --git a/lib/jxl/progressive_split.h b/lib/jxl/progressive_split.h new file mode 100644 index 0000000..68ab7bc --- /dev/null +++ b/lib/jxl/progressive_split.h @@ -0,0 +1,149 @@ +// 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_PROGRESSIVE_SPLIT_H_ +#define LIB_JXL_PROGRESSIVE_SPLIT_H_ + +#include +#include + +#include +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/frame_header.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/splines.h" + +// Functions to split DCT coefficients in multiple passes. All the passes of a +// single frame are added together. + +namespace jxl { + +constexpr size_t kNoDownsamplingFactor = std::numeric_limits::max(); + +struct PassDefinition { + // Side of the square of the coefficients that should be kept in each 8x8 + // block. Must be greater than 1, and at most 8. Should be in non-decreasing + // order. + size_t num_coefficients; + + // How much to shift the encoded values by, with rounding. + size_t shift; + + // Whether or not we should include only salient blocks. + // TODO(veluca): ignored for now. + bool salient_only; + + // If specified, this indicates that if the requested downsampling factor is + // sufficiently high, then it is fine to stop decoding after this pass. + // By default, passes are not marked as being suitable for any downsampling. + size_t suitable_for_downsampling_of_at_least; +}; + +struct ProgressiveMode { + size_t num_passes = 1; + PassDefinition passes[kMaxNumPasses] = {PassDefinition{ + /*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/1}}; + + ProgressiveMode() = default; + + template + explicit ProgressiveMode(const PassDefinition (&p)[nump]) { + JXL_ASSERT(nump <= kMaxNumPasses); + num_passes = nump; + PassDefinition previous_pass{ + /*num_coefficients=*/1, /*shift=*/0, + /*salient_only=*/false, + /*suitable_for_downsampling_of_at_least=*/kNoDownsamplingFactor}; + size_t last_downsampling_factor = kNoDownsamplingFactor; + for (size_t i = 0; i < nump; i++) { + JXL_ASSERT(p[i].num_coefficients > previous_pass.num_coefficients || + (p[i].num_coefficients == previous_pass.num_coefficients && + !p[i].salient_only && previous_pass.salient_only) || + (p[i].num_coefficients == previous_pass.num_coefficients && + p[i].shift < previous_pass.shift)); + JXL_ASSERT(p[i].suitable_for_downsampling_of_at_least == + kNoDownsamplingFactor || + p[i].suitable_for_downsampling_of_at_least <= + last_downsampling_factor); + if (p[i].suitable_for_downsampling_of_at_least != kNoDownsamplingFactor) { + last_downsampling_factor = p[i].suitable_for_downsampling_of_at_least; + } + previous_pass = passes[i] = p[i]; + } + } +}; + +class ProgressiveSplitter { + public: + void SetProgressiveMode(ProgressiveMode mode) { mode_ = mode; } + + void SetSaliencyMap(const ImageF* saliency_map) { + saliency_map_ = saliency_map; + } + + void SetSaliencyThreshold(float threshold) { + saliency_threshold_ = threshold; + } + + size_t GetNumPasses() const { return mode_.num_passes; } + + void InitPasses(Passes* JXL_RESTRICT passes) const { + passes->num_passes = static_cast(GetNumPasses()); + passes->num_downsample = 0; + JXL_ASSERT(passes->num_passes != 0); + passes->shift[passes->num_passes - 1] = 0; + if (passes->num_passes == 1) return; // Done, arrays are empty + + for (uint32_t i = 0; i < mode_.num_passes - 1; ++i) { + const size_t min_downsampling_factor = + mode_.passes[i].suitable_for_downsampling_of_at_least; + passes->shift[i] = mode_.passes[i].shift; + if (1 < min_downsampling_factor && + min_downsampling_factor != kNoDownsamplingFactor) { + passes->downsample[passes->num_downsample] = min_downsampling_factor; + passes->last_pass[passes->num_downsample] = i; + passes->num_downsample += 1; + } + } + } + + template + void SplitACCoefficients(const T* JXL_RESTRICT block, size_t size, + const AcStrategy& acs, size_t bx, size_t by, + size_t offset, + T* JXL_RESTRICT output[kMaxNumPasses][3]); + + private: + bool SuperblockIsSalient(size_t row_start, size_t col_start, size_t num_rows, + size_t num_cols) const; + ProgressiveMode mode_; + + // Not owned, must remain valid. + const ImageF* saliency_map_ = nullptr; + float saliency_threshold_ = 0.0; +}; + +extern template void ProgressiveSplitter::SplitACCoefficients( + const int32_t* JXL_RESTRICT, size_t, const AcStrategy&, size_t, size_t, + size_t, int32_t* JXL_RESTRICT[kMaxNumPasses][3]); + +extern template void ProgressiveSplitter::SplitACCoefficients( + const int16_t* JXL_RESTRICT, size_t, const AcStrategy&, size_t, size_t, + size_t, int16_t* JXL_RESTRICT[kMaxNumPasses][3]); + +} // namespace jxl + +#endif // LIB_JXL_PROGRESSIVE_SPLIT_H_ diff --git a/lib/jxl/quant_weights.cc b/lib/jxl/quant_weights.cc new file mode 100644 index 0000000..316fd94 --- /dev/null +++ b/lib/jxl/quant_weights.cc @@ -0,0 +1,1184 @@ +// 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/quant_weights.h" + +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/dec_modular.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" + +namespace jxl { + +// kQuantWeights[N * N * c + N * y + x] is the relative weight of the (x, y) +// coefficient in component c. Higher weights correspond to finer quantization +// intervals and more bits spent in encoding. + +namespace { + +static constexpr const float kAlmostZero = 1e-8f; + +void GetQuantWeightsDCT2(const QuantEncoding::DCT2Weights& dct2weights, + float* weights) { + for (size_t c = 0; c < 3; c++) { + size_t start = c * 64; + weights[start] = 0xBAD; + weights[start + 1] = weights[start + 8] = dct2weights[c][0]; + weights[start + 9] = dct2weights[c][1]; + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + weights[start + y * 8 + x + 2] = dct2weights[c][2]; + weights[start + (y + 2) * 8 + x] = dct2weights[c][2]; + } + } + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + weights[start + (y + 2) * 8 + x + 2] = dct2weights[c][3]; + } + } + for (size_t y = 0; y < 4; y++) { + for (size_t x = 0; x < 4; x++) { + weights[start + y * 8 + x + 4] = dct2weights[c][4]; + weights[start + (y + 4) * 8 + x] = dct2weights[c][4]; + } + } + for (size_t y = 0; y < 4; y++) { + for (size_t x = 0; x < 4; x++) { + weights[start + (y + 4) * 8 + x + 4] = dct2weights[c][5]; + } + } + } +} + +void GetQuantWeightsIdentity(const QuantEncoding::IdWeights& idweights, + float* weights) { + for (size_t c = 0; c < 3; c++) { + for (int i = 0; i < 64; i++) { + weights[64 * c + i] = idweights[c][0]; + } + weights[64 * c + 1] = idweights[c][1]; + weights[64 * c + 8] = idweights[c][1]; + weights[64 * c + 9] = idweights[c][2]; + } +} + +float Mult(float v) { + if (v > 0) return 1 + v; + return 1 / (1 - v); +} + +float Interpolate(float pos, float max, const float* array, size_t len) { + float scaled_pos = pos * (len - 1) / max; + size_t idx = scaled_pos; + JXL_ASSERT(idx + 1 < len); + float a = array[idx]; + float b = array[idx + 1]; + return a * pow(b / a, scaled_pos - idx); +} + +// Computes quant weights for a COLS*ROWS-sized transform, using num_bands +// eccentricity bands and num_ebands eccentricity bands. If print_mode is 1, +// prints the resulting matrix; if print_mode is 2, prints the matrix in a +// format suitable for a 3d plot with gnuplot. +template +Status GetQuantWeights( + size_t ROWS, size_t COLS, + const DctQuantWeightParams::DistanceBandsArray& distance_bands, + size_t num_bands, float* out) { + for (size_t c = 0; c < 3; c++) { + if (print_mode) { + fprintf(stderr, "Channel %zu\n", c); + } + float bands[DctQuantWeightParams::kMaxDistanceBands] = { + distance_bands[c][0]}; + if (bands[0] < kAlmostZero) return JXL_FAILURE("Invalid distance bands"); + for (size_t i = 1; i < num_bands; i++) { + bands[i] = bands[i - 1] * Mult(distance_bands[c][i]); + if (bands[i] < kAlmostZero) return JXL_FAILURE("Invalid distance bands"); + } + for (size_t y = 0; y < ROWS; y++) { + for (size_t x = 0; x < COLS; x++) { + float dx = 1.0f * x / (COLS - 1); + float dy = 1.0f * y / (ROWS - 1); + float distance = std::sqrt(dx * dx + dy * dy); + float weight = + num_bands == 1 + ? bands[0] + : Interpolate(distance, std::sqrt(2) + 1e-6f, bands, num_bands); + + if (print_mode == 1) { + fprintf(stderr, "%15.12f, ", weight); + } + if (print_mode == 2) { + fprintf(stderr, "%zu %zu %15.12f\n", x, y, weight); + } + out[c * COLS * ROWS + y * COLS + x] = weight; + } + if (print_mode) fprintf(stderr, "\n"); + if (print_mode == 1) fprintf(stderr, "\n"); + } + if (print_mode) fprintf(stderr, "\n"); + } + return true; +} + +Status DecodeDctParams(BitReader* br, DctQuantWeightParams* params) { + params->num_distance_bands = + br->ReadFixedBits() + 1; + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < params->num_distance_bands; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, ¶ms->distance_bands[c][i])); + } + if (params->distance_bands[c][0] < kAlmostZero) { + return JXL_FAILURE("Distance band seed is too small"); + } + params->distance_bands[c][0] *= 64.0f; + } + return true; +} + +Status Decode(BitReader* br, QuantEncoding* encoding, size_t required_size_x, + size_t required_size_y, size_t idx, + ModularFrameDecoder* modular_frame_decoder) { + size_t required_size = required_size_x * required_size_y; + required_size_x *= kBlockDim; + required_size_y *= kBlockDim; + int mode = br->ReadFixedBits(); + switch (mode) { + case QuantEncoding::kQuantModeLibrary: { + encoding->predefined = br->ReadFixedBits(); + if (encoding->predefined >= kNumPredefinedTables) { + return JXL_FAILURE("Invalid predefined table"); + } + break; + } + case QuantEncoding::kQuantModeID: { + if (required_size != 1) return JXL_FAILURE("Invalid mode"); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 3; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &encoding->idweights[c][i])); + if (std::abs(encoding->idweights[c][i]) < kAlmostZero) { + return JXL_FAILURE("ID Quantizer is too small"); + } + encoding->idweights[c][i] *= 64; + } + } + break; + } + case QuantEncoding::kQuantModeDCT2: { + if (required_size != 1) return JXL_FAILURE("Invalid mode"); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 6; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &encoding->dct2weights[c][i])); + if (std::abs(encoding->dct2weights[c][i]) < kAlmostZero) { + return JXL_FAILURE("Quantizer is too small"); + } + encoding->dct2weights[c][i] *= 64; + } + } + break; + } + case QuantEncoding::kQuantModeDCT4X8: { + if (required_size != 1) return JXL_FAILURE("Invalid mode"); + for (size_t c = 0; c < 3; c++) { + JXL_RETURN_IF_ERROR( + F16Coder::Read(br, &encoding->dct4x8multipliers[c])); + if (std::abs(encoding->dct4x8multipliers[c]) < kAlmostZero) { + return JXL_FAILURE("DCT4X8 multiplier is too small"); + } + } + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); + break; + } + case QuantEncoding::kQuantModeDCT4: { + if (required_size != 1) return JXL_FAILURE("Invalid mode"); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 2; i++) { + JXL_RETURN_IF_ERROR( + F16Coder::Read(br, &encoding->dct4multipliers[c][i])); + if (std::abs(encoding->dct4multipliers[c][i]) < kAlmostZero) { + return JXL_FAILURE("DCT4 multiplier is too small"); + } + } + } + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); + break; + } + case QuantEncoding::kQuantModeAFV: { + if (required_size != 1) return JXL_FAILURE("Invalid mode"); + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < 9; i++) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &encoding->afv_weights[c][i])); + } + for (size_t i = 0; i < 6; i++) { + encoding->afv_weights[c][i] *= 64; + } + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params_afv_4x4)); + } + break; + } + case QuantEncoding::kQuantModeDCT: { + JXL_RETURN_IF_ERROR(DecodeDctParams(br, &encoding->dct_params)); + break; + } + case QuantEncoding::kQuantModeRAW: { + // Set mode early, to avoid mem-leak. + encoding->mode = QuantEncoding::kQuantModeRAW; + JXL_RETURN_IF_ERROR(ModularFrameDecoder::DecodeQuantTable( + required_size_x, required_size_y, br, encoding, idx, + modular_frame_decoder)); + break; + } + default: + return JXL_FAILURE("Invalid quantization table encoding"); + } + encoding->mode = QuantEncoding::Mode(mode); + return true; +} + +// TODO(veluca): SIMD-fy. With 256x256, this is actually slow. +Status ComputeQuantTable(const QuantEncoding& encoding, + float* JXL_RESTRICT table, + float* JXL_RESTRICT inv_table, size_t table_num, + DequantMatrices::QuantTable kind, size_t* pos) { + std::vector weights(3 * kMaxQuantTableSize); + + constexpr size_t N = kBlockDim; + size_t wrows = 8 * DequantMatrices::required_size_x[kind], + wcols = 8 * DequantMatrices::required_size_y[kind]; + size_t num = wrows * wcols; + + switch (encoding.mode) { + case QuantEncoding::kQuantModeLibrary: { + // Library and copy quant encoding should get replaced by the actual + // parameters by the caller. + JXL_ASSERT(false); + break; + } + case QuantEncoding::kQuantModeID: { + JXL_ASSERT(num == kDCTBlockSize); + GetQuantWeightsIdentity(encoding.idweights, weights.data()); + break; + } + case QuantEncoding::kQuantModeDCT2: { + JXL_ASSERT(num == kDCTBlockSize); + GetQuantWeightsDCT2(encoding.dct2weights, weights.data()); + break; + } + case QuantEncoding::kQuantModeDCT4: { + JXL_ASSERT(num == kDCTBlockSize); + float weights4x4[3 * 4 * 4]; + // Always use 4x4 GetQuantWeights for DCT4 quantization tables. + JXL_RETURN_IF_ERROR( + GetQuantWeights(4, 4, encoding.dct_params.distance_bands, + encoding.dct_params.num_distance_bands, weights4x4)); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < kBlockDim; y++) { + for (size_t x = 0; x < kBlockDim; x++) { + weights[c * num + y * kBlockDim + x] = + weights4x4[c * 16 + (y / 2) * 4 + (x / 2)]; + } + } + weights[c * num + 1] /= encoding.dct4multipliers[c][0]; + weights[c * num + N] /= encoding.dct4multipliers[c][0]; + weights[c * num + N + 1] /= encoding.dct4multipliers[c][1]; + } + break; + } + case QuantEncoding::kQuantModeDCT4X8: { + JXL_ASSERT(num == kDCTBlockSize); + float weights4x8[3 * 4 * 8]; + // Always use 4x8 GetQuantWeights for DCT4X8 quantization tables. + JXL_RETURN_IF_ERROR( + GetQuantWeights(4, 8, encoding.dct_params.distance_bands, + encoding.dct_params.num_distance_bands, weights4x8)); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < kBlockDim; y++) { + for (size_t x = 0; x < kBlockDim; x++) { + weights[c * num + y * kBlockDim + x] = + weights4x8[c * 32 + (y / 2) * 8 + x]; + } + } + weights[c * num + N] /= encoding.dct4x8multipliers[c]; + } + break; + } + case QuantEncoding::kQuantModeDCT: { + JXL_RETURN_IF_ERROR(GetQuantWeights( + wrows, wcols, encoding.dct_params.distance_bands, + encoding.dct_params.num_distance_bands, weights.data())); + break; + } + case QuantEncoding::kQuantModeRAW: { + if (!encoding.qraw.qtable || encoding.qraw.qtable->size() != 3 * num) { + return JXL_FAILURE("Invalid table encoding"); + } + for (size_t i = 0; i < 3 * num; i++) { + weights[i] = + 1.f / (encoding.qraw.qtable_den * (*encoding.qraw.qtable)[i]); + } + break; + } + case QuantEncoding::kQuantModeAFV: { + constexpr float kFreqs[] = { + 0xBAD, + 0xBAD, + 0.8517778890324296, + 5.37778436506804, + 0xBAD, + 0xBAD, + 4.734747904497923, + 5.449245381693219, + 1.6598270267479331, + 4, + 7.275749096817861, + 10.423227632456525, + 2.662932286148962, + 7.630657783650829, + 8.962388608184032, + 12.97166202570235, + }; + + float weights4x8[3 * 4 * 8]; + JXL_RETURN_IF_ERROR(( + GetQuantWeights(4, 8, encoding.dct_params.distance_bands, + encoding.dct_params.num_distance_bands, weights4x8))); + float weights4x4[3 * 4 * 4]; + JXL_RETURN_IF_ERROR((GetQuantWeights( + 4, 4, encoding.dct_params_afv_4x4.distance_bands, + encoding.dct_params_afv_4x4.num_distance_bands, weights4x4))); + + constexpr float lo = 0.8517778890324296; + constexpr float hi = 12.97166202570235 - lo + 1e-6; + for (size_t c = 0; c < 3; c++) { + float bands[4]; + bands[0] = encoding.afv_weights[c][5]; + if (bands[0] < kAlmostZero) return JXL_FAILURE("Invalid AFV bands"); + for (size_t i = 1; i < 4; i++) { + bands[i] = bands[i - 1] * Mult(encoding.afv_weights[c][i + 5]); + if (bands[i] < kAlmostZero) return JXL_FAILURE("Invalid AFV bands"); + } + size_t start = c * 64; + auto set_weight = [&start, &weights](size_t x, size_t y, float val) { + weights[start + y * 8 + x] = val; + }; + weights[start] = 1; // Not used, but causes MSAN error otherwise. + // Weights for (0, 1) and (1, 0). + set_weight(0, 1, encoding.afv_weights[c][0]); + set_weight(1, 0, encoding.afv_weights[c][1]); + // AFV special weights for 3-pixel corner. + set_weight(0, 2, encoding.afv_weights[c][2]); + set_weight(2, 0, encoding.afv_weights[c][3]); + set_weight(2, 2, encoding.afv_weights[c][4]); + + // All other AFV weights. + for (size_t y = 0; y < 4; y++) { + for (size_t x = 0; x < 4; x++) { + if (x < 2 && y < 2) continue; + float val = Interpolate(kFreqs[y * 4 + x] - lo, hi, bands, 4); + set_weight(2 * x, 2 * y, val); + } + } + + // Put 4x8 weights in odd rows, except (1, 0). + for (size_t y = 0; y < kBlockDim / 2; y++) { + for (size_t x = 0; x < kBlockDim; x++) { + if (x == 0 && y == 0) continue; + weights[c * num + (2 * y + 1) * kBlockDim + x] = + weights4x8[c * 32 + y * 8 + x]; + } + } + // Put 4x4 weights in even rows / odd columns, except (0, 1). + for (size_t y = 0; y < kBlockDim / 2; y++) { + for (size_t x = 0; x < kBlockDim / 2; x++) { + if (x == 0 && y == 0) continue; + weights[c * num + (2 * y) * kBlockDim + 2 * x + 1] = + weights4x4[c * 16 + y * 4 + x]; + } + } + } + break; + } + } + size_t prev_pos = *pos; + for (size_t c = 0; c < 3; c++) { + for (size_t i = 0; i < num; i++) { + float inv_val = weights[c * num + i]; + if (inv_val > 1.0f / kAlmostZero || inv_val < kAlmostZero) { + return JXL_FAILURE("Invalid quantization table"); + } + float val = 1.0f / inv_val; + table[*pos] = val; + inv_table[*pos] = inv_val; + (*pos)++; + } + } + // Ensure that the lowest frequencies have a 0 inverse table. + // This does not affect en/decoding, but allows AC strategy selection to be + // slightly simpler. + size_t xs = DequantMatrices::required_size_x[kind]; + size_t ys = DequantMatrices::required_size_y[kind]; + CoefficientLayout(&ys, &xs); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < ys; y++) { + for (size_t x = 0; x < xs; x++) { + inv_table[prev_pos + c * ys * xs * kDCTBlockSize + y * kBlockDim * xs + + x] = 0; + } + } + } + return true; +} + +} // namespace + +// These definitions are needed before C++17. +constexpr size_t DequantMatrices::required_size_[]; +constexpr size_t DequantMatrices::required_size_x[]; +constexpr size_t DequantMatrices::required_size_y[]; +constexpr DequantMatrices::QuantTable DequantMatrices::kQuantTable[]; + +Status DequantMatrices::Decode(BitReader* br, + ModularFrameDecoder* modular_frame_decoder) { + size_t all_default = br->ReadBits(1); + size_t num_tables = all_default ? 0 : static_cast(kNum); + encodings_.clear(); + encodings_.resize(kNum, QuantEncoding::Library(0)); + for (size_t i = 0; i < num_tables; i++) { + JXL_RETURN_IF_ERROR( + jxl::Decode(br, &encodings_[i], required_size_x[i % kNum], + required_size_y[i % kNum], i, modular_frame_decoder)); + } + return DequantMatrices::Compute(); +} + +Status DequantMatrices::DecodeDC(BitReader* br) { + bool all_default = br->ReadBits(1); + if (!all_default) { + for (size_t c = 0; c < 3; c++) { + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &dc_quant_[c])); + dc_quant_[c] *= 1.0f / 128.0f; + // Negative values and nearly zero are invalid values. + if (dc_quant_[c] < kAlmostZero) { + return JXL_FAILURE("Invalid dc_quant: coefficient is too small."); + } + inv_dc_quant_[c] = 1.0f / dc_quant_[c]; + } + } + return true; +} + +constexpr float V(float v) { return static_cast(v); } + +namespace { +struct DequantMatricesLibraryDef { + // DCT8 + static constexpr const QuantEncodingInternal DCT() { + return QuantEncodingInternal::DCT(DctQuantWeightParams({{{{ + V(3150.0), + V(0.0), + V(-0.4), + V(-0.4), + V(-0.4), + V(-2.0), + }}, + {{ + V(560.0), + V(0.0), + V(-0.3), + V(-0.3), + V(-0.3), + V(-0.3), + }}, + {{ + V(512.0), + V(-2.0), + V(-1.0), + V(0.0), + V(-1.0), + V(-2.0), + }}}}, + 6)); + } + + // Identity + static constexpr const QuantEncodingInternal IDENTITY() { + return QuantEncodingInternal::Identity({{{{ + V(280.0), + V(3160.0), + V(3160.0), + }}, + {{ + V(60.0), + V(864.0), + V(864.0), + }}, + {{ + V(18.0), + V(200.0), + V(200.0), + }}}}); + } + + // DCT2 + static constexpr const QuantEncodingInternal DCT2X2() { + return QuantEncodingInternal::DCT2({{{{ + V(3840.0), + V(2560.0), + V(1280.0), + V(640.0), + V(480.0), + V(300.0), + }}, + {{ + V(960.0), + V(640.0), + V(320.0), + V(180.0), + V(140.0), + V(120.0), + }}, + {{ + V(640.0), + V(320.0), + V(128.0), + V(64.0), + V(32.0), + V(16.0), + }}}}); + } + + // DCT4 (quant_kind 3) + static constexpr const QuantEncodingInternal DCT4X4() { + return QuantEncodingInternal::DCT4(DctQuantWeightParams({{{{ + V(2200.0), + V(0.0), + V(0.0), + V(0.0), + }}, + {{ + V(392.0), + V(0.0), + V(0.0), + V(0.0), + }}, + {{ + V(112.0), + V(-0.25), + V(-0.25), + V(-0.5), + }}}}, + 4), + /* kMul */ + {{{{ + V(1.0), + V(1.0), + }}, + {{ + V(1.0), + V(1.0), + }}, + {{ + V(1.0), + V(1.0), + }}}}); + } + + // DCT16 + static constexpr const QuantEncodingInternal DCT16X16() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(8996.8725711814115328), + V(-1.3000777393353804), + V(-0.49424529824571225), + V(-0.439093774457103443), + V(-0.6350101832695744), + V(-0.90177264050827612), + V(-1.6162099239887414), + }}, + {{ + V(3191.48366296844234752), + V(-0.67424582104194355), + V(-0.80745813428471001), + V(-0.44925837484843441), + V(-0.35865440981033403), + V(-0.31322389111877305), + V(-0.37615025315725483), + }}, + {{ + V(1157.50408145487200256), + V(-2.0531423165804414), + V(-1.4), + V(-0.50687130033378396), + V(-0.42708730624733904), + V(-1.4856834539296244), + V(-4.9209142884401604), + }}}}, + 7)); + } + + // DCT32 + static constexpr const QuantEncodingInternal DCT32X32() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(15718.40830982518931456), + V(-1.025), + V(-0.98), + V(-0.9012), + V(-0.4), + V(-0.48819395464), + V(-0.421064), + V(-0.27), + }}, + {{ + V(7305.7636810695983104), + V(-0.8041958212306401), + V(-0.7633036457487539), + V(-0.55660379990111464), + V(-0.49785304658857626), + V(-0.43699592683512467), + V(-0.40180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(3803.53173721215041536), + V(-3.060733579805728), + V(-2.0413270132490346), + V(-2.0235650159727417), + V(-0.5495389509954993), + V(-0.4), + V(-0.4), + V(-0.3), + }}}}, + 8)); + } + + // DCT16X8 + static constexpr const QuantEncodingInternal DCT8X16() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(7240.7734393502), + V(-0.7), + V(-0.7), + V(-0.2), + V(-0.2), + V(-0.2), + V(-0.5), + }}, + {{ + V(1448.15468787004), + V(-0.5), + V(-0.5), + V(-0.5), + V(-0.2), + V(-0.2), + V(-0.2), + }}, + {{ + V(506.854140754517), + V(-1.4), + V(-0.2), + V(-0.5), + V(-0.5), + V(-1.5), + V(-3.6), + }}}}, + 7)); + } + + // DCT32X8 + static constexpr const QuantEncodingInternal DCT8X32() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(16283.2494710648897), + V(-1.7812845336559429), + V(-1.6309059012653515), + V(-1.0382179034313539), + V(-0.85), + V(-0.7), + V(-0.9), + V(-1.2360638576849587), + }}, + {{ + V(5089.15750884921511936), + V(-0.320049391452786891), + V(-0.35362849922161446), + V(-0.30340000000000003), + V(-0.61), + V(-0.5), + V(-0.5), + V(-0.6), + }}, + {{ + V(3397.77603275308720128), + V(-0.321327362693153371), + V(-0.34507619223117997), + V(-0.70340000000000003), + V(-0.9), + V(-1.0), + V(-1.0), + V(-1.1754605576265209), + }}}}, + 8)); + } + + // DCT32X16 + static constexpr const QuantEncodingInternal DCT16X32() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(13844.97076442300573), + V(-0.97113799999999995), + V(-0.658), + V(-0.42026), + V(-0.22712), + V(-0.2206), + V(-0.226), + V(-0.6), + }}, + {{ + V(4798.964084220744293), + V(-0.61125308982767057), + V(-0.83770786552491361), + V(-0.79014862079498627), + V(-0.2692727459704829), + V(-0.38272769465388551), + V(-0.22924222653091453), + V(-0.20719098826199578), + }}, + {{ + V(1807.236946760964614), + V(-1.2), + V(-1.2), + V(-0.7), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + + // DCT4X8 and 8x4 + static constexpr const QuantEncodingInternal DCT4X8() { + return QuantEncodingInternal::DCT4X8( + DctQuantWeightParams({{ + {{ + V(2198.050556016380522), + V(-0.96269623020744692), + V(-0.76194253026666783), + V(-0.6551140670773547), + }}, + {{ + V(764.3655248643528689), + V(-0.92630200888366945), + V(-0.9675229603596517), + V(-0.27845290869168118), + }}, + {{ + V(527.107573587542228), + V(-1.4594385811273854), + V(-1.450082094097871593), + V(-1.5843722511996204), + }}, + }}, + 4), + /* kMuls */ + {{ + V(1.0), + V(1.0), + V(1.0), + }}); + } + // AFV + static const QuantEncodingInternal AFV0() { + return QuantEncodingInternal::AFV(DCT4X8().dct_params, DCT4X4().dct_params, + {{{{ + // 4x4/4x8 DC tendency. + V(3072.0), + V(3072.0), + // AFV corner. + V(256.0), + V(256.0), + V(256.0), + // AFV high freqs. + V(414.0), + V(0.0), + V(0.0), + V(0.0), + }}, + {{ + // 4x4/4x8 DC tendency. + V(1024.0), + V(1024.0), + // AFV corner. + V(50), + V(50), + V(50), + // AFV high freqs. + V(58.0), + V(0.0), + V(0.0), + V(0.0), + }}, + {{ + // 4x4/4x8 DC tendency. + V(384.0), + V(384.0), + // AFV corner. + V(12.0), + V(12.0), + V(12.0), + // AFV high freqs. + V(22.0), + V(-0.25), + V(-0.25), + V(-0.25), + }}}}); + } + + // DCT64 + static const QuantEncodingInternal DCT64X64() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(0.9 * 26629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(0.9 * 9311.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(0.9 * 4992.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + + // DCT64X32 + static const QuantEncodingInternal DCT32X64() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(0.65 * 23629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(0.65 * 8611.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(0.65 * 4492.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + // DCT128X128 + static const QuantEncodingInternal DCT128X128() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(1.8 * 26629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(1.8 * 9311.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(1.8 * 4992.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + + // DCT128X64 + static const QuantEncodingInternal DCT64X128() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(1.3 * 23629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(1.3 * 8611.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(1.3 * 4492.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + // DCT256X256 + static const QuantEncodingInternal DCT256X256() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(3.6 * 26629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(3.6 * 9311.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(3.6 * 4992.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } + + // DCT256X128 + static const QuantEncodingInternal DCT128X256() { + return QuantEncodingInternal::DCT( + DctQuantWeightParams({{{{ + V(2.6 * 23629.073922049845), + V(-1.025), + V(-0.78), + V(-0.65012), + V(-0.19041574084286472), + V(-0.20819395464), + V(-0.421064), + V(-0.32733845535848671), + }}, + {{ + V(2.6 * 8611.3238710010046), + V(-0.3041958212306401), + V(-0.3633036457487539), + V(-0.35660379990111464), + V(-0.3443074455424403), + V(-0.33699592683512467), + V(-0.30180866526242109), + V(-0.27321683125358037), + }}, + {{ + V(2.6 * 4492.2486445538634), + V(-1.2), + V(-1.2), + V(-0.8), + V(-0.7), + V(-0.7), + V(-0.4), + V(-0.5), + }}}}, + 8)); + } +}; +} // namespace + +const DequantMatrices::DequantLibraryInternal DequantMatrices::LibraryInit() { + static_assert(kNum == 17, + "Update this function when adding new quantization kinds."); + static_assert(kNumPredefinedTables == 1, + "Update this function when adding new quantization matrices to " + "the library."); + + // The library and the indices need to be kept in sync manually. + static_assert(0 == DCT, "Update the DequantLibrary array below."); + static_assert(1 == IDENTITY, "Update the DequantLibrary array below."); + static_assert(2 == DCT2X2, "Update the DequantLibrary array below."); + static_assert(3 == DCT4X4, "Update the DequantLibrary array below."); + static_assert(4 == DCT16X16, "Update the DequantLibrary array below."); + static_assert(5 == DCT32X32, "Update the DequantLibrary array below."); + static_assert(6 == DCT8X16, "Update the DequantLibrary array below."); + static_assert(7 == DCT8X32, "Update the DequantLibrary array below."); + static_assert(8 == DCT16X32, "Update the DequantLibrary array below."); + static_assert(9 == DCT4X8, "Update the DequantLibrary array below."); + static_assert(10 == AFV0, "Update the DequantLibrary array below."); + static_assert(11 == DCT64X64, "Update the DequantLibrary array below."); + static_assert(12 == DCT32X64, "Update the DequantLibrary array below."); + static_assert(13 == DCT128X128, "Update the DequantLibrary array below."); + static_assert(14 == DCT64X128, "Update the DequantLibrary array below."); + static_assert(15 == DCT256X256, "Update the DequantLibrary array below."); + static_assert(16 == DCT128X256, "Update the DequantLibrary array below."); + return DequantMatrices::DequantLibraryInternal{{ + DequantMatricesLibraryDef::DCT(), + DequantMatricesLibraryDef::IDENTITY(), + DequantMatricesLibraryDef::DCT2X2(), + DequantMatricesLibraryDef::DCT4X4(), + DequantMatricesLibraryDef::DCT16X16(), + DequantMatricesLibraryDef::DCT32X32(), + DequantMatricesLibraryDef::DCT8X16(), + DequantMatricesLibraryDef::DCT8X32(), + DequantMatricesLibraryDef::DCT16X32(), + DequantMatricesLibraryDef::DCT4X8(), + DequantMatricesLibraryDef::AFV0(), + DequantMatricesLibraryDef::DCT64X64(), + DequantMatricesLibraryDef::DCT32X64(), + // Same default for large transforms (128+) as for 64x* transforms. + DequantMatricesLibraryDef::DCT128X128(), + DequantMatricesLibraryDef::DCT64X128(), + DequantMatricesLibraryDef::DCT256X256(), + DequantMatricesLibraryDef::DCT128X256(), + }}; +} + +const QuantEncoding* DequantMatrices::Library() { + static const DequantMatrices::DequantLibraryInternal kDequantLibrary = + DequantMatrices::LibraryInit(); + // Downcast the result to a const QuantEncoding* from QuantEncodingInternal* + // since the subclass (QuantEncoding) doesn't add any new members and users + // will need to upcast to QuantEncodingInternal to access the members of that + // class. This allows to have kDequantLibrary as a constexpr value while still + // allowing to create QuantEncoding::RAW() instances that use std::vector in + // C++11. + return reinterpret_cast(kDequantLibrary.data()); +} + +Status DequantMatrices::Compute() { + size_t pos = 0; + + struct DefaultMatrices { + DefaultMatrices() { + const QuantEncoding* library = Library(); + size_t pos = 0; + for (size_t i = 0; i < kNum; i++) { + JXL_CHECK(ComputeQuantTable(library[i], table, inv_table, i, + QuantTable(i), &pos)); + } + JXL_CHECK(pos == kTotalTableSize); + } + HWY_ALIGN_MAX float table[kTotalTableSize]; + HWY_ALIGN_MAX float inv_table[kTotalTableSize]; + }; + + static const DefaultMatrices& default_matrices = + *hwy::MakeUniqueAligned().release(); + + JXL_ASSERT(encodings_.size() == kNum); + + bool has_nondefault_matrix = false; + for (const auto& enc : encodings_) { + if (enc.mode != QuantEncoding::kQuantModeLibrary) { + has_nondefault_matrix = true; + } + } + if (has_nondefault_matrix) { + table_storage_ = hwy::AllocateAligned(2 * kTotalTableSize); + table_ = table_storage_.get(); + inv_table_ = table_storage_.get() + kTotalTableSize; + for (size_t table = 0; table < kNum; table++) { + size_t prev_pos = pos; + if (encodings_[table].mode == QuantEncoding::kQuantModeLibrary) { + size_t num = required_size_[table] * kDCTBlockSize; + memcpy(table_storage_.get() + prev_pos, + default_matrices.table + prev_pos, num * sizeof(float) * 3); + memcpy(table_storage_.get() + kTotalTableSize + prev_pos, + default_matrices.inv_table + prev_pos, num * sizeof(float) * 3); + pos += num * 3; + } else { + JXL_RETURN_IF_ERROR( + ComputeQuantTable(encodings_[table], table_storage_.get(), + table_storage_.get() + kTotalTableSize, table, + QuantTable(table), &pos)); + } + } + JXL_ASSERT(pos == kTotalTableSize); + } else { + table_ = default_matrices.table; + inv_table_ = default_matrices.inv_table; + } + + return true; +} + +} // namespace jxl diff --git a/lib/jxl/quant_weights.h b/lib/jxl/quant_weights.h new file mode 100644 index 0000000..816362f --- /dev/null +++ b/lib/jxl/quant_weights.h @@ -0,0 +1,469 @@ +// 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_QUANT_WEIGHTS_H_ +#define LIB_JXL_QUANT_WEIGHTS_H_ + +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/cache_aligned.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/image.h" + +namespace jxl { + +template +constexpr T ArraySum(T (&a)[N], size_t i = N - 1) { + static_assert(N > 0, "Trying to compute the sum of an empty array"); + return i == 0 ? a[0] : a[i] + ArraySum(a, i - 1); +} + +static constexpr size_t kMaxQuantTableSize = AcStrategy::kMaxCoeffArea; +static constexpr size_t kNumPredefinedTables = 1; +static constexpr size_t kCeilLog2NumPredefinedTables = 0; +static constexpr size_t kLog2NumQuantModes = 3; + +struct DctQuantWeightParams { + static constexpr size_t kLog2MaxDistanceBands = 4; + static constexpr size_t kMaxDistanceBands = 1 + (1 << kLog2MaxDistanceBands); + typedef std::array, 3> + DistanceBandsArray; + + size_t num_distance_bands = 0; + DistanceBandsArray distance_bands = {}; + + constexpr DctQuantWeightParams() : num_distance_bands(0) {} + + constexpr DctQuantWeightParams(const DistanceBandsArray& dist_bands, + size_t num_dist_bands) + : num_distance_bands(num_dist_bands), distance_bands(dist_bands) {} + + template + explicit DctQuantWeightParams(const float dist_bands[3][num_dist_bands]) { + num_distance_bands = num_dist_bands; + for (size_t c = 0; c < 3; c++) { + memcpy(distance_bands[c].data(), dist_bands[c], + sizeof(float) * num_dist_bands); + } + } +}; + +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +struct QuantEncodingInternal { + enum Mode { + kQuantModeLibrary, + kQuantModeID, + kQuantModeDCT2, + kQuantModeDCT4, + kQuantModeDCT4X8, + kQuantModeAFV, + kQuantModeDCT, + kQuantModeRAW, + }; + + template + struct Tag {}; + + typedef std::array, 3> IdWeights; + typedef std::array, 3> DCT2Weights; + typedef std::array, 3> DCT4Multipliers; + typedef std::array, 3> AFVWeights; + typedef std::array DCT4x8Multipliers; + + static constexpr QuantEncodingInternal Library(uint8_t predefined) { + return ((predefined < kNumPredefinedTables) || + JXL_ABORT("Assert predefined < kNumPredefinedTables")), + QuantEncodingInternal(Tag(), predefined); + } + constexpr QuantEncodingInternal(Tag /* tag */, + uint8_t predefined) + : mode(kQuantModeLibrary), predefined(predefined) {} + + // Identity + // xybweights is an array of {xweights, yweights, bweights}. + static constexpr QuantEncodingInternal Identity(const IdWeights& xybweights) { + return QuantEncodingInternal(Tag(), xybweights); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const IdWeights& xybweights) + : mode(kQuantModeID), idweights(xybweights) {} + + // DCT2 + static constexpr QuantEncodingInternal DCT2(const DCT2Weights& xybweights) { + return QuantEncodingInternal(Tag(), xybweights); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const DCT2Weights& xybweights) + : mode(kQuantModeDCT2), dct2weights(xybweights) {} + + // DCT4 + static constexpr QuantEncodingInternal DCT4( + const DctQuantWeightParams& params, const DCT4Multipliers& xybmul) { + return QuantEncodingInternal(Tag(), params, xybmul); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const DctQuantWeightParams& params, + const DCT4Multipliers& xybmul) + : mode(kQuantModeDCT4), dct_params(params), dct4multipliers(xybmul) {} + + // DCT4x8 + static constexpr QuantEncodingInternal DCT4X8( + const DctQuantWeightParams& params, const DCT4x8Multipliers& xybmul) { + return QuantEncodingInternal(Tag(), params, xybmul); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const DctQuantWeightParams& params, + const DCT4x8Multipliers& xybmul) + : mode(kQuantModeDCT4X8), dct_params(params), dct4x8multipliers(xybmul) {} + + // DCT + static constexpr QuantEncodingInternal DCT( + const DctQuantWeightParams& params) { + return QuantEncodingInternal(Tag(), params); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const DctQuantWeightParams& params) + : mode(kQuantModeDCT), dct_params(params) {} + + // AFV + static constexpr QuantEncodingInternal AFV( + const DctQuantWeightParams& params4x8, + const DctQuantWeightParams& params4x4, const AFVWeights& weights) { + return QuantEncodingInternal(Tag(), params4x8, params4x4, + weights); + } + constexpr QuantEncodingInternal(Tag /* tag */, + const DctQuantWeightParams& params4x8, + const DctQuantWeightParams& params4x4, + const AFVWeights& weights) + : mode(kQuantModeAFV), + dct_params(params4x8), + afv_weights(weights), + dct_params_afv_4x4(params4x4) {} + + // This constructor is not constexpr so it can't be used in any of the + // constexpr cases above. + explicit QuantEncodingInternal(Mode mode) : mode(mode) {} + + Mode mode; + + // Weights for DCT4+ tables. + DctQuantWeightParams dct_params; + + union { + // Weights for identity. + IdWeights idweights; + + // Weights for DCT2. + DCT2Weights dct2weights; + + // Extra multipliers for coefficients 01/10 and 11 for DCT4 and AFV. + DCT4Multipliers dct4multipliers; + + // Weights for AFV. {0, 1} are used directly for coefficients (0, 1) and (1, + // 0); {2, 3, 4} are used directly corner DC, (1,0) - (0,1) and (0, 1) + + // (1, 0) - (0, 0) inside the AFV block. Values from 5 to 8 are interpolated + // as in GetQuantWeights for DC and are used for other coefficients. + AFVWeights afv_weights = {}; + + // Extra multipliers for coefficients 01 or 10 for DCT4X8 and DCT8X4. + DCT4x8Multipliers dct4x8multipliers; + + // Only used in kQuantModeRAW mode. + struct { + // explicit quantization table (like in JPEG) + std::vector* qtable = nullptr; + float qtable_den = 1.f / (8 * 255); + } qraw; + }; + + // Weights for 4x4 sub-block in AFV. + DctQuantWeightParams dct_params_afv_4x4; + + union { + // Which predefined table to use. Only used if mode is kQuantModeLibrary. + uint8_t predefined = 0; + + // Which other quant table to copy; must copy from a table that comes before + // the current one. Only used if mode is kQuantModeCopy. + uint8_t source; + }; +}; + +class QuantEncoding final : public QuantEncodingInternal { + public: + QuantEncoding(const QuantEncoding& other) + : QuantEncodingInternal( + static_cast(other)) { + if (mode == kQuantModeRAW && qraw.qtable) { + // Need to make a copy of the passed *qtable. + qraw.qtable = new std::vector(*other.qraw.qtable); + } + } + QuantEncoding(QuantEncoding&& other) noexcept + : QuantEncodingInternal( + static_cast(other)) { + // Steal the qtable from the other object if any. + if (mode == kQuantModeRAW) { + other.qraw.qtable = nullptr; + } + } + QuantEncoding& operator=(const QuantEncoding& other) { + if (mode == kQuantModeRAW && qraw.qtable) { + delete qraw.qtable; + } + *static_cast(this) = + QuantEncodingInternal(static_cast(other)); + if (mode == kQuantModeRAW && qraw.qtable) { + // Need to make a copy of the passed *qtable. + qraw.qtable = new std::vector(*other.qraw.qtable); + } + return *this; + } + + ~QuantEncoding() { + if (mode == kQuantModeRAW && qraw.qtable) { + delete qraw.qtable; + } + } + + // Wrappers of the QuantEncodingInternal:: static functions that return a + // QuantEncoding instead. This is using the explicit and private cast from + // QuantEncodingInternal to QuantEncoding, which would be inlined anyway. + // In general, you should use this wrappers. The only reason to directly + // create a QuantEncodingInternal instance is if you need a constexpr version + // of this class. Note that RAW() is not supported in that case since it uses + // a std::vector. + static QuantEncoding Library(uint8_t predefined) { + return QuantEncoding(QuantEncodingInternal::Library(predefined)); + } + static QuantEncoding Identity(const IdWeights& xybweights) { + return QuantEncoding(QuantEncodingInternal::Identity(xybweights)); + } + static QuantEncoding DCT2(const DCT2Weights& xybweights) { + return QuantEncoding(QuantEncodingInternal::DCT2(xybweights)); + } + static QuantEncoding DCT4(const DctQuantWeightParams& params, + const DCT4Multipliers& xybmul) { + return QuantEncoding(QuantEncodingInternal::DCT4(params, xybmul)); + } + static QuantEncoding DCT4X8(const DctQuantWeightParams& params, + const DCT4x8Multipliers& xybmul) { + return QuantEncoding(QuantEncodingInternal::DCT4X8(params, xybmul)); + } + static QuantEncoding DCT(const DctQuantWeightParams& params) { + return QuantEncoding(QuantEncodingInternal::DCT(params)); + } + static QuantEncoding AFV(const DctQuantWeightParams& params4x8, + const DctQuantWeightParams& params4x4, + const AFVWeights& weights) { + return QuantEncoding( + QuantEncodingInternal::AFV(params4x8, params4x4, weights)); + } + + // RAW, note that this one is not a constexpr one. + static QuantEncoding RAW(const std::vector& qtable, int shift = 0) { + QuantEncoding encoding(kQuantModeRAW); + encoding.qraw.qtable = new std::vector(); + *encoding.qraw.qtable = qtable; + encoding.qraw.qtable_den = (1 << shift) * (1.f / (8 * 255)); + return encoding; + } + + private: + explicit QuantEncoding(const QuantEncodingInternal& other) + : QuantEncodingInternal(other) {} + + explicit QuantEncoding(QuantEncodingInternal::Mode mode) + : QuantEncodingInternal(mode) {} +}; + +// A constexpr QuantEncodingInternal instance is often downcasted to the +// QuantEncoding subclass even if the instance wasn't an instance of the +// subclass. This is safe because user will upcast to QuantEncodingInternal to +// access any of its members. +static_assert(sizeof(QuantEncoding) == sizeof(QuantEncodingInternal), + "Don't add any members to QuantEncoding"); + +// Let's try to keep these 2**N for possible future simplicity. +const float kInvDCQuant[3] = { + 4096.0f, + 512.0f, + 256.0f, +}; + +const float kDCQuant[3] = { + 1.0f / kInvDCQuant[0], + 1.0f / kInvDCQuant[1], + 1.0f / kInvDCQuant[2], +}; + +class ModularFrameEncoder; +class ModularFrameDecoder; + +class DequantMatrices { + public: + enum QuantTable : size_t { + DCT = 0, + IDENTITY, + DCT2X2, + DCT4X4, + DCT16X16, + DCT32X32, + // DCT16X8 + DCT8X16, + // DCT32X8 + DCT8X32, + // DCT32X16 + DCT16X32, + DCT4X8, + // DCT8X4 + AFV0, + // AFV1 + // AFV2 + // AFV3 + DCT64X64, + // DCT64X32, + DCT32X64, + DCT128X128, + // DCT128X64, + DCT64X128, + DCT256X256, + // DCT256X128, + DCT128X256, + kNum + }; + + static constexpr QuantTable kQuantTable[] = { + QuantTable::DCT, QuantTable::IDENTITY, QuantTable::DCT2X2, + QuantTable::DCT4X4, QuantTable::DCT16X16, QuantTable::DCT32X32, + QuantTable::DCT8X16, QuantTable::DCT8X16, QuantTable::DCT8X32, + QuantTable::DCT8X32, QuantTable::DCT16X32, QuantTable::DCT16X32, + QuantTable::DCT4X8, QuantTable::DCT4X8, QuantTable::AFV0, + QuantTable::AFV0, QuantTable::AFV0, QuantTable::AFV0, + QuantTable::DCT64X64, QuantTable::DCT32X64, QuantTable::DCT32X64, + QuantTable::DCT128X128, QuantTable::DCT64X128, QuantTable::DCT64X128, + QuantTable::DCT256X256, QuantTable::DCT128X256, QuantTable::DCT128X256, + }; + static_assert(AcStrategy::kNumValidStrategies == + sizeof(kQuantTable) / sizeof *kQuantTable, + "Update this array when adding or removing AC strategies."); + + DequantMatrices() { + encodings_.resize(size_t(QuantTable::kNum), QuantEncoding::Library(0)); + size_t pos = 0; + size_t offsets[kNum * 3]; + for (size_t i = 0; i < size_t(QuantTable::kNum); i++) { + encodings_[i] = QuantEncoding::Library(0); + size_t num = required_size_[i] * kDCTBlockSize; + for (size_t c = 0; c < 3; c++) { + offsets[3 * i + c] = pos + c * num; + } + pos += 3 * num; + } + for (size_t i = 0; i < AcStrategy::kNumValidStrategies; i++) { + for (size_t c = 0; c < 3; c++) { + table_offsets_[i * 3 + c] = offsets[kQuantTable[i] * 3 + c]; + } + } + // Default quantization tables need to be valid. + JXL_CHECK(Compute()); + } + + static const QuantEncoding* Library(); + + typedef std::array + DequantLibraryInternal; + // Return the array of library kNumPredefinedTables QuantEncoding entries as + // a constexpr array. Use Library() to obtain a pointer to the copy in the + // .cc file. + static const DequantLibraryInternal LibraryInit(); + + JXL_INLINE size_t MatrixOffset(size_t quant_kind, size_t c) const { + JXL_DASSERT(quant_kind < AcStrategy::kNumValidStrategies); + return table_offsets_[quant_kind * 3 + c]; + } + + // Returns aligned memory. + JXL_INLINE const float* Matrix(size_t quant_kind, size_t c) const { + JXL_DASSERT(quant_kind < AcStrategy::kNumValidStrategies); + return &table_[MatrixOffset(quant_kind, c)]; + } + + JXL_INLINE const float* InvMatrix(size_t quant_kind, size_t c) const { + JXL_DASSERT(quant_kind < AcStrategy::kNumValidStrategies); + return &inv_table_[MatrixOffset(quant_kind, c)]; + } + + // DC quants are used in modular mode for XYB multipliers. + JXL_INLINE float DCQuant(size_t c) const { return dc_quant_[c]; } + JXL_INLINE const float* DCQuants() const { return dc_quant_; } + + JXL_INLINE float InvDCQuant(size_t c) const { return inv_dc_quant_[c]; } + + // For encoder. + void SetEncodings(const std::vector& encodings) { + encodings_ = encodings; + } + + // For encoder. + void SetDCQuant(const float dc[3]) { + for (size_t c = 0; c < 3; c++) { + dc_quant_[c] = 1.0f / dc[c]; + inv_dc_quant_[c] = dc[c]; + } + } + + Status Decode(BitReader* br, + ModularFrameDecoder* modular_frame_decoder = nullptr); + Status DecodeDC(BitReader* br); + + const std::vector& encodings() const { return encodings_; } + + static constexpr size_t required_size_x[] = {1, 1, 1, 1, 2, 4, 1, 1, 2, + 1, 1, 8, 4, 16, 8, 32, 16}; + static_assert(kNum == sizeof(required_size_x) / sizeof(*required_size_x), + "Update this array when adding or removing quant tables."); + + static constexpr size_t required_size_y[] = {1, 1, 1, 1, 2, 4, 2, 4, 4, + 1, 1, 8, 8, 16, 16, 32, 32}; + static_assert(kNum == sizeof(required_size_y) / sizeof(*required_size_y), + "Update this array when adding or removing quant tables."); + + private: + Status Compute(); + + static constexpr size_t required_size_[] = { + 1, 1, 1, 1, 4, 16, 2, 4, 8, 1, 1, 64, 32, 256, 128, 1024, 512}; + static_assert(kNum == sizeof(required_size_) / sizeof(*required_size_), + "Update this array when adding or removing quant tables."); + static constexpr size_t kTotalTableSize = + ArraySum(required_size_) * kDCTBlockSize * 3; + + // kTotalTableSize entries followed by kTotalTableSize for inv_table + hwy::AlignedFreeUniquePtr table_storage_; + const float* table_; + const float* inv_table_; + float dc_quant_[3] = {kDCQuant[0], kDCQuant[1], kDCQuant[2]}; + float inv_dc_quant_[3] = {kInvDCQuant[0], kInvDCQuant[1], kInvDCQuant[2]}; + size_t table_offsets_[AcStrategy::kNumValidStrategies * 3]; + std::vector encodings_; +}; + +} // namespace jxl + +#endif // LIB_JXL_QUANT_WEIGHTS_H_ diff --git a/lib/jxl/quant_weights_test.cc b/lib/jxl/quant_weights_test.cc new file mode 100644 index 0000000..2392c74 --- /dev/null +++ b/lib/jxl/quant_weights_test.cc @@ -0,0 +1,240 @@ +// 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/quant_weights.h" + +#include + +#include +#include +#include // HWY_ALIGN_MAX +#include +#include +#include + +#include "lib/jxl/dct_for_test.h" +#include "lib/jxl/dec_transforms_testonly.h" +#include "lib/jxl/enc_modular.h" +#include "lib/jxl/enc_quant_weights.h" +#include "lib/jxl/enc_transforms.h" + +namespace jxl { +namespace { + +template +void CheckSimilar(T a, T b) { + EXPECT_EQ(a, b); +} +// minimum exponent = -15. +template <> +void CheckSimilar(float a, float b) { + float m = std::max(std::abs(a), std::abs(b)); + // 10 bits of precision are used in the format. Relative error should be + // below 2^-10. + EXPECT_LE(std::abs(a - b), m / 1024.0f) << "a: " << a << " b: " << b; +} + +TEST(QuantWeightsTest, DC) { + DequantMatrices mat; + float dc_quant[3] = {1e+5, 1e+3, 1e+1}; + DequantMatricesSetCustomDC(&mat, dc_quant); + for (size_t c = 0; c < 3; c++) { + CheckSimilar(mat.InvDCQuant(c), dc_quant[c]); + } +} + +void RoundtripMatrices(const std::vector& encodings) { + ASSERT_TRUE(encodings.size() == DequantMatrices::kNum); + DequantMatrices mat; + CodecMetadata metadata; + FrameHeader frame_header(&metadata); + ModularFrameEncoder encoder(frame_header, CompressParams{}); + DequantMatricesSetCustom(&mat, encodings, &encoder); + const std::vector& encodings_dec = mat.encodings(); + for (size_t i = 0; i < encodings.size(); i++) { + const QuantEncoding& e = encodings[i]; + const QuantEncoding& d = encodings_dec[i]; + // Check values roundtripped correctly. + EXPECT_EQ(e.mode, d.mode); + EXPECT_EQ(e.predefined, d.predefined); + EXPECT_EQ(e.source, d.source); + + EXPECT_EQ(static_cast(e.dct_params.num_distance_bands), + static_cast(d.dct_params.num_distance_bands)); + for (size_t c = 0; c < 3; c++) { + for (size_t j = 0; j < DctQuantWeightParams::kMaxDistanceBands; j++) { + CheckSimilar(e.dct_params.distance_bands[c][j], + d.dct_params.distance_bands[c][j]); + } + } + + if (e.mode == QuantEncoding::kQuantModeRAW) { + EXPECT_FALSE(!e.qraw.qtable); + EXPECT_FALSE(!d.qraw.qtable); + EXPECT_EQ(e.qraw.qtable->size(), d.qraw.qtable->size()); + for (size_t j = 0; j < e.qraw.qtable->size(); j++) { + EXPECT_EQ((*e.qraw.qtable)[j], (*d.qraw.qtable)[j]); + } + EXPECT_NEAR(e.qraw.qtable_den, d.qraw.qtable_den, 1e-7f); + } else { + // modes different than kQuantModeRAW use one of the other fields used + // here, which all happen to be arrays of floats. + for (size_t c = 0; c < 3; c++) { + for (size_t j = 0; j < 3; j++) { + CheckSimilar(e.idweights[c][j], d.idweights[c][j]); + } + for (size_t j = 0; j < 6; j++) { + CheckSimilar(e.dct2weights[c][j], d.dct2weights[c][j]); + } + for (size_t j = 0; j < 2; j++) { + CheckSimilar(e.dct4multipliers[c][j], d.dct4multipliers[c][j]); + } + CheckSimilar(e.dct4x8multipliers[c], d.dct4x8multipliers[c]); + for (size_t j = 0; j < 9; j++) { + CheckSimilar(e.afv_weights[c][j], d.afv_weights[c][j]); + } + for (size_t j = 0; j < DctQuantWeightParams::kMaxDistanceBands; j++) { + CheckSimilar(e.dct_params_afv_4x4.distance_bands[c][j], + d.dct_params_afv_4x4.distance_bands[c][j]); + } + } + } + } +} + +TEST(QuantWeightsTest, AllDefault) { + std::vector encodings(DequantMatrices::kNum, + QuantEncoding::Library(0)); + RoundtripMatrices(encodings); +} + +void TestSingleQuantMatrix(DequantMatrices::QuantTable kind) { + std::vector encodings(DequantMatrices::kNum, + QuantEncoding::Library(0)); + encodings[kind] = DequantMatrices::Library()[kind]; + RoundtripMatrices(encodings); +} + +// Ensure we can reasonably represent default quant tables. +TEST(QuantWeightsTest, DCT) { TestSingleQuantMatrix(DequantMatrices::DCT); } +TEST(QuantWeightsTest, IDENTITY) { + TestSingleQuantMatrix(DequantMatrices::IDENTITY); +} +TEST(QuantWeightsTest, DCT2X2) { + TestSingleQuantMatrix(DequantMatrices::DCT2X2); +} +TEST(QuantWeightsTest, DCT4X4) { + TestSingleQuantMatrix(DequantMatrices::DCT4X4); +} +TEST(QuantWeightsTest, DCT16X16) { + TestSingleQuantMatrix(DequantMatrices::DCT16X16); +} +TEST(QuantWeightsTest, DCT32X32) { + TestSingleQuantMatrix(DequantMatrices::DCT32X32); +} +TEST(QuantWeightsTest, DCT8X16) { + TestSingleQuantMatrix(DequantMatrices::DCT8X16); +} +TEST(QuantWeightsTest, DCT8X32) { + TestSingleQuantMatrix(DequantMatrices::DCT8X32); +} +TEST(QuantWeightsTest, DCT16X32) { + TestSingleQuantMatrix(DequantMatrices::DCT16X32); +} +TEST(QuantWeightsTest, DCT4X8) { + TestSingleQuantMatrix(DequantMatrices::DCT4X8); +} +TEST(QuantWeightsTest, AFV0) { TestSingleQuantMatrix(DequantMatrices::AFV0); } +TEST(QuantWeightsTest, RAW) { + std::vector encodings(DequantMatrices::kNum, + QuantEncoding::Library(0)); + std::vector matrix(3 * 32 * 32); + std::mt19937 rng; + std::uniform_int_distribution dist(1, 255); + for (size_t i = 0; i < matrix.size(); i++) matrix[i] = dist(rng); + encodings[DequantMatrices::kQuantTable[AcStrategy::DCT32X32]] = + QuantEncoding::RAW(matrix, 2); + RoundtripMatrices(encodings); +} + +class QuantWeightsTargetTest : public hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(QuantWeightsTargetTest); + +TEST_P(QuantWeightsTargetTest, DCTUniform) { + constexpr float kUniformQuant = 4; + float weights[3][2] = {{1.0f / kUniformQuant, 0}, + {1.0f / kUniformQuant, 0}, + {1.0f / kUniformQuant, 0}}; + DctQuantWeightParams dct_params(weights); + std::vector encodings(DequantMatrices::kNum, + QuantEncoding::DCT(dct_params)); + DequantMatrices dequant_matrices; + CodecMetadata metadata; + FrameHeader frame_header(&metadata); + ModularFrameEncoder encoder(frame_header, CompressParams{}); + DequantMatricesSetCustom(&dequant_matrices, encodings, &encoder); + + const float dc_quant[3] = {1.0f / kUniformQuant, 1.0f / kUniformQuant, + 1.0f / kUniformQuant}; + DequantMatricesSetCustomDC(&dequant_matrices, dc_quant); + + HWY_ALIGN_MAX float scratch_space[16 * 16 * 2]; + + // DCT8 + { + HWY_ALIGN_MAX float pixels[64]; + std::iota(std::begin(pixels), std::end(pixels), 0); + HWY_ALIGN_MAX float coeffs[64]; + const AcStrategy::Type dct = AcStrategy::DCT; + TransformFromPixels(dct, pixels, 8, coeffs, scratch_space); + HWY_ALIGN_MAX double slow_coeffs[64]; + for (size_t i = 0; i < 64; i++) slow_coeffs[i] = pixels[i]; + DCTSlow<8>(slow_coeffs); + + for (size_t i = 0; i < 64; i++) { + // DCTSlow doesn't multiply/divide by 1/N, so we do it manually. + slow_coeffs[i] = roundf(slow_coeffs[i] / kUniformQuant) * kUniformQuant; + coeffs[i] = roundf(coeffs[i] / dequant_matrices.Matrix(dct, 0)[i]) * + dequant_matrices.Matrix(dct, 0)[i]; + } + IDCTSlow<8>(slow_coeffs); + TransformToPixels(dct, coeffs, pixels, 8, scratch_space); + for (size_t i = 0; i < 64; i++) { + EXPECT_NEAR(pixels[i], slow_coeffs[i], 1e-4); + } + } + + // DCT16 + { + HWY_ALIGN_MAX float pixels[64 * 4]; + std::iota(std::begin(pixels), std::end(pixels), 0); + HWY_ALIGN_MAX float coeffs[64 * 4]; + const AcStrategy::Type dct = AcStrategy::DCT16X16; + TransformFromPixels(dct, pixels, 16, coeffs, scratch_space); + HWY_ALIGN_MAX double slow_coeffs[64 * 4]; + for (size_t i = 0; i < 64 * 4; i++) slow_coeffs[i] = pixels[i]; + DCTSlow<16>(slow_coeffs); + + for (size_t i = 0; i < 64 * 4; i++) { + slow_coeffs[i] = roundf(slow_coeffs[i] / kUniformQuant) * kUniformQuant; + coeffs[i] = roundf(coeffs[i] / dequant_matrices.Matrix(dct, 0)[i]) * + dequant_matrices.Matrix(dct, 0)[i]; + } + + IDCTSlow<16>(slow_coeffs); + TransformToPixels(dct, coeffs, pixels, 16, scratch_space); + for (size_t i = 0; i < 64 * 4; i++) { + EXPECT_NEAR(pixels[i], slow_coeffs[i], 1e-4); + } + } + + // Check that all matrices have the same DC quantization, i.e. that they all + // have the same scaling. + for (size_t i = 0; i < AcStrategy::kNumValidStrategies; i++) { + EXPECT_NEAR(dequant_matrices.Matrix(i, 0)[0], kUniformQuant, 1e-6); + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/quantizer-inl.h b/lib/jxl/quantizer-inl.h new file mode 100644 index 0000000..2627148 --- /dev/null +++ b/lib/jxl/quantizer-inl.h @@ -0,0 +1,73 @@ +// 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. + +#if defined(LIB_JXL_QUANTIZER_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_QUANTIZER_INL_H_ +#undef LIB_JXL_QUANTIZER_INL_H_ +#else +#define LIB_JXL_QUANTIZER_INL_H_ +#endif + +#include + +#include +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::Rebind; +using hwy::HWY_NAMESPACE::Vec; + +template +HWY_INLINE HWY_MAYBE_UNUSED Vec> AdjustQuantBias( + DI di, const size_t c, const Vec quant_i, + const float* HWY_RESTRICT biases) { + const Rebind df; + +#if JXL_HIGH_PRECISION + const auto quant = ConvertTo(df, quant_i); + + // Compare |quant|, keep sign bit for negating result. + const auto kSign = BitCast(df, Set(di, INT32_MIN)); + const auto sign = And(quant, kSign); // TODO(janwas): = abs ^ orig + const auto abs_quant = AndNot(kSign, quant); + + // If |x| is 1, kZeroBias creates a different bias for each channel. + // We're implementing the following: + // if (quant == 0) return 0; + // if (quant == 1) return biases[c]; + // if (quant == -1) return -biases[c]; + // return quant - biases[3] / quant; + + // Integer comparison is not helpful because Clang incurs bypass penalties + // from unnecessarily mixing integer and float. + const auto is_01 = abs_quant < Set(df, 1.125f); + const auto not_0 = abs_quant > Zero(df); + + // Bitwise logic is faster than quant * biases[c]. + const auto one_bias = IfThenElseZero(not_0, Xor(Set(df, biases[c]), sign)); + + // About 2E-5 worse than ReciprocalNR or division. + const auto bias = + NegMulAdd(Set(df, biases[3]), ApproximateReciprocal(quant), quant); + + return IfThenElse(is_01, one_bias, bias); +#else + auto sign = IfThenElseZero(quant_i < Zero(di), Set(di, INT32_MIN)); + return BitCast(df, IfThenElse(Abs(quant_i) == Set(di, 1), + sign | BitCast(di, Set(df, biases[c])), + BitCast(di, ConvertTo(df, quant_i)))); +#endif +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_QUANTIZER_INL_H_ diff --git a/lib/jxl/quantizer.cc b/lib/jxl/quantizer.cc new file mode 100644 index 0000000..2a7480f --- /dev/null +++ b/lib/jxl/quantizer.cc @@ -0,0 +1,146 @@ +// 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/quantizer.h" + +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/robust_statistics.h" +#include "lib/jxl/field_encodings.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/quant_weights.h" + +namespace jxl { + +static const int kDefaultQuant = 64; + +constexpr int Quantizer::kQuantMax; + +Quantizer::Quantizer(const DequantMatrices* dequant) + : Quantizer(dequant, kDefaultQuant, kGlobalScaleDenom / kDefaultQuant) {} + +Quantizer::Quantizer(const DequantMatrices* dequant, int quant_dc, + int global_scale) + : global_scale_(global_scale), quant_dc_(quant_dc), dequant_(dequant) { + JXL_ASSERT(dequant_ != nullptr); + RecomputeFromGlobalScale(); + inv_quant_dc_ = inv_global_scale_ / quant_dc_; + + memcpy(zero_bias_, kZeroBiasDefault, sizeof(kZeroBiasDefault)); +} + +void Quantizer::ComputeGlobalScaleAndQuant(float quant_dc, float quant_median, + float quant_median_absd) { + // Target value for the median value in the quant field. + const float kQuantFieldTarget = 3.80987740592518214386f; + // We reduce the median of the quant field by the median absolute deviation: + // higher resolution on highly varying quant fields. + float scale = kGlobalScaleDenom * (quant_median - quant_median_absd) / + kQuantFieldTarget; + // Ensure that new_global_scale is positive and no more than 1<<15. + if (scale < 1) scale = 1; + if (scale > (1 << 15)) scale = 1 << 15; + int new_global_scale = static_cast(scale); + // Ensure that quant_dc_ will always be at least + // kGlobalScaleDenom/kGlobalScaleNumerator. + const int scaled_quant_dc = + static_cast(quant_dc * kGlobalScaleNumerator); + if (new_global_scale > scaled_quant_dc) { + new_global_scale = scaled_quant_dc; + if (new_global_scale <= 0) new_global_scale = 1; + } + global_scale_ = new_global_scale; + // Code below uses inv_global_scale_. + RecomputeFromGlobalScale(); + + float fval = quant_dc * inv_global_scale_ + 0.5f; + fval = std::min(1 << 16, fval); + const int new_quant_dc = static_cast(fval); + quant_dc_ = new_quant_dc; + + // quant_dc_ was updated, recompute values. + RecomputeFromGlobalScale(); +} + +void Quantizer::SetQuantFieldRect(const ImageF& qf, const Rect& rect, + ImageI* JXL_RESTRICT raw_quant_field) { + for (size_t y = 0; y < rect.ysize(); ++y) { + const float* JXL_RESTRICT row_qf = rect.ConstRow(qf, y); + int32_t* JXL_RESTRICT row_qi = rect.Row(raw_quant_field, y); + for (size_t x = 0; x < rect.xsize(); ++x) { + int val = ClampVal(row_qf[x] * inv_global_scale_ + 0.5f); + row_qi[x] = val; + } + } +} + +void Quantizer::SetQuantField(const float quant_dc, const ImageF& qf, + ImageI* JXL_RESTRICT raw_quant_field) { + JXL_CHECK(SameSize(*raw_quant_field, qf)); + std::vector data(qf.xsize() * qf.ysize()); + for (size_t y = 0; y < qf.ysize(); ++y) { + const float* JXL_RESTRICT row_qf = qf.Row(y); + for (size_t x = 0; x < qf.xsize(); ++x) { + float quant = row_qf[x]; + data[qf.xsize() * y + x] = quant; + } + } + const float quant_median = Median(&data); + const float quant_median_absd = MedianAbsoluteDeviation(data, quant_median); + ComputeGlobalScaleAndQuant(quant_dc, quant_median, quant_median_absd); + SetQuantFieldRect(qf, Rect(qf), raw_quant_field); +} + +void Quantizer::SetQuant(float quant_dc, float quant_ac, + ImageI* JXL_RESTRICT raw_quant_field) { + ComputeGlobalScaleAndQuant(quant_dc, quant_ac, 0); + int val = ClampVal(quant_ac * inv_global_scale_ + 0.5f); + FillImage(val, raw_quant_field); +} + +Status QuantizerParams::VisitFields(Visitor* JXL_RESTRICT visitor) { + JXL_QUIET_RETURN_IF_ERROR(visitor->U32( + BitsOffset(11, 1), BitsOffset(11, 2049), BitsOffset(12, 4097), + BitsOffset(16, 8193), 1, &global_scale)); + JXL_QUIET_RETURN_IF_ERROR(visitor->U32(Val(16), BitsOffset(5, 1), + BitsOffset(8, 1), BitsOffset(16, 1), 1, + &quant_dc)); + return true; +} + +Status Quantizer::Encode(BitWriter* writer, size_t layer, + AuxOut* aux_out) const { + QuantizerParams params; + params.global_scale = global_scale_; + params.quant_dc = quant_dc_; + return Bundle::Write(params, writer, layer, aux_out); +} + +Status Quantizer::Decode(BitReader* reader) { + QuantizerParams params; + JXL_RETURN_IF_ERROR(Bundle::Read(reader, ¶ms)); + global_scale_ = static_cast(params.global_scale); + quant_dc_ = static_cast(params.quant_dc); + RecomputeFromGlobalScale(); + return true; +} + +void Quantizer::DumpQuantizationMap(const ImageI& raw_quant_field) const { + printf("Global scale: %d (%.7f)\nDC quant: %d\n", global_scale_, + global_scale_ * 1.0 / kGlobalScaleDenom, quant_dc_); + printf("AC quantization Map:\n"); + for (size_t y = 0; y < raw_quant_field.ysize(); ++y) { + for (size_t x = 0; x < raw_quant_field.xsize(); ++x) { + printf(" %3d", raw_quant_field.Row(y)[x]); + } + printf("\n"); + } +} + +} // namespace jxl diff --git a/lib/jxl/quantizer.h b/lib/jxl/quantizer.h new file mode 100644 index 0000000..f2da45f --- /dev/null +++ b/lib/jxl/quantizer.h @@ -0,0 +1,178 @@ +// 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_QUANTIZER_H_ +#define LIB_JXL_QUANTIZER_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "lib/jxl/ac_strategy.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/bits.h" +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/profiler.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_util.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/enc_bit_writer.h" +#include "lib/jxl/fields.h" +#include "lib/jxl/image.h" +#include "lib/jxl/linalg.h" +#include "lib/jxl/quant_weights.h" + +// Quantizes DC and AC coefficients, with separate quantization tables according +// to the quant_kind (which is currently computed from the AC strategy and the +// block index inside that strategy). + +namespace jxl { + +static constexpr int kGlobalScaleDenom = 1 << 16; +static constexpr int kGlobalScaleNumerator = 4096; + +// zero-biases for quantizing channels X, Y, B +static constexpr float kZeroBiasDefault[3] = {0.5f, 0.5f, 0.5f}; + +// Returns adjusted version of a quantized integer, such that its value is +// closer to the expected value of the original. +// The residuals of AC coefficients that we quantize are not uniformly +// distributed. Numerical experiments show that they have a distribution with +// the "shape" of 1/(1+x^2) [up to some coefficients]. This means that the +// expected value of a coefficient that gets quantized to x will not be x +// itself, but (at least with reasonable approximation): +// - 0 if x is 0 +// - x * biases[c] if x is 1 or -1 +// - x - biases[3]/x otherwise +// This follows from computing the distribution of the quantization bias, which +// can be approximated fairly well by /x when |x| is at least two. +static constexpr float kBiasNumerator = 0.145f; + +static constexpr float kDefaultQuantBias[4] = { + 1.0f - 0.05465007330715401f, + 1.0f - 0.07005449891748593f, + 1.0f - 0.049935103337343655f, + 0.145f, +}; + +class Quantizer { + public: + explicit Quantizer(const DequantMatrices* dequant); + Quantizer(const DequantMatrices* dequant, int quant_dc, int global_scale); + + static constexpr int kQuantMax = 256; + + static JXL_INLINE int ClampVal(float val) { + return static_cast(std::max(1.0f, std::min(val, kQuantMax))); + } + + // Recomputes other derived fields after global_scale_ has changed. + void RecomputeFromGlobalScale() { + global_scale_float_ = global_scale_ * (1.0 / kGlobalScaleDenom); + inv_global_scale_ = 1.0 * kGlobalScaleDenom / global_scale_; + inv_quant_dc_ = inv_global_scale_ / quant_dc_; + for (size_t c = 0; c < 3; c++) { + mul_dc_[c] = GetDcStep(c); + inv_mul_dc_[c] = GetInvDcStep(c); + } + } + + // Returns scaling factor such that Scale() * (RawDC() or RawQuantField()) + // pixels yields the same float values returned by GetQuantField. + JXL_INLINE float Scale() const { return global_scale_float_; } + + // Reciprocal of Scale(). + JXL_INLINE float InvGlobalScale() const { return inv_global_scale_; } + + void SetQuantFieldRect(const ImageF& qf, const Rect& rect, + ImageI* JXL_RESTRICT raw_quant_field); + + void SetQuantField(float quant_dc, const ImageF& qf, + ImageI* JXL_RESTRICT raw_quant_field); + + void SetQuant(float quant_dc, float quant_ac, + ImageI* JXL_RESTRICT raw_quant_field); + + // Returns the DC quantization base value, which is currently global (not + // adaptive). The actual scale factor used to dequantize pixels in channel c + // is: inv_quant_dc() * dequant_->DCQuant(c). + float inv_quant_dc() const { return inv_quant_dc_; } + + // Dequantize by multiplying with this times dequant_matrix. + float inv_quant_ac(int32_t quant) const { return inv_global_scale_ / quant; } + + Status Encode(BitWriter* writer, size_t layer, AuxOut* aux_out) const; + + Status Decode(BitReader* reader); + + void DumpQuantizationMap(const ImageI& raw_quant_field) const; + + JXL_INLINE const float* DequantMatrix(size_t quant_kind, size_t c) const { + return dequant_->Matrix(quant_kind, c); + } + + JXL_INLINE const float* InvDequantMatrix(size_t quant_kind, size_t c) const { + return dequant_->InvMatrix(quant_kind, c); + } + + JXL_INLINE size_t DequantMatrixOffset(size_t quant_kind, size_t c) const { + return dequant_->MatrixOffset(quant_kind, c); + } + + // Calculates DC quantization step. + JXL_INLINE float GetDcStep(size_t c) const { + return inv_quant_dc_ * dequant_->DCQuant(c); + } + JXL_INLINE float GetInvDcStep(size_t c) const { + return dequant_->InvDCQuant(c) * (global_scale_float_ * quant_dc_); + } + + JXL_INLINE const float* MulDC() const { return mul_dc_; } + JXL_INLINE const float* InvMulDC() const { return inv_mul_dc_; } + + JXL_INLINE void ClearDCMul() { + std::fill(mul_dc_, mul_dc_ + 4, 1); + std::fill(inv_mul_dc_, inv_mul_dc_ + 4, 1); + } + + void ComputeGlobalScaleAndQuant(float quant_dc, float quant_median, + float quant_median_absd); + + private: + float mul_dc_[4]; + float inv_mul_dc_[4]; + + // These are serialized: + int global_scale_; + int quant_dc_; + + // These are derived from global_scale_: + float inv_global_scale_; + float global_scale_float_; // reciprocal of inv_global_scale_ + float inv_quant_dc_; + + float zero_bias_[3]; + const DequantMatrices* dequant_; +}; + +struct QuantizerParams : public Fields { + QuantizerParams() { Bundle::Init(this); } + const char* Name() const override { return "QuantizerParams"; } + + Status VisitFields(Visitor* JXL_RESTRICT visitor) override; + + uint32_t global_scale; + uint32_t quant_dc; +}; + +} // namespace jxl + +#endif // LIB_JXL_QUANTIZER_H_ diff --git a/lib/jxl/quantizer_test.cc b/lib/jxl/quantizer_test.cc new file mode 100644 index 0000000..f3d3c90 --- /dev/null +++ b/lib/jxl/quantizer_test.cc @@ -0,0 +1,82 @@ +// 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/quantizer.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/image_ops.h" +#include "lib/jxl/image_test_utils.h" + +namespace jxl { +namespace { + +void TestEquivalence(int qxsize, int qysize, const Quantizer& quantizer1, + const Quantizer& quantizer2) { + ASSERT_NEAR(quantizer1.inv_quant_dc(), quantizer2.inv_quant_dc(), 1e-7); +} + +TEST(QuantizerTest, QuantizerParams) { + for (uint32_t i = 1; i < 10000; ++i) { + QuantizerParams p; + p.global_scale = i; + size_t extension_bits = 0, total_bits = 0; + EXPECT_TRUE(Bundle::CanEncode(p, &extension_bits, &total_bits)); + EXPECT_EQ(0u, extension_bits); + EXPECT_GE(total_bits, 4u); + } +} + +TEST(QuantizerTest, BitStreamRoundtripSameQuant) { + const int qxsize = 8; + const int qysize = 8; + DequantMatrices dequant; + Quantizer quantizer1(&dequant); + ImageI raw_quant_field(qxsize, qysize); + quantizer1.SetQuant(0.17f, 0.17f, &raw_quant_field); + BitWriter writer; + EXPECT_TRUE(quantizer1.Encode(&writer, 0, nullptr)); + writer.ZeroPadToByte(); + const size_t bits_written = writer.BitsWritten(); + Quantizer quantizer2(&dequant, qxsize, qysize); + BitReader reader(writer.GetSpan()); + EXPECT_TRUE(quantizer2.Decode(&reader)); + EXPECT_TRUE(reader.JumpToByteBoundary()); + EXPECT_EQ(reader.TotalBitsConsumed(), bits_written); + EXPECT_TRUE(reader.Close()); + TestEquivalence(qxsize, qysize, quantizer1, quantizer2); +} + +TEST(QuantizerTest, BitStreamRoundtripRandomQuant) { + const int qxsize = 8; + const int qysize = 8; + DequantMatrices dequant; + Quantizer quantizer1(&dequant); + ImageI raw_quant_field(qxsize, qysize); + quantizer1.SetQuant(0.17f, 0.17f, &raw_quant_field); + std::mt19937_64 rng; + std::uniform_int_distribution<> uniform(1, 256); + float quant_dc = 0.17f; + ImageF qf(qxsize, qysize); + RandomFillImage(&qf, 1.0f); + quantizer1.SetQuantField(quant_dc, qf, &raw_quant_field); + BitWriter writer; + EXPECT_TRUE(quantizer1.Encode(&writer, 0, nullptr)); + writer.ZeroPadToByte(); + const size_t bits_written = writer.BitsWritten(); + Quantizer quantizer2(&dequant, qxsize, qysize); + BitReader reader(writer.GetSpan()); + EXPECT_TRUE(quantizer2.Decode(&reader)); + EXPECT_TRUE(reader.JumpToByteBoundary()); + EXPECT_EQ(reader.TotalBitsConsumed(), bits_written); + EXPECT_TRUE(reader.Close()); + TestEquivalence(qxsize, qysize, quantizer1, quantizer2); +} +} // namespace +} // namespace jxl diff --git a/lib/jxl/rational_polynomial-inl.h b/lib/jxl/rational_polynomial-inl.h new file mode 100644 index 0000000..87bddd1 --- /dev/null +++ b/lib/jxl/rational_polynomial-inl.h @@ -0,0 +1,94 @@ +// 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. + +// Fast SIMD evaluation of rational polynomials for approximating functions. + +#if defined(LIB_JXL_RATIONAL_POLYNOMIAL_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_RATIONAL_POLYNOMIAL_INL_H_ +#undef LIB_JXL_RATIONAL_POLYNOMIAL_INL_H_ +#else +#define LIB_JXL_RATIONAL_POLYNOMIAL_INL_H_ +#endif + +#include + +#include +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// Primary template: default to actual division. +template +struct FastDivision { + HWY_INLINE V operator()(const V n, const V d) const { return n / d; } +}; +// Partial specialization for float vectors. +template +struct FastDivision { + // One Newton-Raphson iteration. + static HWY_INLINE V ReciprocalNR(const V x) { + const auto rcp = ApproximateReciprocal(x); + const auto sum = rcp + rcp; + const auto x_rcp = x * rcp; + return NegMulAdd(x_rcp, rcp, sum); + } + + V operator()(const V n, const V d) const { +#if 1 // Faster on SKX + return n / d; +#else + return n * ReciprocalNR(d); +#endif + } +}; + +// Approximates smooth functions via rational polynomials (i.e. dividing two +// polynomials). Evaluates polynomials via Horner's scheme, which is faster than +// Clenshaw recurrence for Chebyshev polynomials. LoadDup128 allows us to +// specify constants (replicated 4x) independently of the lane count. +template +HWY_INLINE HWY_MAYBE_UNUSED V EvalRationalPolynomial(const D d, const V x, + const T (&p)[NP], + const T (&q)[NQ]) { + constexpr size_t kDegP = NP / 4 - 1; + constexpr size_t kDegQ = NQ / 4 - 1; + auto yp = LoadDup128(d, &p[kDegP * 4]); + auto yq = LoadDup128(d, &q[kDegQ * 4]); + // We use pointer arithmetic to refer to &p[(kDegP - n) * 4] to avoid a + // compiler warning that the index is out of bounds since we are already + // checking that it is not out of bounds with (kDegP >= n) and the access + // will be optimized away. Similarly with q and kDegQ. + HWY_FENCE; + if (kDegP >= 1) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 1) * 4))); + if (kDegQ >= 1) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 1) * 4))); + HWY_FENCE; + if (kDegP >= 2) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 2) * 4))); + if (kDegQ >= 2) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 2) * 4))); + HWY_FENCE; + if (kDegP >= 3) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 3) * 4))); + if (kDegQ >= 3) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 3) * 4))); + HWY_FENCE; + if (kDegP >= 4) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 4) * 4))); + if (kDegQ >= 4) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 4) * 4))); + HWY_FENCE; + if (kDegP >= 5) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 5) * 4))); + if (kDegQ >= 5) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 5) * 4))); + HWY_FENCE; + if (kDegP >= 6) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 6) * 4))); + if (kDegQ >= 6) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 6) * 4))); + HWY_FENCE; + if (kDegP >= 7) yp = MulAdd(yp, x, LoadDup128(d, p + ((kDegP - 7) * 4))); + if (kDegQ >= 7) yq = MulAdd(yq, x, LoadDup128(d, q + ((kDegQ - 7) * 4))); + + return FastDivision()(yp, yq); +} + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); +#endif // LIB_JXL_RATIONAL_POLYNOMIAL_INL_H_ diff --git a/lib/jxl/rational_polynomial_test.cc b/lib/jxl/rational_polynomial_test.cc new file mode 100644 index 0000000..699afd0 --- /dev/null +++ b/lib/jxl/rational_polynomial_test.cc @@ -0,0 +1,239 @@ +// 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 + +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/rational_polynomial_test.cc" +#include +#include +#include + +#include "lib/jxl/base/descriptive_statistics.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/common.h" +#include "lib/jxl/rational_polynomial-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +using T = float; // required by EvalLog2 +using D = HWY_FULL(T); + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::ShiftLeft; +using hwy::HWY_NAMESPACE::ShiftRight; + +// Generic: only computes polynomial +struct EvalPoly { + template + T operator()(T x, const T (&p)[NP], const T (&q)[NQ]) const { + const HWY_FULL(T) d; + const auto vx = Set(d, x); + const auto approx = EvalRationalPolynomial(d, vx, p, q); + return GetLane(approx); + } +}; + +// Range reduction for log2 +struct EvalLog2 { + template + T operator()(T x, const T (&p)[NP], const T (&q)[NQ]) const { + const HWY_FULL(T) d; + auto vx = Set(d, x); + + const HWY_FULL(int32_t) di; + const auto x_bits = BitCast(di, vx); + // Cannot handle negative numbers / NaN. + JXL_DASSERT(AllTrue(Abs(x_bits) == x_bits)); + + // Range reduction to [-1/3, 1/3] - 3 integer, 2 float ops + const auto exp_bits = x_bits - Set(di, 0x3f2aaaab); // = 2/3 + // Shifted exponent = log2; also used to clear mantissa. + const auto exp_shifted = ShiftRight<23>(exp_bits); + const auto mantissa = BitCast(d, x_bits - ShiftLeft<23>(exp_shifted)); + const auto exp_val = ConvertTo(d, exp_shifted); + vx = mantissa - Set(d, 1.0f); + + const auto approx = EvalRationalPolynomial(d, vx, p, q) + exp_val; + return GetLane(approx); + } +}; + +// Functions to approximate: + +T LinearToSrgb8Direct(T val) { + if (val < 0.0) return 0.0; + if (val >= 255.0) return 255.0; + if (val <= 10.0 / 12.92) return val * 12.92; + return 255.0 * (std::pow(val / 255.0, 1.0 / 2.4) * 1.055 - 0.055); +} + +T SimpleGamma(T v) { + static const T kGamma = 0.387494322593; + static const T limit = 43.01745241042018; + T bright = v - limit; + if (bright >= 0) { + static const T mul = 0.0383723643799; + v -= bright * mul; + } + static const T limit2 = 94.68634353321337; + T bright2 = v - limit2; + if (bright2 >= 0) { + static const T mul = 0.22885405968; + v -= bright2 * mul; + } + static const T offset = 0.156775786057; + static const T scale = 8.898059160493739; + T retval = scale * (offset + pow(v, kGamma)); + return retval; +} + +// Runs CaratheodoryFejer and verifies the polynomial using a lot of samples to +// return the biggest error. +template +T RunApproximation(T x0, T x1, const T (&p)[NP], const T (&q)[NQ], + const Eval& eval, T func_to_approx(T)) { + Stats err; + + T lastPrint = 0; + // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) + for (T x = x0; x <= x1; x += (x1 - x0) / 10000.0) { + const T f = func_to_approx(x); + const T g = eval(x, p, q); + err.Notify(fabs(g - f)); + if (x == x0 || x - lastPrint > (x1 - x0) / 20.0) { + printf("x: %11.6f, f: %11.6f, g: %11.6f, e: %11.6f\n", x, f, g, + fabs(g - f)); + lastPrint = x; + } + } + printf("%s\n", err.ToString().c_str()); + + return err.Max(); +} + +void TestSimpleGamma() { + const T p[4 * (6 + 1)] = { + HWY_REP4(-5.0646949363741811E-05), HWY_REP4(6.7369380528439771E-05), + HWY_REP4(8.9376652530412794E-05), HWY_REP4(2.1153513301520462E-06), + HWY_REP4(-6.9130322970386449E-08), HWY_REP4(3.9424752749293728E-10), + HWY_REP4(1.2360288207619576E-13)}; + + const T q[4 * (6 + 1)] = { + HWY_REP4(-6.6389733798591366E-06), HWY_REP4(1.3299859726565908E-05), + HWY_REP4(3.8538748358398873E-06), HWY_REP4(-2.8707687262928236E-08), + HWY_REP4(-6.6897385800005434E-10), HWY_REP4(6.1428748869186003E-12), + HWY_REP4(-2.5475738169252870E-15)}; + + const T err = RunApproximation(0.77, 274.579999999999984, p, q, EvalPoly(), + SimpleGamma); + EXPECT_LT(err, 0.05); +} + +void TestLinearToSrgb8Direct() { + const T p[4 * (5 + 1)] = { + HWY_REP4(-9.5357499040105154E-05), HWY_REP4(4.6761186249798248E-04), + HWY_REP4(2.5708174333943594E-04), HWY_REP4(1.5250087770436082E-05), + HWY_REP4(1.1946768008931187E-07), HWY_REP4(5.9916446295972850E-11)}; + + const T q[4 * (4 + 1)] = { + HWY_REP4(1.8932479758079768E-05), HWY_REP4(2.7312342474687321E-05), + HWY_REP4(4.3901204783327006E-06), HWY_REP4(1.0417787306920273E-07), + HWY_REP4(3.0084206762140419E-10)}; + + const T err = + RunApproximation(0.77, 255, p, q, EvalPoly(), LinearToSrgb8Direct); + EXPECT_LT(err, 0.05); +} + +void TestExp() { + const T p[4 * (2 + 1)] = {HWY_REP4(9.6266879665530902E-01), + HWY_REP4(4.8961265681586763E-01), + HWY_REP4(8.2619259189548433E-02)}; + const T q[4 * (2 + 1)] = {HWY_REP4(9.6259895571622622E-01), + HWY_REP4(-4.7272457588933831E-01), + HWY_REP4(7.4802088567547664E-02)}; + const T err = + RunApproximation(-1, 1, p, q, EvalPoly(), [](T x) { return T(exp(x)); }); + EXPECT_LT(err, 1E-4); +} + +void TestNegExp() { + // 4,3 is the min required for monotonicity; max error in 0,10: 751 ppm + // no benefit for k>50. + const T p[4 * (4 + 1)] = { + HWY_REP4(5.9580258551150123E-02), HWY_REP4(-2.5073728806886408E-02), + HWY_REP4(4.1561830213689248E-03), HWY_REP4(-3.1815408488900372E-04), + HWY_REP4(9.3866690094906802E-06)}; + const T q[4 * (3 + 1)] = { + HWY_REP4(5.9579108238812878E-02), HWY_REP4(3.4542074345478582E-02), + HWY_REP4(8.7263562483501714E-03), HWY_REP4(1.4095109143061216E-03)}; + + const T err = + RunApproximation(0, 10, p, q, EvalPoly(), [](T x) { return T(exp(-x)); }); + EXPECT_LT(err, sizeof(T) == 8 ? 2E-5 : 3E-5); +} + +void TestSin() { + const T p[4 * (6 + 1)] = { + HWY_REP4(1.5518122109203780E-05), HWY_REP4(2.3388958643675966E+00), + HWY_REP4(-8.6705520940849157E-01), HWY_REP4(-1.9702294764873535E-01), + HWY_REP4(1.2193404314472320E-01), HWY_REP4(-1.7373966109788839E-02), + HWY_REP4(7.8829435883034796E-04)}; + const T q[4 * (5 + 1)] = { + HWY_REP4(2.3394371422557279E+00), HWY_REP4(-8.7028221081288615E-01), + HWY_REP4(2.0052872219658430E-01), HWY_REP4(-3.2460335995264836E-02), + HWY_REP4(3.1546157932479282E-03), HWY_REP4(-1.6692542019380155E-04)}; + + const T err = RunApproximation(0, Pi(1) * 2, p, q, EvalPoly(), + [](T x) { return T(sin(x)); }); + EXPECT_LT(err, sizeof(T) == 8 ? 5E-4 : 7E-4); +} + +void TestLog() { + HWY_ALIGN const T p[4 * (2 + 1)] = {HWY_REP4(-1.8503833400518310E-06), + HWY_REP4(1.4287160470083755E+00), + HWY_REP4(7.4245873327820566E-01)}; + HWY_ALIGN const T q[4 * (2 + 1)] = {HWY_REP4(9.9032814277590719E-01), + HWY_REP4(1.0096718572241148E+00), + HWY_REP4(1.7409343003366853E-01)}; + const T err = RunApproximation(1E-6, 1000, p, q, EvalLog2(), std::log2); + printf("%E\n", err); +} + +HWY_NOINLINE void TestRationalPolynomial() { + TestSimpleGamma(); + TestLinearToSrgb8Direct(); + TestExp(); + TestNegExp(); + TestSin(); + TestLog(); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +class RationalPolynomialTest : public hwy::TestWithParamTarget {}; +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(RationalPolynomialTest); + +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestSimpleGamma); +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestLinearToSrgb8Direct); +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestExp); +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestNegExp); +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestSin); +HWY_EXPORT_AND_TEST_P(RationalPolynomialTest, TestLog); + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/robust_statistics_test.cc b/lib/jxl/robust_statistics_test.cc new file mode 100644 index 0000000..22ee56a --- /dev/null +++ b/lib/jxl/robust_statistics_test.cc @@ -0,0 +1,150 @@ +// 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/robust_statistics.h" + +#include + +#include // partial_sum +#include + +#include "gtest/gtest.h" +#include "lib/jxl/noise_distributions.h" + +namespace jxl { +namespace { + +TEST(RobustStatisticsTest, TestMode) { + // Enough to populate bins. We have to sort this many values. + constexpr size_t kReps = 15000; + constexpr size_t kBins = 101; + + std::mt19937 rng(65537); + + // Place Poisson mean at 1/10, 2/10 .. 9/10 of the bin range. + for (int frac = 1; frac < 10; ++frac) { + printf("===========================frac %d\n", frac); + + NoisePoisson noise(frac * kBins / 10); + std::vector values; + values.reserve(kReps); + + uint32_t bins[kBins] = {0}; + + std::uniform_real_distribution jitter(-1E-3f, 1E-3f); + for (size_t rep = 0; rep < kReps; ++rep) { + // Scale back to integer, add jitter to avoid too many repeated values. + const float poisson = noise(0.0f, &rng) * 1E3f + jitter(rng); + + values.push_back(poisson); + + const int idx_bin = static_cast(poisson); + if (idx_bin < static_cast(kBins)) { + bins[idx_bin] += 1; + } // else skip instead of clamping to avoid bias + } + + // // Print histogram + // for (const uint32_t b : bins) { + // printf("%u\n", b); + // } + + // (Smoothed) argmax and median for verification + float smoothed[kBins]; + smoothed[0] = bins[0]; + smoothed[kBins - 1] = bins[kBins - 1]; + for (size_t i = 1; i < kBins - 1; ++i) { + smoothed[i] = (2 * bins[i] + bins[i - 1] + bins[i + 1]) * 0.25f; + } + const float argmax = + std::max_element(smoothed, smoothed + kBins) - smoothed; + const float median = Median(&values); + + std::sort(values.begin(), values.end()); + const float hsm = HalfSampleMode()(values.data(), values.size()); + + uint32_t cdf[kBins]; + std::partial_sum(bins, bins + kBins, cdf); + const int hrm = HalfRangeMode()(cdf, kBins); + + const auto is_near = [](const float expected, const float actual) { + return std::abs(expected - actual) <= 1.0f + 1E-5f; + }; + EXPECT_TRUE(is_near(hsm, argmax) || is_near(hsm, median)); + EXPECT_TRUE(is_near(hrm, argmax) || is_near(hrm, median)); + + printf("hsm %.1f hrm %d argmax %.1f median %f\n", hsm, hrm, argmax, median); + const int center = static_cast(argmax); + printf("%d %d %d %d %d\n", bins[center - 2], bins[center - 1], bins[center], + bins[center + 1], bins[center + 2]); + } +} + +// Ensures Median3/5 return the same results as Median. +TEST(RobustStatisticsTest, TestMedian) { + std::vector v3(3), v5(5); + + std::uniform_real_distribution dist(-100.0f, 100.0f); + std::mt19937 rng(129); + +#ifdef NDEBUG + constexpr size_t kReps = 100000; +#else + constexpr size_t kReps = 100; +#endif + for (size_t i = 0; i < kReps; ++i) { + v3[0] = dist(rng); + v3[1] = dist(rng); + v3[2] = dist(rng); + for (size_t j = 0; j < 5; ++j) { + v5[j] = dist(rng); + } + + JXL_ASSERT(Median(&v3) == Median3(v3[0], v3[1], v3[2])); + JXL_ASSERT(Median(&v5) == Median5(v5[0], v5[1], v5[2], v5[3], v5[4])); + } +} + +template +void TestLine(const Noise& noise, float max_l1_limit, float mad_limit) { + std::vector points; + Line perfect(0.6f, 2.0f); + + // Random spacing of X (must be unique) + float x = -100.0f; + std::mt19937_64 rng(129); + std::uniform_real_distribution x_dist(1E-6f, 10.0f); + for (size_t ix = 0; ix < 500; ++ix) { + x += x_dist(rng); + const float y = noise(perfect(x), &rng); + points.emplace_back(x, y); + // printf("%f,%f\n", x, y); + } + + Line est(points); + float max_l1, mad; + EvaluateQuality(est, points, &max_l1, &mad); + printf("x %f slope=%.2f b=%.2f max_l1 %f mad %f\n", x, est.slope(), + est.intercept(), max_l1, mad); + + EXPECT_LE(max_l1, max_l1_limit); + EXPECT_LE(mad, mad_limit); +} + +TEST(RobustStatisticsTest, CleanLine) { + const NoiseNone noise; + TestLine(noise, 1E-6, 1E-7); +} +TEST(RobustStatisticsTest, Uniform) { + const NoiseUniform noise(-100.0f, 100.0f); + TestLine(noise, 107, 53); +} +TEST(RobustStatisticsTest, Gauss) { + const NoiseGaussian noise(10.0f); + TestLine(noise, 37, 7); +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/roundtrip_test.cc b/lib/jxl/roundtrip_test.cc new file mode 100644 index 0000000..dd99ad6 --- /dev/null +++ b/lib/jxl/roundtrip_test.cc @@ -0,0 +1,615 @@ +// 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 "gtest/gtest.h" +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "jxl/encode.h" +#include "jxl/encode_cxx.h" +#include "lib/extras/codec.h" +#include "lib/jxl/dec_external_image.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/encode_internal.h" +#include "lib/jxl/icc_codec.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace { + +// Converts a test image to a CodecInOut. +// icc_profile can be empty to automatically deduce profile from the pixel +// format, or filled in to force this ICC profile +jxl::CodecInOut ConvertTestImage(const std::vector& buf, + const size_t xsize, const size_t ysize, + const JxlPixelFormat& pixel_format, + const jxl::PaddedBytes& icc_profile) { + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + + bool is_gray = + pixel_format.num_channels == 1 || pixel_format.num_channels == 2; + bool has_alpha = + pixel_format.num_channels == 2 || pixel_format.num_channels == 4; + + io.metadata.m.color_encoding.SetColorSpace(is_gray ? jxl::ColorSpace::kGray + : jxl::ColorSpace::kRGB); + if (has_alpha) { + // Note: alpha > 16 not yet supported by the C++ codec + switch (pixel_format.data_type) { + case JXL_TYPE_UINT8: + io.metadata.m.SetAlphaBits(8); + break; + case JXL_TYPE_UINT16: + case JXL_TYPE_UINT32: + case JXL_TYPE_FLOAT: + case JXL_TYPE_FLOAT16: + io.metadata.m.SetAlphaBits(16); + break; + default: + EXPECT_TRUE(false) << "Roundtrip tests for data type " + << pixel_format.data_type << " not yet implemented."; + } + } + size_t bitdepth = 0; + bool float_in = false; + switch (pixel_format.data_type) { + case JXL_TYPE_FLOAT: + bitdepth = 32; + float_in = true; + io.metadata.m.SetFloat32Samples(); + break; + case JXL_TYPE_FLOAT16: + bitdepth = 16; + float_in = true; + io.metadata.m.SetFloat16Samples(); + break; + case JXL_TYPE_UINT8: + bitdepth = 8; + float_in = false; + io.metadata.m.SetUintSamples(8); + break; + case JXL_TYPE_UINT16: + bitdepth = 16; + float_in = false; + io.metadata.m.SetUintSamples(16); + break; + default: + EXPECT_TRUE(false) << "Roundtrip tests for data type " + << pixel_format.data_type << " not yet implemented."; + } + jxl::ColorEncoding color_encoding; + if (!icc_profile.empty()) { + jxl::PaddedBytes icc_profile_copy(icc_profile); + EXPECT_TRUE(color_encoding.SetICC(std::move(icc_profile_copy))); + } else if (pixel_format.data_type == JXL_TYPE_FLOAT) { + color_encoding = jxl::ColorEncoding::LinearSRGB(is_gray); + } else { + color_encoding = jxl::ColorEncoding::SRGB(is_gray); + } + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(buf.data(), buf.size()), xsize, ysize, + color_encoding, has_alpha, + /*alpha_is_premultiplied=*/false, + /*bits_per_sample=*/bitdepth, pixel_format.endianness, + /*flipped_y=*/false, /*pool=*/nullptr, &io.Main(), float_in)); + return io; +} + +template +T ConvertTestPixel(const float val); + +template <> +float ConvertTestPixel(const float val) { + return val; +} + +template <> +uint16_t ConvertTestPixel(const float val) { + return (uint16_t)(val * UINT16_MAX); +} + +template <> +uint8_t ConvertTestPixel(const float val) { + return (uint8_t)(val * UINT8_MAX); +} + +// Returns a test image. +template +std::vector GetTestImage(const size_t xsize, const size_t ysize, + const JxlPixelFormat& pixel_format) { + std::vector pixels(xsize * ysize * pixel_format.num_channels); + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + for (size_t chan = 0; chan < pixel_format.num_channels; chan++) { + float val; + switch (chan % 4) { + case 0: + val = static_cast(y) / static_cast(ysize); + break; + case 1: + val = static_cast(x) / static_cast(xsize); + break; + case 2: + val = static_cast(x + y) / static_cast(xsize + ysize); + break; + case 3: + val = static_cast(x * y) / static_cast(xsize * ysize); + break; + } + pixels[(y * xsize + x) * pixel_format.num_channels + chan] = + ConvertTestPixel(val); + } + } + } + std::vector bytes(pixels.size() * sizeof(T)); + memcpy(bytes.data(), pixels.data(), sizeof(T) * pixels.size()); + return bytes; +} + +void EncodeWithEncoder(JxlEncoder* enc, std::vector* compressed) { + compressed->resize(64); + uint8_t* next_out = compressed->data(); + size_t avail_out = compressed->size() - (next_out - compressed->data()); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed->data(); + compressed->resize(compressed->size() * 2); + next_out = compressed->data() + offset; + avail_out = compressed->size() - offset; + } + } + compressed->resize(next_out - compressed->data()); + EXPECT_EQ(JXL_ENC_SUCCESS, process_result); +} + +// Generates some pixels using using some dimensions and pixel_format, +// compresses them, and verifies that the decoded version is similar to the +// original pixels. +template +void VerifyRoundtripCompression(const size_t xsize, const size_t ysize, + const JxlPixelFormat& input_pixel_format, + const JxlPixelFormat& output_pixel_format, + const bool lossless, const bool use_container) { + const std::vector original_bytes = + GetTestImage(xsize, ysize, input_pixel_format); + jxl::CodecInOut original_io = + ConvertTestImage(original_bytes, xsize, ysize, input_pixel_format, {}); + + JxlEncoder* enc = JxlEncoderCreate(nullptr); + EXPECT_NE(nullptr, enc); + + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc, use_container)); + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &input_pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = lossless; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); + JxlColorEncoding color_encoding; + if (input_pixel_format.data_type == JXL_TYPE_FLOAT) { + JxlColorEncodingSetToLinearSRGB( + &color_encoding, + /*is_gray=*/input_pixel_format.num_channels < 3); + } else { + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/input_pixel_format.num_channels < 3); + } + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); + JxlEncoderOptions* opts = JxlEncoderOptionsCreate(enc, nullptr); + JxlEncoderOptionsSetLossless(opts, lossless); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddImageFrame(opts, &input_pixel_format, + (void*)original_bytes.data(), + original_bytes.size())); + JxlEncoderCloseInput(enc); + + std::vector compressed; + EncodeWithEncoder(enc, &compressed); + JxlEncoderDestroy(enc); + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_NE(nullptr, dec); + + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)); + + JxlDecoderSetInput(dec, next_in, avail_in); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderImageOutBufferSize( + dec, &output_pixel_format, &buffer_size)); + if (&input_pixel_format == &output_pixel_format) { + EXPECT_EQ(buffer_size, original_bytes.size()); + } + + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + + size_t icc_profile_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize(dec, &output_pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, + &icc_profile_size)); + jxl::PaddedBytes icc_profile(icc_profile_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile( + dec, &output_pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile.data(), icc_profile.size())); + + std::vector decoded_bytes(buffer_size); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer( + dec, &output_pixel_format, + decoded_bytes.data(), decoded_bytes.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + JxlDecoderDestroy(dec); + + jxl::CodecInOut decoded_io = ConvertTestImage( + decoded_bytes, xsize, ysize, output_pixel_format, icc_profile); + + jxl::ButteraugliParams ba; + float butteraugli_score = ButteraugliDistance(original_io, decoded_io, ba, + /*distmap=*/nullptr, nullptr); + if (lossless) { + EXPECT_LE(butteraugli_score, 0.0f); + } else { + EXPECT_LE(butteraugli_score, 2.0f); + } +} + +} // namespace + +TEST(RoundtripTest, FloatFrameRoundtripTest) { + for (int use_container = 0; use_container < 2; use_container++) { + for (int lossless = 0; lossless < 2; lossless++) { + for (uint32_t num_channels = 1; num_channels < 5; num_channels++) { + // There's no support (yet) for lossless extra float channels, so we + // don't test it. + if (num_channels % 2 != 0 || !lossless) { + JxlPixelFormat pixel_format = JxlPixelFormat{ + num_channels, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + VerifyRoundtripCompression(63, 129, pixel_format, pixel_format, + (bool)lossless, + (bool)use_container); + } + } + } + } +} + +TEST(RoundtripTest, Uint16FrameRoundtripTest) { + for (int use_container = 0; use_container < 2; use_container++) { + for (int lossless = 0; lossless < 2; lossless++) { + for (uint32_t num_channels = 1; num_channels < 5; num_channels++) { + JxlPixelFormat pixel_format = + JxlPixelFormat{num_channels, JXL_TYPE_UINT16, JXL_NATIVE_ENDIAN, 0}; + VerifyRoundtripCompression(63, 129, pixel_format, + pixel_format, (bool)lossless, + (bool)use_container); + } + } + } +} + +TEST(RoundtripTest, Uint8FrameRoundtripTest) { + for (int use_container = 0; use_container < 2; use_container++) { + for (int lossless = 0; lossless < 2; lossless++) { + for (uint32_t num_channels = 1; num_channels < 5; num_channels++) { + JxlPixelFormat pixel_format = + JxlPixelFormat{num_channels, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; + VerifyRoundtripCompression(63, 129, pixel_format, pixel_format, + (bool)lossless, + (bool)use_container); + } + } + } +} + +TEST(RoundtripTest, TestNonlinearSrgbAsXybEncoded) { + for (int use_container = 0; use_container < 2; use_container++) { + for (uint32_t num_channels = 1; num_channels < 5; num_channels++) { + JxlPixelFormat pixel_format_in = + JxlPixelFormat{num_channels, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; + JxlPixelFormat pixel_format_out = + JxlPixelFormat{num_channels, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + VerifyRoundtripCompression( + 63, 129, pixel_format_in, pixel_format_out, + /*lossless=*/false, (bool)use_container); + } + } +} + +TEST(RoundtripTest, ExtraBoxesTest) { + JxlPixelFormat pixel_format = + JxlPixelFormat{4, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0}; + const size_t xsize = 61; + const size_t ysize = 71; + + const std::vector original_bytes = + GetTestImage(xsize, ysize, pixel_format); + jxl::CodecInOut original_io = + ConvertTestImage(original_bytes, xsize, ysize, pixel_format, {}); + + JxlEncoder* enc = JxlEncoderCreate(nullptr); + EXPECT_NE(nullptr, enc); + + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc, true)); + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = false; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); + JxlColorEncoding color_encoding; + if (pixel_format.data_type == JXL_TYPE_FLOAT) { + JxlColorEncodingSetToLinearSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + } else { + JxlColorEncodingSetToSRGB(&color_encoding, + /*is_gray=*/pixel_format.num_channels < 3); + } + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); + JxlEncoderOptions* opts = JxlEncoderOptionsCreate(enc, nullptr); + JxlEncoderOptionsSetLossless(opts, false); + EXPECT_EQ( + JXL_ENC_SUCCESS, + JxlEncoderAddImageFrame(opts, &pixel_format, (void*)original_bytes.data(), + original_bytes.size())); + JxlEncoderCloseInput(enc); + + std::vector compressed; + EncodeWithEncoder(enc, &compressed); + JxlEncoderDestroy(enc); + + std::vector extra_data(1023); + jxl::AppendBoxHeader(jxl::MakeBoxType("crud"), extra_data.size(), false, + &compressed); + compressed.insert(compressed.end(), extra_data.begin(), extra_data.end()); + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_NE(nullptr, dec); + + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)); + + JxlDecoderSetInput(dec, next_in, avail_in); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &pixel_format, &buffer_size)); + EXPECT_EQ(buffer_size, original_bytes.size()); + + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + + size_t icc_profile_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize(dec, &pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, + &icc_profile_size)); + jxl::PaddedBytes icc_profile(icc_profile_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile( + dec, &pixel_format, JXL_COLOR_PROFILE_TARGET_DATA, + icc_profile.data(), icc_profile.size())); + + std::vector decoded_bytes(buffer_size); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer(dec, &pixel_format, + decoded_bytes.data(), + decoded_bytes.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + JxlDecoderDestroy(dec); + + jxl::CodecInOut decoded_io = + ConvertTestImage(decoded_bytes, xsize, ysize, pixel_format, icc_profile); + + jxl::ButteraugliParams ba; + float butteraugli_score = ButteraugliDistance(original_io, decoded_io, ba, + /*distmap=*/nullptr, nullptr); + EXPECT_LE(butteraugli_score, 2.0f); +} + +static const unsigned char kEncodedTestProfile[] = { + 0x1f, 0x8b, 0x1, 0x13, 0x10, 0x0, 0x0, 0x0, 0x20, 0x4c, 0xcc, 0x3, + 0xe7, 0xa0, 0xa5, 0xa2, 0x90, 0xa4, 0x27, 0xe8, 0x79, 0x1d, 0xe3, 0x26, + 0x57, 0x54, 0xef, 0x0, 0xe8, 0x97, 0x2, 0xce, 0xa1, 0xd7, 0x85, 0x16, + 0xb4, 0x29, 0x94, 0x58, 0xf2, 0x56, 0xc0, 0x76, 0xea, 0x23, 0xec, 0x7c, + 0x73, 0x51, 0x41, 0x40, 0x23, 0x21, 0x95, 0x4, 0x75, 0x12, 0xc9, 0xcc, + 0x16, 0xbd, 0xb6, 0x99, 0xad, 0xf8, 0x75, 0x35, 0xb6, 0x42, 0xae, 0xae, + 0xae, 0x86, 0x56, 0xf8, 0xcc, 0x16, 0x30, 0xb3, 0x45, 0xad, 0xd, 0x40, + 0xd6, 0xd1, 0xd6, 0x99, 0x40, 0xbe, 0xe2, 0xdc, 0x31, 0x7, 0xa6, 0xb9, + 0x27, 0x92, 0x38, 0x0, 0x3, 0x5e, 0x2c, 0xbe, 0xe6, 0xfb, 0x19, 0xbf, + 0xf3, 0x6d, 0xbc, 0x4d, 0x64, 0xe5, 0xba, 0x76, 0xde, 0x31, 0x65, 0x66, + 0x14, 0xa6, 0x3a, 0xc5, 0x8f, 0xb1, 0xb4, 0xba, 0x1f, 0xb1, 0xb8, 0xd4, + 0x75, 0xba, 0x18, 0x86, 0x95, 0x3c, 0x26, 0xf6, 0x25, 0x62, 0x53, 0xfd, + 0x9c, 0x94, 0x76, 0xf6, 0x95, 0x2c, 0xb1, 0xfd, 0xdc, 0xc0, 0xe4, 0x3f, + 0xb3, 0xff, 0x67, 0xde, 0xd5, 0x94, 0xcc, 0xb0, 0x83, 0x2f, 0x28, 0x93, + 0x92, 0x3, 0xa1, 0x41, 0x64, 0x60, 0x62, 0x70, 0x80, 0x87, 0xaf, 0xe7, + 0x60, 0x4a, 0x20, 0x23, 0xb3, 0x11, 0x7, 0x38, 0x38, 0xd4, 0xa, 0x66, + 0xb5, 0x93, 0x41, 0x90, 0x19, 0x17, 0x18, 0x60, 0xa5, 0xb, 0x7a, 0x24, + 0xaa, 0x20, 0x81, 0xac, 0xa9, 0xa1, 0x70, 0xa6, 0x12, 0x8a, 0x4a, 0xa3, + 0xa0, 0xf9, 0x9a, 0x97, 0xe7, 0xa8, 0xac, 0x8, 0xa8, 0xc4, 0x2a, 0x86, + 0xa7, 0x69, 0x1e, 0x67, 0xe6, 0xbe, 0xa4, 0xd3, 0xff, 0x91, 0x61, 0xf6, + 0x8a, 0xe6, 0xb5, 0xb3, 0x61, 0x9f, 0x19, 0x17, 0x98, 0x27, 0x6b, 0xe9, + 0x8, 0x98, 0xe1, 0x21, 0x4a, 0x9, 0xb5, 0xd7, 0xca, 0xfa, 0x94, 0xd0, + 0x69, 0x1a, 0xeb, 0x52, 0x1, 0x4e, 0xf5, 0xf6, 0xdf, 0x7f, 0xe7, 0x29, + 0x70, 0xee, 0x4, 0xda, 0x2f, 0xa4, 0xff, 0xfe, 0xbb, 0x6f, 0xa8, 0xff, + 0xfe, 0xdb, 0xaf, 0x8, 0xf6, 0x72, 0xa1, 0x40, 0x5d, 0xf0, 0x2d, 0x8, + 0x82, 0x5b, 0x87, 0xbd, 0x10, 0x8, 0xe9, 0x7, 0xee, 0x4b, 0x80, 0xda, + 0x4a, 0x4, 0xc5, 0x5e, 0xa0, 0xb7, 0x1e, 0x60, 0xb0, 0x59, 0x76, 0x60, + 0xb, 0x2e, 0x19, 0x8a, 0x2e, 0x1c, 0xe6, 0x6, 0x20, 0xb8, 0x64, 0x18, + 0x2a, 0xcf, 0x51, 0x94, 0xd4, 0xee, 0xc3, 0xfe, 0x39, 0x74, 0xd4, 0x2b, + 0x48, 0xc9, 0x83, 0x4c, 0x9b, 0xd0, 0x4c, 0x35, 0x10, 0xe3, 0x9, 0xf7, + 0x72, 0xf0, 0x7a, 0xe, 0xbf, 0x7d, 0x36, 0x2e, 0x19, 0x7e, 0x3f, 0xc, + 0xf7, 0x93, 0xe7, 0xf4, 0x1d, 0x32, 0xc6, 0xb0, 0x89, 0xad, 0xe0, 0x28, + 0xc1, 0xa7, 0x59, 0xe3, 0x0, +}; + +TEST(RoundtripTest, TestICCProfile) { + // JxlEncoderSetICCProfile parses the ICC profile, so a valid profile is + // needed. The profile should be passed correctly through the roundtrip. + jxl::BitReader reader(jxl::Span(kEncodedTestProfile, + sizeof(kEncodedTestProfile))); + jxl::PaddedBytes icc; + ASSERT_TRUE(ReadICC(&reader, &icc)); + ASSERT_TRUE(reader.Close()); + + JxlPixelFormat format = + JxlPixelFormat{3, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; + + size_t xsize = 25; + size_t ysize = 37; + const std::vector original_bytes = + GetTestImage(xsize, ysize, format); + + JxlEncoder* enc = JxlEncoderCreate(nullptr); + EXPECT_NE(nullptr, enc); + + JxlBasicInfo basic_info; + jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &format); + basic_info.xsize = xsize; + basic_info.ysize = ysize; + basic_info.uses_original_profile = JXL_FALSE; + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); + + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderSetICCProfile(enc, icc.data(), icc.size())); + JxlEncoderOptions* opts = JxlEncoderOptionsCreate(enc, nullptr); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddImageFrame(opts, &format, (void*)original_bytes.data(), + original_bytes.size())); + JxlEncoderCloseInput(enc); + + std::vector compressed; + EncodeWithEncoder(enc, &compressed); + JxlEncoderDestroy(enc); + + JxlDecoder* dec = JxlDecoderCreate(nullptr); + EXPECT_NE(nullptr, dec); + + const uint8_t* next_in = compressed.data(); + size_t avail_in = compressed.size(); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)); + + JxlDecoderSetInput(dec, next_in, avail_in); + EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec)); + size_t buffer_size; + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderImageOutBufferSize(dec, &format, &buffer_size)); + EXPECT_EQ(buffer_size, original_bytes.size()); + + JxlBasicInfo info; + EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec, &info)); + EXPECT_EQ(xsize, info.xsize); + EXPECT_EQ(ysize, info.ysize); + + EXPECT_EQ(JXL_DEC_COLOR_ENCODING, JxlDecoderProcessInput(dec)); + + size_t dec_icc_size; + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderGetICCProfileSize( + dec, &format, JXL_COLOR_PROFILE_TARGET_ORIGINAL, &dec_icc_size)); + EXPECT_EQ(icc.size(), dec_icc_size); + jxl::PaddedBytes dec_icc(dec_icc_size); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderGetColorAsICCProfile(dec, &format, + JXL_COLOR_PROFILE_TARGET_ORIGINAL, + dec_icc.data(), dec_icc.size())); + + std::vector decoded_bytes(buffer_size); + + EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetImageOutBuffer(dec, &format, decoded_bytes.data(), + decoded_bytes.size())); + + EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec)); + + EXPECT_EQ(icc, dec_icc); + + JxlDecoderDestroy(dec); +} + +TEST(RoundtripTest, JXL_TRANSCODE_JPEG_TEST(TestJPEGReconstruction)) { + const std::string jpeg_path = + "imagecompression.info/flower_foveon.png.im_q85_420.jpg"; + const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); + jxl::CodecInOut orig_io; + ASSERT_TRUE( + SetFromBytes(jxl::Span(orig), &orig_io, /*pool=*/nullptr)); + + JxlEncoderPtr enc = JxlEncoderMake(nullptr); + JxlEncoderOptions* options = JxlEncoderOptionsCreate(enc.get(), NULL); + + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc.get(), JXL_TRUE)); + EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderStoreJPEGMetadata(enc.get(), JXL_TRUE)); + EXPECT_EQ(JXL_ENC_SUCCESS, + JxlEncoderAddJPEGFrame(options, orig.data(), orig.size())); + JxlEncoderCloseInput(enc.get()); + + std::vector compressed; + EncodeWithEncoder(enc.get(), &compressed); + + JxlDecoderPtr dec = JxlDecoderMake(nullptr); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSubscribeEvents( + dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE)); + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); + EXPECT_EQ(JXL_DEC_JPEG_RECONSTRUCTION, JxlDecoderProcessInput(dec.get())); + std::vector reconstructed_buffer(128); + EXPECT_EQ(JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer(dec.get(), reconstructed_buffer.data(), + reconstructed_buffer.size())); + size_t used = 0; + JxlDecoderStatus dec_process_result = JXL_DEC_JPEG_NEED_MORE_OUTPUT; + while (dec_process_result == JXL_DEC_JPEG_NEED_MORE_OUTPUT) { + used = reconstructed_buffer.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + reconstructed_buffer.resize(reconstructed_buffer.size() * 2); + EXPECT_EQ( + JXL_DEC_SUCCESS, + JxlDecoderSetJPEGBuffer(dec.get(), reconstructed_buffer.data() + used, + reconstructed_buffer.size() - used)); + dec_process_result = JxlDecoderProcessInput(dec.get()); + } + ASSERT_EQ(JXL_DEC_FULL_IMAGE, dec_process_result); + used = reconstructed_buffer.size() - JxlDecoderReleaseJPEGBuffer(dec.get()); + ASSERT_EQ(used, orig.size()); + EXPECT_EQ(0, memcmp(reconstructed_buffer.data(), orig.data(), used)); +} diff --git a/lib/jxl/sanitizers.h b/lib/jxl/sanitizers.h new file mode 100644 index 0000000..2123cfa --- /dev/null +++ b/lib/jxl/sanitizers.h @@ -0,0 +1,246 @@ +// 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_SANITIZERS_H_ +#define LIB_JXL_SANITIZERS_H_ + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/image.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 + +#if JXL_MEMORY_SANITIZER +#include + +#include +#include +#include + +#include "lib/jxl/base/status.h" +#include "sanitizer/msan_interface.h" +#endif + +namespace jxl { +namespace msan { + +#if JXL_MEMORY_SANITIZER + +// Chosen so that kSanitizerSentinel is four copies of kSanitizerSentinelByte. +constexpr uint8_t kSanitizerSentinelByte = 0x48; +constexpr float kSanitizerSentinel = 205089.125f; + +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonMemory(const volatile void* m, + size_t size) { + __msan_poison(m, size); +} + +static JXL_INLINE JXL_MAYBE_UNUSED void UnpoisonMemory(const volatile void* m, + size_t size) { + __msan_unpoison(m, size); +} + +// Mark all the bytes of an image (including padding) as poisoned bytes. +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonImage(const PlaneBase& im) { + PoisonMemory(im.bytes(), im.bytes_per_row() * im.ysize()); +} + +template +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonImage(const Image3& im) { + PoisonImage(im.Plane(0)); + PoisonImage(im.Plane(1)); + PoisonImage(im.Plane(2)); +} + +// Print the uninitialized regions of an image. +template +static JXL_INLINE JXL_MAYBE_UNUSED void PrintImageUninitialized( + const Plane& im) { + fprintf(stderr, "Uninitialized regions for image of size %zux%zu:\n", + im.xsize(), im.ysize()); + + // A segment of uninitialized pixels in a row, in the format [first, second). + typedef std::pair PixelSegment; + + // Helper class to merge and print a list of rows of PixelSegment that may be + // the same over big ranges of rows. This compacts the output to ranges of + // rows like "[y0, y1): [x0, x1) [x2, x3)". + class RowsMerger { + public: + // Add a new row the list of rows. If the row is the same as the previous + // one it will be merged showing a range of rows [y0, y1), but if the new + // row is different the current range of rows (if any) will be printed and a + // new one will be started. + void AddRow(size_t y, std::vector&& new_row) { + if (start_y_ != -1 && new_row != segments_) { + PrintRow(y); + } + if (new_row.empty()) { + // Skip ranges with no uninitialized pixels. + start_y_ = -1; + segments_.clear(); + return; + } + if (start_y_ == -1) { + start_y_ = y; + segments_ = std::move(new_row); + } + } + + // Print the contents of the range of rows [start_y_, end_y) if any. + void PrintRow(size_t end_y) { + if (start_y_ == -1) return; + if (segments_.empty()) { + start_y_ = -1; + return; + } + if (end_y - start_y_ > 1) { + fprintf(stderr, " y=[%zd, %zu):", start_y_, end_y); + } else { + fprintf(stderr, " y=[%zd]:", start_y_); + } + for (const auto& seg : segments_) { + if (seg.first + 1 == seg.second) { + fprintf(stderr, " [%zd]", seg.first); + } else { + fprintf(stderr, " [%zd, %zu)", seg.first, seg.second); + } + } + fprintf(stderr, "\n"); + start_y_ = -1; + } + + private: + std::vector segments_; + // Row number of the first row in the range of rows that have |segments| as + // the undefined segments. + ssize_t start_y_ = -1; + } rows_merger; + + class SegmentsMerger { + public: + void AddValue(size_t x) { + if (row.empty() || row.back().second != x) { + row.emplace_back(x, x + 1); + } else { + row.back().second = x + 1; + } + } + + std::vector row; + }; + + for (size_t y = 0; y < im.ysize(); y++) { + auto* row = im.Row(y); + SegmentsMerger seg_merger; + size_t x = 0; + while (x < im.xsize()) { + intptr_t ret = + __msan_test_shadow(row + x, (im.xsize() - x) * sizeof(row[0])); + if (ret < 0) break; + size_t next_x = x + ret / sizeof(row[0]); + seg_merger.AddValue(next_x); + x = next_x + 1; + } + rows_merger.AddRow(y, std::move(seg_merger.row)); + } + rows_merger.PrintRow(im.ysize()); +} + +// Check that all the pixels in the provided rect of the image are initialized +// (not poisoned). If any of the values is poisoned it will abort. +template +static JXL_INLINE JXL_MAYBE_UNUSED void CheckImageInitialized( + const Plane& im, const Rect& r, const char* message) { + JXL_ASSERT(r.x0() <= im.xsize()); + JXL_ASSERT(r.x0() + r.xsize() <= im.xsize()); + JXL_ASSERT(r.y0() <= im.ysize()); + JXL_ASSERT(r.y0() + r.ysize() <= im.ysize()); + for (size_t y = r.y0(); y < r.y0() + r.ysize(); y++) { + const auto* row = im.Row(y); + intptr_t ret = __msan_test_shadow(row + r.x0(), sizeof(*row) * r.xsize()); + if (ret != -1) { + JXL_DEBUG(1, + "Checking an image of %zu x %zu, rect x0=%zu, y0=%zu, " + "xsize=%zu, ysize=%zu", + im.xsize(), im.ysize(), r.x0(), r.y0(), r.xsize(), r.ysize()); + size_t x = ret / sizeof(*row); + JXL_DEBUG(1, "CheckImageInitialized failed at x=%zu, y=%zu: %s", + r.x0() + x, y, message ? message : ""); + PrintImageUninitialized(im); + } + // This will report an error if memory is not initialized. + __msan_check_mem_is_initialized(row + r.x0(), sizeof(*row) * r.xsize()); + } +} + +template +static JXL_INLINE JXL_MAYBE_UNUSED void CheckImageInitialized( + const Image3& im, const Rect& r, const char* message) { + for (size_t c = 0; c < 3; c++) { + std::string str_message(message); + str_message += " c=" + std::to_string(c); + CheckImageInitialized(im.Plane(c), r, str_message.c_str()); + } +} + +#define JXL_CHECK_IMAGE_INITIALIZED(im, r) \ + ::jxl::msan::CheckImageInitialized(im, r, "im=" #im ", r=" #r); + +#else // JXL_MEMORY_SANITIZER + +// In non-msan mode these functions don't use volatile since it is not needed +// for the empty functions. + +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonMemory(const void*, size_t) {} +static JXL_INLINE JXL_MAYBE_UNUSED void UnpoisonMemory(const void*, size_t) {} + +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonImage(const PlaneBase& im) {} +template +static JXL_INLINE JXL_MAYBE_UNUSED void PoisonImage(const Plane& im) {} + +#define JXL_CHECK_IMAGE_INITIALIZED(im, r) + +#endif + +} // namespace msan +} // namespace jxl + +#endif // LIB_JXL_SANITIZERS_H_ diff --git a/lib/jxl/speed_tier_test.cc b/lib/jxl/speed_tier_test.cc new file mode 100644 index 0000000..4e7c9f9 --- /dev/null +++ b/lib/jxl/speed_tier_test.cc @@ -0,0 +1,112 @@ +// 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 + +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/padded_bytes.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_cache.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" +#include "lib/jxl/image.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { +namespace { + +struct SpeedTierTestParams { + explicit SpeedTierTestParams(const SpeedTier speed_tier, + const bool shrink8 = false) + : speed_tier(speed_tier), shrink8(shrink8) {} + SpeedTier speed_tier; + bool shrink8; +}; + +std::ostream& operator<<(std::ostream& os, SpeedTierTestParams params) { + auto previous_flags = os.flags(); + os << std::boolalpha; + os << "SpeedTierTestParams{" << SpeedTierName(params.speed_tier) + << ", /*shrink8=*/" << params.shrink8 << "}"; + os.flags(previous_flags); + return os; +} + +class SpeedTierTest : public testing::TestWithParam {}; + +JXL_GTEST_INSTANTIATE_TEST_SUITE_P( + SpeedTierTestInstantiation, SpeedTierTest, + testing::Values(SpeedTierTestParams{SpeedTier::kCheetah, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kCheetah, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kThunder, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kThunder, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kLightning, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kLightning, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kFalcon, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kFalcon, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kHare, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kHare, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kWombat, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kWombat, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kSquirrel, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kSquirrel, + /*shrink8=*/false}, + SpeedTierTestParams{SpeedTier::kKitten, + /*shrink8=*/true}, + SpeedTierTestParams{SpeedTier::kKitten, + /*shrink8=*/false}, + // Only downscaled image for Tortoise mode. + SpeedTierTestParams{SpeedTier::kTortoise, + /*shrink8=*/true})); + +TEST_P(SpeedTierTest, Roundtrip) { + const PaddedBytes orig = + ReadTestData("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ThreadPoolInternal pool(8); + ASSERT_TRUE(SetFromBytes(Span(orig), &io, &pool)); + + const SpeedTierTestParams& params = GetParam(); + + if (params.shrink8) { + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + } + + CompressParams cparams; + cparams.speed_tier = params.speed_tier; + DecompressParams dparams; + + CodecInOut io2; + test::Roundtrip(&io, cparams, dparams, nullptr, &io2); + + // Can be 2.2 in non-hare mode. + EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, + /*distmap=*/nullptr, /*pool=*/nullptr), + 2.8); +} +} // namespace +} // namespace jxl diff --git a/lib/jxl/splines.cc b/lib/jxl/splines.cc new file mode 100644 index 0000000..8653445 --- /dev/null +++ b/lib/jxl/splines.cc @@ -0,0 +1,563 @@ +// 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/splines.h" + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/common.h" +#include "lib/jxl/dct_scales.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/opsin_params.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/splines.cc" +#include +#include + +#include "lib/jxl/fast_math-inl.h" +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// Given a set of DCT coefficients, this returns the result of performing cosine +// interpolation on the original samples. +float ContinuousIDCT(const float dct[32], float t) { + // We compute here the DCT-3 of the `dct` vector, rescaled by a factor of + // sqrt(32). This is such that an input vector vector {x, 0, ..., 0} produces + // a constant result of x. dct[0] was scaled in Dequantize() to allow uniform + // treatment of all the coefficients. + constexpr float kMultipliers[32] = { + kPi / 32 * 0, kPi / 32 * 1, kPi / 32 * 2, kPi / 32 * 3, kPi / 32 * 4, + kPi / 32 * 5, kPi / 32 * 6, kPi / 32 * 7, kPi / 32 * 8, kPi / 32 * 9, + kPi / 32 * 10, kPi / 32 * 11, kPi / 32 * 12, kPi / 32 * 13, kPi / 32 * 14, + kPi / 32 * 15, kPi / 32 * 16, kPi / 32 * 17, kPi / 32 * 18, kPi / 32 * 19, + kPi / 32 * 20, kPi / 32 * 21, kPi / 32 * 22, kPi / 32 * 23, kPi / 32 * 24, + kPi / 32 * 25, kPi / 32 * 26, kPi / 32 * 27, kPi / 32 * 28, kPi / 32 * 29, + kPi / 32 * 30, kPi / 32 * 31, + }; + HWY_CAPPED(float, 32) df; + auto result = Zero(df); + const auto tandhalf = Set(df, t + 0.5f); + for (int i = 0; i < 32; i += Lanes(df)) { + auto cos_arg = LoadU(df, kMultipliers + i) * tandhalf; + auto cos = FastCosf(df, cos_arg); + auto local_res = LoadU(df, dct + i) * cos; + result = MulAdd(Set(df, square_root<2>::value), local_res, result); + } + return GetLane(SumOfLanes(result)); +} + +template +void DrawSegment(DF df, const SplineSegment& segment, bool add, size_t y, + size_t x, float* JXL_RESTRICT rows[3]) { + Rebind di; + const auto inv_sigma = Set(df, segment.inv_sigma); + const auto half = Set(df, 0.5f); + const auto one_over_2s2 = Set(df, 0.353553391f); + const auto sigma_over_4_times_intensity = + Set(df, segment.sigma_over_4_times_intensity); + const auto dx = ConvertTo(df, Iota(di, x)) - Set(df, segment.center_x); + const auto dy = Set(df, y - segment.center_y); + const auto sqd = MulAdd(dx, dx, dy * dy); + const auto distance = Sqrt(sqd); + const auto one_dimensional_factor = + FastErff(df, MulAdd(distance, half, one_over_2s2) * inv_sigma) - + FastErff(df, MulSub(distance, half, one_over_2s2) * inv_sigma); + auto local_intensity = sigma_over_4_times_intensity * one_dimensional_factor * + one_dimensional_factor; + for (size_t c = 0; c < 3; ++c) { + const auto cm = Set(df, add ? segment.color[c] : -segment.color[c]); + const auto in = LoadU(df, rows[c] + x); + StoreU(MulAdd(cm, local_intensity, in), df, rows[c] + x); + } +} + +void DrawSegment(const SplineSegment& segment, bool add, size_t y, ssize_t x0, + ssize_t x1, float* JXL_RESTRICT rows[3]) { + ssize_t x = std::max(x0, segment.xbegin); + x1 = std::min(x1, segment.xend); + HWY_FULL(float) df; + for (; x + static_cast(Lanes(df)) <= x1; x += Lanes(df)) { + DrawSegment(df, segment, add, y, x, rows); + } + for (; x < x1; ++x) { + DrawSegment(HWY_CAPPED(float, 1)(), segment, add, y, x, rows); + } +} + +void ComputeSegments(const Spline::Point& center, const float intensity, + const float color[3], const float sigma, + std::vector& segments, + std::vector>& segments_by_y) { + // Sanity check sigma, inverse sigma and intensity + if (!(std::isfinite(sigma) && sigma != 0.0f && std::isfinite(1.0f / sigma) && + std::isfinite(intensity))) { + return; + } +#if JXL_HIGH_PRECISION + constexpr float kDistanceExp = 5; +#else + // About 30% faster. + constexpr float kDistanceExp = 3; +#endif + // We cap from below colors to at least 0.01. + float max_color = 0.01f; + for (size_t c = 0; c < 3; c++) { + max_color = std::max(max_color, std::abs(color[c] * intensity)); + } + // Distance beyond which max_color*intensity*exp(-d^2 / (2 * sigma^2)) drops + // below 10^-kDistanceExp. + const float maximum_distance = + std::sqrt(-2 * sigma * sigma * + (std::log(0.1) * kDistanceExp - std::log(max_color))); + SplineSegment segment; + segment.center_y = center.y; + segment.center_x = center.x; + memcpy(segment.color, color, sizeof(segment.color)); + segment.sigma = sigma; + segment.inv_sigma = 1.0f / sigma; + segment.sigma_over_4_times_intensity = .25f * sigma * intensity; + segment.xbegin = std::max(0, center.x - maximum_distance + 0.5f); + segment.xend = center.x + maximum_distance + 1.5f; // one-past-the-end + segment.maximum_distance = maximum_distance; + ssize_t y0 = center.y - maximum_distance + .5f; + ssize_t y1 = center.y + maximum_distance + 1.5f; // one-past-the-end + for (ssize_t y = std::max(y0, 0); y < y1; y++) { + segments_by_y.emplace_back(y, segments.size()); + } + segments.push_back(segment); +} + +void DrawSegments(Image3F* const opsin, const Rect& opsin_rect, + const Rect& image_rect, bool add, + const SplineSegment* segments, const size_t* segment_indices, + const size_t* segment_y_start) { + JXL_ASSERT(image_rect.ysize() == 1); + float* JXL_RESTRICT rows[3] = { + opsin_rect.PlaneRow(opsin, 0, 0) - image_rect.x0(), + opsin_rect.PlaneRow(opsin, 1, 0) - image_rect.x0(), + opsin_rect.PlaneRow(opsin, 2, 0) - image_rect.x0(), + }; + size_t y = image_rect.y0(); + for (size_t i = segment_y_start[y]; i < segment_y_start[y + 1]; i++) { + DrawSegment(segments[segment_indices[i]], add, y, image_rect.x0(), + image_rect.x0() + image_rect.xsize(), rows); + } +} + +void SegmentsFromPoints( + const Spline& spline, + const std::vector>& points_to_draw, + float arc_length, std::vector& segments, + std::vector>& segments_by_y) { + float inv_arc_length = 1.0f / arc_length; + int k = 0; + for (const auto& point_to_draw : points_to_draw) { + const Spline::Point& point = point_to_draw.first; + const float multiplier = point_to_draw.second; + const float progress_along_arc = + std::min(1.f, (k * kDesiredRenderingDistance) * inv_arc_length); + ++k; + float color[3]; + for (size_t c = 0; c < 3; ++c) { + color[c] = + ContinuousIDCT(spline.color_dct[c], (32 - 1) * progress_along_arc); + } + const float sigma = + ContinuousIDCT(spline.sigma_dct, (32 - 1) * progress_along_arc); + ComputeSegments(point, multiplier, color, sigma, segments, segments_by_y); + } +} +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +HWY_EXPORT(SegmentsFromPoints); +HWY_EXPORT(DrawSegments); + +namespace { + +// Maximum number of spline control points per frame is +// std::min(kMaxNumControlPoints, xsize * ysize / 2) +constexpr size_t kMaxNumControlPoints = 1u << 20u; +constexpr size_t kMaxNumControlPointsPerPixelRatio = 2; + +// X, Y, B, sigma. +float ColorQuantizationWeight(const int32_t adjustment, const int channel, + const int i) { + const float multiplier = adjustment >= 0 ? 1.f + .125f * adjustment + : 1.f / (1.f + .125f * -adjustment); + + static constexpr float kChannelWeight[] = {0.0042f, 0.075f, 0.07f, .3333f}; + + return multiplier / kChannelWeight[channel]; +} + +Status DecodeAllStartingPoints(std::vector* const points, + BitReader* const br, ANSSymbolReader* reader, + const std::vector& context_map, + size_t num_splines) { + points->clear(); + points->reserve(num_splines); + int64_t last_x = 0; + int64_t last_y = 0; + for (size_t i = 0; i < num_splines; i++) { + int64_t x = + reader->ReadHybridUint(kStartingPositionContext, br, context_map); + int64_t y = + reader->ReadHybridUint(kStartingPositionContext, br, context_map); + if (i != 0) { + x = UnpackSigned(x) + last_x; + y = UnpackSigned(y) + last_y; + } + points->emplace_back(static_cast(x), static_cast(y)); + last_x = x; + last_y = y; + } + return true; +} + +struct Vector { + float x, y; + Vector operator-() const { return {-x, -y}; } + Vector operator+(const Vector& other) const { + return {x + other.x, y + other.y}; + } + float SquaredNorm() const { return x * x + y * y; } +}; +Vector operator*(const float k, const Vector& vec) { + return {k * vec.x, k * vec.y}; +} + +Spline::Point operator+(const Spline::Point& p, const Vector& vec) { + return {p.x + vec.x, p.y + vec.y}; +} +Spline::Point operator-(const Spline::Point& p, const Vector& vec) { + return p + -vec; +} +Vector operator-(const Spline::Point& a, const Spline::Point& b) { + return {a.x - b.x, a.y - b.y}; +} + +std::vector DrawCentripetalCatmullRomSpline( + std::vector points) { + if (points.size() <= 1) return points; + // Number of points to compute between each control point. + static constexpr int kNumPoints = 16; + std::vector result; + result.reserve((points.size() - 1) * kNumPoints + 1); + points.insert(points.begin(), points[0] + (points[0] - points[1])); + points.push_back(points[points.size() - 1] + + (points[points.size() - 1] - points[points.size() - 2])); + // points has at least 4 elements at this point. + for (size_t start = 0; start < points.size() - 3; ++start) { + // 4 of them are used, and we draw from p[1] to p[2]. + const Spline::Point* const p = &points[start]; + result.push_back(p[1]); + float t[4] = {0}; + for (int k = 1; k < 4; ++k) { + t[k] = std::sqrt(hypotf(p[k].x - p[k - 1].x, p[k].y - p[k - 1].y)) + + t[k - 1]; + } + for (int i = 1; i < kNumPoints; ++i) { + const float tt = + t[1] + (static_cast(i) / kNumPoints) * (t[2] - t[1]); + Spline::Point a[3]; + for (int k = 0; k < 3; ++k) { + a[k] = p[k] + ((tt - t[k]) / (t[k + 1] - t[k])) * (p[k + 1] - p[k]); + } + Spline::Point b[2]; + for (int k = 0; k < 2; ++k) { + b[k] = a[k] + ((tt - t[k]) / (t[k + 2] - t[k])) * (a[k + 1] - a[k]); + } + result.push_back(b[0] + ((tt - t[1]) / (t[2] - t[1])) * (b[1] - b[0])); + } + } + result.push_back(points[points.size() - 2]); + return result; +} + +// Move along the line segments defined by `points`, `kDesiredRenderingDistance` +// pixels at a time, and call `functor` with each point and the actual distance +// to the previous point (which will always be kDesiredRenderingDistance except +// possibly for the very last point). +template +void ForEachEquallySpacedPoint(const Points& points, const Functor& functor) { + JXL_ASSERT(!points.empty()); + Spline::Point current = points.front(); + functor(current, kDesiredRenderingDistance); + auto next = points.begin(); + while (next != points.end()) { + const Spline::Point* previous = ¤t; + float arclength_from_previous = 0.f; + for (;;) { + if (next == points.end()) { + functor(*previous, arclength_from_previous); + return; + } + const float arclength_to_next = + std::sqrt((*next - *previous).SquaredNorm()); + if (arclength_from_previous + arclength_to_next >= + kDesiredRenderingDistance) { + current = + *previous + ((kDesiredRenderingDistance - arclength_from_previous) / + arclength_to_next) * + (*next - *previous); + functor(current, kDesiredRenderingDistance); + break; + } + arclength_from_previous += arclength_to_next; + previous = &*next; + ++next; + } + } +} + +} // namespace + +QuantizedSpline::QuantizedSpline(const Spline& original, + const int32_t quantization_adjustment, + float ytox, float ytob) { + JXL_ASSERT(!original.control_points.empty()); + control_points_.reserve(original.control_points.size() - 1); + const Spline::Point& starting_point = original.control_points.front(); + int previous_x = static_cast(roundf(starting_point.x)), + previous_y = static_cast(roundf(starting_point.y)); + int previous_delta_x = 0, previous_delta_y = 0; + for (auto it = original.control_points.begin() + 1; + it != original.control_points.end(); ++it) { + const int new_x = static_cast(roundf(it->x)); + const int new_y = static_cast(roundf(it->y)); + const int new_delta_x = new_x - previous_x; + const int new_delta_y = new_y - previous_y; + control_points_.emplace_back(new_delta_x - previous_delta_x, + new_delta_y - previous_delta_y); + previous_delta_x = new_delta_x; + previous_delta_y = new_delta_y; + previous_x = new_x; + previous_y = new_y; + } + + for (int c = 0; c < 3; ++c) { + float factor = c == 0 ? ytox : c == 1 ? 0 : ytob; + for (int i = 0; i < 32; ++i) { + const float coefficient = + original.color_dct[c][i] - + factor * color_dct_[1][i] / + ColorQuantizationWeight(quantization_adjustment, 1, i); + color_dct_[c][i] = static_cast( + roundf(coefficient * + ColorQuantizationWeight(quantization_adjustment, c, i))); + } + } + for (int i = 0; i < 32; ++i) { + sigma_dct_[i] = static_cast( + roundf(original.sigma_dct[i] * + ColorQuantizationWeight(quantization_adjustment, 3, i))); + } +} + +Spline QuantizedSpline::Dequantize(const Spline::Point& starting_point, + const int32_t quantization_adjustment, + float ytox, float ytob) const { + Spline result; + + result.control_points.reserve(control_points_.size() + 1); + int current_x = static_cast(roundf(starting_point.x)), + current_y = static_cast(roundf(starting_point.y)); + result.control_points.push_back(Spline::Point{static_cast(current_x), + static_cast(current_y)}); + int current_delta_x = 0, current_delta_y = 0; + for (const auto& point : control_points_) { + current_delta_x += point.first; + current_delta_y += point.second; + current_x += current_delta_x; + current_y += current_delta_y; + result.control_points.push_back(Spline::Point{ + static_cast(current_x), static_cast(current_y)}); + } + + for (int c = 0; c < 3; ++c) { + for (int i = 0; i < 32; ++i) { + result.color_dct[c][i] = + color_dct_[c][i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) / + ColorQuantizationWeight(quantization_adjustment, c, i); + } + } + for (int i = 0; i < 32; ++i) { + result.color_dct[0][i] += ytox * result.color_dct[1][i]; + result.color_dct[2][i] += ytob * result.color_dct[1][i]; + } + for (int i = 0; i < 32; ++i) { + result.sigma_dct[i] = + sigma_dct_[i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) / + ColorQuantizationWeight(quantization_adjustment, 3, i); + } + + return result; +} + +Status QuantizedSpline::Decode(const std::vector& context_map, + ANSSymbolReader* const decoder, + BitReader* const br, size_t max_control_points, + size_t* total_num_control_points) { + const size_t num_control_points = + decoder->ReadHybridUint(kNumControlPointsContext, br, context_map); + *total_num_control_points += num_control_points; + if (*total_num_control_points > max_control_points) { + return JXL_FAILURE("Too many control points: %zu", + *total_num_control_points); + } + control_points_.resize(num_control_points); + for (std::pair& control_point : control_points_) { + control_point.first = UnpackSigned( + decoder->ReadHybridUint(kControlPointsContext, br, context_map)); + control_point.second = UnpackSigned( + decoder->ReadHybridUint(kControlPointsContext, br, context_map)); + } + + const auto decode_dct = [decoder, br, &context_map](int dct[32]) -> Status { + for (int i = 0; i < 32; ++i) { + dct[i] = + UnpackSigned(decoder->ReadHybridUint(kDCTContext, br, context_map)); + } + return true; + }; + for (int c = 0; c < 3; ++c) { + JXL_RETURN_IF_ERROR(decode_dct(color_dct_[c])); + } + JXL_RETURN_IF_ERROR(decode_dct(sigma_dct_)); + return true; +} + +void Splines::Clear() { + quantization_adjustment_ = 0; + splines_.clear(); + starting_points_.clear(); + segments_.clear(); + segment_indices_.clear(); + segment_y_start_.clear(); +} + +Status Splines::Decode(jxl::BitReader* br, size_t num_pixels) { + std::vector context_map; + ANSCode code; + JXL_RETURN_IF_ERROR( + DecodeHistograms(br, kNumSplineContexts, &code, &context_map)); + ANSSymbolReader decoder(&code, br); + const size_t num_splines = + 1 + decoder.ReadHybridUint(kNumSplinesContext, br, context_map); + size_t max_control_points = std::min( + kMaxNumControlPoints, num_pixels / kMaxNumControlPointsPerPixelRatio); + if (num_splines > max_control_points) { + return JXL_FAILURE("Too many splines: %zu", num_splines); + } + JXL_RETURN_IF_ERROR(DecodeAllStartingPoints(&starting_points_, br, &decoder, + context_map, num_splines)); + + quantization_adjustment_ = UnpackSigned( + decoder.ReadHybridUint(kQuantizationAdjustmentContext, br, context_map)); + + splines_.clear(); + splines_.reserve(num_splines); + size_t num_control_points = num_splines; + for (size_t i = 0; i < num_splines; ++i) { + QuantizedSpline spline; + JXL_RETURN_IF_ERROR(spline.Decode(context_map, &decoder, br, + max_control_points, &num_control_points)); + splines_.push_back(std::move(spline)); + } + + JXL_RETURN_IF_ERROR(decoder.CheckANSFinalState()); + + if (!HasAny()) { + return JXL_FAILURE("Decoded splines but got none"); + } + + return true; +} + +void Splines::AddTo(Image3F* const opsin, const Rect& opsin_rect, + const Rect& image_rect) const { + return Apply(opsin, opsin_rect, image_rect); +} + +void Splines::SubtractFrom(Image3F* const opsin) const { + return Apply(opsin, Rect(*opsin), Rect(*opsin)); +} + +Status Splines::InitializeDrawCache(size_t image_xsize, size_t image_ysize, + const ColorCorrelationMap& cmap) { + // TODO(veluca): avoid storing segments that are entirely outside image + // boundaries. + segments_.clear(); + segment_indices_.clear(); + segment_y_start_.clear(); + std::vector> segments_by_y; + for (size_t i = 0; i < splines_.size(); ++i) { + const Spline spline = + splines_[i].Dequantize(starting_points_[i], quantization_adjustment_, + cmap.YtoXRatio(0), cmap.YtoBRatio(0)); + if (std::adjacent_find(spline.control_points.begin(), + spline.control_points.end()) != + spline.control_points.end()) { + return JXL_FAILURE("identical successive control points in spline %zu", + i); + } + std::vector> points_to_draw; + ForEachEquallySpacedPoint( + DrawCentripetalCatmullRomSpline(spline.control_points), + [&](const Spline::Point& point, const float multiplier) { + points_to_draw.emplace_back(point, multiplier); + }); + const float arc_length = + (points_to_draw.size() - 2) * kDesiredRenderingDistance + + points_to_draw.back().second; + if (arc_length <= 0.f) { + // This spline wouldn't have any effect. + continue; + } + HWY_DYNAMIC_DISPATCH(SegmentsFromPoints) + (spline, points_to_draw, arc_length, segments_, segments_by_y); + } + std::sort(segments_by_y.begin(), segments_by_y.end()); + segment_indices_.resize(segments_by_y.size()); + segment_y_start_.resize(image_ysize + 1); + for (size_t i = 0; i < segments_by_y.size(); i++) { + segment_indices_[i] = segments_by_y[i].second; + size_t y = segments_by_y[i].first; + if (y < image_ysize) { + segment_y_start_[y + 1]++; + } + } + for (size_t y = 0; y < image_ysize; y++) { + segment_y_start_[y + 1] += segment_y_start_[y]; + } + return true; +} + +template +void Splines::Apply(Image3F* const opsin, const Rect& opsin_rect, + const Rect& image_rect) const { + if (segments_.empty()) return; + for (size_t iy = 0; iy < image_rect.ysize(); iy++) { + HWY_DYNAMIC_DISPATCH(DrawSegments) + (opsin, opsin_rect.Line(iy), image_rect.Line(iy), add, segments_.data(), + segment_indices_.data(), segment_y_start_.data()); + } +} + +} // namespace jxl +#endif // HWY_ONCE diff --git a/lib/jxl/splines.h b/lib/jxl/splines.h new file mode 100644 index 0000000..2682762 --- /dev/null +++ b/lib/jxl/splines.h @@ -0,0 +1,146 @@ +// 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_SPLINES_H_ +#define LIB_JXL_SPLINES_H_ + +#include +#include + +#include +#include + +#include "lib/jxl/ans_params.h" +#include "lib/jxl/aux_out.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/chroma_from_luma.h" +#include "lib/jxl/dec_ans.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/entropy_coder.h" +#include "lib/jxl/image.h" + +namespace jxl { + +static constexpr float kDesiredRenderingDistance = 1.f; + +enum SplineEntropyContexts : size_t { + kQuantizationAdjustmentContext = 0, + kStartingPositionContext, + kNumSplinesContext, + kNumControlPointsContext, + kControlPointsContext, + kDCTContext, + kNumSplineContexts +}; + +struct Spline { + struct Point { + Point() : x(0.0f), y(0.0f) {} + Point(float x, float y) : x(x), y(y) {} + float x, y; + bool operator==(const Point& other) const { + return std::fabs(x - other.x) < 1e-3f && std::fabs(y - other.y) < 1e-3f; + } + }; + std::vector control_points; + // X, Y, B. + float color_dct[3][32]; + // Splines are draws by normalized Gaussian splatting. This controls the + // Gaussian's parameter along the spline. + float sigma_dct[32]; +}; + +class QuantizedSplineEncoder; + +class QuantizedSpline { + public: + QuantizedSpline() = default; + explicit QuantizedSpline(const Spline& original, + int32_t quantization_adjustment, float ytox, + float ytob); + + Spline Dequantize(const Spline::Point& starting_point, + int32_t quantization_adjustment, float ytox, + float ytob) const; + + Status Decode(const std::vector& context_map, + ANSSymbolReader* decoder, BitReader* br, + size_t max_control_points, size_t* total_num_control_points); + + private: + friend class QuantizedSplineEncoder; + + std::vector> + control_points_; // Double delta-encoded. + int color_dct_[3][32] = {}; + int sigma_dct_[32] = {}; +}; + +// A single "drawable unit" of a spline, i.e. a line of the region in which we +// render each Gaussian. The structure doesn't actually depend on the exact +// row, which allows reuse for different y values (which are tracked +// separately). +struct SplineSegment { + ssize_t xbegin, xend; + float center_x, center_y; + float maximum_distance; + float sigma; + float inv_sigma; + float sigma_over_4_times_intensity; + float color[3]; +}; + +class Splines { + public: + Splines() = default; + explicit Splines(const int32_t quantization_adjustment, + std::vector splines, + std::vector starting_points) + : quantization_adjustment_(quantization_adjustment), + splines_(std::move(splines)), + starting_points_(std::move(starting_points)) {} + + bool HasAny() const { return !splines_.empty(); } + + void Clear(); + + Status Decode(BitReader* br, size_t num_pixels); + + void AddTo(Image3F* opsin, const Rect& opsin_rect, + const Rect& image_rect) const; + void SubtractFrom(Image3F* opsin) const; + + const std::vector& QuantizedSplines() const { + return splines_; + } + const std::vector& StartingPoints() const { + return starting_points_; + } + + int32_t GetQuantizationAdjustment() const { return quantization_adjustment_; } + + Status InitializeDrawCache(size_t image_xsize, size_t image_ysize, + const ColorCorrelationMap& cmap); + + private: + template + void Apply(Image3F* opsin, const Rect& opsin_rect, + const Rect& image_rect) const; + + // If positive, quantization weights are multiplied by 1 + this/8, which + // increases precision. If negative, they are divided by 1 - this/8. If 0, + // they are unchanged. + int32_t quantization_adjustment_ = 0; + std::vector splines_; + std::vector starting_points_; + std::vector segments_; + std::vector segment_indices_; + std::vector segment_y_start_; +}; + +} // namespace jxl + +#endif // LIB_JXL_SPLINES_H_ diff --git a/lib/jxl/splines_gbench.cc b/lib/jxl/splines_gbench.cc new file mode 100644 index 0000000..78ff6d4 --- /dev/null +++ b/lib/jxl/splines_gbench.cc @@ -0,0 +1,52 @@ +// 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 "benchmark/benchmark.h" +#include "lib/jxl/splines.h" + +namespace jxl { +namespace { + +constexpr int kQuantizationAdjustment = 0; +const ColorCorrelationMap* const cmap = new ColorCorrelationMap; +const float kYToX = cmap->YtoXRatio(0); +const float kYToB = cmap->YtoBRatio(0); + +void BM_Splines(benchmark::State& state) { + const size_t n = state.range(); + + std::vector spline_data = { + {/*control_points=*/{ + {9, 54}, {118, 159}, {97, 3}, {10, 40}, {150, 25}, {120, 300}}, + /*color_dct=*/ + {{0.03125f, 0.00625f, 0.003125f}, {1.f, 0.321875f}, {1.f, 0.24375f}}, + /*sigma_dct=*/{0.3125f, 0.f, 0.f, 0.0625f}}}; + std::vector quantized_splines; + std::vector starting_points; + for (const Spline& spline : spline_data) { + quantized_splines.emplace_back(spline, kQuantizationAdjustment, kYToX, + kYToB); + starting_points.push_back(spline.control_points.front()); + } + Splines splines(kQuantizationAdjustment, std::move(quantized_splines), + std::move(starting_points)); + + Image3F drawing_area(320, 320); + ZeroFillImage(&drawing_area); + for (auto _ : state) { + for (size_t i = 0; i < n; ++i) { + JXL_CHECK(splines.InitializeDrawCache(drawing_area.xsize(), + drawing_area.ysize(), *cmap)); + splines.AddTo(&drawing_area, Rect(drawing_area), Rect(drawing_area)); + } + } + + state.SetItemsProcessed(n * state.iterations()); +} + +BENCHMARK(BM_Splines)->Range(1, 1 << 10); + +} // namespace +} // namespace jxl diff --git a/lib/jxl/splines_test.cc b/lib/jxl/splines_test.cc new file mode 100644 index 0000000..e85574c --- /dev/null +++ b/lib/jxl/splines_test.cc @@ -0,0 +1,339 @@ +// 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/splines.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "lib/extras/codec.h" +#include "lib/jxl/dec_file.h" +#include "lib/jxl/enc_butteraugli_comparator.h" +#include "lib/jxl/enc_splines.h" +#include "lib/jxl/image_test_utils.h" +#include "lib/jxl/testdata.h" + +namespace jxl { + +std::ostream& operator<<(std::ostream& os, const Spline::Point& p) { + return os << "(" << p.x << ", " << p.y << ")"; +} + +std::ostream& operator<<(std::ostream& os, const Spline& spline) { + return os << "(spline with " << spline.control_points.size() + << " control points)"; +} + +namespace { + +using ::testing::AllOf; +using ::testing::Field; +using ::testing::FloatNear; +using ::testing::Pointwise; + +constexpr int kQuantizationAdjustment = 0; +const ColorCorrelationMap* const cmap = new ColorCorrelationMap; +const float kYToX = cmap->YtoXRatio(0); +const float kYToB = cmap->YtoBRatio(0); + +constexpr float kTolerance = 0.003125; + +std::vector DequantizeSplines(const Splines& splines) { + const auto& quantized_splines = splines.QuantizedSplines(); + const auto& starting_points = splines.StartingPoints(); + JXL_ASSERT(quantized_splines.size() == starting_points.size()); + + std::vector dequantized; + for (size_t i = 0; i < quantized_splines.size(); ++i) { + dequantized.push_back(quantized_splines[i].Dequantize( + starting_points[i], kQuantizationAdjustment, kYToX, kYToB)); + } + return dequantized; +} + +MATCHER(ControlPointIs, "") { + const Spline::Point& actual = std::get<0>(arg); + const Spline::Point& expected = std::get<1>(arg); + return testing::ExplainMatchResult( + AllOf(Field(&Spline::Point::x, FloatNear(expected.x, kTolerance)), + Field(&Spline::Point::y, FloatNear(expected.y, kTolerance))), + actual, result_listener); +} + +MATCHER(ControlPointsMatch, "") { + const Spline& actual = std::get<0>(arg); + const Spline& expected = std::get<1>(arg); + return testing::ExplainMatchResult( + Field(&Spline::control_points, + Pointwise(ControlPointIs(), expected.control_points)), + actual, result_listener); +} + +MATCHER(SplinesMatch, "") { + const Spline& actual = std::get<0>(arg); + const Spline& expected = std::get<1>(arg); + if (!testing::ExplainMatchResult(ControlPointsMatch(), arg, + result_listener)) { + return false; + } + for (int i = 0; i < 3; ++i) { + size_t color_dct_size = + sizeof(expected.color_dct[i]) / sizeof(expected.color_dct[i][0]); + for (size_t j = 0; j < color_dct_size; j++) { + testing::StringMatchResultListener color_dct_listener; + if (!testing::ExplainMatchResult( + FloatNear(expected.color_dct[i][j], kTolerance), + actual.color_dct[i][j], &color_dct_listener)) { + *result_listener << ", where color_dct[" << i << "][" << j + << "] don't match, " << color_dct_listener.str(); + return false; + } + } + } + size_t sigma_dct_size = + sizeof(expected.sigma_dct) / sizeof(expected.sigma_dct[0]); + for (size_t i = 0; i < sigma_dct_size; i++) { + testing::StringMatchResultListener sigma_listener; + if (!testing::ExplainMatchResult( + FloatNear(expected.sigma_dct[i], kTolerance), actual.sigma_dct[i], + &sigma_listener)) { + *result_listener << ", where sigma_dct[" << i << "] don't match, " + << sigma_listener.str(); + return false; + } + } + return true; +} + +} // namespace + +TEST(SplinesTest, Serialization) { + std::vector spline_data = { + {/*control_points=*/{ + {109, 54}, {218, 159}, {80, 3}, {110, 274}, {94, 185}, {17, 277}}, + /*color_dct=*/ + {{36.3, 39.7, 23.2, 67.5, 4.4, 71.5, 62.3, 32.3, 92.2, 10.1, 10.8, + 9.2, 6.1, 10.5, 79.1, 7, 24.6, 90.8, 5.5, 84, 43.8, 49, + 33.5, 78.9, 54.5, 77.9, 62.1, 51.4, 36.4, 14.3, 83.7, 35.4}, + {9.4, 53.4, 9.5, 74.9, 72.7, 26.7, 7.9, 0.9, 84.9, 23.2, 26.5, + 31.1, 91, 11.7, 74.1, 39.3, 23.7, 82.5, 4.8, 2.7, 61.2, 96.4, + 13.7, 66.7, 62.9, 82.4, 5.9, 98.7, 21.5, 7.9, 51.7, 63.1}, + {48, 39.3, 6.9, 26.3, 33.3, 6.2, 1.7, 98.9, 59.9, 59.6, 95, + 61.3, 82.7, 53, 6.1, 30.4, 34.7, 96.9, 93.4, 17, 38.8, 80.8, + 63, 18.6, 43.6, 32.3, 61, 20.2, 24.3, 28.3, 69.1, 62.4}}, + /*sigma_dct=*/{32.7, 21.5, 44.4, 1.8, 45.8, 90.6, 29.3, 59.2, + 23.7, 85.2, 84.8, 27.2, 42.1, 84.1, 50.6, 17.6, + 93.7, 4.9, 2.6, 69.8, 94.9, 52, 24.3, 18.8, + 12.1, 95.7, 28.5, 81.4, 89.9, 31.4, 74.8, 52}}, + {/*control_points=*/{{172, 309}, + {196, 277}, + {42, 238}, + {114, 350}, + {307, 290}, + {316, 269}, + {124, 66}, + {233, 267}}, + /*color_dct=*/ + {{15, 28.9, 22, 6.6, 41.8, 83, 8.6, 56.8, 68.9, 9.7, 5.4, + 19.8, 70.8, 90, 52.5, 65.2, 7.8, 23.5, 26.4, 72.2, 64.7, 87.1, + 1.3, 67.5, 46, 68.4, 65.4, 35.5, 29.1, 13, 41.6, 23.9}, + {47.7, 79.4, 62.7, 29.1, 96.8, 18.5, 17.6, 15.2, 80.5, 56, 96.2, + 59.9, 26.7, 96.1, 92.3, 42.1, 35.8, 54, 23.2, 55, 76, 35.8, + 58.4, 88.7, 2.4, 78.1, 95.6, 27.5, 6.6, 78.5, 24.1, 69.8}, + {43.8, 96.5, 0.9, 95.1, 49.1, 71.2, 25.1, 33.6, 75.2, 95, 82.1, + 19.7, 10.5, 44.9, 50, 93.3, 83.5, 99.5, 64.6, 54, 3.5, 99.7, + 45.3, 82.1, 22.4, 37.9, 60, 32.2, 12.6, 4.6, 65.5, 96.4}}, + /*sigma_dct=*/{72.5, 2.6, 41.7, 2.2, 39.7, 79.1, 69.6, 19.9, + 92.3, 71.5, 41.9, 62.1, 30, 49.4, 70.3, 45.3, + 62.5, 47.2, 46.7, 41.2, 90.8, 46.8, 91.2, 55, + 8.1, 69.6, 25.4, 84.7, 61.7, 27.6, 3.7, 46.9}}, + {/*control_points=*/{{100, 186}, + {257, 97}, + {170, 49}, + {25, 169}, + {309, 104}, + {232, 237}, + {385, 101}, + {122, 168}, + {26, 300}, + {390, 88}}, + /*color_dct=*/ + {{16.9, 64.8, 4.2, 10.6, 23.5, 17, 79.3, 5.7, 60.4, 16.6, 94.9, + 63.7, 87.6, 10.5, 3.8, 61.1, 22.9, 81.9, 80.4, 40.5, 45.9, 25.4, + 39.8, 30, 50.2, 90.4, 27.9, 93.7, 65.1, 48.2, 22.3, 43.9}, + {24.9, 66, 3.5, 90.2, 97.1, 15.8, 35.6, 0.6, 68, 39.6, 24.4, + 85.9, 57.7, 77.6, 47.5, 67.9, 4.3, 5.4, 91.2, 58.5, 0.1, 52.2, + 3.5, 47.8, 63.2, 43.5, 85.8, 35.8, 50.2, 35.9, 19.2, 48.2}, + {82.8, 44.9, 76.4, 39.5, 94.1, 14.3, 89.8, 10, 10.5, 74.5, 56.3, + 65.8, 7.8, 23.3, 52.8, 99.3, 56.8, 46, 76.7, 13.5, 67, 22.4, + 29.9, 43.3, 70.3, 26, 74.3, 53.9, 62, 19.1, 49.3, 46.7}}, + /*sigma_dct=*/{83.5, 1.7, 25.1, 18.7, 46.5, 75.3, 28, 62.3, + 50.3, 23.3, 85.6, 96, 45.8, 33.1, 33.4, 52.9, + 26.3, 58.5, 19.6, 70, 92.6, 22.5, 57, 21.6, + 76.8, 87.5, 22.9, 66.3, 35.7, 35.6, 56.8, 67.2}}, + }; + + std::vector quantized_splines; + std::vector starting_points; + for (const Spline& spline : spline_data) { + quantized_splines.emplace_back(spline, kQuantizationAdjustment, kYToX, + kYToB); + starting_points.push_back(spline.control_points.front()); + } + + Splines splines(kQuantizationAdjustment, std::move(quantized_splines), + std::move(starting_points)); + const std::vector quantized_spline_data = DequantizeSplines(splines); + EXPECT_THAT(quantized_spline_data, + Pointwise(ControlPointsMatch(), spline_data)); + + BitWriter writer; + EncodeSplines(splines, &writer, kLayerSplines, HistogramParams(), nullptr); + writer.ZeroPadToByte(); + const size_t bits_written = writer.BitsWritten(); + + printf("Wrote %zu bits of splines.\n", bits_written); + + BitReader reader(writer.GetSpan()); + Splines decoded_splines; + ASSERT_TRUE(decoded_splines.Decode(&reader, /*num_pixels=*/1000)); + ASSERT_TRUE(reader.JumpToByteBoundary()); + EXPECT_EQ(reader.TotalBitsConsumed(), bits_written); + ASSERT_TRUE(reader.Close()); + + const std::vector decoded_spline_data = + DequantizeSplines(decoded_splines); + EXPECT_THAT(decoded_spline_data, + Pointwise(SplinesMatch(), quantized_spline_data)); +} + +#ifdef JXL_CRASH_ON_ERROR +TEST(SplinesTest, DISABLED_TooManySplinesTest) { +#else +TEST(SplinesTest, TooManySplinesTest) { +#endif + // This is more than the limit for 1000 pixels. + const size_t kNumSplines = 300; + + std::vector quantized_splines; + std::vector starting_points; + for (size_t i = 0; i < kNumSplines; i++) { + Spline spline = { + /*control_points=*/{{1.f + i, 2}, {10.f + i, 25}, {30.f + i, 300}}, + /*color_dct=*/ + {{1.f, 0.2f, 0.1f}, {35.7f, 10.3f}, {35.7f, 7.8f}}, + /*sigma_dct=*/{10.f, 0.f, 0.f, 2.f}}; + quantized_splines.emplace_back(spline, kQuantizationAdjustment, kYToX, + kYToB); + starting_points.push_back(spline.control_points.front()); + } + + Splines splines(kQuantizationAdjustment, std::move(quantized_splines), + std::move(starting_points)); + BitWriter writer; + EncodeSplines(splines, &writer, kLayerSplines, + HistogramParams(SpeedTier::kFalcon, 1), nullptr); + writer.ZeroPadToByte(); + // Re-read splines. + BitReader reader(writer.GetSpan()); + Splines decoded_splines; + EXPECT_FALSE(decoded_splines.Decode(&reader, /*num_pixels=*/1000)); + EXPECT_TRUE(reader.Close()); +} + +#ifdef JXL_CRASH_ON_ERROR +TEST(SplinesTest, DISABLED_DuplicatePoints) { +#else +TEST(SplinesTest, DuplicatePoints) { +#endif + std::vector control_points{ + {9, 54}, {118, 159}, {97, 3}, // Repeated. + {97, 3}, {10, 40}, {150, 25}, {120, 300}}; + Spline spline{control_points, + /*color_dct=*/ + {{1.f, 0.2f, 0.1f}, {35.7f, 10.3f}, {35.7f, 7.8f}}, + /*sigma_dct=*/{10.f, 0.f, 0.f, 2.f}}; + std::vector spline_data{spline}; + std::vector quantized_splines; + std::vector starting_points; + for (const Spline& spline : spline_data) { + quantized_splines.emplace_back(spline, kQuantizationAdjustment, kYToX, + kYToB); + starting_points.push_back(spline.control_points.front()); + } + Splines splines(kQuantizationAdjustment, std::move(quantized_splines), + std::move(starting_points)); + + Image3F image(320, 320); + ZeroFillImage(&image); + EXPECT_FALSE( + splines.InitializeDrawCache(image.xsize(), image.ysize(), *cmap)); +} + +TEST(SplinesTest, Drawing) { + CodecInOut io_expected; + const PaddedBytes orig = ReadTestData("jxl/splines.png"); + ASSERT_TRUE(SetFromBytes(Span(orig), &io_expected, + /*pool=*/nullptr)); + + std::vector control_points{{9, 54}, {118, 159}, {97, 3}, + {10, 40}, {150, 25}, {120, 300}}; + const Spline spline{ + control_points, + /*color_dct=*/ + {{0.03125f, 0.00625f, 0.003125f}, {1.f, 0.321875f}, {1.f, 0.24375f}}, + /*sigma_dct=*/{0.3125f, 0.f, 0.f, 0.0625f}}; + std::vector spline_data = {spline}; + std::vector quantized_splines; + std::vector starting_points; + for (const Spline& spline : spline_data) { + quantized_splines.emplace_back(spline, kQuantizationAdjustment, kYToX, + kYToB); + starting_points.push_back(spline.control_points.front()); + } + Splines splines(kQuantizationAdjustment, std::move(quantized_splines), + std::move(starting_points)); + + Image3F image(320, 320); + ZeroFillImage(&image); + ASSERT_TRUE(splines.InitializeDrawCache(image.xsize(), image.ysize(), *cmap)); + splines.AddTo(&image, Rect(image), Rect(image)); + + OpsinParams opsin_params{}; + opsin_params.Init(kDefaultIntensityTarget); + (void)OpsinToLinearInplace(&image, /*pool=*/nullptr, opsin_params); + + CodecInOut io_actual; + io_actual.SetFromImage(CopyImage(image), ColorEncoding::LinearSRGB()); + ASSERT_TRUE(io_actual.TransformTo(io_expected.Main().c_current())); + + VerifyRelativeError(*io_expected.Main().color(), *io_actual.Main().color(), + 1e-2f, 1e-1f); +} + +TEST(SplinesTest, ClearedEveryFrame) { + CodecInOut io_expected; + const PaddedBytes bytes_expected = + ReadTestData("jxl/spline_on_first_frame.png"); + ASSERT_TRUE(SetFromBytes(Span(bytes_expected), &io_expected, + /*pool=*/nullptr)); + CodecInOut io_actual; + const PaddedBytes bytes_actual = + ReadTestData("jxl/spline_on_first_frame.jxl"); + ASSERT_TRUE(DecodeFile(DecompressParams(), bytes_actual, &io_actual, + /*pool=*/nullptr)); + ASSERT_TRUE(io_actual.TransformTo(ColorEncoding::SRGB())); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < io_actual.ysize(); ++y) { + float* const JXL_RESTRICT row = io_actual.Main().color()->PlaneRow(c, y); + for (size_t x = 0; x < io_actual.xsize(); ++x) { + row[x] = Clamp1(row[x], 0.f, 1.f); + } + } + } + VerifyRelativeError(*io_expected.Main().color(), *io_actual.Main().color(), + 1e-2f, 1e-1f); +} + +} // namespace jxl diff --git a/lib/jxl/test_utils.h b/lib/jxl/test_utils.h new file mode 100644 index 0000000..e7e8f67 --- /dev/null +++ b/lib/jxl/test_utils.h @@ -0,0 +1,390 @@ +// 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_TEST_UTILS_H_ +#define LIB_JXL_TEST_UTILS_H_ + +// Macros and functions useful for tests. + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "jxl/codestream_header.h" +#include "jxl/encode.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/codec_in_out.h" +#include "lib/jxl/color_encoding_internal.h" +#include "lib/jxl/common.h" // JPEGXL_ENABLE_TRANSCODE_JPEG +#include "lib/jxl/dec_file.h" +#include "lib/jxl/dec_params.h" +#include "lib/jxl/enc_external_image.h" +#include "lib/jxl/enc_file.h" +#include "lib/jxl/enc_params.h" + +#ifdef JXL_DISABLE_SLOW_TESTS +#define JXL_SLOW_TEST(X) DISABLED_##X +#else +#define JXL_SLOW_TEST(X) X +#endif // JXL_DISABLE_SLOW_TESTS + +#if JPEGXL_ENABLE_TRANSCODE_JPEG +#define JXL_TRANSCODE_JPEG_TEST(X) X +#else +#define JXL_TRANSCODE_JPEG_TEST(X) DISABLED_##X +#endif // JPEGXL_ENABLE_TRANSCODE_JPEG + +#ifdef THREAD_SANITIZER +#define JXL_TSAN_SLOW_TEST(X) DISABLED_##X +#else +#define JXL_TSAN_SLOW_TEST(X) X +#endif // THREAD_SANITIZER + +// googletest before 1.10 didn't define INSTANTIATE_TEST_SUITE_P() but instead +// used INSTANTIATE_TEST_CASE_P which is now deprecated. +#ifdef INSTANTIATE_TEST_SUITE_P +#define JXL_GTEST_INSTANTIATE_TEST_SUITE_P INSTANTIATE_TEST_SUITE_P +#else +#define JXL_GTEST_INSTANTIATE_TEST_SUITE_P INSTANTIATE_TEST_CASE_P +#endif + +namespace jxl { +namespace test { + +void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info, + const JxlPixelFormat* pixel_format) { + JxlEncoderInitBasicInfo(basic_info); + switch (pixel_format->data_type) { + case JXL_TYPE_FLOAT: + basic_info->bits_per_sample = 32; + basic_info->exponent_bits_per_sample = 8; + break; + case JXL_TYPE_FLOAT16: + basic_info->bits_per_sample = 16; + basic_info->exponent_bits_per_sample = 5; + break; + case JXL_TYPE_UINT8: + basic_info->bits_per_sample = 8; + basic_info->exponent_bits_per_sample = 0; + break; + case JXL_TYPE_UINT16: + basic_info->bits_per_sample = 16; + basic_info->exponent_bits_per_sample = 0; + break; + case JXL_TYPE_UINT32: + basic_info->bits_per_sample = 32; + basic_info->exponent_bits_per_sample = 0; + break; + case JXL_TYPE_BOOLEAN: + basic_info->bits_per_sample = 1; + basic_info->exponent_bits_per_sample = 0; + break; + } + if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) { + basic_info->alpha_exponent_bits = 0; + if (basic_info->bits_per_sample == 32) { + basic_info->alpha_bits = 16; + } else { + basic_info->alpha_bits = basic_info->bits_per_sample; + } + } else { + basic_info->alpha_exponent_bits = 0; + basic_info->alpha_bits = 0; + } +} + +MATCHER_P(MatchesPrimariesAndTransferFunction, color_encoding, "") { + return arg.primaries == color_encoding.primaries && + arg.tf.IsSame(color_encoding.tf); +} + +MATCHER(MatchesPrimariesAndTransferFunction, "") { + return testing::ExplainMatchResult( + MatchesPrimariesAndTransferFunction(std::get<1>(arg)), std::get<0>(arg), + result_listener); +} + +// Returns compressed size [bytes]. +size_t Roundtrip(const CodecInOut* io, const CompressParams& cparams, + const DecompressParams& dparams, ThreadPool* pool, + CodecInOut* JXL_RESTRICT io2, AuxOut* aux_out = nullptr) { + PaddedBytes compressed; + + std::vector original_metadata_encodings; + std::vector original_current_encodings; + for (const ImageBundle& ib : io->frames) { + // Remember original encoding, will be returned by decoder. + original_metadata_encodings.push_back(ib.metadata()->color_encoding); + // c_current should not change during encoding. + original_current_encodings.push_back(ib.c_current()); + } + + std::unique_ptr enc_state = + jxl::make_unique(); + EXPECT_TRUE( + EncodeFile(cparams, io, enc_state.get(), &compressed, aux_out, pool)); + + std::vector metadata_encodings_1; + for (const ImageBundle& ib1 : io->frames) { + metadata_encodings_1.push_back(ib1.metadata()->color_encoding); + } + + // Should still be in the same color space after encoding. + EXPECT_THAT(metadata_encodings_1, + testing::Pointwise(MatchesPrimariesAndTransferFunction(), + original_metadata_encodings)); + + EXPECT_TRUE(DecodeFile(dparams, compressed, io2, pool)); + + std::vector metadata_encodings_2; + std::vector current_encodings_2; + for (const ImageBundle& ib2 : io2->frames) { + metadata_encodings_2.push_back(ib2.metadata()->color_encoding); + current_encodings_2.push_back(ib2.c_current()); + } + + EXPECT_THAT(io2->frames, testing::SizeIs(io->frames.size())); + // We always produce the original color encoding if a color transform hook is + // set. + EXPECT_THAT(current_encodings_2, + testing::Pointwise(MatchesPrimariesAndTransferFunction(), + original_current_encodings)); + + // Decoder returns the originals passed to the encoder. + EXPECT_THAT(metadata_encodings_2, + testing::Pointwise(MatchesPrimariesAndTransferFunction(), + original_metadata_encodings)); + + return compressed.size(); +} + +void CoalesceGIFAnimationWithAlpha(CodecInOut* io) { + ImageBundle canvas = io->frames[0].Copy(); + for (size_t i = 1; i < io->frames.size(); i++) { + const ImageBundle& frame = io->frames[i]; + ImageBundle rendered = canvas.Copy(); + for (size_t y = 0; y < frame.ysize(); y++) { + float* row0 = + rendered.color()->PlaneRow(0, frame.origin.y0 + y) + frame.origin.x0; + float* row1 = + rendered.color()->PlaneRow(1, frame.origin.y0 + y) + frame.origin.x0; + float* row2 = + rendered.color()->PlaneRow(2, frame.origin.y0 + y) + frame.origin.x0; + float* rowa = + rendered.alpha()->Row(frame.origin.y0 + y) + frame.origin.x0; + const float* row0f = frame.color().PlaneRow(0, y); + const float* row1f = frame.color().PlaneRow(1, y); + const float* row2f = frame.color().PlaneRow(2, y); + const float* rowaf = frame.alpha().Row(y); + for (size_t x = 0; x < frame.xsize(); x++) { + if (rowaf[x] != 0) { + row0[x] = row0f[x]; + row1[x] = row1f[x]; + row2[x] = row2f[x]; + rowa[x] = rowaf[x]; + } + } + } + if (frame.use_for_next_frame) { + canvas = rendered.Copy(); + } + io->frames[i] = std::move(rendered); + } +} + +// A POD descriptor of a ColorEncoding. Only used in tests as the return value +// of AllEncodings(). +struct ColorEncodingDescriptor { + ColorSpace color_space; + WhitePoint white_point; + Primaries primaries; + TransferFunction tf; + RenderingIntent rendering_intent; +}; + +static inline ColorEncoding ColorEncodingFromDescriptor( + const ColorEncodingDescriptor& desc) { + ColorEncoding c; + c.SetColorSpace(desc.color_space); + c.white_point = desc.white_point; + c.primaries = desc.primaries; + c.tf.SetTransferFunction(desc.tf); + c.rendering_intent = desc.rendering_intent; + return c; +} + +// Define the operator<< for tests. +static inline ::std::ostream& operator<<(::std::ostream& os, + const ColorEncodingDescriptor& c) { + return os << "ColorEncoding/" << Description(ColorEncodingFromDescriptor(c)); +} + +// Returns ColorEncodingDescriptors, which are only used in tests. To obtain a +// ColorEncoding object call ColorEncodingFromDescriptor and then call +// ColorEncoding::CreateProfile() on that object to generate a profile. +std::vector AllEncodings() { + std::vector all_encodings; + all_encodings.reserve(300); + ColorEncoding c; + + for (ColorSpace cs : Values()) { + if (cs == ColorSpace::kUnknown || cs == ColorSpace::kXYB) continue; + c.SetColorSpace(cs); + + for (WhitePoint wp : Values()) { + if (wp == WhitePoint::kCustom) continue; + if (c.ImplicitWhitePoint() && c.white_point != wp) continue; + c.white_point = wp; + + for (Primaries primaries : Values()) { + if (primaries == Primaries::kCustom) continue; + if (!c.HasPrimaries()) continue; + c.primaries = primaries; + + for (TransferFunction tf : Values()) { + if (tf == TransferFunction::kUnknown) continue; + if (c.tf.SetImplicit() && + (c.tf.IsGamma() || c.tf.GetTransferFunction() != tf)) { + continue; + } + c.tf.SetTransferFunction(tf); + + for (RenderingIntent ri : Values()) { + ColorEncodingDescriptor cdesc; + cdesc.color_space = cs; + cdesc.white_point = wp; + cdesc.primaries = primaries; + cdesc.tf = tf; + cdesc.rendering_intent = ri; + all_encodings.push_back(cdesc); + } + } + } + } + } + + return all_encodings; +} + +// Returns a test image with some autogenerated pixel content, using 16 bits per +// channel, big endian order, 1 to 4 channels +// The seed parameter allows to create images with different pixel content. +std::vector GetSomeTestImage(size_t xsize, size_t ysize, + size_t num_channels, uint16_t seed) { + // Cause more significant image difference for successive seeds. + std::mt19937 std_rng(seed); + std::uniform_int_distribution std_distr(0, 65535); + + // Returns random integer in interval (0, max_value - 1) + auto rng = [&std_rng, &std_distr](size_t max_value) -> size_t { + return static_cast(std_distr(std_rng) / 65536.0f * max_value); + }; + + // Dark background gradient color + uint16_t r0 = rng(32768); + uint16_t g0 = rng(32768); + uint16_t b0 = rng(32768); + uint16_t a0 = rng(32768); + uint16_t r1 = rng(32768); + uint16_t g1 = rng(32768); + uint16_t b1 = rng(32768); + uint16_t a1 = rng(32768); + + // Circle with different color + size_t circle_x = rng(xsize); + size_t circle_y = rng(ysize); + size_t circle_r = rng(std::min(xsize, ysize)); + + // Rectangle with random noise + size_t rect_x0 = rng(xsize); + size_t rect_y0 = rng(ysize); + size_t rect_x1 = rng(xsize); + size_t rect_y1 = rng(ysize); + if (rect_x1 < rect_x0) std::swap(rect_x0, rect_y1); + if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1); + + size_t num_pixels = xsize * ysize; + // 16 bits per channel, big endian, 4 channels + std::vector pixels(num_pixels * num_channels * 2); + // Create pixel content to test, actual content does not matter as long as it + // can be compared after roundtrip. + for (size_t y = 0; y < ysize; y++) { + for (size_t x = 0; x < xsize; x++) { + uint16_t r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize; + uint16_t g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize; + uint16_t b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize; + uint16_t a = a0 * (ysize - y - 1) / ysize + a1 * y / ysize; + // put some shape in there for visual debugging + if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) < + circle_r * circle_r) { + r = (65535 - x * y) ^ seed; + g = (x << 8) + y + seed; + b = (y << 8) + x * seed; + a = 32768 + x * 256 - y; + } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) { + r = rng(65536); + g = rng(65536); + b = rng(65536); + a = rng(65536); + } + size_t i = (y * xsize + x) * 2 * num_channels; + pixels[i + 0] = (r >> 8); + pixels[i + 1] = (r & 255); + if (num_channels >= 2) { + // This may store what is called 'g' in the alpha channel of a 2-channel + // image, but that's ok since the content is arbitrary + pixels[i + 2] = (g >> 8); + pixels[i + 3] = (g & 255); + } + if (num_channels >= 3) { + pixels[i + 4] = (b >> 8); + pixels[i + 5] = (b & 255); + } + if (num_channels >= 4) { + pixels[i + 6] = (a >> 8); + pixels[i + 7] = (a & 255); + } + } + } + return pixels; +} + +// Returns a CodecInOut based on the buf, xsize, ysize, and the assumption +// that the buffer was created using `GetSomeTestImage`. +jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector& buf, + size_t num_channels, size_t xsize, + size_t ysize) { + jxl::CodecInOut io; + io.SetSize(xsize, ysize); + io.metadata.m.SetAlphaBits(16); + io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB( + /*is_gray=*/num_channels == 1 || num_channels == 2); + EXPECT_TRUE(ConvertFromExternal( + jxl::Span(buf.data(), buf.size()), xsize, ysize, + jxl::ColorEncoding::SRGB(/*is_gray=*/num_channels == 1 || + num_channels == 2), + /*has_alpha=*/num_channels == 2 || num_channels == 4, + /*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN, + /*flipped_y=*/false, /*pool=*/nullptr, + /*ib=*/&io.Main(), /*float_in=*/false)); + return io; +} + +} // namespace test + +bool operator==(const jxl::PaddedBytes& a, const jxl::PaddedBytes& b) { + if (a.size() != b.size()) return false; + if (memcmp(a.data(), b.data(), a.size()) != 0) return false; + return true; +} + +// Allow using EXPECT_EQ on jxl::PaddedBytes +bool operator!=(const jxl::PaddedBytes& a, const jxl::PaddedBytes& b) { + return !(a == b); +} +} // namespace jxl + +#endif // LIB_JXL_TEST_UTILS_H_ diff --git a/lib/jxl/testdata.h b/lib/jxl/testdata.h new file mode 100644 index 0000000..28d1015 --- /dev/null +++ b/lib/jxl/testdata.h @@ -0,0 +1,60 @@ +// 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_TESTDATA_H_ +#define LIB_JXL_TESTDATA_H_ + +#ifdef __EMSCRIPTEN__ +#include +#endif + +#include + +#include "lib/jxl/base/file_io.h" + +namespace jxl { + +static inline PaddedBytes ReadTestData(const std::string& filename) { + std::string full_path = std::string(TEST_DATA_PATH "/") + filename; + PaddedBytes data; + bool ok = ReadFile(full_path, &data); +#ifdef __EMSCRIPTEN__ + // Fallback in case FS is not supported in current JS engine. + if (!ok) { + // {size_t size, uint8_t* bytes} pair. + uint32_t size_bytes[2] = {0, 0}; + EM_ASM( + { + let buffer = null; + try { + buffer = readbuffer(UTF8ToString($0)); + } catch { + } + if (!buffer) return; + let bytes = new Uint8Array(buffer); + let size = bytes.length; + let out = _malloc(size); + if (!out) return; + HEAP8.set(bytes, out); + HEAP32[$1 >> 2] = size; + HEAP32[($1 + 4) >> 2] = out; + }, + full_path.c_str(), size_bytes); + size_t size = size_bytes[0]; + uint8_t* bytes = reinterpret_cast(size_bytes[1]); + if (size) { + data.append(bytes, bytes + size); + free(reinterpret_cast(bytes)); + ok = true; + } + } +#endif + JXL_CHECK(ok); + return data; +} + +} // namespace jxl + +#endif // LIB_JXL_TESTDATA_H_ diff --git a/lib/jxl/tf_gbench.cc b/lib/jxl/tf_gbench.cc new file mode 100644 index 0000000..9c010d4 --- /dev/null +++ b/lib/jxl/tf_gbench.cc @@ -0,0 +1,143 @@ +// 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 "benchmark/benchmark.h" +#include "lib/jxl/image_ops.h" + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/tf_gbench.cc" +#include +#include + +#include "lib/jxl/transfer_functions-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +#define RUN_BENCHMARK(F) \ + constexpr size_t kNum = 1 << 12; \ + HWY_FULL(float) d; \ + /* Three parallel runs, as this will run on R, G and B. */ \ + auto sum1 = Zero(d); \ + auto sum2 = Zero(d); \ + auto sum3 = Zero(d); \ + for (auto _ : state) { \ + auto x = Set(d, 1e-5); \ + auto v1 = Set(d, 1e-5); \ + auto v2 = Set(d, 1.1e-5); \ + auto v3 = Set(d, 1.2e-5); \ + for (size_t i = 0; i < kNum; i++) { \ + sum1 += F(d, v1); \ + sum2 += F(d, v2); \ + sum3 += F(d, v3); \ + v1 += x; \ + v2 += x; \ + v3 += x; \ + } \ + } \ + /* floats per second */ \ + state.SetItemsProcessed(kNum* state.iterations() * Lanes(d) * 3); \ + benchmark::DoNotOptimize(sum1 + sum2 + sum3); + +#define RUN_BENCHMARK_SCALAR(F) \ + constexpr size_t kNum = 1 << 12; \ + /* Three parallel runs, as this will run on R, G and B. */ \ + float sum1 = 0, sum2 = 0, sum3 = 0; \ + for (auto _ : state) { \ + float x = 1e-5; \ + float v1 = 1e-5; \ + float v2 = 1.1e-5; \ + float v3 = 1.2e-5; \ + for (size_t i = 0; i < kNum; i++) { \ + sum1 += F(v1); \ + sum2 += F(v2); \ + sum3 += F(v3); \ + v1 += x; \ + v2 += x; \ + v3 += x; \ + } \ + } \ + /* floats per second */ \ + state.SetItemsProcessed(kNum* state.iterations() * 3); \ + benchmark::DoNotOptimize(sum1 + sum2 + sum3); + +HWY_NOINLINE void BM_FastSRGB(benchmark::State& state) { + RUN_BENCHMARK(FastLinearToSRGB); +} + +HWY_NOINLINE void BM_TFSRGB(benchmark::State& state) { + RUN_BENCHMARK(TF_SRGB().EncodedFromDisplay); +} + +HWY_NOINLINE void BM_PQDFE(benchmark::State& state) { + RUN_BENCHMARK(TF_PQ().DisplayFromEncoded); +} + +HWY_NOINLINE void BM_PQEFD(benchmark::State& state) { + RUN_BENCHMARK(TF_PQ().EncodedFromDisplay); +} + +HWY_NOINLINE void BM_PQSlowDFE(benchmark::State& state) { + RUN_BENCHMARK_SCALAR(TF_PQ().DisplayFromEncoded); +} + +HWY_NOINLINE void BM_PQSlowEFD(benchmark::State& state) { + RUN_BENCHMARK_SCALAR(TF_PQ().EncodedFromDisplay); +} +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { +namespace { + +HWY_EXPORT(BM_FastSRGB); +HWY_EXPORT(BM_TFSRGB); +HWY_EXPORT(BM_PQDFE); +HWY_EXPORT(BM_PQEFD); +HWY_EXPORT(BM_PQSlowDFE); +HWY_EXPORT(BM_PQSlowEFD); + +float SRGB_pow(float x) { + return x < 0.0031308f ? 12.92f * x : 1.055f * powf(x, 1.0f / 2.4f) - 0.055f; +} + +void BM_FastSRGB(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_FastSRGB)(state); +} +void BM_TFSRGB(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_TFSRGB)(state); +} +void BM_PQDFE(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_PQDFE)(state); +} +void BM_PQEFD(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_PQEFD)(state); +} +void BM_PQSlowDFE(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_PQSlowDFE)(state); +} +void BM_PQSlowEFD(benchmark::State& state) { + HWY_DYNAMIC_DISPATCH(BM_PQSlowEFD)(state); +} + +void BM_SRGB_pow(benchmark::State& state) { RUN_BENCHMARK_SCALAR(SRGB_pow); } + +BENCHMARK(BM_FastSRGB); +BENCHMARK(BM_TFSRGB); +BENCHMARK(BM_SRGB_pow); +BENCHMARK(BM_PQDFE); +BENCHMARK(BM_PQEFD); +BENCHMARK(BM_PQSlowDFE); +BENCHMARK(BM_PQSlowEFD); + +} // namespace +} // namespace jxl +#endif diff --git a/lib/jxl/toc.cc b/lib/jxl/toc.cc new file mode 100644 index 0000000..3a2193e --- /dev/null +++ b/lib/jxl/toc.cc @@ -0,0 +1,97 @@ +// 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/toc.h" + +#include + +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/coeff_order.h" +#include "lib/jxl/coeff_order_fwd.h" +#include "lib/jxl/common.h" +#include "lib/jxl/fields.h" + +namespace jxl { +size_t MaxBits(const size_t num_sizes) { + const size_t entry_bits = U32Coder::MaxEncodedBits(kTocDist) * num_sizes; + // permutation bit (not its tokens!), padding, entries, padding. + return 1 + kBitsPerByte + entry_bits + kBitsPerByte; +} + +Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, + std::vector* JXL_RESTRICT offsets, + std::vector* JXL_RESTRICT sizes, + uint64_t* total_size) { + if (toc_entries > 65536) { + // Prevent out of memory if invalid JXL codestream causes a bogus amount + // of toc_entries such as 2720436919446 to be computed. + // TODO(lode): verify whether 65536 is a reasonable upper bound + return JXL_FAILURE("too many toc entries"); + } + + const auto check_bit_budget = [&](size_t num_entries) -> Status { + // U32Coder reads 2 bits to recognize variant and kTocDist cheapest variant + // is Bits(10), this way at least 12 bits are required per toc-entry. + size_t minimal_bit_cost = num_entries * (2 + 10); + size_t bit_budget = reader->TotalBytes() * 8; + size_t expenses = reader->TotalBitsConsumed(); + if ((expenses <= bit_budget) && + (minimal_bit_cost <= bit_budget - expenses)) { + return true; + } + return JXL_STATUS(StatusCode::kNotEnoughBytes, "Not enough bytes for TOC"); + }; + + JXL_DASSERT(offsets != nullptr && sizes != nullptr); + std::vector permutation; + if (reader->ReadFixedBits<1>() == 1 && toc_entries > 0) { + // Skip permutation description if the toc_entries is 0. + JXL_RETURN_IF_ERROR(check_bit_budget(toc_entries)); + permutation.resize(toc_entries); + JXL_RETURN_IF_ERROR( + DecodePermutation(/*skip=*/0, toc_entries, permutation.data(), reader)); + } + + JXL_RETURN_IF_ERROR(reader->JumpToByteBoundary()); + JXL_RETURN_IF_ERROR(check_bit_budget(toc_entries)); + sizes->clear(); + sizes->reserve(toc_entries); + for (size_t i = 0; i < toc_entries; ++i) { + sizes->push_back(U32Coder::Read(kTocDist, reader)); + } + JXL_RETURN_IF_ERROR(reader->JumpToByteBoundary()); + JXL_RETURN_IF_ERROR(check_bit_budget(0)); + + // Prefix sum starting with 0 and ending with the offset of the last group + offsets->clear(); + offsets->reserve(toc_entries); + uint64_t offset = 0; + for (size_t i = 0; i < toc_entries; ++i) { + if (offset + (*sizes)[i] < offset) { + return JXL_FAILURE("group offset overflow"); + } + offsets->push_back(offset); + offset += (*sizes)[i]; + } + if (total_size) { + *total_size = offset; + } + + if (!permutation.empty()) { + std::vector permuted_offsets; + std::vector permuted_sizes; + permuted_offsets.reserve(toc_entries); + permuted_sizes.reserve(toc_entries); + for (coeff_order_t index : permutation) { + permuted_offsets.push_back((*offsets)[index]); + permuted_sizes.push_back((*sizes)[index]); + } + std::swap(*offsets, permuted_offsets); + std::swap(*sizes, permuted_sizes); + } + + return true; +} +} // namespace jxl diff --git a/lib/jxl/toc.h b/lib/jxl/toc.h new file mode 100644 index 0000000..ffebdf9 --- /dev/null +++ b/lib/jxl/toc.h @@ -0,0 +1,50 @@ +// 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_TOC_H_ +#define LIB_JXL_TOC_H_ + +#include +#include + +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/dec_bit_reader.h" +#include "lib/jxl/field_encodings.h" + +namespace jxl { + +// (2+bits) = 2,3,4 bytes so encoders can patch TOC after encoding. +// 30 is sufficient for 4K channels of uncompressed 16-bit samples. +constexpr U32Enc kTocDist(Bits(10), BitsOffset(14, 1024), BitsOffset(22, 17408), + BitsOffset(30, 4211712)); + +size_t MaxBits(const size_t num_sizes); + +// TODO(veluca): move these to FrameDimensions. +static JXL_INLINE size_t AcGroupIndex(size_t pass, size_t group, + size_t num_groups, size_t num_dc_groups, + bool has_ac_global) { + return 1 + num_dc_groups + static_cast(has_ac_global) + + pass * num_groups + group; +} + +static JXL_INLINE size_t NumTocEntries(size_t num_groups, size_t num_dc_groups, + size_t num_passes, bool has_ac_global) { + if (num_groups == 1 && num_passes == 1) return 1; + return AcGroupIndex(0, 0, num_groups, num_dc_groups, has_ac_global) + + num_groups * num_passes; +} + +Status ReadGroupOffsets(size_t toc_entries, BitReader* JXL_RESTRICT reader, + std::vector* JXL_RESTRICT offsets, + std::vector* JXL_RESTRICT sizes, + uint64_t* total_size); + +} // namespace jxl + +#endif // LIB_JXL_TOC_H_ diff --git a/lib/jxl/toc_test.cc b/lib/jxl/toc_test.cc new file mode 100644 index 0000000..e2f6c9e --- /dev/null +++ b/lib/jxl/toc_test.cc @@ -0,0 +1,93 @@ +// 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/toc.h" + +#include + +#include "gtest/gtest.h" +#include "lib/jxl/aux_out_fwd.h" +#include "lib/jxl/base/span.h" +#include "lib/jxl/common.h" +#include "lib/jxl/enc_toc.h" + +namespace jxl { +namespace { + +void Roundtrip(size_t num_entries, bool permute, std::mt19937* rng) { + // Generate a random permutation. + std::vector permutation(num_entries); + std::vector inv_permutation(num_entries); + for (size_t i = 0; i < num_entries; i++) { + permutation[i] = i; + inv_permutation[i] = i; + } + if (permute) { + std::shuffle(permutation.begin(), permutation.end(), *rng); + for (size_t i = 0; i < num_entries; i++) { + inv_permutation[permutation[i]] = i; + } + } + + // Generate num_entries groups of random (byte-aligned) length + std::vector group_codes(num_entries); + for (BitWriter& writer : group_codes) { + const size_t max_bits = (*rng)() & 0xFFF; + BitWriter::Allotment allotment(&writer, max_bits + kBitsPerByte); + size_t i = 0; + for (; i + BitWriter::kMaxBitsPerCall < max_bits; + i += BitWriter::kMaxBitsPerCall) { + writer.Write(BitWriter::kMaxBitsPerCall, 0); + } + for (; i < max_bits; i += 1) { + writer.Write(/*n_bits=*/1, 0); + } + writer.ZeroPadToByte(); + AuxOut aux_out; + ReclaimAndCharge(&writer, &allotment, 0, &aux_out); + } + + BitWriter writer; + AuxOut aux_out; + ASSERT_TRUE(WriteGroupOffsets(group_codes, permute ? &permutation : nullptr, + &writer, &aux_out)); + + BitReader reader(writer.GetSpan()); + std::vector group_offsets; + std::vector group_sizes; + uint64_t total_size; + ASSERT_TRUE(ReadGroupOffsets(num_entries, &reader, &group_offsets, + &group_sizes, &total_size)); + ASSERT_EQ(num_entries, group_offsets.size()); + ASSERT_EQ(num_entries, group_sizes.size()); + EXPECT_TRUE(reader.Close()); + + uint64_t prefix_sum = 0; + for (size_t i = 0; i < num_entries; ++i) { + EXPECT_EQ(prefix_sum, group_offsets[inv_permutation[i]]); + + EXPECT_EQ(0u, group_codes[i].BitsWritten() % kBitsPerByte); + prefix_sum += group_codes[i].BitsWritten() / kBitsPerByte; + + if (i + 1 < num_entries) { + EXPECT_EQ( + group_offsets[inv_permutation[i]] + group_sizes[inv_permutation[i]], + group_offsets[inv_permutation[i + 1]]); + } + } + EXPECT_EQ(prefix_sum, total_size); +} + +TEST(TocTest, Test) { + std::mt19937 rng(12345); + for (size_t num_entries = 0; num_entries < 10; ++num_entries) { + for (bool permute : std::vector{false, true}) { + Roundtrip(num_entries, permute, &rng); + } + } +} + +} // namespace +} // namespace jxl diff --git a/lib/jxl/transfer_functions-inl.h b/lib/jxl/transfer_functions-inl.h new file mode 100644 index 0000000..43069ac --- /dev/null +++ b/lib/jxl/transfer_functions-inl.h @@ -0,0 +1,397 @@ +// 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. + +// Transfer functions for color encodings. + +#if defined(LIB_JXL_TRANSFER_FUNCTIONS_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_TRANSFER_FUNCTIONS_INL_H_ +#undef LIB_JXL_TRANSFER_FUNCTIONS_INL_H_ +#else +#define LIB_JXL_TRANSFER_FUNCTIONS_INL_H_ +#endif + +#include +#include +#include + +#include "lib/jxl/base/compiler_specific.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/rational_polynomial-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// Definitions for BT.2100-2 transfer functions (used inside/outside SIMD): +// "display" is linear light (nits) normalized to [0, 1]. +// "encoded" is a nonlinear encoding (e.g. PQ) in [0, 1]. +// "scene" is a linear function of photon counts, normalized to [0, 1]. + +// Despite the stated ranges, we need unbounded transfer functions: see +// http://www.littlecms.com/CIC18_UnboundedCMM.pdf. Inputs can be negative or +// above 1 due to chromatic adaptation. To avoid severe round-trip errors caused +// by clamping, we mirror negative inputs via copysign (f(-x) = -f(x), see +// https://developer.apple.com/documentation/coregraphics/cgcolorspace/1644735-extendedsrgb) +// and extend the function domains above 1. + +// Hybrid Log-Gamma. +class TF_HLG { + public: + // EOTF. e = encoded. + JXL_INLINE double DisplayFromEncoded(const double e) const { + const double lifted = e * (1.0 - kBeta) + kBeta; + return OOTF(InvOETF(lifted)); + } + + // Inverse EOTF. d = display. + JXL_INLINE double EncodedFromDisplay(const double d) const { + const double lifted = OETF(InvOOTF(d)); + const double e = (lifted - kBeta) * (1.0 / (1.0 - kBeta)); + return e; + } + + // Maximum error 5e-7. + template + JXL_INLINE V EncodedFromDisplay(D d, V x) const { + const hwy::HWY_NAMESPACE::Rebind du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = And(x, kSign); + x = AndNot(kSign, x); // abs + const V below_div12 = Sqrt(Set(d, 3.0f) * x); + const V e = + MulAdd(Set(d, kA * 0.693147181f), + FastLog2f(d, MulAdd(Set(d, 12), x, Set(d, -kB))), Set(d, kC)); + const V magnitude = IfThenElse(x <= Set(d, kDiv12), below_div12, e); + const V lifted = Or(AndNot(kSign, magnitude), original_sign); + const V kMul = Set(d, 1.0f / (1.0f - kBeta)); + const V kAdd = Set(d, -kBeta / (1.0f - kBeta)); + return MulAdd(kMul, lifted, kAdd); + } + + private: + // OETF (defines the HLG approach). s = scene, returns encoded. + JXL_INLINE double OETF(double s) const { + if (s == 0.0) return 0.0; + const double original_sign = s; + s = std::abs(s); + + if (s <= kDiv12) return copysignf(std::sqrt(3.0 * s), original_sign); + + const double e = kA * std::log(12 * s - kB) + kC; + JXL_ASSERT(e > 0.0); + return copysignf(e, original_sign); + } + + // e = encoded, returns scene. + JXL_INLINE double InvOETF(double e) const { + if (e == 0.0) return 0.0; + const double original_sign = e; + e = std::abs(e); + + if (e <= 0.5) return copysignf(e * e * (1.0 / 3), original_sign); + + const double s = (std::exp((e - kC) * kRA) + kB) * kDiv12; + JXL_ASSERT(s >= 0); + return copysignf(s, original_sign); + } + + // s = scene, returns display. + JXL_INLINE double OOTF(const double s) const { + // The actual (red channel) OOTF is RD = alpha * YS^(gamma-1) * RS, where + // YS = 0.2627 * RS + 0.6780 * GS + 0.0593 * BS. Let alpha = 1 so we return + // "display" (normalized [0, 1]) instead of nits. Our transfer function + // interface does not allow a dependency on YS. Fortunately, the system + // gamma at 334 nits is 1.0, so this reduces to RD = RS. + return s; + } + + // d = display, returns scene. + JXL_INLINE double InvOOTF(const double d) const { + return d; // see OOTF(). + } + + // Assume 1000:1 contrast @ 200 nits => gamma 0.9 + static constexpr double kBeta = 0.04; // = sqrt(3 * contrast^(1/gamma)) + + static constexpr double kA = 0.17883277; + static constexpr double kRA = 1.0 / kA; + static constexpr double kB = 1 - 4 * kA; + static constexpr double kC = 0.5599107295; + static constexpr double kDiv12 = 1.0 / 12; +}; + +class TF_709 { + public: + JXL_INLINE double EncodedFromDisplay(const double d) const { + if (d < kThresh) return kMulLow * d; + return kMulHi * std::pow(d, kPowHi) + kSub; + } + + // Maximum error 1e-6. + template + JXL_INLINE V EncodedFromDisplay(D d, V x) const { + auto low = Set(d, kMulLow) * x; + auto hi = + MulAdd(Set(d, kMulHi), FastPowf(d, x, Set(d, kPowHi)), Set(d, kSub)); + return IfThenElse(x <= Set(d, kThresh), low, hi); + } + + private: + static constexpr double kThresh = 0.018; + static constexpr double kMulLow = 4.5; + static constexpr double kMulHi = 1.099; + static constexpr double kPowHi = 0.45; + static constexpr double kSub = -0.099; +}; + +// Perceptual Quantization +class TF_PQ { + public: + // EOTF (defines the PQ approach). e = encoded. + JXL_INLINE double DisplayFromEncoded(double e) const { + if (e == 0.0) return 0.0; + const double original_sign = e; + e = std::abs(e); + + const double xp = std::pow(e, 1.0 / kM2); + const double num = std::max(xp - kC1, 0.0); + const double den = kC2 - kC3 * xp; + JXL_DASSERT(den != 0.0); + const double d = std::pow(num / den, 1.0 / kM1); + JXL_DASSERT(d >= 0.0); // Equal for e ~= 1E-9 + return copysignf(d, original_sign); + } + + // Maximum error 3e-6 + template + JXL_INLINE V DisplayFromEncoded(D d, V x) const { + const hwy::HWY_NAMESPACE::Rebind du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = And(x, kSign); + x = AndNot(kSign, x); // abs + // 4-over-4-degree rational polynomial approximation on x+x*x. This improves + // the maximum error by about 5x over a rational polynomial for x. + auto xpxx = MulAdd(x, x, x); + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + HWY_REP4(2.62975656e-04f), HWY_REP4(-6.23553089e-03f), + HWY_REP4(7.38602301e-01f), HWY_REP4(2.64553172e+00f), + HWY_REP4(5.50034862e-01f), + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + HWY_REP4(4.21350107e+02f), HWY_REP4(-4.28736818e+02f), + HWY_REP4(1.74364667e+02f), HWY_REP4(-3.39078883e+01f), + HWY_REP4(2.67718770e+00f), + }; + auto magnitude = EvalRationalPolynomial(d, xpxx, p, q); + return Or(AndNot(kSign, magnitude), original_sign); + } + + // Inverse EOTF. d = display. + JXL_INLINE double EncodedFromDisplay(double d) const { + if (d == 0.0) return 0.0; + const double original_sign = d; + d = std::abs(d); + + const double xp = std::pow(d, kM1); + const double num = kC1 + xp * kC2; + const double den = 1.0 + xp * kC3; + const double e = std::pow(num / den, kM2); + JXL_DASSERT(e > 0.0); + return copysignf(e, original_sign); + } + + // Maximum error 7e-7. + template + JXL_INLINE V EncodedFromDisplay(D d, V x) const { + const hwy::HWY_NAMESPACE::Rebind du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = And(x, kSign); + x = AndNot(kSign, x); // abs + // 4-over-4-degree rational polynomial approximation on x**0.25, with two + // different polynomials above and below 1e-4. + auto xto025 = Sqrt(Sqrt(x)); + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + HWY_REP4(1.351392e-02f), HWY_REP4(-1.095778e+00f), + HWY_REP4(5.522776e+01f), HWY_REP4(1.492516e+02f), + HWY_REP4(4.838434e+01f), + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + HWY_REP4(1.012416e+00f), HWY_REP4(2.016708e+01f), + HWY_REP4(9.263710e+01f), HWY_REP4(1.120607e+02f), + HWY_REP4(2.590418e+01f), + }; + + HWY_ALIGN constexpr float plo[(4 + 1) * 4] = { + HWY_REP4(9.863406e-06f), HWY_REP4(3.881234e-01f), + HWY_REP4(1.352821e+02f), HWY_REP4(6.889862e+04f), + HWY_REP4(-2.864824e+05f), + }; + HWY_ALIGN constexpr float qlo[(4 + 1) * 4] = { + HWY_REP4(3.371868e+01f), HWY_REP4(1.477719e+03f), + HWY_REP4(1.608477e+04f), HWY_REP4(-4.389884e+04f), + HWY_REP4(-2.072546e+05f), + }; + + auto magnitude = IfThenElse(x < Set(d, 1e-4f), + EvalRationalPolynomial(d, xto025, plo, qlo), + EvalRationalPolynomial(d, xto025, p, q)); + return Or(AndNot(kSign, magnitude), original_sign); + } + + private: + static constexpr double kM1 = 2610.0 / 16384; + static constexpr double kM2 = (2523.0 / 4096) * 128; + static constexpr double kC1 = 3424.0 / 4096; + static constexpr double kC2 = (2413.0 / 4096) * 32; + static constexpr double kC3 = (2392.0 / 4096) * 32; +}; + +// sRGB +class TF_SRGB { + public: + template + JXL_INLINE V DisplayFromEncoded(V x) const { + const HWY_FULL(float) d; + const HWY_FULL(uint32_t) du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = And(x, kSign); + x = AndNot(kSign, x); // abs + + // TODO(janwas): range reduction + // Computed via af_cheb_rational (k=100); replicated 4x. + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, 2.200248328e-04f, + 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, 1.043637593e-02f, + 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, 1.624820318e-01f, + 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, 7.961564959e-01f, + 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f, 8.210152774e-01f, + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + 2.631846970e-01f, 2.631846970e-01f, 2.631846970e-01f, + 2.631846970e-01f, 1.076976492e+00f, 1.076976492e+00f, + 1.076976492e+00f, 1.076976492e+00f, 4.987528350e-01f, + 4.987528350e-01f, 4.987528350e-01f, 4.987528350e-01f, + -5.512498495e-02f, -5.512498495e-02f, -5.512498495e-02f, + -5.512498495e-02f, 6.521209011e-03f, 6.521209011e-03f, + 6.521209011e-03f, 6.521209011e-03f, + }; + const V linear = x * Set(d, kLowDivInv); + const V poly = EvalRationalPolynomial(d, x, p, q); + const V magnitude = + IfThenElse(x > Set(d, kThreshSRGBToLinear), poly, linear); + return Or(AndNot(kSign, magnitude), original_sign); + } + + // Error ~5e-07 + template + JXL_INLINE V EncodedFromDisplay(D d, V x) const { + const hwy::HWY_NAMESPACE::Rebind du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = And(x, kSign); + x = AndNot(kSign, x); // abs + + // Computed via af_cheb_rational (k=100); replicated 4x. + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + -5.135152395e-04f, -5.135152395e-04f, -5.135152395e-04f, + -5.135152395e-04f, 5.287254571e-03f, 5.287254571e-03f, + 5.287254571e-03f, 5.287254571e-03f, 3.903842876e-01f, + 3.903842876e-01f, 3.903842876e-01f, 3.903842876e-01f, + 1.474205315e+00f, 1.474205315e+00f, 1.474205315e+00f, + 1.474205315e+00f, 7.352629620e-01f, 7.352629620e-01f, + 7.352629620e-01f, 7.352629620e-01f, + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, 1.004519624e-02f, + 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, 3.036675394e-01f, + 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, 1.340816930e+00f, + 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, 9.258482155e-01f, + 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, 2.424867759e-02f, + }; + const V linear = x * Set(d, kLowDiv); + const V poly = EvalRationalPolynomial(d, Sqrt(x), p, q); + const V magnitude = + IfThenElse(x > Set(d, kThreshLinearToSRGB), poly, linear); + return Or(AndNot(kSign, magnitude), original_sign); + } + + private: + static constexpr float kThreshSRGBToLinear = 0.04045f; + static constexpr float kThreshLinearToSRGB = 0.0031308f; + static constexpr float kLowDiv = 12.92f; + static constexpr float kLowDivInv = 1.0f / kLowDiv; +}; + +// Linear to sRGB conversion with error of at most 1.2e-4. +template +V FastLinearToSRGB(D d, V v) { + const hwy::HWY_NAMESPACE::Rebind du; + const hwy::HWY_NAMESPACE::Rebind di; + // Convert to 0.25 - 0.5 range. + auto v025_05 = + BitCast(d, (BitCast(du, v) | Set(du, 0x3e800000)) & Set(du, 0x3effffff)); + // third degree polynomial approximation between 0.25 and 0.5 + // of 1.055/2^(7/2.4) * x^(1/2.4) * 0.5. A degree 4 polynomial only improves + // accuracy by about 3x. + auto d1 = MulAdd(v025_05, Set(d, 0.059914046f), Set(d, -0.108894556f)); + auto d2 = MulAdd(d1, v025_05, Set(d, 0.107963754f)); + auto pow = MulAdd(d2, v025_05, Set(d, 0.018092343f)); + // Compute extra multiplier depending on exponent. Valid exponent range for + // [0.0031308f, 1.0) is 0...8 after subtracting 118. + // The next three constants contain a representation of the powers of + // 2**(1/2.4) = 2**(5/12) times two; in particular, bits from 26 to 31 are + // always the same and in k2to512powers_basebits, and the two arrays contain + // the next groups of 8 bits. This ends up being a 22-bit representation (with + // a mantissa of 13 bits). The choice of polynomial to approximate is such + // that the multiplication factor has the highest 5 bits constant, and that + // the factor for the lowest possible exponent is a power of two (thus making + // the additional bits 0, which is used to correctly merge back together the + // floats). + constexpr uint32_t k2to512powers_basebits = 0x40000000; + HWY_ALIGN constexpr uint8_t k2to512powers_25to18bits[16] = { + 0x0, 0xa, 0x19, 0x26, 0x32, 0x41, 0x4d, 0x5c, + 0x68, 0x75, 0x83, 0x8f, 0xa0, 0xaa, 0xb9, 0xc6, + }; + HWY_ALIGN constexpr uint8_t k2to512powers_17to10bits[16] = { + 0x0, 0xb7, 0x4, 0xd, 0xcb, 0xe7, 0x41, 0x68, + 0x51, 0xd1, 0xeb, 0xf2, 0x0, 0xb7, 0x4, 0xd, + }; + // Note that vld1q_s8_x2 on ARM seems to actually be slower. +#if HWY_TARGET != HWY_SCALAR + using hwy::HWY_NAMESPACE::ShiftLeft; + using hwy::HWY_NAMESPACE::ShiftRight; + // Every lane of exp is now (if cast to byte) {0, 0, 0, }. + auto exp = ShiftRight<23>(BitCast(di, v)) - Set(di, 118); + auto pow25to18bits = TableLookupBytes( + LoadDup128(di, + reinterpret_cast(k2to512powers_25to18bits)), + exp); + auto pow17to10bits = TableLookupBytes( + LoadDup128(di, + reinterpret_cast(k2to512powers_17to10bits)), + exp); + // Now, pow* contain {0, 0, 0, }. Here + // we take advantage of the fact that each table has its position 0 equal to + // 0. + // We can now just reassemble the float. + auto mul = + BitCast(d, ShiftLeft<18>(pow25to18bits) | ShiftLeft<10>(pow17to10bits) | + Set(di, k2to512powers_basebits)); +#else + // Fallback for scalar. + uint32_t exp = ((BitCast(di, v).raw >> 23) - 118) & 0xf; + auto mul = BitCast(d, Set(di, (k2to512powers_25to18bits[exp] << 18) | + (k2to512powers_17to10bits[exp] << 10) | + k2to512powers_basebits)); +#endif + return IfThenElse(v < Set(d, 0.0031308f), v * Set(d, 12.92f), + MulAdd(pow, mul, Set(d, -0.055))); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_TRANSFER_FUNCTIONS_INL_H_ diff --git a/lib/jxl/transpose-inl.h b/lib/jxl/transpose-inl.h new file mode 100644 index 0000000..d12b129 --- /dev/null +++ b/lib/jxl/transpose-inl.h @@ -0,0 +1,201 @@ +// 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. + +// Block transpose for DCT/IDCT + +#if defined(LIB_JXL_TRANSPOSE_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_TRANSPOSE_INL_H_ +#undef LIB_JXL_TRANSPOSE_INL_H_ +#else +#define LIB_JXL_TRANSPOSE_INL_H_ +#endif + +#include + +#include +#include + +#include "lib/jxl/base/status.h" +#include "lib/jxl/dct_block-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +#ifndef JXL_INLINE_TRANSPOSE +// Workaround for issue #42 - (excessive?) inlining causes invalid codegen. +#if defined(__arm__) +#define JXL_INLINE_TRANSPOSE HWY_NOINLINE +#else +#define JXL_INLINE_TRANSPOSE HWY_INLINE +#endif +#endif // JXL_INLINE_TRANSPOSE + +// Simple wrapper that ensures that a function will not be inlined. +template +JXL_NOINLINE void NoInlineWrapper(const T& f, const Args&... args) { + return f(args...); +} + +template +struct TransposeSimdTag {}; + +// TODO(veluca): it's not super useful to have this in the SIMD namespace. +template +JXL_INLINE_TRANSPOSE void GenericTransposeBlock(TransposeSimdTag, + const From& from, const To& to, + size_t ROWSp, size_t COLSp) { + size_t ROWS = ROWS_or_0 == 0 ? ROWSp : ROWS_or_0; + size_t COLS = COLS_or_0 == 0 ? COLSp : COLS_or_0; + for (size_t n = 0; n < ROWS; ++n) { + for (size_t m = 0; m < COLS; ++m) { + to.Write(from.Read(n, m), m, n); + } + } +} + +// TODO(veluca): AVX3? +#if HWY_CAP_GE256 +constexpr bool TransposeUseSimd(size_t ROWS, size_t COLS) { + return ROWS % 8 == 0 && COLS % 8 == 0; +} + +template +JXL_INLINE_TRANSPOSE void GenericTransposeBlock(TransposeSimdTag, + const From& from, const To& to, + size_t ROWSp, size_t COLSp) { + size_t ROWS = ROWS_or_0 == 0 ? ROWSp : ROWS_or_0; + size_t COLS = COLS_or_0 == 0 ? COLSp : COLS_or_0; + static_assert(MaxLanes(BlockDesc<8>()) == 8, "Invalid descriptor size"); + static_assert(ROWS_or_0 % 8 == 0, "Invalid number of rows"); + static_assert(COLS_or_0 % 8 == 0, "Invalid number of columns"); + for (size_t n = 0; n < ROWS; n += 8) { + for (size_t m = 0; m < COLS; m += 8) { + auto i0 = from.LoadPart(BlockDesc<8>(), n + 0, m + 0); + auto i1 = from.LoadPart(BlockDesc<8>(), n + 1, m + 0); + auto i2 = from.LoadPart(BlockDesc<8>(), n + 2, m + 0); + auto i3 = from.LoadPart(BlockDesc<8>(), n + 3, m + 0); + auto i4 = from.LoadPart(BlockDesc<8>(), n + 4, m + 0); + auto i5 = from.LoadPart(BlockDesc<8>(), n + 5, m + 0); + auto i6 = from.LoadPart(BlockDesc<8>(), n + 6, m + 0); + auto i7 = from.LoadPart(BlockDesc<8>(), n + 7, m + 0); + // Surprisingly, this straightforward implementation (24 cycles on port5) + // is faster than load128+insert and LoadDup128+ConcatUpperLower+blend. + const auto q0 = InterleaveLower(i0, i2); + const auto q1 = InterleaveLower(i1, i3); + const auto q2 = InterleaveUpper(i0, i2); + const auto q3 = InterleaveUpper(i1, i3); + const auto q4 = InterleaveLower(i4, i6); + const auto q5 = InterleaveLower(i5, i7); + const auto q6 = InterleaveUpper(i4, i6); + const auto q7 = InterleaveUpper(i5, i7); + + const auto r0 = InterleaveLower(q0, q1); + const auto r1 = InterleaveUpper(q0, q1); + const auto r2 = InterleaveLower(q2, q3); + const auto r3 = InterleaveUpper(q2, q3); + const auto r4 = InterleaveLower(q4, q5); + const auto r5 = InterleaveUpper(q4, q5); + const auto r6 = InterleaveLower(q6, q7); + const auto r7 = InterleaveUpper(q6, q7); + + i0 = ConcatLowerLower(r4, r0); + i1 = ConcatLowerLower(r5, r1); + i2 = ConcatLowerLower(r6, r2); + i3 = ConcatLowerLower(r7, r3); + i4 = ConcatUpperUpper(r4, r0); + i5 = ConcatUpperUpper(r5, r1); + i6 = ConcatUpperUpper(r6, r2); + i7 = ConcatUpperUpper(r7, r3); + to.StorePart(BlockDesc<8>(), i0, m + 0, n + 0); + to.StorePart(BlockDesc<8>(), i1, m + 1, n + 0); + to.StorePart(BlockDesc<8>(), i2, m + 2, n + 0); + to.StorePart(BlockDesc<8>(), i3, m + 3, n + 0); + to.StorePart(BlockDesc<8>(), i4, m + 4, n + 0); + to.StorePart(BlockDesc<8>(), i5, m + 5, n + 0); + to.StorePart(BlockDesc<8>(), i6, m + 6, n + 0); + to.StorePart(BlockDesc<8>(), i7, m + 7, n + 0); + } + } +} +#elif HWY_TARGET != HWY_SCALAR +constexpr bool TransposeUseSimd(size_t ROWS, size_t COLS) { + return ROWS % 4 == 0 && COLS % 4 == 0; +} + +template +JXL_INLINE_TRANSPOSE void GenericTransposeBlock(TransposeSimdTag, + const From& from, const To& to, + size_t ROWSp, size_t COLSp) { + size_t ROWS = ROWS_or_0 == 0 ? ROWSp : ROWS_or_0; + size_t COLS = COLS_or_0 == 0 ? COLSp : COLS_or_0; + static_assert(MaxLanes(BlockDesc<4>()) == 4, "Invalid descriptor size"); + static_assert(ROWS_or_0 % 4 == 0, "Invalid number of rows"); + static_assert(COLS_or_0 % 4 == 0, "Invalid number of columns"); + for (size_t n = 0; n < ROWS; n += 4) { + for (size_t m = 0; m < COLS; m += 4) { + const auto p0 = from.LoadPart(BlockDesc<4>(), n + 0, m + 0); + const auto p1 = from.LoadPart(BlockDesc<4>(), n + 1, m + 0); + const auto p2 = from.LoadPart(BlockDesc<4>(), n + 2, m + 0); + const auto p3 = from.LoadPart(BlockDesc<4>(), n + 3, m + 0); + + const auto q0 = InterleaveLower(p0, p2); + const auto q1 = InterleaveLower(p1, p3); + const auto q2 = InterleaveUpper(p0, p2); + const auto q3 = InterleaveUpper(p1, p3); + + const auto r0 = InterleaveLower(q0, q1); + const auto r1 = InterleaveUpper(q0, q1); + const auto r2 = InterleaveLower(q2, q3); + const auto r3 = InterleaveUpper(q2, q3); + + to.StorePart(BlockDesc<4>(), r0, m + 0, n + 0); + to.StorePart(BlockDesc<4>(), r1, m + 1, n + 0); + to.StorePart(BlockDesc<4>(), r2, m + 2, n + 0); + to.StorePart(BlockDesc<4>(), r3, m + 3, n + 0); + } + } +} +#else +constexpr bool TransposeUseSimd(size_t ROWS, size_t COLS) { return false; } +#endif + +template +struct Transpose { + template + static void Run(const From& from, const To& to) { + // This does not guarantee anything, just saves from the most stupid + // mistakes. + JXL_DASSERT(from.Address(0, 0) != to.Address(0, 0)); + TransposeSimdTag tag; + GenericTransposeBlock(tag, from, to, N, M); + } +}; + +// Avoid inlining and unrolling transposes for large blocks. +template +struct Transpose< + N, M, typename std::enable_if<(N >= 8 && M >= 8 && N * M >= 512)>::type> { + template + static void Run(const From& from, const To& to) { + // This does not guarantee anything, just saves from the most stupid + // mistakes. + JXL_DASSERT(from.Address(0, 0) != to.Address(0, 0)); + TransposeSimdTag tag; + constexpr void (*transpose)(TransposeSimdTag, + const From&, const To&, size_t, size_t) = + GenericTransposeBlock<0, 0, From, To>; + NoInlineWrapper(transpose, tag, from, to, N, M); + } +}; + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_TRANSPOSE_INL_H_ diff --git a/lib/jxl/xorshift128plus-inl.h b/lib/jxl/xorshift128plus-inl.h new file mode 100644 index 0000000..6c18651 --- /dev/null +++ b/lib/jxl/xorshift128plus-inl.h @@ -0,0 +1,88 @@ +// 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. + +// Fast but weak random generator. + +#if defined(LIB_JXL_XORSHIFT128PLUS_INL_H_) == defined(HWY_TARGET_TOGGLE) +#ifdef LIB_JXL_XORSHIFT128PLUS_INL_H_ +#undef LIB_JXL_XORSHIFT128PLUS_INL_H_ +#else +#define LIB_JXL_XORSHIFT128PLUS_INL_H_ +#endif + +#include + +#include +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { +namespace { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::ShiftLeft; +using hwy::HWY_NAMESPACE::ShiftRight; + +// Adapted from https://github.com/vpxyz/xorshift/blob/master/xorshift128plus/ +// (MIT-license) +class Xorshift128Plus { + public: + // 8 independent generators (= single iteration for AVX-512) + enum { N = 8 }; + + explicit HWY_MAYBE_UNUSED Xorshift128Plus(const uint64_t seed) { + // Init state using SplitMix64 generator + s0_[0] = SplitMix64(seed + 0x9E3779B97F4A7C15ull); + s1_[0] = SplitMix64(s0_[0]); + for (size_t i = 1; i < N; ++i) { + s0_[i] = SplitMix64(s1_[i - 1]); + s1_[i] = SplitMix64(s0_[i]); + } + } + + HWY_INLINE HWY_MAYBE_UNUSED void Fill(uint64_t* HWY_RESTRICT random_bits) { +#if HWY_CAP_INTEGER64 + const HWY_FULL(uint64_t) d; + for (size_t i = 0; i < N; i += Lanes(d)) { + auto s1 = Load(d, s0_ + i); + const auto s0 = Load(d, s1_ + i); + const auto bits = s1 + s0; // b, c + Store(s0, d, s0_ + i); + s1 ^= ShiftLeft<23>(s1); + Store(bits, d, random_bits + i); + s1 ^= s0 ^ ShiftRight<18>(s1) ^ ShiftRight<5>(s0); + Store(s1, d, s1_ + i); + } +#else + for (size_t i = 0; i < N; ++i) { + auto s1 = s0_[i]; + const auto s0 = s1_[i]; + const auto bits = s1 + s0; // b, c + s0_[i] = s0; + s1 ^= s1 << 23; + random_bits[i] = bits; + s1 ^= s0 ^ (s1 >> 18) ^ (s0 >> 5); + s1_[i] = s1; + } +#endif + } + + private: + static uint64_t SplitMix64(uint64_t z) { + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; + return z ^ (z >> 31); + } + + HWY_ALIGN uint64_t s0_[N]; + HWY_ALIGN uint64_t s1_[N]; +}; + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#endif // LIB_JXL_XORSHIFT128PLUS_INL_H_ diff --git a/lib/jxl/xorshift128plus_test.cc b/lib/jxl/xorshift128plus_test.cc new file mode 100644 index 0000000..a5d3c4a --- /dev/null +++ b/lib/jxl/xorshift128plus_test.cc @@ -0,0 +1,372 @@ +// 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 +#include + +#include +#include + +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/jxl/xorshift128plus_test.cc" +#include +#include +#include + +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/thread_pool_internal.h" +#include "lib/jxl/xorshift128plus-inl.h" + +HWY_BEFORE_NAMESPACE(); +namespace jxl { +namespace HWY_NAMESPACE { + +// These templates are not found via ADL. +using hwy::HWY_NAMESPACE::ShiftRight; + +// Define to nonzero in order to print the (new) golden outputs. +#define PRINT_RESULTS 0 + +const size_t kVectors = 64; + +#if PRINT_RESULTS + +template +void Print(const uint64_t (&result)[kNumLanes]) { + printf("{ "); + for (int i = 0; i < kNumLanes; ++i) { + if (i != 0) { + printf(", "); + } + printf("0x%016llXull", result[i]); + } + printf("},\n"); +} + +#else // PRINT_RESULTS + +const uint64_t kExpected[kVectors][Xorshift128Plus::N] = { + {0x6E901576D477CBB1ull, 0xE9E53789195DA2A2ull, 0xB681F6DDA5E0AE99ull, + 0x8EFD18CE21FD6896ull, 0xA898A80DF75CF532ull, 0x50CEB2C9E2DE7E32ull, + 0x3CA7C2FEB25C0DD0ull, 0xA4D0866B80B4D836ull}, + {0x8CD6A1E6233D3A26ull, 0x3D4603ADE98B112Dull, 0xDC427AF674019E36ull, + 0xE28B4D230705AC53ull, 0x7297E9BBA88783DDull, 0x34D3D23CFCD9B41Aull, + 0x5A223615ADBE96B8ull, 0xE5EB529027CFBD01ull}, + {0xC1894CF00DFAC6A2ull, 0x18EDF8AE9085E404ull, 0x8E936625296B4CCDull, + 0x31971EF3A14A899Bull, 0xBE87535FCE0BF26Aull, 0x576F7A752BC6649Full, + 0xA44CBADCE0C6B937ull, 0x3DBA819BB17A353Aull}, + {0x27CE38DFCC1C5EB6ull, 0x920BEB5606340256ull, 0x3986CBC40C9AFC2Cull, + 0xE22BCB3EEB1E191Eull, 0x6E1FCDD3602A8FBAull, 0x052CB044E5415A29ull, + 0x46266646EFB9ECD7ull, 0x8F44914618D29335ull}, + {0xDD30AEDF72A362C5ull, 0xBC1D824E16BB98F4ull, 0x9EA6009C2AA3D2F1ull, + 0xF65C0FBBE17AF081ull, 0x22424D06A8738991ull, 0x8A62763F2B7611D2ull, + 0x2F3E89F722637939ull, 0x84D338BEF50AFD50ull}, + {0x00F46494898E2B0Bull, 0x81239DC4FB8E8003ull, 0x414AD93EC5773FE7ull, + 0x791473C450E4110Full, 0x87F127BF68C959ACull, 0x6429282D695EF67Bull, + 0x661082E11546CBA8ull, 0x5815D53FA5436BFDull}, + {0xB3DEADAB9BE6E0F9ull, 0xAA1B7B8F7CED0202ull, 0x4C5ED437699D279Eull, + 0xA4471727F1CB39D3ull, 0xE439DA193F802F70ull, 0xF89401BB04FA6493ull, + 0x3B08045A4FE898BAull, 0x32137BFE98227950ull}, + {0xFBAE4A092897FEF3ull, 0x0639F6CE56E71C8Eull, 0xF0AD6465C07F0C1Eull, + 0xFF8E28563361DCE5ull, 0xC2013DB7F86BC6B9ull, 0x8EFCC0503330102Full, + 0x3F6B767EA5C4DA40ull, 0xB9864B950B2232E1ull}, + {0x76EB58DE8E5EC22Aull, 0x9BBBF49A18B32F4Full, 0xC8405F02B2B2FAB9ull, + 0xC3E122A5F146BC34ull, 0xC90BB046660F5765ull, 0xB933981310DBECCFull, + 0x5A2A7BFC9126FD1Cull, 0x8BB388C94DF87901ull}, + {0x753EB89AD63EF3C3ull, 0xF24AAF40C89D65ADull, 0x23F68931C1A6AA6Dull, + 0xF47E79BF702C6DD0ull, 0xA3AD113244EE7EAEull, 0xD42CBEA28F793DC3ull, + 0xD896FCF1820F497Cull, 0x042B86D2818948C1ull}, + {0x8F2A4FC5A4265763ull, 0xEC499E6F95EAA10Cull, 0xE3786D4ECCD0DEB5ull, + 0xC725C53D3AC4CC43ull, 0x065A4ACBBF83610Eull, 0x35C61C9FEF167129ull, + 0x7B720AEAA7D70048ull, 0x14206B841377D039ull}, + {0xAD27D78BF96055F6ull, 0x5F43B20FF47ADCD4ull, 0xE184C2401E2BF71Eull, + 0x30B263D78990045Dull, 0xC22F00EBFF9BA201ull, 0xAE7F86522B53A562ull, + 0x2853312BC039F0A4ull, 0x868D619E6549C3C8ull}, + {0xFD5493D8AE9A8371ull, 0x773D5E224DF61B3Bull, 0x5377C54FBB1A8280ull, + 0xCAD4DE3B8265CAFAull, 0xCDF3F19C91EBD5F6ull, 0xC8EA0F182D73BD78ull, + 0x220502D593433FF1ull, 0xB81205E612DC31B1ull}, + {0x8F32A39EAEDA4C70ull, 0x1D4B0914AA4DAC7Full, 0x56EF1570F3A8B405ull, + 0x29812CB17404A592ull, 0x97A2AAF69CAE90F2ull, 0x12BF5E02778BBFE5ull, + 0x9D4B55AD42A05FD2ull, 0x06C2BAB5E6086620ull}, + {0x8DB4B9648302B253ull, 0xD756AD9E3AEA12C7ull, 0x68709B7F11D4B188ull, + 0x7CC299DDCD707A4Bull, 0x97B860C370A7661Dull, 0xCECD314FC20E64F5ull, + 0x55F412CDFB4C7EC3ull, 0x55EE97591193B525ull}, + {0xCF70F3ACA96E6254ull, 0x022FEDECA2E09F46ull, 0x686823DB60AE1ECFull, + 0xFD36190D3739830Eull, 0x74E1C09027F68120ull, 0xB5883A835C093842ull, + 0x93E1EFB927E9E4E3ull, 0xB2721E249D7E5EBEull}, + {0x69B6E21C44188CB8ull, 0x5D6CFB853655A7AAull, 0x3E001A0B425A66DCull, + 0x8C57451103A5138Full, 0x7BF8B4BE18EAB402ull, 0x494102EB8761A365ull, + 0xB33796A9F6A81F0Eull, 0x10005AB3BCCFD960ull}, + {0xB2CF25740AE965DCull, 0x6F7C1DF7EF53D670ull, 0x648DD6087AC2251Eull, + 0x040955D9851D487Dull, 0xBD550FC7E21A7F66ull, 0x57408F484DEB3AB5ull, + 0x481E24C150B506C1ull, 0x72C0C3EAF91A40D6ull}, + {0x1997A481858A5D39ull, 0x539718F4BEF50DC1ull, 0x2EC4DC4787E7E368ull, + 0xFF1CE78879419845ull, 0xE219A93DD6F6DD30ull, 0x85328618D02FEC1Aull, + 0xC86E02D969181B20ull, 0xEBEC8CD8BBA34E6Eull}, + {0x28B55088A16CE947ull, 0xDD25AC11E6350195ull, 0xBD1F176694257B1Cull, + 0x09459CCF9FCC9402ull, 0xF8047341E386C4E4ull, 0x7E8E9A9AD984C6C0ull, + 0xA4661E95062AA092ull, 0x70A9947005ED1152ull}, + {0x4C01CF75DBE98CCDull, 0x0BA076CDFC7373B9ull, 0x6C5E7A004B57FB59ull, + 0x336B82297FD3BC56ull, 0x7990C0BE74E8D60Full, 0xF0275CC00EC5C8C8ull, + 0x6CF29E682DFAD2E9ull, 0xFA4361524BD95D72ull}, + {0x631D2A19FF62F018ull, 0x41C43863B985B3FAull, 0xE052B2267038EFD9ull, + 0xE2A535FAC575F430ull, 0xE004EEA90B1FF5B8ull, 0x42DFE2CA692A1F26ull, + 0x90FB0BFC9A189ECCull, 0x4484102BD3536BD0ull}, + {0xD027134E9ACCA5A5ull, 0xBBAB4F966D476A9Bull, 0x713794A96E03D693ull, + 0x9F6335E6B94CD44Aull, 0xC5090C80E7471617ull, 0x6D9C1B0C87B58E33ull, + 0x1969CE82E31185A5ull, 0x2099B97E87754EBEull}, + {0x60EBAF4ED934350Full, 0xC26FBF0BA5E6ECFFull, 0x9E54150F0312EC57ull, + 0x0973B48364ED0041ull, 0x800A523241426CFCull, 0x03AB5EC055F75989ull, + 0x8CF315935DEEB40Aull, 0x83D3FC0190BD1409ull}, + {0x26D35394CF720A51ull, 0xCE9EAA15243CBAFEull, 0xE2B45FBAF21B29E0ull, + 0xDB92E98EDE73F9E0ull, 0x79B16F5101C26387ull, 0x1AC15959DE88C86Full, + 0x387633AEC6D6A580ull, 0xA6FC05807BFC5EB8ull}, + {0x2D26C8E47C6BADA9ull, 0x820E6EC832D52D73ull, 0xB8432C3E0ED0EE5Bull, + 0x0F84B3C4063AAA87ull, 0xF393E4366854F651ull, 0x749E1B4D2366A567ull, + 0x805EACA43480D004ull, 0x244EBF3AA54400A5ull}, + {0xBFDC3763AA79F75Aull, 0x9E3A74CC751F41DBull, 0xF401302A149DBC55ull, + 0x6B25F7973D7BF7BCull, 0x13371D34FDBC3DAEull, 0xC5E1998C8F484DCDull, + 0x7031B8AE5C364464ull, 0x3847F0C4F3DA2C25ull}, + {0x24C6387D2C0F1225ull, 0x77CCE960255C67A4ull, 0x21A0947E497B10EBull, + 0xBB5DB73A825A9D7Eull, 0x26294A41999E553Dull, 0x3953E0089F87D925ull, + 0x3DAE6E5D4E5EAAFEull, 0x74B545460341A7AAull}, + {0x710E5EB08A7DB820ull, 0x7E43C4E77CAEA025ull, 0xD4C91529C8B060C1ull, + 0x09AE26D8A7B0CA29ull, 0xAB9F356BB360A772ull, 0xB68834A25F19F6E9ull, + 0x79B8D9894C5734E2ull, 0xC6847E7C8FFD265Full}, + {0x10C4BCB06A5111E6ull, 0x57CB50955B6A2516ull, 0xEF53C87798B6995Full, + 0xAB38E15BBD8D0197ull, 0xA51C6106EFF73C93ull, 0x83D7F0E2270A7134ull, + 0x0923FD330397FCE5ull, 0xF9DE54EDFE58FB45ull}, + {0x07D44833ACCD1A94ull, 0xAAD3C9E945E2F9F3ull, 0xABF4C879B876AA37ull, + 0xF29C69A21B301619ull, 0x2DDCE959111C788Bull, 0x7CEDB48F8AC1729Bull, + 0x93F3BA9A02B659BEull, 0xF20A87FF17933CBEull}, + {0x8E96EBE93180CFE6ull, 0x94CAA12873937079ull, 0x05F613D9380D4189ull, + 0xBCAB40C1DC79F38Aull, 0x0AD8907B7C61D19Eull, 0x88534E189D103910ull, + 0x2DB2FAABA160AB8Full, 0xA070E7506B06F15Cull}, + {0x6FB1FCDAFFEF87A9ull, 0xE735CF25337A090Dull, 0x172C6EDCEFEF1825ull, + 0x76957EA49EF0542Dull, 0x819BF4CD250F7C49ull, 0xD6FF23E4AD00C4D4ull, + 0xE79673C1EC358FF0ull, 0xAC9C048144337938ull}, + {0x4C5387FF258B3AF4ull, 0xEDB68FAEC2CB1AA3ull, 0x02A624E67B4E1DA4ull, + 0x5C44797A38E08AF2ull, 0x36546A70E9411B4Bull, 0x47C17B24D2FD9675ull, + 0x101957AAA020CA26ull, 0x47A1619D4779F122ull}, + {0xF84B8BCDC92D9A3Cull, 0x951D7D2C74B3066Bull, 0x7AC287C06EDDD9B2ull, + 0x4C38FC476608D38Full, 0x224D793B19CB4BCDull, 0x835A255899BF1A41ull, + 0x4AD250E9F62DB4ABull, 0xD9B44F4B58781096ull}, + {0xABBAF99A8EB5C6B8ull, 0xFB568E900D3A9F56ull, 0x11EDF63D23C5DF11ull, + 0xA9C3011D3FA7C5A8ull, 0xAEDD3CF11AFFF725ull, 0xABCA472B5F1EDD6Bull, + 0x0600B6BB5D879804ull, 0xDB4DE007F22191A0ull}, + {0xD76CC9EFF0CE9392ull, 0xF5E0A772B59BA49Aull, 0x7D1AE1ED0C1261B5ull, + 0x79224A33B5EA4F4Aull, 0x6DD825D80C40EA60ull, 0x47FC8E747E51C953ull, + 0x695C05F72888BF98ull, 0x1A012428440B9015ull}, + {0xD754DD61F9B772BFull, 0xC4A2FCF4C0F9D4EBull, 0x461167CDF67A24A2ull, + 0x434748490EBCB9D4ull, 0x274DD9CDCA5781DEull, 0x36BAC63BA9A85209ull, + 0x30324DAFDA36B70Full, 0x337570DB4FE6DAB3ull}, + {0xF46CBDD57C551546ull, 0x8E02507E676DA3E3ull, 0xD826245A8C15406Dull, + 0xDFB38A5B71113B72ull, 0x5EA38454C95B16B5ull, 0x28C054FB87ABF3E1ull, + 0xAA2724C0BA1A8096ull, 0xECA83EC980304F2Full}, + {0x6AA76EC294EB3303ull, 0x42D4CDB2A8032E3Bull, 0x7999EDF75DCD8735ull, + 0xB422BFFE696CCDCCull, 0x8F721461FD7CCDFEull, 0x148E1A5814FDE253ull, + 0x4DC941F4375EF8FFull, 0x27B2A9E0EB5B49CFull}, + {0xCEA592EF9343EBE1ull, 0xF7D38B5FA7698903ull, 0x6CCBF352203FEAB6ull, + 0x830F3095FCCDA9C5ull, 0xDBEEF4B81B81C8F4ull, 0x6D7EB9BCEECA5CF9ull, + 0xC58ABB0FBE436C69ull, 0xE4B97E6DB2041A4Bull}, + {0x7E40FC772978AF14ull, 0xCDDA4BBAE28354A1ull, 0xE4F993B832C32613ull, + 0xD3608093C68A4B35ull, 0x9A3B60E01BEE3699ull, 0x03BEF248F3288713ull, + 0x70B9294318F3E9B4ull, 0x8D2ABB913B8610DEull}, + {0x37F209128E7D8B2Cull, 0x81D2AB375BD874BCull, 0xA716A1B7373F7408ull, + 0x0CEE97BEC4706540ull, 0xA40C5FD9CDBC1512ull, 0x73CAF6C8918409E7ull, + 0x45E11BCEDF0BBAA1ull, 0x612C612BFF6E6605ull}, + {0xF8ECB14A12D0F649ull, 0xDA683CD7C01BA1ACull, 0xA2203F7510E124C1ull, + 0x7F83E52E162F3C78ull, 0x77D2BB73456ACADBull, 0x37FC34FC840BBA6Full, + 0x3076BC7D4C6EBC1Full, 0x4F514123632B5FA9ull}, + {0x44D789DED935E884ull, 0xF8291591E09FEC9Full, 0xD9CED2CF32A2E4B7ull, + 0x95F70E1EB604904Aull, 0xDE438FE43C14F6ABull, 0x4C8D23E4FAFCF8D8ull, + 0xC716910A3067EB86ull, 0x3D6B7915315095D3ull}, + {0x3170FDBADAB92095ull, 0x8F1963933FC5650Bull, 0x72F94F00ABECFEABull, + 0x6E3AE826C6AAB4CEull, 0xA677A2BF31068258ull, 0x9660CDC4F363AF10ull, + 0xD81A15A152379EF1ull, 0x5D7D285E1080A3F9ull}, + {0xDAD5DDFF9A2249B3ull, 0x6F9721D926103FAEull, 0x1418CBB83FFA349Aull, + 0xE71A30AD48C012B2ull, 0xBE76376C63751132ull, 0x3496467ACA713AE6ull, + 0x8D7EC01369F991A3ull, 0xD8C73A88B96B154Eull}, + {0x8B5D9C74AEB4833Aull, 0xF914FB3F867B912Full, 0xB894EA034936B1DCull, + 0x8A16D21BE51C4F5Bull, 0x31FF048ED582D98Eull, 0xB95AB2F4DC65B820ull, + 0x04082B9170561AF7ull, 0xA215610A5DC836FAull}, + {0xB2ADE592C092FAACull, 0x7A1E683BCBF13294ull, 0xC7A4DBF86858C096ull, + 0x3A49940F97BFF316ull, 0xCAE5C06B82C46703ull, 0xC7F413A0F951E2BDull, + 0x6665E7BB10EB5916ull, 0x86F84A5A94EDE319ull}, + {0x4EA199D8FAA79CA3ull, 0xDFA26E5BF1981704ull, 0x0F5E081D37FA4E01ull, + 0x9CB632F89CD675CDull, 0x4A09DB89D48C0304ull, 0x88142742EA3C7672ull, + 0xAC4F149E6D2E9BDBull, 0x6D9E1C23F8B1C6C6ull}, + {0xD58BE47B92DEC0E9ull, 0x8E57573645E34328ull, 0x4CC094CCB5FB5126ull, + 0x5F1D66AF6FB40E3Cull, 0x2BA15509132D3B00ull, 0x0D6545646120E567ull, + 0x3CF680C45C223666ull, 0x96B28E32930179DAull}, + {0x5900C45853AC7990ull, 0x61881E3E8B7FF169ull, 0x4DE5F835DF2230FFull, + 0x4427A9E7932F73FFull, 0x9B641BAD379A8C8Dull, 0xDF271E5BF98F4E5Cull, + 0xDFDA16DB830FF5EEull, 0x371C7E7CFB89C0E9ull}, + {0x4410A8576247A250ull, 0x6AD2DA12B45AC0D9ull, 0x18DFC72AAC85EECCull, + 0x06FC8BB2A0EF25C8ull, 0xEB287619C85E6118ull, 0x19553ECA67F25A2Cull, + 0x3B9557F1DCEC5BAAull, 0x7BAD9E8B710D1079ull}, + {0x34F365D66BD22B28ull, 0xE6E124B9F10F835Dull, 0x0573C38ABF2B24DCull, + 0xD32E6AF10A0125AEull, 0x383590ACEA979519ull, 0x8376ED7A39E28205ull, + 0xF0B7F184DCBDA435ull, 0x062A203390E31794ull}, + {0xA2AFFD7E41918760ull, 0x7F90FC1BD0819C86ull, 0x5033C08E5A969533ull, + 0x2707AF5C6D039590ull, 0x57BBD5980F17DF9Cull, 0xD3FE6E61D763268Aull, + 0x9E0A0AE40F335A3Bull, 0x43CF4EB0A99613C5ull}, + {0xD4D2A397CE1A7C2Eull, 0x3DF7CE7CC3212DADull, 0x0880F0D5D356C75Aull, + 0xA8AFC44DD03B1346ull, 0x79263B46C13A29E0ull, 0x11071B3C0ED58E7Aull, + 0xED46DC9F538406BFull, 0x2C94974F2B94843Dull}, + {0xE246E13C39AB5D5Eull, 0xAC1018489D955B20ull, 0x8601B558771852B8ull, + 0x110BD4C06DB40173ull, 0x738FC8A18CCA0EBBull, 0x6673E09BE0EA76E5ull, + 0x024BC7A0C7527877ull, 0x45E6B4652E2EC34Eull}, + {0xD1ED26A1A375CDC8ull, 0xAABC4E896A617CB8ull, 0x0A9C9E8E57D753C6ull, + 0xA3774A75FEB4C30Eull, 0x30B816C01C93E49Eull, 0xF405BABC06D2408Cull, + 0xCC0CE6B4CE788ABCull, 0x75E7922D0447956Cull}, + {0xD07C1676A698BC95ull, 0x5F9AEA4840E2D860ull, 0xD5FC10D58BDF6F02ull, + 0xF190A2AD4BC2EEA7ull, 0x0C24D11F51726931ull, 0xDB646899A16B6512ull, + 0x7BC10670047B1DD8ull, 0x2413A5ABCD45F092ull}, + {0x4E66892190CFD923ull, 0xF10162440365EC8Eull, 0x158ACA5A6A2280AEull, + 0x0D60ED11C0224166ull, 0x7CD2E9A71B9D7488ull, 0x450D7289706AB2A3ull, + 0x88FAE34EC9A0D7DCull, 0x96FF9103575A97DAull}, + {0x77990FAC6046C446ull, 0xB174B5FB30C76676ull, 0xE352CE3EB56CF82Aull, + 0xC6039B6873A9A082ull, 0xE3F80F3AE333148Aull, 0xB853BA24BA3539B9ull, + 0xE8863E52ECCB0C74ull, 0x309B4CC1092CC245ull}, + {0xBC2B70BEE8388D9Full, 0xE48D92AE22216DCEull, 0xF15F3BF3E2C15D8Full, + 0x1DD964D4812D8B24ull, 0xD56AF02FB4665E4Cull, 0x98002200595BD9A3ull, + 0x049246D50BB8FA12ull, 0x1B542DF485B579B9ull}, + {0x2347409ADFA8E497ull, 0x36015C2211D62498ull, 0xE9F141F32EB82690ull, + 0x1F839912D0449FB9ull, 0x4E4DCFFF2D02D97Cull, 0xF8A03AB4C0F625C9ull, + 0x0605F575795DAC5Cull, 0x4746C9BEA0DDA6B1ull}, + {0xCA5BB519ECE7481Bull, 0xFD496155E55CA945ull, 0xF753B9DBB1515F81ull, + 0x50549E8BAC0F70E7ull, 0x8614FB0271E21C60ull, 0x60C72947EB0F0070ull, + 0xA6511C10AEE742B6ull, 0x48FB48F2CACCB43Eull}}; + +#endif // PRINT_RESULTS + +// Ensures Xorshift128+ returns consistent and unchanging values. +void TestGolden() { + HWY_ALIGN Xorshift128Plus rng(12345); + for (uint64_t vector = 0; vector < kVectors; ++vector) { + HWY_ALIGN uint64_t lanes[Xorshift128Plus::N]; + rng.Fill(lanes); +#if PRINT_RESULTS + Print(lanes); +#else + for (size_t i = 0; i < Xorshift128Plus::N; ++i) { + ASSERT_EQ(kExpected[vector][i], lanes[i]) + << "Where vector=" << vector << " i=" << i; + } +#endif + } +} + +// Output changes when given different seeds +void TestSeedChanges() { + HWY_ALIGN uint64_t lanes[Xorshift128Plus::N]; + + std::vector first; + constexpr size_t kNumSeeds = 16384; + first.reserve(kNumSeeds); + + // All 14-bit seeds + for (size_t seed = 0; seed < kNumSeeds; ++seed) { + HWY_ALIGN Xorshift128Plus rng(seed); + + rng.Fill(lanes); + first.push_back(lanes[0]); + } + + // All outputs are unique + ASSERT_EQ(kNumSeeds, first.size()); + std::sort(first.begin(), first.end()); + first.erase(std::unique(first.begin(), first.end()), first.end()); + EXPECT_EQ(kNumSeeds, first.size()); +} + +void TestFloat() { + ThreadPoolInternal pool(8); + +#ifdef JXL_DISABLE_SLOW_TESTS + const uint32_t kMaxSeed = 2048; +#else // JXL_DISABLE_SLOW_TESTS + const uint32_t kMaxSeed = 16384; // All 14-bit seeds +#endif // JXL_DISABLE_SLOW_TESTS + pool.Run(0, kMaxSeed, ThreadPool::SkipInit(), + [](const int seed, const int /*thread*/) { + HWY_ALIGN Xorshift128Plus rng(seed); + + const HWY_FULL(uint32_t) du; + const HWY_FULL(float) df; + HWY_ALIGN uint64_t batch[Xorshift128Plus::N]; + HWY_ALIGN float lanes[MaxLanes(df)]; + double sum = 0.0; + size_t count = 0; + const size_t kReps = 2000; + for (size_t reps = 0; reps < kReps; ++reps) { + rng.Fill(batch); + for (size_t i = 0; i < Xorshift128Plus::N * 2; i += Lanes(df)) { + const auto bits = + Load(du, reinterpret_cast(batch) + i); + // 1.0 + 23 random mantissa bits = [1, 2) + const auto rand12 = + BitCast(df, ShiftRight<9>(bits) | Set(du, 0x3F800000)); + const auto rand01 = rand12 - Set(df, 1.0f); + Store(rand01, df, lanes); + for (float lane : lanes) { + sum += lane; + count += 1; + EXPECT_LE(lane, 1.0f); + EXPECT_GE(lane, 0.0f); + } + } + } + + // Verify average (uniform distribution) + EXPECT_NEAR(0.5, sum / count, 0.00702); + }); +} + +// Not more than one 64-bit zero +void TestNotZero() { + ThreadPoolInternal pool(8); + +#ifdef JXL_DISABLE_SLOW_TESTS + const uint32_t kMaxSeed = 500; +#else // JXL_DISABLE_SLOW_TESTS + const uint32_t kMaxSeed = 2000; +#endif // JXL_DISABLE_SLOW_TESTS + pool.Run(0, kMaxSeed, ThreadPool::SkipInit(), + [](const int task, const int /*thread*/) { + HWY_ALIGN uint64_t lanes[Xorshift128Plus::N]; + + HWY_ALIGN Xorshift128Plus rng(task); + size_t num_zero = 0; + for (size_t vectors = 0; vectors < 10000; ++vectors) { + rng.Fill(lanes); + for (uint64_t lane : lanes) { + num_zero += static_cast(lane == 0); + } + } + EXPECT_LE(num_zero, 1u); + }); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace jxl +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace jxl { + +class Xorshift128Test : public hwy::TestWithParamTarget {}; + +HWY_TARGET_INSTANTIATE_TEST_SUITE_P(Xorshift128Test); + +HWY_EXPORT_AND_TEST_P(Xorshift128Test, TestNotZero); +HWY_EXPORT_AND_TEST_P(Xorshift128Test, TestGolden); +HWY_EXPORT_AND_TEST_P(Xorshift128Test, TestSeedChanges); +HWY_EXPORT_AND_TEST_P(Xorshift128Test, TestFloat); + +} // namespace jxl +#endif diff --git a/lib/jxl_benchmark.cmake b/lib/jxl_benchmark.cmake new file mode 100644 index 0000000..4b20b40 --- /dev/null +++ b/lib/jxl_benchmark.cmake @@ -0,0 +1,45 @@ +# 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. + +# All files ending in "_gbench.cc" are considered Google benchmark files and +# should be listed here. +set(JPEGXL_INTERNAL_SOURCES_GBENCH + extras/tone_mapping_gbench.cc + jxl/dec_external_image_gbench.cc + jxl/enc_external_image_gbench.cc + jxl/splines_gbench.cc + jxl/tf_gbench.cc +) + +# benchmark.h doesn't work in our MINGW set up since it ends up including the +# wrong stdlib header. We don't run gbench on MINGW targets anyway. +if(NOT MINGW) + +# This is the Google benchmark project (https://github.com/google/benchmark). +find_package(benchmark QUIET) + +if(benchmark_FOUND) + if(JPEGXL_STATIC AND NOT MINGW) + # benchmark::benchmark hardcodes the librt.so which obviously doesn't + # compile in static mode. + set_target_properties(benchmark::benchmark PROPERTIES + INTERFACE_LINK_LIBRARIES "Threads::Threads;-lrt") + endif() + + # Compiles all the benchmark files into a single binary. Individual benchmarks + # can be run with --benchmark_filter. + add_executable(jxl_gbench "${JPEGXL_INTERNAL_SOURCES_GBENCH}") + + target_compile_definitions(jxl_gbench PRIVATE + -DTEST_DATA_PATH="${PROJECT_SOURCE_DIR}/third_party/testdata") + target_link_libraries(jxl_gbench + jxl_extras-static + jxl-static + benchmark::benchmark + benchmark::benchmark_main + ) +endif() # benchmark_FOUND + +endif() # MINGW diff --git a/lib/jxl_extras.cmake b/lib/jxl_extras.cmake new file mode 100644 index 0000000..602b809 --- /dev/null +++ b/lib/jxl_extras.cmake @@ -0,0 +1,116 @@ +# 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. + +# codec_jpg is included always for loading of lossless reconstruction but +# decoding to pixels is only supported if libjpeg is found and +# JPEGXL_ENABLE_JPEG=1. +set(JPEGXL_EXTRAS_SOURCES + extras/codec.cc + extras/codec.h + extras/codec_jpg.cc + extras/codec_jpg.h + extras/codec_pgx.cc + extras/codec_pgx.h + extras/codec_png.cc + extras/codec_png.h + extras/codec_pnm.cc + extras/codec_pnm.h + extras/codec_psd.cc + extras/codec_psd.h + extras/color_description.cc + extras/color_description.h + extras/color_hints.cc + extras/color_hints.h + extras/time.cc + extras/time.h + extras/tone_mapping.cc + extras/tone_mapping.h +) + +# We only define a static library for jxl_extras since it uses internal parts +# of jxl library which are not accessible from outside the library in the +# shared library case. +add_library(jxl_extras-static STATIC EXCLUDE_FROM_ALL + "${JPEGXL_EXTRAS_SOURCES}") +target_compile_options(jxl_extras-static PRIVATE "${JPEGXL_INTERNAL_FLAGS}") +set_property(TARGET jxl_extras-static PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jxl_extras-static PUBLIC "${PROJECT_SOURCE_DIR}") +target_link_libraries(jxl_extras-static PUBLIC + jxl-static + lodepng +) + +find_package(GIF 5.1) +if(GIF_FOUND) + target_sources(jxl_extras-static PRIVATE + extras/codec_gif.cc + extras/codec_gif.h + ) + target_include_directories(jxl_extras-static PUBLIC "${GIF_INCLUDE_DIRS}") + target_link_libraries(jxl_extras-static PUBLIC ${GIF_LIBRARIES}) + target_compile_definitions(jxl_extras-static PUBLIC -DJPEGXL_ENABLE_GIF=1) + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libgif-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libgif COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() + +find_package(JPEG) +if(JPEG_FOUND) + target_include_directories(jxl_extras-static PUBLIC "${JPEG_INCLUDE_DIRS}") + target_link_libraries(jxl_extras-static PUBLIC ${JPEG_LIBRARIES}) + target_compile_definitions(jxl_extras-static PUBLIC -DJPEGXL_ENABLE_JPEG=1) + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libjpeg-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libjpeg COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() + +find_package(ZLIB) # dependency of PNG +find_package(PNG) +if(PNG_FOUND AND ZLIB_FOUND) + target_sources(jxl_extras-static PRIVATE + extras/codec_apng.cc + extras/codec_apng.h + ) + target_include_directories(jxl_extras-static PUBLIC "${PNG_INCLUDE_DIRS}") + target_link_libraries(jxl_extras-static PUBLIC ${PNG_LIBRARIES}) + target_compile_definitions(jxl_extras-static PUBLIC -DJPEGXL_ENABLE_APNG=1) + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/zlib1g-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.zlib COPYONLY) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libpng-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libpng COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() + +if (JPEGXL_ENABLE_SJPEG) + target_compile_definitions(jxl_extras-static PUBLIC -DJPEGXL_ENABLE_SJPEG=1) + target_link_libraries(jxl_extras-static PUBLIC sjpeg) +endif () + +if (JPEGXL_ENABLE_OPENEXR) +pkg_check_modules(OpenEXR IMPORTED_TARGET OpenEXR) +if (OpenEXR_FOUND) + target_sources(jxl_extras-static PRIVATE + extras/codec_exr.cc + extras/codec_exr.h + ) + target_compile_definitions(jxl_extras-static PUBLIC -DJPEGXL_ENABLE_EXR=1) + target_link_libraries(jxl_extras-static PUBLIC PkgConfig::OpenEXR) + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libopenexr-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.libopenexr COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR + # OpenEXR generates exceptions, so we need exception support to catch them. + # Actually those flags counteract the ones set in JPEGXL_INTERNAL_FLAGS. + if (NOT WIN32) + set_source_files_properties(extras/codec_exr.cc PROPERTIES COMPILE_FLAGS -fexceptions) + if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + set_source_files_properties(extras/codec_exr.cc PROPERTIES COMPILE_FLAGS -fcxx-exceptions) + endif() + endif() +endif() # OpenEXR_FOUND +endif() # JPEGXL_ENABLE_OPENEXR diff --git a/lib/jxl_profiler.cmake b/lib/jxl_profiler.cmake new file mode 100644 index 0000000..8faa626 --- /dev/null +++ b/lib/jxl_profiler.cmake @@ -0,0 +1,31 @@ +# 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. + +set(JPEGXL_PROFILER_SOURCES + profiler/profiler.cc + profiler/profiler.h + profiler/tsc_timer.h +) + +### Static library. +add_library(jxl_profiler STATIC ${JPEGXL_PROFILER_SOURCES}) +target_link_libraries(jxl_profiler PUBLIC hwy) + +target_compile_options(jxl_profiler PRIVATE ${JPEGXL_INTERNAL_FLAGS}) +target_compile_options(jxl_profiler PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +set_property(TARGET jxl_profiler PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories(jxl_profiler + PRIVATE "${PROJECT_SOURCE_DIR}") + +set_target_properties(jxl_profiler PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 +) + +# Make every library linking against the jxl_profiler define this macro to +# enable the profiler. +target_compile_definitions(jxl_profiler + PUBLIC -DPROFILER_ENABLED=1) diff --git a/lib/jxl_tests.cmake b/lib/jxl_tests.cmake new file mode 100644 index 0000000..06ef83e --- /dev/null +++ b/lib/jxl_tests.cmake @@ -0,0 +1,137 @@ +# 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. + +set(TEST_FILES + extras/codec_pgx_test.cc + extras/codec_test.cc + extras/color_description_test.cc + jxl/ac_strategy_test.cc + jxl/adaptive_reconstruction_test.cc + jxl/alpha_test.cc + jxl/ans_common_test.cc + jxl/ans_test.cc + jxl/bit_reader_test.cc + jxl/bits_test.cc + jxl/blending_test.cc + jxl/butteraugli_test.cc + jxl/byte_order_test.cc + jxl/coeff_order_test.cc + jxl/color_encoding_internal_test.cc + jxl/color_management_test.cc + jxl/compressed_image_test.cc + jxl/convolve_test.cc + jxl/data_parallel_test.cc + jxl/dct_test.cc + jxl/decode_test.cc + jxl/descriptive_statistics_test.cc + jxl/enc_external_image_test.cc + jxl/enc_photon_noise_test.cc + jxl/encode_test.cc + jxl/entropy_coder_test.cc + jxl/fast_math_test.cc + jxl/fields_test.cc + jxl/filters_internal_test.cc + jxl/gaborish_test.cc + jxl/gamma_correct_test.cc + jxl/gauss_blur_test.cc + jxl/gradient_test.cc + jxl/iaca_test.cc + jxl/icc_codec_test.cc + jxl/image_bundle_test.cc + jxl/image_ops_test.cc + jxl/jxl_test.cc + jxl/lehmer_code_test.cc + jxl/linalg_test.cc + jxl/modular_test.cc + jxl/opsin_image_test.cc + jxl/opsin_inverse_test.cc + jxl/optimize_test.cc + jxl/padded_bytes_test.cc + jxl/passes_test.cc + jxl/patch_dictionary_test.cc + jxl/preview_test.cc + jxl/quant_weights_test.cc + jxl/quantizer_test.cc + jxl/rational_polynomial_test.cc + jxl/robust_statistics_test.cc + jxl/roundtrip_test.cc + jxl/speed_tier_test.cc + jxl/splines_test.cc + jxl/toc_test.cc + jxl/xorshift128plus_test.cc + threads/thread_parallel_runner_test.cc + ### Files before this line are handled by build_cleaner.py + # TODO(deymo): Move this to tools/ + ../tools/box/box_test.cc +) + +# Test-only library code. +set(TESTLIB_FILES + jxl/dct_for_test.h + jxl/dec_transforms_testonly.cc + jxl/dec_transforms_testonly.h + jxl/fake_parallel_runner_testonly.h + jxl/image_test_utils.h + jxl/test_utils.h + jxl/testdata.h +) + +find_package(GTest) + +# Library with test-only code shared between all tests. +add_library(jxl_testlib-static STATIC ${TESTLIB_FILES}) + target_compile_options(jxl_testlib-static PRIVATE + ${JPEGXL_INTERNAL_FLAGS} + ${JPEGXL_COVERAGE_FLAGS} + ) +target_compile_definitions(jxl_testlib-static PUBLIC + -DTEST_DATA_PATH="${PROJECT_SOURCE_DIR}/third_party/testdata") +target_include_directories(jxl_testlib-static PUBLIC + "${PROJECT_SOURCE_DIR}" +) +target_link_libraries(jxl_testlib-static hwy) + +# Individual test binaries: +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests) +foreach (TESTFILE IN LISTS TEST_FILES) + # The TESTNAME is the name without the extension or directory. + get_filename_component(TESTNAME ${TESTFILE} NAME_WE) + add_executable(${TESTNAME} ${TESTFILE}) + if(JPEGXL_EMSCRIPTEN) + # The emscripten linking step takes too much memory and crashes during the + # wasm-opt step when using -O2 optimization level + set_target_properties(${TESTNAME} PROPERTIES LINK_FLAGS "\ + -O1 \ + -s TOTAL_MEMORY=1536MB \ + -s SINGLE_FILE=1 \ + ") + endif() + target_compile_options(${TESTNAME} PRIVATE + ${JPEGXL_INTERNAL_FLAGS} + # Add coverage flags to the test binary so code in the private headers of + # the library is also instrumented when running tests that execute it. + ${JPEGXL_COVERAGE_FLAGS} + ) + target_link_libraries(${TESTNAME} + box + jxl-static + jxl_threads-static + jxl_extras-static + jxl_testlib-static + gmock + GTest::GTest + GTest::Main + ) + # Output test targets in the test directory. + set_target_properties(${TESTNAME} PROPERTIES PREFIX "tests/") + if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + set_target_properties(${TESTNAME} PROPERTIES COMPILE_FLAGS "-Wno-error") + endif () + if(${CMAKE_VERSION} VERSION_LESS "3.10.3") + gtest_discover_tests(${TESTNAME} TIMEOUT 240) + else () + gtest_discover_tests(${TESTNAME} DISCOVERY_TIMEOUT 240) + endif () +endforeach () diff --git a/lib/jxl_threads.cmake b/lib/jxl_threads.cmake new file mode 100644 index 0000000..85ceda4 --- /dev/null +++ b/lib/jxl_threads.cmake @@ -0,0 +1,100 @@ +# 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. + +find_package(Threads REQUIRED) + +set(JPEGXL_THREADS_SOURCES + threads/resizable_parallel_runner.cc + threads/thread_parallel_runner.cc + threads/thread_parallel_runner_internal.cc + threads/thread_parallel_runner_internal.h +) + +### Define the jxl_threads shared or static target library. The ${target} +# parameter should already be created with add_library(), but this function +# sets all the remaining common properties. +function(_set_jxl_threads _target) + +target_compile_options(${_target} PRIVATE ${JPEGXL_INTERNAL_FLAGS}) +target_compile_options(${_target} PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +set_property(TARGET ${_target} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories(${_target} + PRIVATE + "${PROJECT_SOURCE_DIR}" + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include") + +target_link_libraries(${_target} + PUBLIC ${JPEGXL_COVERAGE_FLAGS} Threads::Threads +) + +set_target_properties(${_target} PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 + DEFINE_SYMBOL JXL_THREADS_INTERNAL_LIBRARY_BUILD +) + +# Always install the library as jxl_threads.{a,so} file without the "-static" +# suffix, except in Windows. +if (NOT WIN32) + set_target_properties(${_target} PROPERTIES OUTPUT_NAME "jxl_threads") +endif() +install(TARGETS ${_target} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +endfunction() + + +### Static library. +add_library(jxl_threads-static STATIC ${JPEGXL_THREADS_SOURCES}) +_set_jxl_threads(jxl_threads-static) + +# Make jxl_threads symbols neither imported nor exported when using the static +# library. These will have hidden visibility anyway in the static library case +# in unix. +target_compile_definitions(jxl_threads-static + PUBLIC -DJXL_THREADS_STATIC_DEFINE) + + +### Public shared library. +if (((NOT DEFINED "${TARGET_SUPPORTS_SHARED_LIBS}") OR + TARGET_SUPPORTS_SHARED_LIBS) AND NOT JPEGXL_STATIC) +add_library(jxl_threads SHARED ${JPEGXL_THREADS_SOURCES}) +_set_jxl_threads(jxl_threads) + +set_target_properties(jxl_threads PROPERTIES + VERSION ${JPEGXL_LIBRARY_VERSION} + SOVERSION ${JPEGXL_LIBRARY_SOVERSION} + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") + +# Compile the shared library such that the JXL_THREADS_EXPORT symbols are +# exported. Users of the library will not set this flag and therefore import +# those symbols. +target_compile_definitions(jxl_threads + PRIVATE -DJXL_THREADS_INTERNAL_LIBRARY_BUILD) + +# Generate the jxl/jxl_threads_export.h header, we only need to generate it once +# but we can use it from both libraries. +generate_export_header(jxl_threads + BASE_NAME JXL_THREADS + EXPORT_FILE_NAME include/jxl/jxl_threads_export.h) +else() +add_library(jxl_threads ALIAS jxl_threads-static) +# When not building the shared library generate the jxl_threads_export.h header +# only based on the static target. +generate_export_header(jxl_threads-static + BASE_NAME JXL_THREADS + EXPORT_FILE_NAME include/jxl/jxl_threads_export.h) +endif() # TARGET_SUPPORTS_SHARED_LIBS AND NOT JPEGXL_STATIC + + +### Add a pkg-config file for libjxl_threads. +set(JPEGXL_THREADS_LIBRARY_REQUIRES "") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/threads/libjxl_threads.pc.in" + "libjxl_threads.pc" @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libjxl_threads.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") diff --git a/lib/lib.gni b/lib/lib.gni new file mode 100644 index 0000000..e4da742 --- /dev/null +++ b/lib/lib.gni @@ -0,0 +1,435 @@ +# 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. + +# Source files definitions for GN-based build systems. + +# Library version macros +libjxl_version_defines = [ + "JPEGXL_MAJOR_VERSION=0", + "JPEGXL_MINOR_VERSION=6", + "JPEGXL_PATCH_VERSION=1", +] + +libjxl_public_headers = [ + "include/jxl/butteraugli.h", + "include/jxl/butteraugli_cxx.h", + "include/jxl/codestream_header.h", + "include/jxl/color_encoding.h", + "include/jxl/decode.h", + "include/jxl/decode_cxx.h", + "include/jxl/encode.h", + "include/jxl/encode_cxx.h", + "include/jxl/memory_manager.h", + "include/jxl/parallel_runner.h", + "include/jxl/types.h", +] + +libjxl_dec_sources = [ + "jxl/ac_context.h", + "jxl/ac_strategy.cc", + "jxl/ac_strategy.h", + "jxl/alpha.cc", + "jxl/alpha.h", + "jxl/ans_common.cc", + "jxl/ans_common.h", + "jxl/ans_params.h", + "jxl/aux_out.cc", + "jxl/aux_out.h", + "jxl/aux_out_fwd.h", + "jxl/base/arch_macros.h", + "jxl/base/bits.h", + "jxl/base/byte_order.h", + "jxl/base/cache_aligned.cc", + "jxl/base/cache_aligned.h", + "jxl/base/compiler_specific.h", + "jxl/base/data_parallel.cc", + "jxl/base/data_parallel.h", + "jxl/base/descriptive_statistics.cc", + "jxl/base/descriptive_statistics.h", + "jxl/base/file_io.h", + "jxl/base/iaca.h", + "jxl/base/os_macros.h", + "jxl/base/override.h", + "jxl/base/padded_bytes.cc", + "jxl/base/padded_bytes.h", + "jxl/base/profiler.h", + "jxl/base/robust_statistics.h", + "jxl/base/span.h", + "jxl/base/status.cc", + "jxl/base/status.h", + "jxl/base/thread_pool_internal.h", + "jxl/blending.cc", + "jxl/blending.h", + "jxl/chroma_from_luma.cc", + "jxl/chroma_from_luma.h", + "jxl/codec_in_out.h", + "jxl/coeff_order.cc", + "jxl/coeff_order.h", + "jxl/coeff_order_fwd.h", + "jxl/color_encoding_internal.cc", + "jxl/color_encoding_internal.h", + "jxl/color_management.cc", + "jxl/color_management.h", + "jxl/common.h", + "jxl/compressed_dc.cc", + "jxl/compressed_dc.h", + "jxl/convolve-inl.h", + "jxl/convolve.cc", + "jxl/convolve.h", + "jxl/dct-inl.h", + "jxl/dct_block-inl.h", + "jxl/dct_scales.cc", + "jxl/dct_scales.h", + "jxl/dct_util.h", + "jxl/dec_ans.cc", + "jxl/dec_ans.h", + "jxl/dec_bit_reader.h", + "jxl/dec_cache.cc", + "jxl/dec_cache.h", + "jxl/dec_context_map.cc", + "jxl/dec_context_map.h", + "jxl/dec_external_image.cc", + "jxl/dec_external_image.h", + "jxl/dec_frame.cc", + "jxl/dec_frame.h", + "jxl/dec_group.cc", + "jxl/dec_group.h", + "jxl/dec_group_border.cc", + "jxl/dec_group_border.h", + "jxl/dec_huffman.cc", + "jxl/dec_huffman.h", + "jxl/dec_modular.cc", + "jxl/dec_modular.h", + "jxl/dec_noise.cc", + "jxl/dec_noise.h", + "jxl/dec_params.h", + "jxl/dec_patch_dictionary.cc", + "jxl/dec_patch_dictionary.h", + "jxl/dec_reconstruct.cc", + "jxl/dec_reconstruct.h", + "jxl/dec_render_pipeline.h", + "jxl/dec_transforms-inl.h", + "jxl/dec_upsample.cc", + "jxl/dec_upsample.h", + "jxl/dec_xyb-inl.h", + "jxl/dec_xyb.cc", + "jxl/dec_xyb.h", + "jxl/decode.cc", + "jxl/decode_to_jpeg.cc", + "jxl/decode_to_jpeg.h", + "jxl/enc_bit_writer.cc", + "jxl/enc_bit_writer.h", + "jxl/entropy_coder.cc", + "jxl/entropy_coder.h", + "jxl/epf.cc", + "jxl/epf.h", + "jxl/fast_math-inl.h", + "jxl/field_encodings.h", + "jxl/fields.cc", + "jxl/fields.h", + "jxl/filters.cc", + "jxl/filters.h", + "jxl/filters_internal.h", + "jxl/frame_header.cc", + "jxl/frame_header.h", + "jxl/gauss_blur.cc", + "jxl/gauss_blur.h", + "jxl/headers.cc", + "jxl/headers.h", + "jxl/huffman_table.cc", + "jxl/huffman_table.h", + "jxl/icc_codec.cc", + "jxl/icc_codec.h", + "jxl/icc_codec_common.cc", + "jxl/icc_codec_common.h", + "jxl/image.cc", + "jxl/image.h", + "jxl/image_bundle.cc", + "jxl/image_bundle.h", + "jxl/image_metadata.cc", + "jxl/image_metadata.h", + "jxl/image_ops.h", + "jxl/jpeg/dec_jpeg_data.cc", + "jxl/jpeg/dec_jpeg_data.h", + "jxl/jpeg/dec_jpeg_data_writer.cc", + "jxl/jpeg/dec_jpeg_data_writer.h", + "jxl/jpeg/dec_jpeg_output_chunk.h", + "jxl/jpeg/dec_jpeg_serialization_state.h", + "jxl/jpeg/jpeg_data.cc", + "jxl/jpeg/jpeg_data.h", + "jxl/jxl_inspection.h", + "jxl/lehmer_code.h", + "jxl/linalg.h", + "jxl/loop_filter.cc", + "jxl/loop_filter.h", + "jxl/luminance.cc", + "jxl/luminance.h", + "jxl/memory_manager_internal.cc", + "jxl/memory_manager_internal.h", + "jxl/modular/encoding/context_predict.h", + "jxl/modular/encoding/dec_ma.cc", + "jxl/modular/encoding/dec_ma.h", + "jxl/modular/encoding/encoding.cc", + "jxl/modular/encoding/encoding.h", + "jxl/modular/encoding/ma_common.h", + "jxl/modular/modular_image.cc", + "jxl/modular/modular_image.h", + "jxl/modular/options.h", + "jxl/modular/transform/palette.h", + "jxl/modular/transform/rct.h", + "jxl/modular/transform/squeeze.cc", + "jxl/modular/transform/squeeze.h", + "jxl/modular/transform/transform.cc", + "jxl/modular/transform/transform.h", + "jxl/noise.h", + "jxl/noise_distributions.h", + "jxl/opsin_params.cc", + "jxl/opsin_params.h", + "jxl/passes_state.cc", + "jxl/passes_state.h", + "jxl/patch_dictionary_internal.h", + "jxl/quant_weights.cc", + "jxl/quant_weights.h", + "jxl/quantizer-inl.h", + "jxl/quantizer.cc", + "jxl/quantizer.h", + "jxl/rational_polynomial-inl.h", + "jxl/sanitizers.h", + "jxl/splines.cc", + "jxl/splines.h", + "jxl/toc.cc", + "jxl/toc.h", + "jxl/transfer_functions-inl.h", + "jxl/transpose-inl.h", + "jxl/xorshift128plus-inl.h", +] + +libjxl_enc_sources = [ + "jxl/butteraugli/butteraugli.cc", + "jxl/butteraugli/butteraugli.h", + "jxl/butteraugli_wrapper.cc", + "jxl/dec_file.cc", + "jxl/dec_file.h", + "jxl/enc_ac_strategy.cc", + "jxl/enc_ac_strategy.h", + "jxl/enc_adaptive_quantization.cc", + "jxl/enc_adaptive_quantization.h", + "jxl/enc_ans.cc", + "jxl/enc_ans.h", + "jxl/enc_ans_params.h", + "jxl/enc_ar_control_field.cc", + "jxl/enc_ar_control_field.h", + "jxl/enc_butteraugli_comparator.cc", + "jxl/enc_butteraugli_comparator.h", + "jxl/enc_butteraugli_pnorm.cc", + "jxl/enc_butteraugli_pnorm.h", + "jxl/enc_cache.cc", + "jxl/enc_cache.h", + "jxl/enc_chroma_from_luma.cc", + "jxl/enc_chroma_from_luma.h", + "jxl/enc_cluster.cc", + "jxl/enc_cluster.h", + "jxl/enc_coeff_order.cc", + "jxl/enc_coeff_order.h", + "jxl/enc_color_management.cc", + "jxl/enc_color_management.h", + "jxl/enc_comparator.cc", + "jxl/enc_comparator.h", + "jxl/enc_context_map.cc", + "jxl/enc_context_map.h", + "jxl/enc_detect_dots.cc", + "jxl/enc_detect_dots.h", + "jxl/enc_dot_dictionary.cc", + "jxl/enc_dot_dictionary.h", + "jxl/enc_entropy_coder.cc", + "jxl/enc_entropy_coder.h", + "jxl/enc_external_image.cc", + "jxl/enc_external_image.h", + "jxl/enc_fast_heuristics.cc", + "jxl/enc_file.cc", + "jxl/enc_file.h", + "jxl/enc_frame.cc", + "jxl/enc_frame.h", + "jxl/enc_gamma_correct.h", + "jxl/enc_group.cc", + "jxl/enc_group.h", + "jxl/enc_heuristics.cc", + "jxl/enc_heuristics.h", + "jxl/enc_huffman.cc", + "jxl/enc_huffman.h", + "jxl/enc_icc_codec.cc", + "jxl/enc_icc_codec.h", + "jxl/enc_image_bundle.cc", + "jxl/enc_image_bundle.h", + "jxl/enc_jxl_skcms.h", + "jxl/enc_modular.cc", + "jxl/enc_modular.h", + "jxl/enc_noise.cc", + "jxl/enc_noise.h", + "jxl/enc_params.h", + "jxl/enc_patch_dictionary.cc", + "jxl/enc_patch_dictionary.h", + "jxl/enc_photon_noise.cc", + "jxl/enc_photon_noise.h", + "jxl/enc_quant_weights.cc", + "jxl/enc_quant_weights.h", + "jxl/enc_splines.cc", + "jxl/enc_splines.h", + "jxl/enc_toc.cc", + "jxl/enc_toc.h", + "jxl/enc_transforms-inl.h", + "jxl/enc_transforms.cc", + "jxl/enc_transforms.h", + "jxl/enc_xyb.cc", + "jxl/enc_xyb.h", + "jxl/encode.cc", + "jxl/encode_internal.h", + "jxl/gaborish.cc", + "jxl/gaborish.h", + "jxl/huffman_tree.cc", + "jxl/huffman_tree.h", + "jxl/jpeg/enc_jpeg_data.cc", + "jxl/jpeg/enc_jpeg_data.h", + "jxl/jpeg/enc_jpeg_data_reader.cc", + "jxl/jpeg/enc_jpeg_data_reader.h", + "jxl/jpeg/enc_jpeg_huffman_decode.cc", + "jxl/jpeg/enc_jpeg_huffman_decode.h", + "jxl/linalg.cc", + "jxl/modular/encoding/enc_encoding.cc", + "jxl/modular/encoding/enc_encoding.h", + "jxl/modular/encoding/enc_ma.cc", + "jxl/modular/encoding/enc_ma.h", + "jxl/modular/transform/enc_palette.cc", + "jxl/modular/transform/enc_palette.h", + "jxl/modular/transform/enc_rct.cc", + "jxl/modular/transform/enc_rct.h", + "jxl/modular/transform/enc_squeeze.cc", + "jxl/modular/transform/enc_squeeze.h", + "jxl/modular/transform/enc_transform.cc", + "jxl/modular/transform/enc_transform.h", + "jxl/optimize.cc", + "jxl/optimize.h", + "jxl/progressive_split.cc", + "jxl/progressive_split.h", +] + +libjxl_gbench_sources = [ + "extras/tone_mapping_gbench.cc", + "jxl/dec_external_image_gbench.cc", + "jxl/enc_external_image_gbench.cc", + "jxl/splines_gbench.cc", + "jxl/tf_gbench.cc", +] + +libjxl_tests_sources = [ + "jxl/ac_strategy_test.cc", + "jxl/adaptive_reconstruction_test.cc", + "jxl/alpha_test.cc", + "jxl/ans_common_test.cc", + "jxl/ans_test.cc", + "jxl/bit_reader_test.cc", + "jxl/bits_test.cc", + "jxl/blending_test.cc", + "jxl/butteraugli_test.cc", + "jxl/byte_order_test.cc", + "jxl/coeff_order_test.cc", + "jxl/color_encoding_internal_test.cc", + "jxl/color_management_test.cc", + "jxl/compressed_image_test.cc", + "jxl/convolve_test.cc", + "jxl/data_parallel_test.cc", + "jxl/dct_test.cc", + "jxl/decode_test.cc", + "jxl/descriptive_statistics_test.cc", + "jxl/enc_external_image_test.cc", + "jxl/enc_photon_noise_test.cc", + "jxl/encode_test.cc", + "jxl/entropy_coder_test.cc", + "jxl/fast_math_test.cc", + "jxl/fields_test.cc", + "jxl/filters_internal_test.cc", + "jxl/gaborish_test.cc", + "jxl/gamma_correct_test.cc", + "jxl/gauss_blur_test.cc", + "jxl/gradient_test.cc", + "jxl/iaca_test.cc", + "jxl/icc_codec_test.cc", + "jxl/image_bundle_test.cc", + "jxl/image_ops_test.cc", + "jxl/jxl_test.cc", + "jxl/lehmer_code_test.cc", + "jxl/linalg_test.cc", + "jxl/modular_test.cc", + "jxl/opsin_image_test.cc", + "jxl/opsin_inverse_test.cc", + "jxl/optimize_test.cc", + "jxl/padded_bytes_test.cc", + "jxl/passes_test.cc", + "jxl/patch_dictionary_test.cc", + "jxl/preview_test.cc", + "jxl/quant_weights_test.cc", + "jxl/quantizer_test.cc", + "jxl/rational_polynomial_test.cc", + "jxl/robust_statistics_test.cc", + "jxl/roundtrip_test.cc", + "jxl/speed_tier_test.cc", + "jxl/splines_test.cc", + "jxl/toc_test.cc", + "jxl/xorshift128plus_test.cc", +] + +# Test-only library code. +libjxl_testlib_sources = [ + "jxl/dct_for_test.h", + "jxl/dec_transforms_testonly.cc", + "jxl/dec_transforms_testonly.h", + "jxl/fake_parallel_runner_testonly.h", + "jxl/image_test_utils.h", + "jxl/test_utils.h", + "jxl/testdata.h", +] + +libjxl_extras_sources = [ + "extras/codec.cc", + "extras/codec.h", + "extras/codec_jpg.cc", + "extras/codec_jpg.h", + "extras/codec_pgx.cc", + "extras/codec_pgx.h", + "extras/codec_png.cc", + "extras/codec_png.h", + "extras/codec_pnm.cc", + "extras/codec_pnm.h", + "extras/codec_psd.cc", + "extras/codec_psd.h", + "extras/color_description.cc", + "extras/color_description.h", + "extras/color_hints.cc", + "extras/color_hints.h", + "extras/time.cc", + "extras/time.h", + "extras/tone_mapping.cc", + "extras/tone_mapping.h", +] + +libjxl_threads_sources = [ + "threads/resizable_parallel_runner.cc", + "threads/thread_parallel_runner.cc", + "threads/thread_parallel_runner_internal.cc", + "threads/thread_parallel_runner_internal.h", +] + +libjxl_threads_public_headers = [ + "include/jxl/resizable_parallel_runner.h", + "include/jxl/resizable_parallel_runner_cxx.h", + "include/jxl/thread_parallel_runner.h", + "include/jxl/thread_parallel_runner_cxx.h", +] + +libjxl_profiler_sources = [ + "profiler/profiler.cc", + "profiler/profiler.h", + "profiler/tsc_timer.h", +] diff --git a/lib/profiler/profiler.cc b/lib/profiler/profiler.cc new file mode 100644 index 0000000..d21ee09 --- /dev/null +++ b/lib/profiler/profiler.cc @@ -0,0 +1,459 @@ +// 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/profiler.h" + +#if PROFILER_ENABLED + +#include +#include +#include // memcpy + +#include // sort +#include +#include // PRIu64 +#include +#include + +#include "lib/jxl/base/robust_statistics.h" // HalfSampleMode + +// Optionally use SIMD in StreamCacheLine if available. +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "lib/profiler/profiler.cc" +#include +#include + +HWY_BEFORE_NAMESPACE(); +namespace profiler { +namespace HWY_NAMESPACE { + +// Overwrites `to` without loading it into cache (read-for-ownership). +// Copies 64 bytes from/to naturally aligned addresses. +void StreamCacheLine(const Packet* HWY_RESTRICT from, Packet* HWY_RESTRICT to) { +#if HWY_TARGET == HWY_SCALAR + hwy::CopyBytes<64>(from, to); +#else + const HWY_CAPPED(uint64_t, 2) d; + HWY_FENCE; + const uint64_t* HWY_RESTRICT from64 = reinterpret_cast(from); + const auto v0 = Load(d, from64 + 0); + const auto v1 = Load(d, from64 + 2); + const auto v2 = Load(d, from64 + 4); + const auto v3 = Load(d, from64 + 6); + // Fences prevent the compiler from reordering loads/stores, which may + // interfere with write-combining. + HWY_FENCE; + uint64_t* HWY_RESTRICT to64 = reinterpret_cast(to); + Stream(v0, d, to64 + 0); + Stream(v1, d, to64 + 2); + Stream(v2, d, to64 + 4); + Stream(v3, d, to64 + 6); + HWY_FENCE; +#endif +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace profiler +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace profiler { + +HWY_EXPORT(StreamCacheLine); + +namespace { + +// How many mebibytes to allocate (if PROFILER_ENABLED) per thread that +// enters at least one zone. Once this buffer is full, the thread will analyze +// packets (two per zone), which introduces observer overhead. +#ifndef PROFILER_THREAD_STORAGE +#define PROFILER_THREAD_STORAGE 32ULL +#endif + +#define PROFILER_PRINT_OVERHEAD 0 + +// Upper bounds for fixed-size data structures (guarded via HWY_ASSERT): +constexpr size_t kMaxDepth = 64; // Maximum nesting of zones. +constexpr size_t kMaxZones = 256; // Total number of zones. + +// Stack of active (entered but not exited) zones. POD, uninitialized. +// Used to deduct child duration from the parent's self time. +struct ActiveZone { + const char* name; + uint64_t entry_timestamp; + uint64_t child_total; +}; + +// Totals for all Zones with the same name. POD, must be zero-initialized. +struct ZoneTotals { + uint64_t total_duration; + const char* name; + uint64_t num_calls; +}; + +template +inline T ClampedSubtract(const T minuend, const T subtrahend) { + if (subtrahend > minuend) { + return 0; + } + return minuend - subtrahend; +} + +} // namespace + +// Per-thread call graph (stack) and ZoneTotals for each zone. +class Results { + public: + Results() { + // Zero-initialize all accumulators (avoids a check for num_zones_ == 0). + memset(zones_, 0, sizeof(zones_)); + } + + // Used for computing overhead when this thread encounters its first Zone. + // This has no observable effect apart from increasing "analyze_elapsed_". + uint64_t ZoneDuration(const Packet* packets) { + HWY_ASSERT(depth_ == 0); + HWY_ASSERT(num_zones_ == 0); + AnalyzePackets(packets, 2); + const uint64_t duration = zones_[0].total_duration; + zones_[0].num_calls = 0; + zones_[0].total_duration = 0; + HWY_ASSERT(depth_ == 0); + num_zones_ = 0; + return duration; + } + + void SetSelfOverhead(const uint64_t self_overhead) { + self_overhead_ = self_overhead; + } + + void SetChildOverhead(const uint64_t child_overhead) { + child_overhead_ = child_overhead; + } + + // Draw all required information from the packets, which can be discarded + // afterwards. Called whenever this thread's storage is full. + void AnalyzePackets(const Packet* HWY_RESTRICT packets, + const size_t num_packets) { + // Ensures prior weakly-ordered streaming stores are globally visible. + hwy::StoreFence(); + + const uint64_t t0 = TicksBefore(); + + for (size_t i = 0; i < num_packets; ++i) { + const uint64_t timestamp = packets[i].timestamp; + // Entering a zone + if (packets[i].name != nullptr) { + HWY_ASSERT(depth_ < kMaxDepth); + zone_stack_[depth_].name = packets[i].name; + zone_stack_[depth_].entry_timestamp = timestamp; + zone_stack_[depth_].child_total = 0; + ++depth_; + continue; + } + + HWY_ASSERT(depth_ != 0); + const ActiveZone& active = zone_stack_[depth_ - 1]; + const uint64_t duration = timestamp - active.entry_timestamp; + const uint64_t self_duration = ClampedSubtract( + duration, self_overhead_ + child_overhead_ + active.child_total); + + UpdateOrAdd(active.name, 1, self_duration); + --depth_; + + // "Deduct" the nested time from its parent's self_duration. + if (depth_ != 0) { + zone_stack_[depth_ - 1].child_total += duration + child_overhead_; + } + } + + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0; + } + + // Incorporates results from another thread. Call after all threads have + // exited any zones. + void Assimilate(const Results& other) { + const uint64_t t0 = TicksBefore(); + HWY_ASSERT(depth_ == 0); + HWY_ASSERT(other.depth_ == 0); + + for (size_t i = 0; i < other.num_zones_; ++i) { + const ZoneTotals& zone = other.zones_[i]; + UpdateOrAdd(zone.name, zone.num_calls, zone.total_duration); + } + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0 + other.analyze_elapsed_; + } + + // Single-threaded. + void Print() { + const uint64_t t0 = TicksBefore(); + MergeDuplicates(); + + // Sort by decreasing total (self) cost. + std::sort(zones_, zones_ + num_zones_, + [](const ZoneTotals& r1, const ZoneTotals& r2) { + return r1.total_duration > r2.total_duration; + }); + + uint64_t total_visible_duration = 0; + for (size_t i = 0; i < num_zones_; ++i) { + const ZoneTotals& r = zones_[i]; + if (r.name[0] != '@') { + total_visible_duration += r.total_duration; + printf("%-40s: %10" PRIu64 " x %15" PRIu64 "= %15" PRIu64 "\n", r.name, + r.num_calls, r.total_duration / r.num_calls, r.total_duration); + } + } + + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0; + printf("Total clocks during analysis: %" PRIu64 "\n", analyze_elapsed_); + printf("Total clocks measured: %" PRIu64 "\n", total_visible_duration); + } + + // Single-threaded. Clears all results as if no zones had been recorded. + void Reset() { + analyze_elapsed_ = 0; + HWY_ASSERT(depth_ == 0); + num_zones_ = 0; + memset(zone_stack_, 0, sizeof(zone_stack_)); + memset(zones_, 0, sizeof(zones_)); + } + + private: + // Updates ZoneTotals of the same name, or inserts a new one if this thread + // has not yet seen that name. Uses a self-organizing list data structure, + // which avoids dynamic memory allocations and is faster than unordered_map. + void UpdateOrAdd(const char* name, const uint64_t num_calls, + const uint64_t duration) { + // Special case for first zone: (maybe) update, without swapping. + if (zones_[0].name == name) { + zones_[0].total_duration += duration; + zones_[0].num_calls += num_calls; + return; + } + + // Look for a zone with the same name. + for (size_t i = 1; i < num_zones_; ++i) { + if (zones_[i].name == name) { + zones_[i].total_duration += duration; + zones_[i].num_calls += num_calls; + // Swap with predecessor (more conservative than move to front, + // but at least as successful). + std::swap(zones_[i - 1], zones_[i]); + return; + } + } + + // Not found; create a new ZoneTotals. + HWY_ASSERT(num_zones_ < kMaxZones); + ZoneTotals* HWY_RESTRICT zone = zones_ + num_zones_; + zone->name = name; + zone->num_calls = num_calls; + zone->total_duration = duration; + ++num_zones_; + } + + // Each instantiation of a function template seems to get its own copy of + // __func__ and GCC doesn't merge them. An N^2 search for duplicates is + // acceptable because we only expect a few dozen zones. + void MergeDuplicates() { + for (size_t i = 0; i < num_zones_; ++i) { + // Add any subsequent duplicates to num_calls and total_duration. + for (size_t j = i + 1; j < num_zones_;) { + if (!strcmp(zones_[i].name, zones_[j].name)) { + zones_[i].num_calls += zones_[j].num_calls; + zones_[i].total_duration += zones_[j].total_duration; + // Fill hole with last item. + zones_[j] = zones_[--num_zones_]; + } else { // Name differed, try next ZoneTotals. + ++j; + } + } + } + } + + uint64_t analyze_elapsed_ = 0; + uint64_t self_overhead_ = 0; + uint64_t child_overhead_ = 0; + + size_t depth_ = 0; // Number of active zones <= kMaxDepth. + size_t num_zones_ = 0; // Number of unique zones <= kMaxZones. + + // After other members to avoid large pointer offsets. + alignas(64) ActiveZone zone_stack_[kMaxDepth]; // Last = newest + alignas(64) ZoneTotals zones_[kMaxZones]; // Self-organizing list +}; + +ThreadSpecific::ThreadSpecific() + : max_packets_(PROFILER_THREAD_STORAGE << 16), // MiB / sizeof(Packet) + packets_(hwy::AllocateAligned(max_packets_)), + num_packets_(0), + results_(hwy::MakeUniqueAligned()) {} + +ThreadSpecific::~ThreadSpecific() {} + +void ThreadSpecific::FlushBuffer() { + if (num_packets_ + kBufferCapacity > max_packets_) { + results_->AnalyzePackets(packets_.get(), num_packets_); + num_packets_ = 0; + } + // This buffering halves observer overhead and decreases the overall + // runtime by about 3%. + HWY_DYNAMIC_DISPATCH(StreamCacheLine) + (buffer_, packets_.get() + num_packets_); + num_packets_ += kBufferCapacity; + buffer_size_ = 0; +} + +void ThreadSpecific::AnalyzeRemainingPackets() { + // Storage full => empty it. + if (num_packets_ + buffer_size_ > max_packets_) { + results_->AnalyzePackets(packets_.get(), num_packets_); + num_packets_ = 0; + } + + // Move buffer to storage + memcpy(packets_.get() + num_packets_, buffer_, buffer_size_ * sizeof(Packet)); + num_packets_ += buffer_size_; + buffer_size_ = 0; + + results_->AnalyzePackets(packets_.get(), num_packets_); + num_packets_ = 0; +} + +void ThreadSpecific::ComputeOverhead() { + // Delay after capturing timestamps before/after the actual zone runs. Even + // with frequency throttling disabled, this has a multimodal distribution, + // including 32, 34, 48, 52, 59, 62. + uint64_t self_overhead; + { + const size_t kNumSamples = 32; + uint32_t samples[kNumSamples]; + for (size_t idx_sample = 0; idx_sample < kNumSamples; ++idx_sample) { + const size_t kNumDurations = 1024; + uint32_t durations[kNumDurations]; + + for (size_t idx_duration = 0; idx_duration < kNumDurations; + ++idx_duration) { + { // + PROFILER_ZONE("Dummy Zone (never shown)"); + } + const uint64_t duration = results_->ZoneDuration(buffer_); + buffer_size_ = 0; + durations[idx_duration] = static_cast(duration); + HWY_ASSERT(num_packets_ == 0); + } + jxl::CountingSort(durations, durations + kNumDurations); + samples[idx_sample] = jxl::HalfSampleMode()(durations, kNumDurations); + } + // Median. + jxl::CountingSort(samples, samples + kNumSamples); + self_overhead = samples[kNumSamples / 2]; +#if PROFILER_PRINT_OVERHEAD + printf("Overhead: %zu\n", self_overhead); +#endif + results_->SetSelfOverhead(self_overhead); + } + + // Delay before capturing start timestamp / after end timestamp. + const size_t kNumSamples = 32; + uint32_t samples[kNumSamples]; + for (size_t idx_sample = 0; idx_sample < kNumSamples; ++idx_sample) { + const size_t kNumDurations = 16; + uint32_t durations[kNumDurations]; + for (size_t idx_duration = 0; idx_duration < kNumDurations; + ++idx_duration) { + const size_t kReps = 10000; + // Analysis time should not be included => must fit within buffer. + HWY_ASSERT(kReps * 2 < max_packets_); + hwy::StoreFence(); + const uint64_t t0 = TicksBefore(); + for (size_t i = 0; i < kReps; ++i) { + PROFILER_ZONE("Dummy"); + } + hwy::StoreFence(); + const uint64_t t1 = TicksAfter(); + HWY_ASSERT(num_packets_ + buffer_size_ == kReps * 2); + buffer_size_ = 0; + num_packets_ = 0; + const uint64_t avg_duration = (t1 - t0 + kReps / 2) / kReps; + durations[idx_duration] = + static_cast(ClampedSubtract(avg_duration, self_overhead)); + } + jxl::CountingSort(durations, durations + kNumDurations); + samples[idx_sample] = jxl::HalfSampleMode()(durations, kNumDurations); + } + jxl::CountingSort(samples, samples + kNumSamples); + const uint64_t child_overhead = samples[9 * kNumSamples / 10]; +#if PROFILER_PRINT_OVERHEAD + printf("Child overhead: %zu\n", child_overhead); +#endif + results_->SetChildOverhead(child_overhead); +} + +namespace { + +// Could be a static member of Zone, but that would expose in header. +std::atomic& GetHead() { + static std::atomic head_{nullptr}; // Owning + return head_; +} + +} // namespace + +// Thread-safe. +ThreadSpecific* Zone::InitThreadSpecific() { + ThreadSpecific* thread_specific = + hwy::MakeUniqueAligned().release(); + + // Insert into unordered list + std::atomic& head = GetHead(); + ThreadSpecific* old_head = head.load(std::memory_order_relaxed); + thread_specific->SetNext(old_head); + while (!head.compare_exchange_weak(old_head, thread_specific, + std::memory_order_release, + std::memory_order_relaxed)) { + thread_specific->SetNext(old_head); + // TODO(janwas): pause + } + + // ComputeOverhead also creates a Zone, so this needs to be set before that + // to prevent infinite recursion. + GetThreadSpecific() = thread_specific; + + thread_specific->ComputeOverhead(); + return thread_specific; +} + +// Single-threaded. +/*static*/ void Zone::PrintResults() { + ThreadSpecific* head = GetHead().load(std::memory_order_relaxed); + ThreadSpecific* p = head; + while (p) { + p->AnalyzeRemainingPackets(); + + // Combine all threads into a single Result. + if (p != head) { + head->GetResults().Assimilate(p->GetResults()); + p->GetResults().Reset(); + } + + p = p->GetNext(); + } + + if (head != nullptr) { + head->GetResults().Print(); + head->GetResults().Reset(); + } +} + +} // namespace profiler + +#endif // HWY_ONCE +#endif // PROFILER_ENABLED diff --git a/lib/profiler/profiler.h b/lib/profiler/profiler.h new file mode 100644 index 0000000..c71f63c --- /dev/null +++ b/lib/profiler/profiler.h @@ -0,0 +1,165 @@ +// 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_PROFILER_PROFILER_H_ +#define LIB_PROFILER_PROFILER_H_ + +// High precision, low overhead time measurements. Returns exact call counts and +// total elapsed time for user-defined 'zones' (code regions, i.e. C++ scopes). +// +// Usage: instrument regions of interest: { PROFILER_ZONE("name"); /*code*/ } or +// void FuncToMeasure() { PROFILER_FUNC; /*code*/ }. +// After all threads have exited any zones, invoke PROFILER_PRINT_RESULTS() to +// print call counts and average durations [CPU cycles] to stdout, sorted in +// descending order of total duration. + +// If zero, this file has no effect and no measurements will be recorded. +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif +#if PROFILER_ENABLED + +#include +#include + +#include +#include + +#include "lib/profiler/tsc_timer.h" + +#if HWY_COMPILER_MSVC +#define PROFILER_PUBLIC +#else +#define PROFILER_PUBLIC __attribute__((visibility("default"))) +#endif + +namespace profiler { + +// Represents zone entry/exit events. POD. +#pragma pack(push, 1) +struct Packet { + // Computing a hash or string table is likely too expensive, and offsets + // from other libraries' string literals can be too large to combine them and + // a full-resolution timestamp into 64 bits. + uint64_t timestamp; + const char* name; // nullptr for exit packets +#if UINTPTR_MAX <= 0xFFFFFFFFu + uint32_t padding; +#endif +}; +#pragma pack(pop) +static_assert(sizeof(Packet) == 16, "Wrong Packet size"); + +class Results; // pImpl + +// Per-thread packet storage, dynamically allocated and aligned. +class ThreadSpecific { + static constexpr size_t kBufferCapacity = 64 / sizeof(Packet); + + public: + PROFILER_PUBLIC explicit ThreadSpecific(); + PROFILER_PUBLIC ~ThreadSpecific(); + + // Depends on Zone => defined out of line. + PROFILER_PUBLIC void ComputeOverhead(); + + HWY_INLINE void WriteEntry(const char* name) { Write(name, TicksBefore()); } + HWY_INLINE void WriteExit() { Write(nullptr, TicksAfter()); } + + PROFILER_PUBLIC void AnalyzeRemainingPackets(); + + // Accessors instead of public member for well-defined data layout. + void SetNext(ThreadSpecific* next) { next_ = next; } + ThreadSpecific* GetNext() const { return next_; } + + Results& GetResults() { return *results_; } + + private: + PROFILER_PUBLIC void FlushBuffer(); + + // Write packet to buffer/storage, emptying them as needed. + void Write(const char* name, const uint64_t timestamp) { + if (buffer_size_ == kBufferCapacity) { // Full + FlushBuffer(); + } + buffer_[buffer_size_].name = name; + buffer_[buffer_size_].timestamp = timestamp; + ++buffer_size_; + } + + // Write-combining buffer to avoid cache pollution. Must be the first + // non-static member to ensure cache-line alignment. + Packet buffer_[kBufferCapacity]; + size_t buffer_size_ = 0; + + // Contiguous storage for zone enter/exit packets. + const size_t max_packets_; + hwy::AlignedFreeUniquePtr packets_; + size_t num_packets_; + + // Linked list of all threads. + ThreadSpecific* next_ = nullptr; // Owned, never released. + + hwy::AlignedUniquePtr results_; +}; + +// RAII zone enter/exit recorder constructed by PROFILER_ZONE; also +// responsible for initializing ThreadSpecific. +class Zone { + public: + HWY_NOINLINE explicit Zone(const char* name) { + HWY_FENCE; + ThreadSpecific* HWY_RESTRICT thread_specific = GetThreadSpecific(); + if (HWY_UNLIKELY(thread_specific == nullptr)) { + thread_specific = InitThreadSpecific(); + } + + thread_specific->WriteEntry(name); + } + + HWY_NOINLINE ~Zone() { GetThreadSpecific()->WriteExit(); } + + // Call exactly once after all threads have exited all zones. + PROFILER_PUBLIC static void PrintResults(); + + private: + // Returns reference to the thread's ThreadSpecific pointer (initially null). + // Function-local static avoids needing a separate definition. + static ThreadSpecific*& GetThreadSpecific() { + static thread_local ThreadSpecific* thread_specific; + return thread_specific; + } + + // Non time-critical. + PROFILER_PUBLIC ThreadSpecific* InitThreadSpecific(); +}; + +// Creates a zone starting from here until the end of the current scope. +// Timestamps will be recorded when entering and exiting the zone. +// To ensure the name pointer remains valid, we require it to be a string +// literal (by merging with ""). We also compare strings by address. +#define PROFILER_ZONE(name) \ + HWY_FENCE; \ + const ::profiler::Zone zone("" name); \ + HWY_FENCE + +// Creates a zone for an entire function (when placed at its beginning). +// Shorter/more convenient than ZONE. +#define PROFILER_FUNC \ + HWY_FENCE; \ + const ::profiler::Zone zone(__func__); \ + HWY_FENCE + +#define PROFILER_PRINT_RESULTS ::profiler::Zone::PrintResults + +} // namespace profiler + +#else // !PROFILER_ENABLED +#define PROFILER_ZONE(name) +#define PROFILER_FUNC +#define PROFILER_PRINT_RESULTS() +#endif + +#endif // LIB_PROFILER_PROFILER_H_ diff --git a/lib/profiler/tsc_timer.h b/lib/profiler/tsc_timer.h new file mode 100644 index 0000000..27db276 --- /dev/null +++ b/lib/profiler/tsc_timer.h @@ -0,0 +1,141 @@ +// 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_PROFILER_TSC_TIMER_H_ +#define LIB_PROFILER_TSC_TIMER_H_ + +// High-resolution (~10 ns) timestamps, using fences to prevent reordering and +// ensure exactly the desired regions are measured. + +#include + +#include +#include +#include // LoadFence + +#if HWY_COMPILER_MSVC +#include +#endif // HWY_COMPILER_MSVC + +namespace profiler { + +// TicksBefore/After return absolute timestamps and must be placed immediately +// before and after the region to measure. The functions are distinct because +// they use different fences. +// +// Background: RDTSC is not 'serializing'; earlier instructions may complete +// after it, and/or later instructions may complete before it. 'Fences' ensure +// regions' elapsed times are independent of such reordering. The only +// documented unprivileged serializing instruction is CPUID, which acts as a +// full fence (no reordering across it in either direction). Unfortunately +// the latency of CPUID varies wildly (perhaps made worse by not initializing +// its EAX input). Because it cannot reliably be deducted from the region's +// elapsed time, it must not be included in the region to measure (i.e. +// between the two RDTSC). +// +// The newer RDTSCP is sometimes described as serializing, but it actually +// only serves as a half-fence with release semantics. Although all +// instructions in the region will complete before the final timestamp is +// captured, subsequent instructions may leak into the region and increase the +// elapsed time. Inserting another fence after the final RDTSCP would prevent +// such reordering without affecting the measured region. +// +// Fortunately, such a fence exists. The LFENCE instruction is only documented +// to delay later loads until earlier loads are visible. However, Intel's +// reference manual says it acts as a full fence (waiting until all earlier +// instructions have completed, and delaying later instructions until it +// completes). AMD assigns the same behavior to MFENCE. +// +// We need a fence before the initial RDTSC to prevent earlier instructions +// from leaking into the region, and arguably another after RDTSC to avoid +// region instructions from completing before the timestamp is recorded. +// When surrounded by fences, the additional RDTSCP half-fence provides no +// benefit, so the initial timestamp can be recorded via RDTSC, which has +// lower overhead than RDTSCP because it does not read TSC_AUX. In summary, +// we define Before = LFENCE/RDTSC/LFENCE; After = RDTSCP/LFENCE. +// +// Using Before+Before leads to higher variance and overhead than After+After. +// However, After+After includes an LFENCE in the region measurements, which +// adds a delay dependent on earlier loads. The combination of Before+After +// is faster than Before+Before and more consistent than Stop+Stop because +// the first LFENCE already delayed subsequent loads before the measured +// region. This combination seems not to have been considered in prior work: +// http://akaros.cs.berkeley.edu/lxr/akaros/kern/arch/x86/rdtsc_test.c +// +// Note: performance counters can measure 'exact' instructions-retired or +// (unhalted) cycle counts. The RDPMC instruction is not serializing and also +// requires fences. Unfortunately, it is not accessible on all OSes and we +// prefer to avoid kernel-mode drivers. Performance counters are also affected +// by several under/over-count errata, so we use the TSC instead. + +// Returns a 64-bit timestamp in unit of 'ticks'; to convert to seconds, +// divide by InvariantTicksPerSecond. Although 32-bit ticks are faster to read, +// they overflow too quickly to measure long regions. +static HWY_INLINE HWY_MAYBE_UNUSED uint64_t TicksBefore() { + uint64_t t; +#if HWY_ARCH_PPC + asm volatile("mfspr %0, %1" : "=r"(t) : "i"(268)); +#elif HWY_ARCH_X86_64 && HWY_COMPILER_MSVC + hwy::LoadFence(); + HWY_FENCE; + t = __rdtsc(); + hwy::LoadFence(); + HWY_FENCE; +#elif HWY_ARCH_X86_64 && (HWY_COMPILER_CLANG || HWY_COMPILER_GCC) + asm volatile( + "lfence\n\t" + "rdtsc\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) + : + // "memory" avoids reordering. rdx = TSC >> 32. + // "cc" = flags modified by SHL. + : "rdx", "memory", "cc"); +#elif HWY_COMPILER_MSVC + // Use std::chrono in MSVC 32-bit. + t = std::chrono::time_point_cast( + std::chrono::steady_clock::now()) + .time_since_epoch() + .count(); +#else + // Fall back to OS - unsure how to reliably query cntvct_el0 frequency. + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + t = ts.tv_sec * 1000000000LL + ts.tv_nsec; +#endif + return t; +} + +static HWY_INLINE HWY_MAYBE_UNUSED uint64_t TicksAfter() { + uint64_t t; +#if HWY_ARCH_X86_64 && HWY_COMPILER_MSVC + HWY_FENCE; + unsigned aux; + t = __rdtscp(&aux); + hwy::LoadFence(); + HWY_FENCE; +#elif HWY_ARCH_X86_64 && (HWY_COMPILER_CLANG || HWY_COMPILER_GCC) + // Use inline asm because __rdtscp generates code to store TSC_AUX (ecx). + asm volatile( + "rdtscp\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) + : + // "memory" avoids reordering. rcx = TSC_AUX. rdx = TSC >> 32. + // "cc" = flags modified by SHL. + : "rcx", "rdx", "memory", "cc"); +#else + t = TicksBefore(); // no difference on other platforms. +#endif + return t; +} + +} // namespace profiler + +#endif // LIB_PROFILER_TSC_TIMER_H_ diff --git a/lib/threads/libjxl_threads.pc.in b/lib/threads/libjxl_threads.pc.in new file mode 100644 index 0000000..8a3275c --- /dev/null +++ b/lib/threads/libjxl_threads.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: libjxl_threads +Description: JPEG XL multi-thread runner using std::threads. +Version: @JPEGXL_LIBRARY_VERSION@ +Requires.private: @JPEGXL_THREADS_LIBRARY_REQUIRES@ +Libs: -L${libdir} -ljxl_threads +Libs.private: -lm +Cflags: -I${includedir} diff --git a/lib/threads/resizable_parallel_runner.cc b/lib/threads/resizable_parallel_runner.cc new file mode 100644 index 0000000..1208a38 --- /dev/null +++ b/lib/threads/resizable_parallel_runner.cc @@ -0,0 +1,195 @@ +// 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 "jxl/resizable_parallel_runner.h" + +#include +#include +#include +#include +#include +#include + +namespace jpegxl { +namespace { + +// A thread pool that allows changing the number of threads it runs. It also +// runs tasks on the calling thread, which can work better on schedulers for +// heterogeneous architectures. +struct ResizeableParallelRunner { + void SetNumThreads(size_t num) { + if (num > 0) { + num -= 1; + } + { + std::unique_lock l(state_mutex_); + num_desired_workers_ = num; + workers_can_proceed_.notify_all(); + } + if (workers_.size() < num) { + for (size_t i = workers_.size(); i < num; i++) { + workers_.emplace_back([this, i]() { WorkerBody(i); }); + } + } + if (workers_.size() > num) { + for (size_t i = num; i < workers_.size(); i++) { + workers_[i].join(); + } + workers_.resize(num); + } + } + + ~ResizeableParallelRunner() { SetNumThreads(0); } + + JxlParallelRetCode Run(void* jxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start, + uint32_t end) { + if (start + 1 == end) { + JxlParallelRetCode ret = init(jxl_opaque, 1); + if (ret != 0) return ret; + + func(jxl_opaque, start, 0); + return ret; + } + + size_t num_workers = std::min(workers_.size() + 1, end - start); + JxlParallelRetCode ret = init(jxl_opaque, num_workers); + if (ret != 0) { + return ret; + } + + { + std::unique_lock l(state_mutex_); + // Avoid waking up more workers than needed. + max_running_workers_ = end - start - 1; + next_task_ = start; + end_task_ = end; + func_ = func; + jxl_opaque_ = jxl_opaque; + work_available_ = true; + num_running_workers_++; + workers_can_proceed_.notify_all(); + } + + DequeueTasks(0); + + while (true) { + std::unique_lock l(state_mutex_); + if (num_running_workers_ == 0) break; + work_done_.wait(l); + } + + return ret; + } + + private: + void WorkerBody(size_t worker_id) { + while (true) { + { + std::unique_lock l(state_mutex_); + // Worker pool was reduced, resize down. + if (worker_id >= num_desired_workers_) { + return; + } + // Nothing to do this time. + if (!work_available_ || worker_id >= max_running_workers_) { + workers_can_proceed_.wait(l); + continue; + } + num_running_workers_++; + } + DequeueTasks(worker_id + 1); + } + } + + void DequeueTasks(size_t thread_id) { + while (true) { + uint32_t task = next_task_++; + if (task >= end_task_) { + std::unique_lock l(state_mutex_); + num_running_workers_--; + work_available_ = false; + if (num_running_workers_ == 0) { + work_done_.notify_all(); + } + break; + } + func_(jxl_opaque_, task, thread_id); + } + } + + // Checks when the worker has something to do, which can be one of: + // - quitting (when worker_id >= num_desired_workers_) + // - having work available for them (work_available_ is true and worker_id >= + // max_running_workers_) + std::condition_variable workers_can_proceed_; + + // Workers are done, and the main thread can proceed (num_running_workers_ == + // 0) + std::condition_variable work_done_; + + std::vector workers_; + + // Protects all the remaining variables, except for func_, jxl_opaque_ and + // end_task_ (for which only the write by the main thread is protected, and + // subsequent uses by workers happen-after it) and next_task_ (which is + // atomic). + std::mutex state_mutex_; + + // Range of tasks still need to be done. + std::atomic next_task_; + uint32_t end_task_; + + // Function to run and its argument. + JxlParallelRunFunction func_; + void* jxl_opaque_; // not owned + + // Variables that control the workers: + // - work_available_ is set to true after a call to Run() and to false at the + // end of it. + // - num_desired_workers_ represents the number of workers that should be + // present. + // - max_running_workers_ represents the number of workers that should be + // executing tasks. + // - num_running_workers_ represents the number of workers that are executing + // tasks. + size_t num_desired_workers_ = 0; + size_t max_running_workers_ = 0; + size_t num_running_workers_ = 0; + bool work_available_ = false; +}; +} // namespace +} // namespace jpegxl + +extern "C" { +JXL_THREADS_EXPORT JxlParallelRetCode JxlResizableParallelRunner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + return static_cast(runner_opaque) + ->Run(jpegxl_opaque, init, func, start_range, end_range); +} + +JXL_THREADS_EXPORT void* JxlResizableParallelRunnerCreate( + const JxlMemoryManager* memory_manager) { + return new jpegxl::ResizeableParallelRunner(); +} + +JXL_THREADS_EXPORT void JxlResizableParallelRunnerSetThreads( + void* runner_opaque, size_t num_threads) { + static_cast(runner_opaque) + ->SetNumThreads(num_threads); +} + +JXL_THREADS_EXPORT void JxlResizableParallelRunnerDestroy(void* runner_opaque) { + delete static_cast(runner_opaque); +} + +JXL_THREADS_EXPORT uint32_t +JxlResizableParallelRunnerSuggestThreads(uint64_t xsize, uint64_t ysize) { + // ~one thread per group. + return std::min(std::thread::hardware_concurrency(), + xsize * ysize / (256 * 256)); +} +} diff --git a/lib/threads/thread_parallel_runner.cc b/lib/threads/thread_parallel_runner.cc new file mode 100644 index 0000000..b9cf4aa --- /dev/null +++ b/lib/threads/thread_parallel_runner.cc @@ -0,0 +1,101 @@ +// 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 "jxl/thread_parallel_runner.h" + +#include + +#include "lib/threads/thread_parallel_runner_internal.h" + +namespace { + +// Default JxlMemoryManager using malloc and free for the jpegxl_threads +// library. Same as the default JxlMemoryManager for the jpegxl library +// itself. + +// Default alloc and free functions. +void* ThreadMemoryManagerDefaultAlloc(void* opaque, size_t size) { + return malloc(size); +} + +void ThreadMemoryManagerDefaultFree(void* opaque, void* address) { + free(address); +} + +// Initializes the memory manager instance with the passed one. The +// MemoryManager passed in |memory_manager| may be NULL or contain NULL +// functions which will be initialized with the default ones. If either alloc +// or free are NULL, then both must be NULL, otherwise this function returns an +// error. +bool ThreadMemoryManagerInit(JxlMemoryManager* self, + const JxlMemoryManager* memory_manager) { + if (memory_manager) { + *self = *memory_manager; + } else { + memset(self, 0, sizeof(*self)); + } + if (!self->alloc != !self->free) { + return false; + } + if (!self->alloc) self->alloc = ThreadMemoryManagerDefaultAlloc; + if (!self->free) self->free = ThreadMemoryManagerDefaultFree; + + return true; +} + +void* ThreadMemoryManagerAlloc(const JxlMemoryManager* memory_manager, + size_t size) { + return memory_manager->alloc(memory_manager->opaque, size); +} + +void ThreadMemoryManagerFree(const JxlMemoryManager* memory_manager, + void* address) { + return memory_manager->free(memory_manager->opaque, address); +} + +} // namespace + +JxlParallelRetCode JxlThreadParallelRunner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + return jpegxl::ThreadParallelRunner::Runner( + runner_opaque, jpegxl_opaque, init, func, start_range, end_range); +} + +/// Starts the given number of worker threads and blocks until they are ready. +/// "num_worker_threads" defaults to one per hyperthread. If zero, all tasks +/// run on the main thread. +void* JxlThreadParallelRunnerCreate(const JxlMemoryManager* memory_manager, + size_t num_worker_threads) { + JxlMemoryManager local_memory_manager; + if (!ThreadMemoryManagerInit(&local_memory_manager, memory_manager)) + return nullptr; + + void* alloc = ThreadMemoryManagerAlloc(&local_memory_manager, + sizeof(jpegxl::ThreadParallelRunner)); + if (!alloc) return nullptr; + // Placement new constructor on allocated memory + jpegxl::ThreadParallelRunner* runner = + new (alloc) jpegxl::ThreadParallelRunner(num_worker_threads); + runner->memory_manager = local_memory_manager; + + return runner; +} + +void JxlThreadParallelRunnerDestroy(void* runner_opaque) { + jpegxl::ThreadParallelRunner* runner = + reinterpret_cast(runner_opaque); + if (runner) { + // Call destructor directly since custom free function is used. + runner->~ThreadParallelRunner(); + ThreadMemoryManagerFree(&runner->memory_manager, runner); + } +} + +// Get default value for num_worker_threads parameter of +// InitJxlThreadParallelRunner. +size_t JxlThreadParallelRunnerDefaultNumWorkerThreads() { + return std::thread::hardware_concurrency(); +} diff --git a/lib/threads/thread_parallel_runner_internal.cc b/lib/threads/thread_parallel_runner_internal.cc new file mode 100644 index 0000000..e868622 --- /dev/null +++ b/lib/threads/thread_parallel_runner_internal.cc @@ -0,0 +1,219 @@ +// 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/threads/thread_parallel_runner_internal.h" + +#include + +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) +#include "sanitizer/common_interface_defs.h" // __sanitizer_print_stack_trace +#endif // defined(*_SANITIZER) + +#include "jxl/thread_parallel_runner.h" +#include "lib/jxl/base/profiler.h" + +namespace { + +// Exits the program after printing a stack trace when possible. +bool Abort() { +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(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 // defined(*_SANITIZER) + +#ifdef _MSC_VER + __debugbreak(); + abort(); +#else + __builtin_trap(); +#endif +} + +// Does not guarantee running the code, use only for debug mode checks. +#if JXL_ENABLE_ASSERT +#define JXL_ASSERT(condition) \ + do { \ + if (!(condition)) { \ + Abort(); \ + } \ + } while (0) +#else +#define JXL_ASSERT(condition) \ + do { \ + } while (0) +#endif +} // namespace + +namespace jpegxl { + +// static +JxlParallelRetCode ThreadParallelRunner::Runner( + void* runner_opaque, void* jpegxl_opaque, JxlParallelRunInit init, + JxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + ThreadParallelRunner* self = + static_cast(runner_opaque); + if (start_range > end_range) return -1; + if (start_range == end_range) return 0; + + int ret = init(jpegxl_opaque, std::max(self->num_worker_threads_, 1)); + if (ret != 0) return ret; + + // Use a sequential run when num_worker_threads_ is zero since we have no + // worker threads. + if (self->num_worker_threads_ == 0) { + const size_t thread = 0; + for (uint32_t task = start_range; task < end_range; ++task) { + func(jpegxl_opaque, task, thread); + } + return 0; + } + + if (self->depth_.fetch_add(1, std::memory_order_acq_rel) != 0) { + return -1; // Must not re-enter. + } + + const WorkerCommand worker_command = + (static_cast(start_range) << 32) + end_range; + // Ensure the inputs do not result in a reserved command. + JXL_ASSERT(worker_command != kWorkerWait); + JXL_ASSERT(worker_command != kWorkerOnce); + JXL_ASSERT(worker_command != kWorkerExit); + + self->data_func_ = func; + self->jpegxl_opaque_ = jpegxl_opaque; + self->num_reserved_.store(0, std::memory_order_relaxed); + + self->StartWorkers(worker_command); + self->WorkersReadyBarrier(); + + if (self->depth_.fetch_add(-1, std::memory_order_acq_rel) != 1) { + return -1; + } + return 0; +} + +// static +void ThreadParallelRunner::RunRange(ThreadParallelRunner* self, + const WorkerCommand command, + const int thread) { + const uint32_t begin = command >> 32; + const uint32_t end = command & 0xFFFFFFFF; + const uint32_t num_tasks = end - begin; + const uint32_t num_worker_threads = self->num_worker_threads_; + + // OpenMP introduced several "schedule" strategies: + // "single" (static assignment of exactly one chunk per thread): slower. + // "dynamic" (allocates k tasks at a time): competitive for well-chosen k. + // "guided" (allocates k tasks, decreases k): computing k = remaining/n + // is faster than halving k each iteration. We prefer this strategy + // because it avoids user-specified parameters. + + for (;;) { +#if 0 + // dynamic + const uint32_t my_size = std::max(num_tasks / (num_worker_threads * 4), 1); +#else + // guided + const uint32_t num_reserved = + self->num_reserved_.load(std::memory_order_relaxed); + // It is possible that more tasks are reserved than ready to run. + const uint32_t num_remaining = + num_tasks - std::min(num_reserved, num_tasks); + const uint32_t my_size = + std::max(num_remaining / (num_worker_threads * 4), 1u); +#endif + const uint32_t my_begin = begin + self->num_reserved_.fetch_add( + my_size, std::memory_order_relaxed); + const uint32_t my_end = std::min(my_begin + my_size, begin + num_tasks); + // Another thread already reserved the last task. + if (my_begin >= my_end) { + break; + } + for (uint32_t task = my_begin; task < my_end; ++task) { + self->data_func_(self->jpegxl_opaque_, task, thread); + } + } +} + +// static +void ThreadParallelRunner::ThreadFunc(ThreadParallelRunner* self, + const int thread) { + // Until kWorkerExit command received: + for (;;) { + std::unique_lock lock(self->mutex_); + // Notify main thread that this thread is ready. + if (++self->workers_ready_ == self->num_threads_) { + self->workers_ready_cv_.notify_one(); + } + RESUME_WAIT: + // Wait for a command. + self->worker_start_cv_.wait(lock); + const WorkerCommand command = self->worker_start_command_; + switch (command) { + case kWorkerWait: // spurious wakeup: + goto RESUME_WAIT; // lock still held, avoid incrementing ready. + case kWorkerOnce: + lock.unlock(); + self->data_func_(self->jpegxl_opaque_, thread, thread); + break; + case kWorkerExit: + return; // exits thread + default: + lock.unlock(); + RunRange(self, command, thread); + break; + } + } +} + +ThreadParallelRunner::ThreadParallelRunner(const int num_worker_threads) +#if defined(__EMSCRIPTEN__) + : num_worker_threads_(0), num_threads_(1) { + // TODO(eustas): find out if pthreads would work for us. + (void)num_worker_threads; +#else + : num_worker_threads_(num_worker_threads), + num_threads_(std::max(num_worker_threads, 1)) { +#endif + PROFILER_ZONE("ThreadParallelRunner ctor"); + + threads_.reserve(num_worker_threads_); + + // Suppress "unused-private-field" warning. + (void)padding1; + (void)padding2; + + // Safely handle spurious worker wakeups. + worker_start_command_ = kWorkerWait; + + for (uint32_t i = 0; i < num_worker_threads_; ++i) { + threads_.emplace_back(ThreadFunc, this, i); + } + + if (num_worker_threads_ != 0) { + WorkersReadyBarrier(); + } + + // Warm up profiler on worker threads so its expensive initialization + // doesn't count towards other timer measurements. + RunOnEachThread( + [](const int task, const int thread) { PROFILER_ZONE("@InitWorkers"); }); +} + +ThreadParallelRunner::~ThreadParallelRunner() { + if (num_worker_threads_ != 0) { + StartWorkers(kWorkerExit); + } + + for (std::thread& thread : threads_) { + JXL_ASSERT(thread.joinable()); + thread.join(); + } +} +} // namespace jpegxl diff --git a/lib/threads/thread_parallel_runner_internal.h b/lib/threads/thread_parallel_runner_internal.h new file mode 100644 index 0000000..372c6a8 --- /dev/null +++ b/lib/threads/thread_parallel_runner_internal.h @@ -0,0 +1,172 @@ +// 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. +// + +// C++ implementation using std::thread of a ::JxlParallelRunner. + +// The main class in this module, ThreadParallelRunner, implements a static +// method ThreadParallelRunner::Runner than can be passed as a +// JxlParallelRunner when using the JPEG XL library. This uses std::thread +// internally and related synchronization functions. The number of threads +// created is fixed at construction time and the threads are re-used for every +// ThreadParallelRunner::Runner call. Only one concurrent Runner() call per +// instance is allowed at a time. +// +// This is a scalable, lower-overhead thread pool runner, especially suitable +// for data-parallel computations in the fork-join model, where clients need to +// know when all tasks have completed. +// +// This thread pool can efficiently load-balance millions of tasks using an +// atomic counter, thus avoiding per-task virtual or system calls. With 48 +// hyperthreads and 1M tasks that add to an atomic counter, overall runtime is +// 10-20x higher when using std::async, and ~200x for a queue-based thread +// pool. +// +// Usage: +// ThreadParallelRunner runner; +// JxlDecode( +// ... , &ThreadParallelRunner::Runner, static_cast(&runner)); + +#ifndef LIB_THREADS_THREAD_PARALLEL_RUNNER_INTERNAL_H_ +#define LIB_THREADS_THREAD_PARALLEL_RUNNER_INTERNAL_H_ + +#include +#include +#include + +#include +#include //NOLINT +#include //NOLINT +#include //NOLINT +#include + +#include "jxl/memory_manager.h" +#include "jxl/parallel_runner.h" + +namespace jpegxl { + +// Main helper class implementing the ::JxlParallelRunner interface. +class ThreadParallelRunner { + public: + // ::JxlParallelRunner interface. + static JxlParallelRetCode Runner(void* runner_opaque, void* jpegxl_opaque, + JxlParallelRunInit init, + JxlParallelRunFunction func, + uint32_t start_range, uint32_t end_range); + + // Starts the given number of worker threads and blocks until they are ready. + // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks + // run on the main thread. + explicit ThreadParallelRunner( + int num_worker_threads = std::thread::hardware_concurrency()); + + // Waits for all threads to exit. + ~ThreadParallelRunner(); + + // Returns number of worker threads created (some may be sleeping and never + // wake up in time to participate in Run). Useful for characterizing + // performance; 0 means "run on main thread". + size_t NumWorkerThreads() const { return num_worker_threads_; } + + // Returns maximum number of main/worker threads that may call Func. Useful + // for allocating per-thread storage. + size_t NumThreads() const { return num_threads_; } + + // Runs func(thread, thread) on all thread(s) that may participate in Run. + // If NumThreads() == 0, runs on the main thread with thread == 0, otherwise + // concurrently called by each worker thread in [0, NumThreads()). + template + void RunOnEachThread(const Func& func) { + if (num_worker_threads_ == 0) { + const int thread = 0; + func(thread, thread); + return; + } + + data_func_ = reinterpret_cast(&CallClosure); + jpegxl_opaque_ = const_cast(static_cast(&func)); + StartWorkers(kWorkerOnce); + WorkersReadyBarrier(); + } + + JxlMemoryManager memory_manager; + + private: + // After construction and between calls to Run, workers are "ready", i.e. + // waiting on worker_start_cv_. They are "started" by sending a "command" + // and notifying all worker_start_cv_ waiters. (That is why all workers + // must be ready/waiting - otherwise, the notification will not reach all of + // them and the main thread waits in vain for them to report readiness.) + using WorkerCommand = uint64_t; + + // Special values; all others encode the begin/end parameters. Note that all + // these are no-op ranges (begin >= end) and therefore never used to encode + // ranges. + static constexpr WorkerCommand kWorkerWait = ~1ULL; + static constexpr WorkerCommand kWorkerOnce = ~2ULL; + static constexpr WorkerCommand kWorkerExit = ~3ULL; + + // Calls f(task, thread). Used for type erasure of Func arguments. The + // signature must match JxlParallelRunFunction, hence a void* argument. + template + static void CallClosure(void* f, const uint32_t task, const size_t thread) { + (*reinterpret_cast(f))(task, thread); + } + + void WorkersReadyBarrier() { + std::unique_lock lock(mutex_); + // Typically only a single iteration. + while (workers_ready_ != threads_.size()) { + workers_ready_cv_.wait(lock); + } + workers_ready_ = 0; + + // Safely handle spurious worker wakeups. + worker_start_command_ = kWorkerWait; + } + + // Precondition: all workers are ready. + void StartWorkers(const WorkerCommand worker_command) { + mutex_.lock(); + worker_start_command_ = worker_command; + // Workers will need this lock, so release it before they wake up. + mutex_.unlock(); + worker_start_cv_.notify_all(); + } + + // Attempts to reserve and perform some work from the global range of tasks, + // which is encoded within "command". Returns after all tasks are reserved. + static void RunRange(ThreadParallelRunner* self, const WorkerCommand command, + const int thread); + + static void ThreadFunc(ThreadParallelRunner* self, int thread); + + // Unmodified after ctor, but cannot be const because we call thread::join(). + std::vector threads_; + + const uint32_t num_worker_threads_; // == threads_.size() + const uint32_t num_threads_; + + std::atomic depth_{0}; // detects if Run is re-entered (not supported). + + std::mutex mutex_; // guards both cv and their variables. + std::condition_variable workers_ready_cv_; + uint32_t workers_ready_ = 0; + std::condition_variable worker_start_cv_; + WorkerCommand worker_start_command_; + + // Written by main thread, read by workers (after mutex lock/unlock). + JxlParallelRunFunction data_func_; + void* jpegxl_opaque_; + + // Updated by workers; padding avoids false sharing. + uint8_t padding1[64]; + std::atomic num_reserved_{0}; + uint8_t padding2[64]; +}; + +} // namespace jpegxl + +#endif // LIB_THREADS_THREAD_PARALLEL_RUNNER_INTERNAL_H_ diff --git a/lib/threads/thread_parallel_runner_test.cc b/lib/threads/thread_parallel_runner_test.cc new file mode 100644 index 0000000..7ff260e --- /dev/null +++ b/lib/threads/thread_parallel_runner_test.cc @@ -0,0 +1,115 @@ +// 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 "gtest/gtest.h" +#include "lib/jxl/base/data_parallel.h" +#include "lib/jxl/base/thread_pool_internal.h" + +namespace jpegxl { +namespace { + +int PopulationCount(uint64_t bits) { + int num_set = 0; + while (bits != 0) { + num_set += bits & 1; + bits >>= 1; + } + return num_set; +} + +// Ensures task parameter is in bounds, every parameter is reached, +// pool can be reused (multiple consecutive Run calls), pool can be destroyed +// (joining with its threads), num_threads=0 works (runs on current thread). +TEST(ThreadParallelRunnerTest, TestPool) { + for (int num_threads = 0; num_threads <= 18; ++num_threads) { + jxl::ThreadPoolInternal pool(num_threads); + for (int num_tasks = 0; num_tasks < 32; ++num_tasks) { + std::vector mementos(num_tasks); + for (int begin = 0; begin < 32; ++begin) { + std::fill(mementos.begin(), mementos.end(), 0); + pool.Run( + begin, begin + num_tasks, jxl::ThreadPool::SkipInit(), + [begin, num_tasks, &mementos](const int task, const int thread) { + // Parameter is in the given range + EXPECT_GE(task, begin); + EXPECT_LT(task, begin + num_tasks); + + // Store mementos to be sure we visited each task. + mementos.at(task - begin) = 1000 + task; + }); + for (int task = begin; task < begin + num_tasks; ++task) { + EXPECT_EQ(1000 + task, mementos.at(task - begin)); + } + } + } + } +} + +// Verify "thread" parameter when processing few tasks. +TEST(ThreadParallelRunnerTest, TestSmallAssignments) { + // WARNING: cumulative total threads must not exceed profiler.h kMaxThreads. + const int kMaxThreads = 8; + for (int num_threads = 1; num_threads <= kMaxThreads; ++num_threads) { + jxl::ThreadPoolInternal pool(num_threads); + + // (Avoid mutex because it may perturb the worker thread scheduling) + std::atomic id_bits{0}; + std::atomic num_calls{0}; + + pool.Run( + 0, num_threads, jxl::ThreadPool::SkipInit(), + [&num_calls, num_threads, &id_bits](const int task, const int thread) { + num_calls.fetch_add(1, std::memory_order_relaxed); + + EXPECT_LT(thread, num_threads); + uint64_t bits = id_bits.load(std::memory_order_relaxed); + while ( + !id_bits.compare_exchange_weak(bits, bits | (1ULL << thread))) { + } + }); + + // Correct number of tasks. + EXPECT_EQ(num_threads, num_calls.load()); + + const int num_participants = PopulationCount(id_bits.load()); + // Can't expect equality because other workers may have woken up too late. + EXPECT_LE(num_participants, num_threads); + } +} + +struct Counter { + Counter() { + // Suppress "unused-field" warning. + (void)padding; + } + void Assimilate(const Counter& victim) { counter += victim.counter; } + int counter = 0; + int padding[31]; +}; + +TEST(ThreadParallelRunnerTest, TestCounter) { + const int kNumThreads = 12; + jxl::ThreadPoolInternal pool(kNumThreads); + alignas(128) Counter counters[kNumThreads]; + + const int kNumTasks = kNumThreads * 19; + pool.Run(0, kNumTasks, jxl::ThreadPool::SkipInit(), + [&counters](const int task, const int thread) { + counters[thread].counter += task; + }); + + int expected = 0; + for (int i = 0; i < kNumTasks; ++i) { + expected += i; + } + + for (int i = 1; i < kNumThreads; ++i) { + counters[0].Assimilate(counters[i]); + } + EXPECT_EQ(expected, counters[0].counter); +} + +} // namespace +} // namespace jpegxl diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..40cdf3f --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,12 @@ +# 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. + +if(NOT WIN32) + add_subdirectory(gdk-pixbuf) +endif() + +add_subdirectory(gimp) + +add_subdirectory(mime) diff --git a/plugins/gdk-pixbuf/CMakeLists.txt b/plugins/gdk-pixbuf/CMakeLists.txt new file mode 100644 index 0000000..0b5c8e2 --- /dev/null +++ b/plugins/gdk-pixbuf/CMakeLists.txt @@ -0,0 +1,76 @@ +# 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. + +find_package(PkgConfig) +pkg_check_modules(Gdk-Pixbuf IMPORTED_TARGET gdk-pixbuf-2.0>=2.36) + +if (NOT Gdk-Pixbuf_FOUND) + message(WARNING "GDK Pixbuf development libraries not found, \ + the Gdk-Pixbuf plugin will not be built") + return () +endif () + +add_library(pixbufloader-jxl SHARED pixbufloader-jxl.c) + +# Mark all symbols as hidden by default. The PkgConfig::Gdk-Pixbuf dependency +# will cause fill_info and fill_vtable entry points to be made public. +set_target_properties(pixbufloader-jxl PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 +) + +# Note: This only needs the decoder library, but we don't install the decoder +# shared library. +target_link_libraries(pixbufloader-jxl jxl jxl_threads skcms-interface PkgConfig::Gdk-Pixbuf) + +pkg_get_variable(GDK_PIXBUF_MODULEDIR gdk-pixbuf-2.0 gdk_pixbuf_moduledir) +install(TARGETS pixbufloader-jxl LIBRARY DESTINATION "${GDK_PIXBUF_MODULEDIR}") + +# Instead of the following, we might instead add the +# mime type image/jxl to +# /usr/share/thumbnailers/gdk-pixbuf-thumbnailer.thumbnailer +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/jxl.thumbnailer DESTINATION share/thumbnailers/) + +if(BUILD_TESTING AND NOT MINGW) + pkg_check_modules(Gdk IMPORTED_TARGET gdk-2.0) + if (Gdk_FOUND) + # Test for loading a .jxl file using the pixbufloader library via GDK. This + # requires to have the image/jxl mime type and loader library configured, + # which we do in a fake environment in the CMAKE_CURRENT_BINARY_DIR. + add_executable(pixbufloader_test pixbufloader_test.cc) + target_link_libraries(pixbufloader_test PkgConfig::Gdk) + + # Create a mime cache for test. + add_custom_command( + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/mime/mime.cache" + COMMAND env XDG_DATA_HOME=${CMAKE_CURRENT_BINARY_DIR} + xdg-mime install --novendor + "${CMAKE_SOURCE_DIR}/plugins/mime/image-jxl.xml" + DEPENDS "${CMAKE_SOURCE_DIR}/plugins/mime/image-jxl.xml" + ) + add_custom_target(pixbufloader_test_mime + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/mime/mime.cache" + ) + add_dependencies(pixbufloader_test pixbufloader_test_mime) + + # Use a fake X server to run the test if xvfb is installed. + find_program (XVFB_PROGRAM xvfb-run) + if(XVFB_PROGRAM) + set(XVFB_PROGRAM_PREFIX "${XVFB_PROGRAM};-a") + else() + set(XVFB_PROGRAM_PREFIX "") + endif() + + add_test( + NAME pixbufloader_test_jxl + COMMAND + ${XVFB_PROGRAM_PREFIX} $ + "${CMAKE_CURRENT_SOURCE_DIR}/loaders_test.cache" + "${CMAKE_SOURCE_DIR}/third_party/testdata/jxl/blending/cropped_traffic_light.jxl" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(pixbufloader_test_jxl PROPERTIES SKIP_RETURN_CODE 254) + endif() # Gdk_FOUND +endif() # BUILD_TESTING diff --git a/plugins/gdk-pixbuf/README.md b/plugins/gdk-pixbuf/README.md new file mode 100644 index 0000000..f7174ba --- /dev/null +++ b/plugins/gdk-pixbuf/README.md @@ -0,0 +1,50 @@ +## JPEG XL GDK Pixbuf + + +The plugin may already have been installed when following the instructions from the +[Installing section of README.md](../../README.md#installing), in which case it should +already be in the correct place, e.g. + +```/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-jxl.so``` + +Otherwise we can copy it manually: + +```bash +sudo cp $your_build_directory/plugins/gdk-pixbuf/libpixbufloader-jxl.so /usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-jxl.so +``` + + +Then we need to update the cache, for example with: + +```bash +sudo /usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders --update-cache +``` + +In order to get thumbnails with this, first one has to add the jxl MIME type, see +[../mime/README.md](../mime/README.md). + +Ensure that the thumbnailer file is installed in the correct place, +`/usr/share/thumbnailers/jxl.thumbnailer` or `/usr/local/share/thumbnailers/jxl.thumbnailer`. + +The file should have been copied automatically when following the instructions +in the [Installing section of README.md](../../README.md#installing), but +otherwise it can be copied manually: + +```bash +sudo cp plugins/gdk-pixbuf/jxl.thumbnailer /usr/local/share/thumbnailers/jxl.thumbnailer +``` + +Update the Mime database with +```bash +update-mime --local +``` +or +```bash +sudo update-desktop-database +``` + +Then possibly delete the thumbnail cache with +```bash +rm -r ~/.cache/thumbnails +``` +and restart the application displaying thumbnails, e.g. `nautilus -q` to display thumbnails. diff --git a/plugins/gdk-pixbuf/jxl.thumbnailer b/plugins/gdk-pixbuf/jxl.thumbnailer new file mode 100644 index 0000000..1bcaab6 --- /dev/null +++ b/plugins/gdk-pixbuf/jxl.thumbnailer @@ -0,0 +1,4 @@ +[Thumbnailer Entry] +TryExec=/usr/bin/gdk-pixbuf-thumbnailer +Exec=/usr/bin/gdk-pixbuf-thumbnailer -s %s %u %o +MimeType=image/jxl; diff --git a/plugins/gdk-pixbuf/loaders_test.cache b/plugins/gdk-pixbuf/loaders_test.cache new file mode 100644 index 0000000..95c62c8 --- /dev/null +++ b/plugins/gdk-pixbuf/loaders_test.cache @@ -0,0 +1,16 @@ +# GdkPixbuf Image Loader Modules file for testing +# Automatically generated file, do not edit +# Created by gdk-pixbuf-query-loaders from gdk-pixbuf-2.42.2 +# +# Generated with: +# GDK_PIXBUF_MODULEDIR=`pwd`/build/plugins/gdk-pixbuf/ gdk-pixbuf-query-loaders +# +# Modified to use the library from the current working directory at runtime. +"./libpixbufloader-jxl.so" +"jxl" 4 "gdk-pixbuf" "JPEG XL image" "BSD-3" +"image/jxl" "" +"jxl" "" +"\377\n" " " 100 +"...\fJXL \r\n\207\n" "zzz " 100 + + diff --git a/plugins/gdk-pixbuf/pixbufloader-jxl.c b/plugins/gdk-pixbuf/pixbufloader-jxl.c new file mode 100644 index 0000000..0a8d9fb --- /dev/null +++ b/plugins/gdk-pixbuf/pixbufloader-jxl.c @@ -0,0 +1,561 @@ +// 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 "jxl/codestream_header.h" +#include "jxl/decode.h" +#include "jxl/resizable_parallel_runner.h" +#include "jxl/types.h" +#include "skcms.h" + +#define GDK_PIXBUF_ENABLE_BACKEND +#include +#undef GDK_PIXBUF_ENABLE_BACKEND + +G_BEGIN_DECLS + +// Information about a single frame. +typedef struct { + uint64_t duration_ms; + GdkPixbuf *data; + gboolean decoded; +} GdkPixbufJxlAnimationFrame; + +// Represent a whole JPEG XL animation; all its fields are owned; as a GObject, +// the Animation struct itself is reference counted (as are the GdkPixbufs for +// individual frames). +struct _GdkPixbufJxlAnimation { + GdkPixbufAnimation parent_instance; + + // GDK interface implementation callbacks. + GdkPixbufModuleSizeFunc image_size_callback; + GdkPixbufModulePreparedFunc pixbuf_prepared_callback; + GdkPixbufModuleUpdatedFunc area_updated_callback; + gpointer user_data; + + // All frames known so far; a frame is added when the JXL_DEC_FRAME event is + // received from the decoder; initially frame.decoded is FALSE, until + // the JXL_DEC_IMAGE event is received. + GArray *frames; + + // JPEG XL decoder and related structures. + JxlParallelRunner *parallel_runner; + JxlDecoder *decoder; + JxlPixelFormat pixel_format; + + // Decoding is `done` when JXL_DEC_SUCCESS is received; calling + // load_increment afterwards gives an error. + gboolean done; + + // Image information. + size_t xsize; + size_t ysize; + gboolean alpha_premultiplied; + gboolean has_animation; + gboolean has_alpha; + uint64_t total_duration_ms; + uint64_t tick_duration_us; + uint64_t repetition_count; // 0 = loop forever + + // ICC profile, to which `icc` might refer to. + gpointer icc_buff; + skcms_ICCProfile icc; +}; + +#define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type()) +G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK, + JXL_ANIMATION, GdkPixbufAnimation); + +G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, + GDK_TYPE_PIXBUF_ANIMATION); + +// Iterator to a given point in time in the animation; contains a pointer to the +// full animation. +struct _GdkPixbufJxlAnimationIter { + GdkPixbufAnimationIter parent_instance; + GdkPixbufJxlAnimation *animation; + size_t current_frame; + uint64_t time_offset; +}; + +#define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \ + (gdk_pixbuf_jxl_animation_iter_get_type()) +G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter, + GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter); +G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter, + GDK_TYPE_PIXBUF_ANIMATION_ITER); + +static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) {} + +static gboolean gdk_pixbuf_jxl_animation_is_static_image( + GdkPixbufAnimation *anim) { + GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; + return !jxl_anim->has_animation; +} + +static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image( + GdkPixbufAnimation *anim) { + GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; + if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL; + GdkPixbufJxlAnimationFrame *frame = + &g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0); + return frame->decoded ? frame->data : NULL; +} + +static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim, + int *width, int *height) { + GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; + if (width) *width = jxl_anim->xsize; + if (height) *height = jxl_anim->ysize; +} + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +static gboolean gdk_pixbuf_jxl_animation_iter_advance( + GdkPixbufAnimationIter *iter, const GTimeVal *current_time); + +static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter( + GdkPixbufAnimation *anim, const GTimeVal *start_time) { + GdkPixbufJxlAnimationIter *iter = + g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL); + iter->animation = (GdkPixbufJxlAnimation *)anim; + iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000; + g_object_ref(iter->animation); + gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter, + start_time); + return (GdkPixbufAnimationIter *)iter; +} +G_GNUC_END_IGNORE_DEPRECATIONS + +static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) { + GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj; + if (decoder_state->frames != NULL) { + for (size_t i = 0; i < decoder_state->frames->len; i++) { + g_object_unref( + g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i) + .data); + } + g_array_free(decoder_state->frames, /*free_segment=*/TRUE); + } + JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner); + JxlDecoderDestroy(decoder_state->decoder); + g_free(decoder_state->icc_buff); +} + +static void gdk_pixbuf_jxl_animation_class_init( + GdkPixbufJxlAnimationClass *klass) { + G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize; + klass->parent_class.is_static_image = + gdk_pixbuf_jxl_animation_is_static_image; + klass->parent_class.get_static_image = + gdk_pixbuf_jxl_animation_get_static_image; + klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size; + klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter; +} + +static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) { +} + +static int gdk_pixbuf_jxl_animation_iter_get_delay_time( + GdkPixbufAnimationIter *iter) { + GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; + if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { + return 0; + } + return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, + jxl_iter->current_frame) + .duration_ms; +} + +static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf( + GdkPixbufAnimationIter *iter) { + GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; + if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { + return NULL; + } + return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, + jxl_iter->current_frame) + .data; +} + +static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame( + GdkPixbufAnimationIter *iter) { + GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; + if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { + return TRUE; + } + return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, + jxl_iter->current_frame) + .decoded; +} + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +static gboolean gdk_pixbuf_jxl_animation_iter_advance( + GdkPixbufAnimationIter *iter, const GTimeVal *current_time) { + GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; + size_t old_frame = jxl_iter->current_frame; + + uint64_t current_time_ms = current_time->tv_sec * 1000ULL + + current_time->tv_usec / 1000 - + jxl_iter->time_offset; + + if (jxl_iter->animation->frames->len == 0) { + jxl_iter->current_frame = 0; + } else if (!jxl_iter->animation->done && + current_time_ms >= jxl_iter->animation->total_duration_ms) { + jxl_iter->current_frame = jxl_iter->animation->frames->len - 1; + } else if (jxl_iter->animation->repetition_count != 0 && + current_time_ms > jxl_iter->animation->repetition_count * + jxl_iter->animation->total_duration_ms) { + jxl_iter->current_frame = jxl_iter->animation->frames->len - 1; + } else { + uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms; + // Guard against divide-by-0 in malicious files. + if (total_duration_ms == 0) total_duration_ms = 1; + uint64_t loop_offset = current_time_ms % total_duration_ms; + jxl_iter->current_frame = 0; + while (true) { + uint64_t duration = + g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, + jxl_iter->current_frame) + .duration_ms; + if (duration >= loop_offset) { + break; + } + loop_offset -= duration; + jxl_iter->current_frame++; + } + } + + return old_frame != jxl_iter->current_frame; +} +G_GNUC_END_IGNORE_DEPRECATIONS + +static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) { + GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj; + g_object_unref(iter->animation); +} + +static void gdk_pixbuf_jxl_animation_iter_class_init( + GdkPixbufJxlAnimationIterClass *klass) { + G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize; + klass->parent_class.get_delay_time = + gdk_pixbuf_jxl_animation_iter_get_delay_time; + klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf; + klass->parent_class.on_currently_loading_frame = + gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame; + klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance; +} + +G_END_DECLS + +static gpointer begin_load(GdkPixbufModuleSizeFunc size_func, + GdkPixbufModulePreparedFunc prepare_func, + GdkPixbufModuleUpdatedFunc update_func, + gpointer user_data, GError **error) { + GdkPixbufJxlAnimation *decoder_state = + g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL); + if (decoder_state == NULL) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Creation of the animation state failed"); + return NULL; + } + decoder_state->image_size_callback = size_func; + decoder_state->pixbuf_prepared_callback = prepare_func; + decoder_state->area_updated_callback = update_func; + decoder_state->user_data = user_data; + decoder_state->frames = + g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE, + sizeof(GdkPixbufJxlAnimationFrame)); + + if (decoder_state->frames == NULL) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Creation of the frame array failed"); + goto cleanup; + } + + if (!(decoder_state->parallel_runner = + JxlResizableParallelRunnerCreate(NULL))) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Creation of the JXL parallel runner failed"); + goto cleanup; + } + + if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Creation of the JXL decoder failed"); + goto cleanup; + } + + JxlDecoderStatus status; + + if ((status = JxlDecoderSetParallelRunner( + decoder_state->decoder, JxlResizableParallelRunner, + decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JxlDecoderSetParallelRunner failed: %x", status); + goto cleanup; + } + if ((status = JxlDecoderSubscribeEvents( + decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) != + JXL_DEC_SUCCESS) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JxlDecoderSubscribeEvents failed: %x", status); + goto cleanup; + } + + decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT; + decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN; + + return decoder_state; +cleanup: + JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner); + JxlDecoderDestroy(decoder_state->decoder); + g_object_unref(decoder_state); + return NULL; +} + +static gboolean stop_load(gpointer context, GError **error) { + g_object_unref(context); + return TRUE; +} + +static void draw_pixels(void *context, size_t x, size_t y, size_t num_pixels, + const void *pixels) { + GdkPixbufJxlAnimation *decoder_state = context; + gboolean has_alpha = decoder_state->pixel_format.num_channels == 4; + + GdkPixbuf *output = + g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, + decoder_state->frames->len - 1) + .data; + + guchar *dst = gdk_pixbuf_get_pixels(output) + + decoder_state->pixel_format.num_channels * x + + gdk_pixbuf_get_rowstride(output) * y; + + skcms_Transform( + pixels, + has_alpha ? skcms_PixelFormat_RGBA_ffff : skcms_PixelFormat_RGB_fff, + decoder_state->alpha_premultiplied ? skcms_AlphaFormat_PremulAsEncoded + : skcms_AlphaFormat_Unpremul, + &decoder_state->icc, dst, + has_alpha ? skcms_PixelFormat_RGBA_8888 : skcms_PixelFormat_RGB_888, + skcms_AlphaFormat_Unpremul, skcms_sRGB_profile(), num_pixels); +} + +static gboolean load_increment(gpointer context, const guchar *buf, guint size, + GError **error) { + GdkPixbufJxlAnimation *decoder_state = context; + if (decoder_state->done == TRUE) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JXL decoder load_increment called after end of file"); + return FALSE; + } + + JxlDecoderStatus status; + + if ((status = JxlDecoderSetInput(decoder_state->decoder, buf, size)) != + JXL_DEC_SUCCESS) { + // Should never happen if things are done properly. + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JXL decoder logic error: %x", status); + return FALSE; + } + + for (;;) { + status = JxlDecoderProcessInput(decoder_state->decoder); + switch (status) { + case JXL_DEC_NEED_MORE_INPUT: { + JxlDecoderReleaseInput(decoder_state->decoder); + return TRUE; + } + + case JXL_DEC_BASIC_INFO: { + JxlBasicInfo info; + if (JxlDecoderGetBasicInfo(decoder_state->decoder, &info) != + JXL_DEC_SUCCESS) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JXLDecoderGetBasicInfo failed"); + return FALSE; + } + decoder_state->pixel_format.num_channels = info.alpha_bits > 0 ? 4 : 3; + decoder_state->alpha_premultiplied = info.alpha_premultiplied; + decoder_state->xsize = info.xsize; + decoder_state->ysize = info.ysize; + decoder_state->has_animation = info.have_animation; + decoder_state->has_alpha = info.alpha_bits > 0; + if (info.have_animation) { + decoder_state->repetition_count = info.animation.num_loops; + decoder_state->tick_duration_us = 1000000ULL * + info.animation.tps_denominator / + info.animation.tps_numerator; + } + gint width = info.xsize; + gint height = info.ysize; + if (decoder_state->image_size_callback) { + decoder_state->image_size_callback(&width, &height, + decoder_state->user_data); + } + + // GDK convention for signaling being interested only in the basic info. + if (width == 0 || height == 0) { + decoder_state->done = TRUE; + return TRUE; + } + + // Set an appropriate number of threads for the image size. + JxlResizableParallelRunnerSetThreads( + decoder_state->parallel_runner, + JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); + break; + } + + case JXL_DEC_COLOR_ENCODING: { + // Get the ICC color profile of the pixel data + size_t icc_size; + if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize( + decoder_state->decoder, + &decoder_state->pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JxlDecoderGetICCProfileSize failed"); + return FALSE; + } + if (!(decoder_state->icc_buff = g_malloc(icc_size))) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Allocating ICC profile failed"); + return FALSE; + } + if (JXL_DEC_SUCCESS != + JxlDecoderGetColorAsICCProfile(decoder_state->decoder, + &decoder_state->pixel_format, + JXL_COLOR_PROFILE_TARGET_DATA, + decoder_state->icc_buff, icc_size)) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JxlDecoderGetColorAsICCProfile failed"); + return FALSE; + } + if (!skcms_Parse(decoder_state->icc_buff, icc_size, + &decoder_state->icc)) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Invalid ICC profile from JXL image decoder"); + return FALSE; + } + break; + } + + case JXL_DEC_FRAME: { + // TODO(veluca): support rescaling. + JxlFrameHeader frame_header; + if (JxlDecoderGetFrameHeader(decoder_state->decoder, &frame_header) != + JXL_DEC_SUCCESS) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Failed to retrieve frame info"); + return FALSE; + } + + { + GdkPixbufJxlAnimationFrame frame; + frame.decoded = FALSE; + frame.duration_ms = + frame_header.duration * decoder_state->tick_duration_us / 1000; + decoder_state->total_duration_ms += frame.duration_ms; + frame.data = + gdk_pixbuf_new(GDK_COLORSPACE_RGB, decoder_state->has_alpha, + /*bits_per_sample=*/8, decoder_state->xsize, + decoder_state->ysize); + if (frame.data == NULL) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Failed to allocate output pixel buffer"); + return FALSE; + } + decoder_state->pixel_format.align = + gdk_pixbuf_get_rowstride(frame.data); + g_array_append_val(decoder_state->frames, frame); + } + if (decoder_state->pixbuf_prepared_callback && + decoder_state->frames->len == 1) { + decoder_state->pixbuf_prepared_callback( + g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, + 0) + .data, + decoder_state->has_animation ? (GdkPixbufAnimation *)decoder_state + : NULL, + decoder_state->user_data); + } + break; + } + + case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutCallback(decoder_state->decoder, + &decoder_state->pixel_format, + draw_pixels, decoder_state)) { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "JxlDecoderSetImageOutCallback failed"); + return FALSE; + } + break; + } + + case JXL_DEC_FULL_IMAGE: { + // TODO(veluca): consider doing partial updates. + if (decoder_state->area_updated_callback) { + GdkPixbuf *output = g_array_index(decoder_state->frames, + GdkPixbufJxlAnimationFrame, 0) + .data; + decoder_state->area_updated_callback( + output, 0, 0, gdk_pixbuf_get_width(output), + gdk_pixbuf_get_height(output), decoder_state->user_data); + } + g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, + decoder_state->frames->len - 1) + .decoded = TRUE; + break; + } + + case JXL_DEC_SUCCESS: { + decoder_state->done = TRUE; + return TRUE; + } + + default: { + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "Unexpected JxlDecoderProcessInput return code: %x", + status); + return FALSE; + } + } + } + return TRUE; +} + +void fill_vtable(GdkPixbufModule *module) { + module->begin_load = begin_load; + module->stop_load = stop_load; + module->load_increment = load_increment; + // TODO(veluca): implement saving. +} + +void fill_info(GdkPixbufFormat *info) { + static GdkPixbufModulePattern signature[] = { + {"\xFF\x0A", " ", 100}, + {"...\x0CJXL \x0D\x0A\x87\x0A", "zzz ", 100}, + {NULL, NULL, 0}, + }; + + static gchar *mime_types[] = {"image/jxl", NULL}; + + static gchar *extensions[] = {"jxl", NULL}; + + info->name = "jxl"; + info->signature = signature; + info->description = "JPEG XL image"; + info->mime_types = mime_types; + info->extensions = extensions; + // TODO(veluca): add writing support. + info->flags = GDK_PIXBUF_FORMAT_THREADSAFE; + info->license = "BSD-3"; +} diff --git a/plugins/gdk-pixbuf/pixbufloader_test.cc b/plugins/gdk-pixbuf/pixbufloader_test.cc new file mode 100644 index 0000000..5e5642d --- /dev/null +++ b/plugins/gdk-pixbuf/pixbufloader_test.cc @@ -0,0 +1,41 @@ +// 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 +#include +#include +#include + +int main(int argc, char* argv[]) { + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char* loaders_cache = argv[1]; + const char* filename = argv[2]; + setenv("GDK_PIXBUF_MODULE_FILE", loaders_cache, true); + + // XDG_DATA_HOME is the path where we look for the mime cache. + // XDG_DATA_DIRS directories are used in addition to XDG_DATA_HOME. + setenv("XDG_DATA_HOME", ".", true); + setenv("XDG_DATA_DIRS", "", true); + + if (!gdk_init_check(nullptr, nullptr)) { + fprintf(stderr, "This test requires a DISPLAY\n"); + // Signals ctest that we should mark this test as skipped. + return 254; + } + GError* error = nullptr; + GdkPixbuf* pb = gdk_pixbuf_new_from_file(filename, &error); + if (pb != nullptr) { + g_object_unref(pb); + return 0; + } else { + fprintf(stderr, "Error loading file: %s\n", filename); + g_assert_no_error(error); + return 1; + } +} diff --git a/plugins/gimp/CMakeLists.txt b/plugins/gimp/CMakeLists.txt new file mode 100644 index 0000000..f0a4900 --- /dev/null +++ b/plugins/gimp/CMakeLists.txt @@ -0,0 +1,28 @@ +# 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. + +find_package(PkgConfig) +pkg_check_modules(Gimp IMPORTED_TARGET gimp-2.0>=2.10 gimpui-2.0>=2.10) + +if (NOT Gimp_FOUND) + message(WARNING "Gimp development libraries not found, the Gimp plugin will not be built") + return () +endif () + +add_executable(file-jxl WIN32 + common.h + common.cc + file-jxl-load.cc + file-jxl-load.h + file-jxl-save.cc + file-jxl-save.h + file-jxl.cc) +target_link_libraries(file-jxl jxl jxl_threads PkgConfig::Gimp) + +target_include_directories(file-jxl PUBLIC + ${PROJECT_SOURCE_DIR}) # for plugins/gimp absolute paths. + +pkg_get_variable(GIMP_LIB_DIR gimp-2.0 gimplibdir) +install(TARGETS file-jxl RUNTIME DESTINATION "${GIMP_LIB_DIR}/plug-ins/file-jxl/") diff --git a/plugins/gimp/common.cc b/plugins/gimp/common.cc new file mode 100644 index 0000000..1a88457 --- /dev/null +++ b/plugins/gimp/common.cc @@ -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. + +#include "plugins/gimp/common.h" + +namespace jxl { + +JpegXlGimpProgress::JpegXlGimpProgress(const char *message) { + cur_progress = 0; + max_progress = 100; + + gimp_progress_init_printf("%s\n", message); +} + +void JpegXlGimpProgress::update() { + gimp_progress_update((float)++cur_progress / (float)max_progress); + return; +} + +void JpegXlGimpProgress::finished() { + gimp_progress_update(1.0); + return; +} + +} // namespace jxl diff --git a/plugins/gimp/common.h b/plugins/gimp/common.h new file mode 100644 index 0000000..95c51bf --- /dev/null +++ b/plugins/gimp/common.h @@ -0,0 +1,45 @@ +// 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 PLUGINS_GIMP_COMMON_H_ +#define PLUGINS_GIMP_COMMON_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#define PLUG_IN_BINARY "file-jxl" +#define SAVE_PROC "file-jxl-save" + +// Defined by both FUIF and glib. +#undef MAX +#undef MIN +#undef CLAMP + +#include "jxl/resizable_parallel_runner.h" +#include "jxl/resizable_parallel_runner_cxx.h" + +namespace jxl { + +class JpegXlGimpProgress { + public: + explicit JpegXlGimpProgress(const char *message); + void update(); + void finished(); + + private: + int cur_progress; + int max_progress; + +}; // class JpegXlGimpProgress + +} // namespace jxl + +#endif // PLUGINS_GIMP_COMMON_H_ diff --git a/plugins/gimp/file-jxl-load.cc b/plugins/gimp/file-jxl-load.cc new file mode 100644 index 0000000..e08a665 --- /dev/null +++ b/plugins/gimp/file-jxl-load.cc @@ -0,0 +1,340 @@ +// 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 "plugins/gimp/file-jxl-load.h" + +#define _PROFILE_TARGET_ JXL_COLOR_PROFILE_TARGET_DATA + +namespace jxl { + +bool LoadJpegXlImage(const gchar *const filename, gint32 *const image_id) { + std::vector icc_profile; + GimpColorProfile *profile = nullptr; + bool is_linear = false; + + gint32 layer; + + gpointer pixels_buffer; + size_t buffer_size; + + GimpImageBaseType image_type; + GimpImageType layer_type = GIMP_RGB_IMAGE; + GimpPrecision precision = GIMP_PRECISION_U16_GAMMA; + JxlBasicInfo info = {}; + JxlPixelFormat format = {}; + + format.num_channels = 4; + format.data_type = JXL_TYPE_UINT8; + format.endianness = JXL_NATIVE_ENDIAN; + format.align = 0; + + JpegXlGimpProgress gimp_load_progress( + ("Opening:" + (std::string)filename).c_str()); + gimp_load_progress.update(); + + // read file + std::ifstream instream(filename, std::ios::in | std::ios::binary); + std::vector compressed((std::istreambuf_iterator(instream)), + std::istreambuf_iterator()); + instream.close(); + + gimp_load_progress.update(); + + // multi-threaded parallel runner. + auto runner = JxlResizableParallelRunnerMake(nullptr); + + auto dec = JxlDecoderMake(nullptr); + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | + JXL_DEC_COLOR_ENCODING | + JXL_DEC_FULL_IMAGE)) { + g_printerr("JXL Error: JxlDecoderSubscribeEvents failed\n"); + return false; + } + + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), + JxlResizableParallelRunner, + runner.get())) { + g_printerr("JXL Error: JxlDecoderSetParallelRunner failed\n"); + return false; + } + + // grand decode loop... Is there a better way to organize this? + JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); + + while (true) { + gimp_load_progress.update(); + + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + + if (status == JXL_DEC_BASIC_INFO) { + // g_message("JXL_DEC_BASIC_INFO"); + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) { + g_printerr("JXL Error: JxlDecoderGetBasicInfo failed\n"); + return false; + } + + JxlResizableParallelRunnerSetThreads( + runner.get(), + JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); + } else if (status == JXL_DEC_COLOR_ENCODING) { + // Load ICC profile + size_t icc_size = 0; + + if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(dec.get(), &format, + _PROFILE_TARGET_, + &icc_size)) { + g_printerr("JXL Warning: JxlDecoderGetICCProfileSize failed\n"); + } + + if (icc_size > 0) { + icc_profile.resize(icc_size); + if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( + dec.get(), &format, _PROFILE_TARGET_, + icc_profile.data(), icc_profile.size())) { + g_printerr("JXL Warning: JxlDecoderGetColorAsICCProfile failed\n"); + } + + profile = gimp_color_profile_new_from_icc_profile( + icc_profile.data(), icc_profile.size(), /*error=*/nullptr); + + if (profile) { + is_linear = gimp_color_profile_is_linear(profile); + g_printerr("JXL Info: Setting is_linear = %d\n", is_linear); + } else { + g_printerr("JXL Warning: Failed to read ICC profile.\n"); + } + } else { + g_printerr("JXL Warning: Empty ICC data.\n"); + } + + // Internal color profile detection... + JxlColorEncoding color_encoding; + if (JXL_DEC_SUCCESS == + JxlDecoderGetColorAsEncodedProfile( + dec.get(), &format, _PROFILE_TARGET_, &color_encoding)) { + g_printerr("JXL Info: Internal profile detected.\n"); + + // figure out linearity of internal profile + switch (color_encoding.transfer_function) { + case JXL_TRANSFER_FUNCTION_LINEAR: + is_linear = true; + break; + + case JXL_TRANSFER_FUNCTION_709: + case JXL_TRANSFER_FUNCTION_PQ: + case JXL_TRANSFER_FUNCTION_HLG: + case JXL_TRANSFER_FUNCTION_GAMMA: + case JXL_TRANSFER_FUNCTION_DCI: + case JXL_TRANSFER_FUNCTION_SRGB: + is_linear = false; + break; + + case JXL_TRANSFER_FUNCTION_UNKNOWN: + default: + if (profile) { + g_printerr( + "Info: Unknown transfer function. " + "ICC profile is present."); + } else { + g_printerr( + "Info: Unknown transfer function. " + "No ICC profile present."); + } + break; + } + + switch (color_encoding.color_space) { + case JXL_COLOR_SPACE_RGB: + if (color_encoding.white_point == JXL_WHITE_POINT_D65 && + color_encoding.primaries == JXL_PRIMARIES_SRGB) { + if (is_linear) { + profile = gimp_color_profile_new_rgb_srgb_linear(); + } else { + profile = gimp_color_profile_new_rgb_srgb(); + } + } else if (!is_linear && + color_encoding.white_point == JXL_WHITE_POINT_D65 && + (color_encoding.primaries_green_xy[0] == 0.2100 || + color_encoding.primaries_green_xy[1] == 0.7100)) { + // Probably Adobe RGB + profile = gimp_color_profile_new_rgb_adobe(); + } else if (profile) { + g_printerr( + "JXL Info: Unknown RGB colorspace. Using ICC profile.\n"); + } else { + g_printerr( + "JXL Info: Unknown RGB colorspace. Treating as sRGB.\n"); + if (is_linear) { + profile = gimp_color_profile_new_rgb_srgb_linear(); + } else { + profile = gimp_color_profile_new_rgb_srgb(); + } + } + break; + + case JXL_COLOR_SPACE_GRAY: + if (!profile) { + if (is_linear) { + profile = gimp_color_profile_new_d65_gray_linear(); + } else { + profile = gimp_color_profile_new_d65_gray_srgb_trc(); + } + } + break; + case JXL_COLOR_SPACE_XYB: + case JXL_COLOR_SPACE_UNKNOWN: + default: + if (profile) { + g_printerr("JXL Info: Unknown colorspace. Using ICC profile.\n"); + } else { + g_error( + "Warning: Unknown colorspace. Treating as sRGB profile.\n"); + + if (is_linear) { + profile = gimp_color_profile_new_rgb_srgb_linear(); + } else { + profile = gimp_color_profile_new_rgb_srgb(); + } + } + break; + } + } + + // set pixel format + if (info.num_color_channels > 1) { + if (info.alpha_bits == 0) { + image_type = GIMP_RGB; + layer_type = GIMP_RGB_IMAGE; + format.num_channels = info.num_color_channels; + } else { + image_type = GIMP_RGB; + layer_type = GIMP_RGBA_IMAGE; + format.num_channels = info.num_color_channels + 1; + } + } else if (info.num_color_channels == 1) { + if (info.alpha_bits == 0) { + image_type = GIMP_GRAY; + layer_type = GIMP_GRAY_IMAGE; + format.num_channels = info.num_color_channels; + } else { + image_type = GIMP_GRAY; + layer_type = GIMP_GRAYA_IMAGE; + format.num_channels = info.num_color_channels + 1; + } + } + + // Set bit depth and linearity + if (info.bits_per_sample <= 8) { + if (is_linear) { + format.data_type = JXL_TYPE_UINT8; + precision = GIMP_PRECISION_U8_LINEAR; + } else { + format.data_type = JXL_TYPE_UINT8; + precision = GIMP_PRECISION_U8_GAMMA; + } + } else if (info.bits_per_sample <= 16) { + if (info.exponent_bits_per_sample > 0) { + if (is_linear) { + format.data_type = JXL_TYPE_FLOAT16; + precision = GIMP_PRECISION_HALF_LINEAR; + } else { + format.data_type = JXL_TYPE_FLOAT16; + precision = GIMP_PRECISION_HALF_GAMMA; + } + } else if (is_linear) { + format.data_type = JXL_TYPE_UINT16; + precision = GIMP_PRECISION_U16_LINEAR; + } else { + format.data_type = JXL_TYPE_UINT16; + precision = GIMP_PRECISION_U16_GAMMA; + } + } else { + if (info.exponent_bits_per_sample > 0) { + if (is_linear) { + format.data_type = JXL_TYPE_FLOAT; + precision = GIMP_PRECISION_FLOAT_LINEAR; + } else { + format.data_type = JXL_TYPE_FLOAT; + precision = GIMP_PRECISION_FLOAT_GAMMA; + } + } else if (is_linear) { + format.data_type = JXL_TYPE_UINT32; + precision = GIMP_PRECISION_U32_LINEAR; + } else { + format.data_type = JXL_TYPE_UINT32; + precision = GIMP_PRECISION_U32_GAMMA; + } + } + + // create new image with profile + *image_id = gimp_image_new_with_precision(info.xsize, info.ysize, + image_type, precision); + + if (profile) { + gimp_image_set_color_profile(*image_id, profile); + } else { + g_printerr("JXL Error: No color profile.\n"); + } + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + // g_message("JXL_DEC_NEED_IMAGE_OUT_BUFFER"); + if (JXL_DEC_SUCCESS != + JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size)) { + g_printerr("JXL Error: JxlDecoderImageOutBufferSize failed\n"); + return false; + } + + pixels_buffer = g_malloc(buffer_size); + + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, + pixels_buffer, + buffer_size)) { + g_printerr("JXL Error: JxlDecoderSetImageOutBuffer failed\n"); + return false; + } + } else if (status == JXL_DEC_FULL_IMAGE) { + // g_message("JXL_DEC_FULL_IMAGE"); + // create and insert layer + layer = gimp_layer_new(*image_id, "Background", info.xsize, info.ysize, + layer_type, /*opacity=*/100, + gimp_image_get_default_new_layer_mode(*image_id)); + + gimp_image_insert_layer(*image_id, layer, /*parent_id=*/-1, + /*position=*/0); + + // move image to layer buffer; need to clear layer buffer to update layer + GeglBuffer *buffer = gimp_drawable_get_buffer(layer); + gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, info.xsize, info.ysize), 0, + nullptr, pixels_buffer, GEGL_AUTO_ROWSTRIDE); + + g_clear_object(&buffer); + } else if (status == JXL_DEC_SUCCESS) { + // g_message("JXL_DEC_SUCCESS"); + // All decoding successfully finished. + // It's not required to call JxlDecoderReleaseInput(dec.get()) + // since the decoder will be destroyed. + break; + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + // g_message("JXL_DEC_NEED_MORE_INPUT"); + g_printerr("JXL Error: Already provided all input\n"); + return false; + } else if (status == JXL_DEC_ERROR) { + // g_message("JXL_DEC_ERROR"); + g_printerr("JXL Error: Decoder error\n"); + return false; + } else { + g_printerr("JXL Error: Unknown decoder status\n"); + return false; + } + } // end grand decode loop + + gimp_load_progress.update(); + gimp_image_set_filename(*image_id, filename); + + gimp_load_progress.finished(); + return true; +} + +} // namespace jxl diff --git a/plugins/gimp/file-jxl-load.h b/plugins/gimp/file-jxl-load.h new file mode 100644 index 0000000..c9ca6d9 --- /dev/null +++ b/plugins/gimp/file-jxl-load.h @@ -0,0 +1,19 @@ +// 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 PLUGINS_GIMP_FILE_JXL_LOAD_H_ +#define PLUGINS_GIMP_FILE_JXL_LOAD_H_ + +#include "jxl/decode.h" +#include "jxl/decode_cxx.h" +#include "plugins/gimp/common.h" + +namespace jxl { + +bool LoadJpegXlImage(const gchar* filename, gint32* image_id); + +} // namespace jxl + +#endif // PLUGINS_GIMP_FILE_JXL_LOAD_H_ diff --git a/plugins/gimp/file-jxl-save.cc b/plugins/gimp/file-jxl-save.cc new file mode 100644 index 0000000..f9923b5 --- /dev/null +++ b/plugins/gimp/file-jxl-save.cc @@ -0,0 +1,888 @@ +// 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 "plugins/gimp/file-jxl-save.h" + +#define PLUG_IN_BINARY "file-jxl" +#define SAVE_PROC "file-jxl-save" + +#define SCALE_WIDTH 200 + +namespace jxl { + +namespace { + +class JpegXlSaveOpts { + public: + float distance; + float quality; + + bool lossless = false; + bool is_linear = false; + bool has_alpha = false; + bool is_gray = false; + + bool advanced_mode = false; + bool use_container = true; + bool save_exif = false; + int encoding_effort = 7; + int faster_decoding = 0; + + std::string babl_format_str = "RGB u16"; + std::string babl_type_str = "u16"; + std::string babl_model_str = "RGB"; + + JxlPixelFormat pixel_format; + JxlBasicInfo basic_info; + + // functions + JpegXlSaveOpts(); + + bool set_distance(float dist); + bool set_quality(float qual); + bool set_dimensions(int x, int y); + bool set_num_channels(int channels); + + bool update_distance(); + bool update_quality(); + + bool set_model(int gimp_model); + + bool update_babl_format(); + bool set_babl_model(std::string model); + bool set_babl_type(std::string type); + + bool set_pixel_type(int type); + bool set_precision(int gimp_precision); + + private: +}; // class JpegXlSaveOpts + +JpegXlSaveOpts jxl_save_opts; + +static bool gui_on_change_quality(GtkAdjustment* adj_qual, + GtkAdjustment* adj_dist) { + jxl_save_opts.quality = gtk_adjustment_get_value(adj_qual); + jxl_save_opts.update_distance(); + gtk_adjustment_set_value(adj_dist, jxl_save_opts.distance); + return true; +} + +static bool gui_on_change_distance(GtkAdjustment* adj_dist, + GtkAdjustment* adj_qual) { + float new_distance = gtk_adjustment_get_value(adj_dist); + jxl_save_opts.distance = new_distance; + jxl_save_opts.update_quality(); + gtk_adjustment_set_value(adj_qual, jxl_save_opts.quality); + + // updating quality can change distance again + // set it again to ensure user value is maintained + if (jxl_save_opts.distance != new_distance) { + gtk_adjustment_set_value(adj_dist, new_distance); + } + return true; +} + +static bool gui_on_change_lossless(GtkWidget* toggle, + GtkAdjustment* adjustments[]) { + GtkAdjustment* adj_distance = adjustments[0]; + GtkAdjustment* adj_quality = adjustments[1]; + GtkAdjustment* adj_effort = adjustments[2]; + + jxl_save_opts.lossless = + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); + + g_message("lossless = %d", jxl_save_opts.lossless); + + if (jxl_save_opts.lossless) { + gimp_scale_entry_set_sensitive((GtkObject*)adj_distance, false); + gimp_scale_entry_set_sensitive((GtkObject*)adj_quality, false); + gtk_adjustment_set_value(adj_quality, 100.0f); + gtk_adjustment_set_value(adj_distance, 0.0f); + + jxl_save_opts.distance = 0; + jxl_save_opts.update_quality(); + + gtk_adjustment_set_value(adj_effort, 3); + jxl_save_opts.encoding_effort = 3; + } else { + gimp_scale_entry_set_sensitive((GtkObject*)adj_distance, true); + gimp_scale_entry_set_sensitive((GtkObject*)adj_quality, true); + gtk_adjustment_set_value(adj_quality, 100.0f); + gtk_adjustment_set_value(adj_distance, 0.1f); + + jxl_save_opts.distance = 0.1f; + jxl_save_opts.update_quality(); + + gtk_adjustment_set_value(adj_effort, 7); + jxl_save_opts.encoding_effort = 7; + } + return true; +} + +static bool gui_on_change_codestream(GtkWidget* toggle) { + jxl_save_opts.use_container = + !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); + return true; +} + +static bool gui_on_change_uses_original_profile(GtkWidget* toggle) { + jxl_save_opts.basic_info.uses_original_profile = + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); + return true; +} + +static bool gui_on_change_advanced_mode(GtkWidget* toggle, + std::vector advanced_opts) { + jxl_save_opts.advanced_mode = + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); + + GtkWidget* frame = advanced_opts[0]; + + gtk_widget_set_sensitive(frame, jxl_save_opts.advanced_mode); + + if (!jxl_save_opts.advanced_mode) { + jxl_save_opts.lossless = false; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(advanced_opts[1]), false); + + jxl_save_opts.use_container = true; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(advanced_opts[2]), false); + + jxl_save_opts.basic_info.uses_original_profile = false; + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(advanced_opts[3]), false); + + // save metadata + // gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(advanced_opts[4]), false); + + jxl_save_opts.encoding_effort = 7; + gtk_adjustment_set_value((GtkAdjustment*)advanced_opts[5], 7); + + jxl_save_opts.faster_decoding = 0; + gtk_adjustment_set_value((GtkAdjustment*)advanced_opts[6], 0); + } + return true; +} + +bool SaveDialog() { + gboolean run; + GtkWidget* dialog; + GtkWidget* content_area; + GtkWidget* main_vbox; + GtkWidget* frame; + GtkWidget* toggle; + GtkWidget* table; + GtkWidget* vbox; + GtkWidget* toggle_lossless; + GtkWidget* frame_advanced; + GtkAdjustment* entry_distance; + GtkAdjustment* entry_quality; + GtkAdjustment* entry_effort; + GtkAdjustment* entry_faster; + + // initialize export dialog + gimp_ui_init(PLUG_IN_BINARY, true); + dialog = gimp_export_dialog_new("JPEG XL", PLUG_IN_BINARY, SAVE_PROC); + + gtk_window_set_resizable(GTK_WINDOW(dialog), true); + content_area = gimp_export_dialog_get_content_area(dialog); + + main_vbox = gtk_vbox_new(false, 6); + gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 6); + gtk_box_pack_start(GTK_BOX(content_area), main_vbox, true, true, 0); + gtk_widget_show(main_vbox); + + // Standard Settings Frame + frame = gtk_frame_new("Standard Settings"); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN); + gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0); + gtk_widget_show(frame); + + vbox = gtk_vbox_new(false, 6); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); + gtk_container_add(GTK_CONTAINER(frame), vbox); + gtk_widget_show(vbox); + + // Butteraugli Distance + static gchar distance_help[] = + "Butteraugli distance. Use lower values for higher quality."; + frame = gtk_frame_new("Butteraugli Distance"); + gimp_help_set_help_data(frame, distance_help, nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); + gtk_box_pack_start(GTK_BOX(vbox), frame, false, false, 0); + gtk_widget_show(frame); + + // Distance Scale + table = gtk_table_new(1, 3, false); + gtk_table_set_col_spacings(GTK_TABLE(table), 6); + gtk_container_add(GTK_CONTAINER(frame), table); + gtk_widget_show(table); + + entry_distance = (GtkAdjustment*)gimp_scale_entry_new( + GTK_TABLE(table), 0, 0, "", SCALE_WIDTH, 0, jxl_save_opts.distance, 0.0, + 45.0, 0.001, 1.0, 3, true, 0.0, 0.0, distance_help, "file-jxl-save"); + + gimp_scale_entry_set_logarithmic((GtkObject*)entry_distance, true); + + // JPEG-style Quality + static gchar quality_help[] = + "JPEG-style Quality setting is remapped to distance. " + "Values roughly match libjpeg quality."; + frame = gtk_frame_new("JPEG-style Quality"); + gimp_help_set_help_data(frame, quality_help, nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); + gtk_box_pack_start(GTK_BOX(vbox), frame, false, false, 0); + gtk_widget_show(frame); + + // Quality Scale + table = gtk_table_new(1, 3, false); + gtk_table_set_col_spacings(GTK_TABLE(table), 6); + gtk_container_add(GTK_CONTAINER(frame), table); + gtk_widget_show(table); + + entry_quality = (GtkAdjustment*)gimp_scale_entry_new( + GTK_TABLE(table), 0, 0, "", SCALE_WIDTH, 0, jxl_save_opts.quality, 0.0, + 100.0, 1.0, 10.0, 2, true, 0.0, 0.0, quality_help, "file-jxl-save"); + + // Distance and Quality Signals + g_signal_connect(entry_distance, "value-changed", + G_CALLBACK(gui_on_change_distance), entry_quality); + g_signal_connect(entry_quality, "value-changed", + G_CALLBACK(gui_on_change_quality), entry_distance); + + // Advanced Settings Frame + std::vector advanced_opts; + + frame_advanced = gtk_frame_new("Advanced Settings"); + gimp_help_set_help_data(frame_advanced, + "Advanced Settings that shouldn't be used.", nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame_advanced), GTK_SHADOW_ETCHED_IN); + gtk_box_pack_start(GTK_BOX(main_vbox), frame_advanced, true, true, 0); + gtk_widget_show(frame_advanced); + + gtk_widget_set_sensitive(frame_advanced, false); + + vbox = gtk_vbox_new(false, 6); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); + gtk_container_add(GTK_CONTAINER(frame_advanced), vbox); + gtk_widget_show(vbox); + + advanced_opts.push_back(frame_advanced); + + // lossless convenience checkbox + static gchar lossless_help[] = + "Compress using modular lossless mode. " + "Effort is set to 3 to improve performance."; + toggle_lossless = gtk_check_button_new_with_label("Lossless Mode"); + gimp_help_set_help_data(toggle_lossless, lossless_help, nullptr); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_lossless), + jxl_save_opts.lossless); + gtk_box_pack_start(GTK_BOX(vbox), toggle_lossless, false, false, 0); + gtk_widget_show(toggle_lossless); + + advanced_opts.push_back(toggle_lossless); + + // save raw codestream + static gchar codestream_help[] = + "Save the raw codestream, without a container. " + "Not recommended. The container is required for " + "metadata, and the overhead is miniscule."; + toggle = gtk_check_button_new_with_label("Raw Codestream"); + gimp_help_set_help_data(toggle, codestream_help, nullptr); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), + !jxl_save_opts.use_container); + gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0); + gtk_widget_show(toggle); + + g_signal_connect(toggle, "toggled", G_CALLBACK(gui_on_change_codestream), + nullptr); + + advanced_opts.push_back(toggle); + + // uses_original_profile + static gchar uses_original_profile_help[] = + "Prevents conversion to XYB colorspace. " + "File sizes are approximately doubled. " + "This option is not recommended."; + toggle = gtk_check_button_new_with_label("Use Original Color Profile"); + gimp_help_set_help_data(toggle, uses_original_profile_help, nullptr); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), + jxl_save_opts.basic_info.uses_original_profile); + gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0); + gtk_widget_show(toggle); + + g_signal_connect(toggle, "toggled", + G_CALLBACK(gui_on_change_uses_original_profile), nullptr); + + advanced_opts.push_back(toggle); + + // Save Exif Metadata + toggle = gtk_check_button_new_with_label("Save Exif Metadata"); + gimp_help_set_help_data( + toggle, "This feature is not yet available in the API.", nullptr); + gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0); + gtk_widget_set_sensitive(toggle, false); + gtk_widget_show(toggle); + + advanced_opts.push_back(toggle); + + // Encoding Effort + static gchar effort_help[] = + "Encoding Effort: Higher number is more effort (slower).\n\tDefault = 7."; + frame = gtk_frame_new("Encoding Effort"); + gimp_help_set_help_data(frame, effort_help, nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); + gtk_box_pack_start(GTK_BOX(vbox), frame, false, false, 0); + gtk_widget_show(frame); + + // Effort Scale + table = gtk_table_new(1, 3, false); + gtk_table_set_col_spacings(GTK_TABLE(table), 6); + gtk_container_add(GTK_CONTAINER(frame), table); + gtk_widget_show(table); + + entry_effort = (GtkAdjustment*)gimp_scale_entry_new( + GTK_TABLE(table), 0, 0, "", SCALE_WIDTH, 0, jxl_save_opts.encoding_effort, + 1, 9, 1, 2, 0, true, 0.0, 0.0, effort_help, "file-jxl-save"); + + // Effort Signals + g_signal_connect(entry_effort, "value-changed", + G_CALLBACK(gimp_int_adjustment_update), + &jxl_save_opts.encoding_effort); + + advanced_opts.push_back((GtkWidget*)entry_effort); + + // signal for lossless toggle + // has to be put here to change effort setting + GtkAdjustment* adjustments[] = {entry_distance, entry_quality, entry_effort}; + g_signal_connect(toggle_lossless, "toggled", + G_CALLBACK(gui_on_change_lossless), adjustments); + + // Faster Decoding + static gchar faster_help[] = + "Faster Decoding to improve decoding speed. " + "Higher values give higher speed at the expense of quality.\n" + "\tDefault = 0."; + frame = gtk_frame_new("Faster Decoding"); + gimp_help_set_help_data(frame, faster_help, nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); + gtk_box_pack_start(GTK_BOX(vbox), frame, false, false, 0); + gtk_widget_show(frame); + + // Faster Decoding Scale + table = gtk_table_new(1, 3, false); + gtk_table_set_col_spacings(GTK_TABLE(table), 6); + gtk_container_add(GTK_CONTAINER(frame), table); + gtk_widget_show(table); + + entry_faster = (GtkAdjustment*)gimp_scale_entry_new( + GTK_TABLE(table), 0, 0, "", SCALE_WIDTH, 0, jxl_save_opts.faster_decoding, + 0, 5, 1, 1, 0, true, 0.0, 0.0, faster_help, "file-jxl-save"); + + advanced_opts.push_back((GtkWidget*)entry_faster); + + // Faster Decoding Signals + g_signal_connect(entry_faster, "value-changed", + G_CALLBACK(gimp_int_adjustment_update), + &jxl_save_opts.faster_decoding); + + // Enable Advanced Settings + frame = gtk_frame_new(0); + gimp_help_set_help_data(frame, "Advanced Settings shouldn't be used.", + nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); + gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0); + gtk_widget_show(frame); + + vbox = gtk_vbox_new(false, 6); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); + gtk_container_add(GTK_CONTAINER(frame), vbox); + gtk_widget_show(vbox); + + static gchar advanced_help[] = "Use advanced settings with care."; + toggle = gtk_check_button_new_with_label("Enable Advanced Settings"); + gimp_help_set_help_data(toggle, advanced_help, nullptr); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), + jxl_save_opts.advanced_mode); + gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0); + gtk_widget_show(toggle); + + g_signal_connect(toggle, "toggled", G_CALLBACK(gui_on_change_advanced_mode), + &advanced_opts); + + // show dialog + gtk_widget_show(dialog); + + GtkAllocation allocation; + gtk_widget_get_allocation(dialog, &allocation); + + int height = allocation.height; + gtk_widget_set_size_request(dialog, height * 1.4, height); + + run = (gimp_dialog_run(GIMP_DIALOG(dialog)) == GTK_RESPONSE_OK); + gtk_widget_destroy(dialog); + + return run; +} // SaveDialog + +JpegXlSaveOpts::JpegXlSaveOpts() { + set_distance(1.0f); + + pixel_format.num_channels = 4; + pixel_format.data_type = JXL_TYPE_UINT8; + pixel_format.endianness = JXL_NATIVE_ENDIAN; + pixel_format.align = 0; + + basic_info.alpha_bits = 0; + basic_info.alpha_exponent_bits = 0; + basic_info.uses_original_profile = false; + basic_info.intensity_target = 255.0f; + basic_info.orientation = JXL_ORIENT_IDENTITY; + + return; +} // JpegXlSaveOpts constructor + +bool JpegXlSaveOpts::set_model(int gimp_model) { + switch (gimp_model) { + case GIMP_GRAY_IMAGE: + if (is_linear) { + set_babl_model("Y"); + set_num_channels(1); + } else { + set_babl_model("Y'"); + set_num_channels(1); + } + return true; + + case GIMP_GRAYA_IMAGE: + if (is_linear) { + set_babl_model("YA"); + set_num_channels(2); + } else { + set_babl_model("Y'A"); + set_num_channels(2); + } + return true; + + case GIMP_RGB_IMAGE: + if (is_linear) { + set_babl_model("RGB"); + set_num_channels(3); + } else { + set_babl_model("R'G'B'"); + set_num_channels(3); + } + return true; + + case GIMP_RGBA_IMAGE: + if (is_linear) { + set_babl_model("RGBA"); + set_num_channels(4); + } else { + set_babl_model("R'G'B'A"); + set_num_channels(4); + } + return true; + + default: + g_printerr("JXL Error: Unsupported pixel format.\n"); + return false; + } +} // JpegXlSaveOpts::set_model + +bool JpegXlSaveOpts::set_distance(float dist) { + distance = dist; + return update_quality(); +} + +bool JpegXlSaveOpts::set_quality(float qual) { + quality = qual; + return update_distance(); +} + +bool JpegXlSaveOpts::update_quality() { + float qual; + + if (distance < 0.1f) { + qual = 100; + } else if (distance <= 6.4) { + qual = 100 - (distance - 0.1) / 0.09f; + lossless = false; + } else { + qual = 30 - 5 * (log(6.25 * distance - 40)) / log(2.5); + lossless = false; + } + + if (qual < 0) { + quality = 0.0; + } else if (qual >= 100) { + quality = 100.0; + } else { + quality = qual; + } + + return true; +} + +bool JpegXlSaveOpts::update_distance() { + float dist; + if (quality >= 30) { + dist = 0.1 + (100 - quality) * 0.09; + } else { + dist = 6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f; + } + + distance = dist; + return true; +} + +bool JpegXlSaveOpts::set_dimensions(int x, int y) { + basic_info.xsize = x; + basic_info.ysize = y; + return true; +} + +bool JpegXlSaveOpts::set_num_channels(int channels) { + switch (channels) { + case 1: + pixel_format.num_channels = 1; + basic_info.num_color_channels = 1; + basic_info.num_extra_channels = 0; + basic_info.alpha_bits = 0; + basic_info.alpha_exponent_bits = 0; + break; + case 2: + pixel_format.num_channels = 2; + basic_info.num_color_channels = 1; + basic_info.num_extra_channels = 1; + basic_info.alpha_bits = basic_info.bits_per_sample; + basic_info.alpha_exponent_bits = basic_info.exponent_bits_per_sample; + break; + case 3: + pixel_format.num_channels = 3; + basic_info.num_color_channels = 3; + basic_info.num_extra_channels = 0; + basic_info.alpha_bits = 0; + basic_info.alpha_exponent_bits = 0; + break; + case 4: + pixel_format.num_channels = 4; + basic_info.num_color_channels = 3; + basic_info.num_extra_channels = 1; + basic_info.alpha_bits = basic_info.bits_per_sample; + basic_info.alpha_exponent_bits = basic_info.exponent_bits_per_sample; + break; + default: + set_num_channels(3); + } // switch + return true; +} // JpegXlSaveOpts::set_num_channels + +bool JpegXlSaveOpts::update_babl_format() { + babl_format_str = babl_model_str + " " + babl_type_str; + return true; +} + +bool JpegXlSaveOpts::set_babl_model(std::string model) { + babl_model_str = model; + return update_babl_format(); +} + +bool JpegXlSaveOpts::set_babl_type(std::string type) { + babl_type_str = type; + return update_babl_format(); +} + +bool JpegXlSaveOpts::set_pixel_type(int type) { + switch (type) { + case JXL_TYPE_FLOAT16: + set_babl_type("half"); + pixel_format.data_type = JXL_TYPE_FLOAT16; + basic_info.bits_per_sample = 16; + basic_info.exponent_bits_per_sample = 5; + break; + + case JXL_TYPE_FLOAT: + set_babl_type("float"); + pixel_format.data_type = JXL_TYPE_FLOAT; + basic_info.bits_per_sample = 32; + basic_info.exponent_bits_per_sample = 8; + break; + + // UINT32 is not yet supported. Using UINT16 instead. + // See documentation of JxlEncoderAddImageFrame(). + case JXL_TYPE_UINT32: + case JXL_TYPE_UINT16: + set_babl_type("u16"); + pixel_format.data_type = JXL_TYPE_UINT16; + basic_info.bits_per_sample = 16; + basic_info.exponent_bits_per_sample = 0; + break; + + case JXL_TYPE_UINT8: + default: + set_babl_type("u8"); + pixel_format.data_type = JXL_TYPE_UINT8; + basic_info.bits_per_sample = 8; + basic_info.exponent_bits_per_sample = 0; + break; + } + + return true; +} // JpegXlSaveOpts::set_pixel_type + +bool JpegXlSaveOpts::set_precision(int gimp_precision) { + switch (gimp_precision) { + // Note: all floating point formats save as linear + // to prevent gamma interpretation problems when viewing. + // See documentation of JxlEncoderAddImageFrame(). + case GIMP_PRECISION_HALF_GAMMA: + case GIMP_PRECISION_HALF_LINEAR: + is_linear = true; + set_pixel_type(JXL_TYPE_FLOAT16); + break; + case GIMP_PRECISION_FLOAT_GAMMA: + case GIMP_PRECISION_FLOAT_LINEAR: + is_linear = true; + set_pixel_type(JXL_TYPE_FLOAT); + break; + + // Note: all INT formats save as non-linear to prevent + // gamma interpretation problems when viewing. + // See documentation of JxlEncoderAddImageFrame(). + case GIMP_PRECISION_U32_GAMMA: + case GIMP_PRECISION_U32_LINEAR: + is_linear = false; + set_pixel_type(JXL_TYPE_UINT32); + break; + case GIMP_PRECISION_U16_GAMMA: + case GIMP_PRECISION_U16_LINEAR: + is_linear = false; + set_pixel_type(JXL_TYPE_UINT16); + break; + + default: + case GIMP_PRECISION_U8_LINEAR: + case GIMP_PRECISION_U8_GAMMA: + is_linear = false; + set_pixel_type(JXL_TYPE_UINT8); + break; + } + return true; +} // JpegXlSaveOpts::set_precision + +} // namespace + +bool SaveJpegXlImage(const gint32 image_id, const gint32 drawable_id, + const gint32 orig_image_id, const gchar* const filename) { + if (!SaveDialog()) { + return true; + } + + gint32 nlayers; + gint32* layers; + + JpegXlGimpProgress gimp_save_progress( + ("Saving JPEG XL file:" + std::string(filename)).c_str()); + gimp_save_progress.update(); + + // try to get ICC color profile... + std::vector icc; + + GimpColorProfile* profile = gimp_image_get_color_profile(image_id); + + if (profile) { + jxl_save_opts.is_linear = gimp_color_profile_is_linear(profile); + jxl_save_opts.is_gray = gimp_color_profile_is_gray(profile); + + g_printerr("JXL Info: Extracting ICC Profile...\n"); + gsize icc_size; + const guint8* const icc_bytes = + gimp_color_profile_get_icc_profile(profile, &icc_size); + + icc.assign(icc_bytes, icc_bytes + icc_size); + } else { + g_printerr("JXL Info: No ICC profile. Exporting image anyway.\n"); + } + + gimp_save_progress.update(); + + jxl_save_opts.set_dimensions(gimp_image_width(image_id), + gimp_image_height(image_id)); + + layers = gimp_image_get_layers(image_id, &nlayers); + + for (gint32 i = 0; i < nlayers; i++) { + if (gimp_drawable_has_alpha(layers[i])) { + jxl_save_opts.has_alpha = true; + break; + } + } + + gimp_save_progress.update(); + + // JxlEncoderAddImageFrame() doesn't currently support + // JXL_TYPE_UINT32. Rebuilding babl_format to match + // the JxlPixelFormat to allow export to UINT16. + // + // When this is no longer necessary, will be able to + // replace extraneous code with just: + // native_format = gegl_buffer_get_format(buffer); + // + const Babl* native_format; + jxl_save_opts.set_precision(gimp_image_get_precision(image_id)); + jxl_save_opts.set_model(gimp_drawable_type(drawable_id)); + native_format = babl_format(jxl_save_opts.babl_format_str.c_str()); + + gimp_save_progress.update(); + + // multi-threaded parallel runner. + auto runner = JxlResizableParallelRunnerMake(nullptr); + + JxlResizableParallelRunnerSetThreads( + runner.get(), + JxlResizableParallelRunnerSuggestThreads(jxl_save_opts.basic_info.xsize, + jxl_save_opts.basic_info.ysize)); + + auto enc = JxlEncoderMake(/*memory_manager=*/nullptr); + JxlEncoderUseContainer(enc.get(), jxl_save_opts.use_container); + + if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(), + JxlResizableParallelRunner, + runner.get())) { + g_printerr("JXL Error: JxlEncoderSetParallelRunner failed\n"); + return false; + } + + if (JXL_ENC_SUCCESS != + JxlEncoderSetBasicInfo(enc.get(), &jxl_save_opts.basic_info)) { + g_printerr("JXL Error: JxlEncoderSetBasicInfo failed\n"); + return false; + } + + // try to use ICC profile + if (icc.size() > 0 && !gimp_color_profile_is_gray(profile)) { + if (JXL_ENC_SUCCESS != + JxlEncoderSetICCProfile(enc.get(), icc.data(), icc.size())) { + g_printerr("JXL Warning: JxlEncoderSetICCProfile failed.\n"); + jxl_save_opts.basic_info.uses_original_profile = false; + } + } else { + g_printerr("JXL Warning: Using internal profile.\n"); + jxl_save_opts.basic_info.uses_original_profile = false; + } + + // detect internal color profile + JxlColorEncoding color_encoding = {}; + + if (jxl_save_opts.is_linear) { + JxlColorEncodingSetToLinearSRGB( + &color_encoding, + /*is_gray=*/jxl_save_opts.pixel_format.num_channels < 3); + } else { + JxlColorEncodingSetToSRGB( + &color_encoding, + /*is_gray=*/jxl_save_opts.pixel_format.num_channels < 3); + } + + if (JXL_ENC_SUCCESS != + JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) { + g_printerr("JXL Warning: JxlEncoderSetColorEncoding failed\n"); + } + + // set encoder options + JxlEncoderOptions* enc_opts; + enc_opts = JxlEncoderOptionsCreate(enc.get(), nullptr); + + JxlEncoderOptionsSetEffort(enc_opts, jxl_save_opts.encoding_effort); + JxlEncoderOptionsSetDecodingSpeed(enc_opts, jxl_save_opts.faster_decoding); + + if (jxl_save_opts.lossless || jxl_save_opts.distance < 0.01f) { + if (jxl_save_opts.basic_info.exponent_bits_per_sample > 0) { + // lossless mode doesn't work with floating point + jxl_save_opts.distance = 0.01; + JxlEncoderOptionsSetLossless(enc_opts, false); + JxlEncoderOptionsSetDistance(enc_opts, 0.01); + } else { + JxlEncoderOptionsSetDistance(enc_opts, 0); + JxlEncoderOptionsSetLossless(enc_opts, true); + } + } else { + JxlEncoderOptionsSetLossless(enc_opts, false); + JxlEncoderOptionsSetDistance(enc_opts, jxl_save_opts.distance); + } + + // process layers and compress into JXL + std::vector compressed; + compressed.resize(262144); + uint8_t* next_out = compressed.data(); + size_t avail_out = compressed.size(); + + size_t buffer_size = jxl_save_opts.basic_info.xsize * + jxl_save_opts.basic_info.ysize * + jxl_save_opts.pixel_format.num_channels * + (jxl_save_opts.basic_info.bits_per_sample >> 3); + + nlayers = 1; // just process one layer for now + for (gint32 i = 0; i < nlayers; i++) { + gimp_save_progress.update(); + + // copy image into buffer... + gpointer pixels_buffer; + pixels_buffer = g_malloc(buffer_size); + + GeglBuffer* buffer = gimp_drawable_get_buffer(layers[i]); + gegl_buffer_get(buffer, + GEGL_RECTANGLE(0, 0, jxl_save_opts.basic_info.xsize, + jxl_save_opts.basic_info.ysize), + 1.0, native_format, pixels_buffer, GEGL_AUTO_ROWSTRIDE, + GEGL_ABYSS_NONE); + + g_clear_object(&buffer); + + gimp_save_progress.update(); + + // compress layer + if (JXL_ENC_SUCCESS != + JxlEncoderAddImageFrame(enc_opts, &jxl_save_opts.pixel_format, + pixels_buffer, buffer_size)) { + g_printerr("JXL Error: JxlEncoderAddImageFrame failed\n"); + return false; + } + + // get data from encoder + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + gimp_save_progress.update(); + + process_result = + JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); + if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + size_t offset = next_out - compressed.data(); + compressed.resize(compressed.size() + 262144); + next_out = compressed.data() + offset; + avail_out = compressed.size() - offset; + } + } + + if (JXL_ENC_SUCCESS != process_result) { + g_printerr("JXL Error: JxlEncoderProcessOutput failed\n"); + return false; + } + } + + JxlEncoderCloseInput(enc.get()); + + compressed.resize(next_out - compressed.data()); + + // write file + std::ofstream outstream(filename, std::ios::out | std::ios::binary); + copy(compressed.begin(), compressed.end(), + std::ostream_iterator(outstream)); + + gimp_save_progress.finished(); + return true; +} // SaveJpegXlImage() + +} // namespace jxl diff --git a/plugins/gimp/file-jxl-save.h b/plugins/gimp/file-jxl-save.h new file mode 100644 index 0000000..9dfa45c --- /dev/null +++ b/plugins/gimp/file-jxl-save.h @@ -0,0 +1,20 @@ +// 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 PLUGINS_GIMP_FILE_JXL_SAVE_H_ +#define PLUGINS_GIMP_FILE_JXL_SAVE_H_ + +#include "jxl/encode.h" +#include "jxl/encode_cxx.h" +#include "plugins/gimp/common.h" + +namespace jxl { + +bool SaveJpegXlImage(gint32 image_id, gint32 drawable_id, gint32 orig_image_id, + const gchar* filename); + +} // namespace jxl + +#endif // PLUGINS_GIMP_FILE_JXL_SAVE_H_ diff --git a/plugins/gimp/file-jxl.cc b/plugins/gimp/file-jxl.cc new file mode 100644 index 0000000..743495a --- /dev/null +++ b/plugins/gimp/file-jxl.cc @@ -0,0 +1,157 @@ +// 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 + +#include + +#include "plugins/gimp/common.h" +#include "plugins/gimp/file-jxl-load.h" +#include "plugins/gimp/file-jxl-save.h" + +namespace jxl { +namespace { + +constexpr char kLoadProc[] = "file-jxl-load"; +constexpr char kSaveProc[] = "file-jxl-save"; + +void Query() { + { + static char run_mode_name[] = "run-mode"; + static char run_mode_description[] = "Run mode"; + static char filename_name[] = "filename"; + static char filename_description[] = "The name of the file to load"; + static char raw_filename_name[] = "raw-filename"; + static char raw_filename_description[] = + "The name of the file, as entered by the user"; + static const GimpParamDef load_args[] = { + {GIMP_PDB_INT32, run_mode_name, run_mode_description}, + {GIMP_PDB_STRING, filename_name, filename_description}, + {GIMP_PDB_STRING, raw_filename_name, raw_filename_description}, + }; + static char image_name[] = "image"; + static char image_description[] = "Loaded image"; + static const GimpParamDef load_return_vals[] = { + {GIMP_PDB_IMAGE, image_name, image_description}, + }; + + gimp_install_procedure( + /*name=*/kLoadProc, /*blurb=*/"Loads JPEG XL image files", + /*help=*/"Loads JPEG XL image files", /*author=*/"JPEG XL Project", + /*copyright=*/"JPEG XL Project", /*date=*/"2019", + /*menu_label=*/"JPEG XL image", /*image_types=*/nullptr, + /*type=*/GIMP_PLUGIN, /*n_params=*/G_N_ELEMENTS(load_args), + /*n_return_vals=*/G_N_ELEMENTS(load_return_vals), /*params=*/load_args, + /*return_vals=*/load_return_vals); + gimp_register_file_handler_mime(kLoadProc, "image/jxl"); + gimp_register_magic_load_handler( + kLoadProc, "jxl", "", + "0,string,\xFF\x0A," + "0,string,\\000\\000\\000\x0CJXL\\040\\015\\012\x87\\012"); + } + + { + static char run_mode_name[] = "run-mode"; + static char run_mode_description[] = "Run mode"; + static char image_name[] = "image"; + static char image_description[] = "Input image"; + static char drawable_name[] = "drawable"; + static char drawable_description[] = "Drawable to save"; + static char filename_name[] = "filename"; + static char filename_description[] = "The name of the file to save"; + static char raw_filename_name[] = "raw-filename"; + static char raw_filename_description[] = "The name of the file to save"; + static const GimpParamDef save_args[] = { + {GIMP_PDB_INT32, run_mode_name, run_mode_description}, + {GIMP_PDB_IMAGE, image_name, image_description}, + {GIMP_PDB_DRAWABLE, drawable_name, drawable_description}, + {GIMP_PDB_STRING, filename_name, filename_description}, + {GIMP_PDB_STRING, raw_filename_name, raw_filename_description}, + }; + + gimp_install_procedure( + /*name=*/kSaveProc, /*blurb=*/"Saves JPEG XL image files", + /*help=*/"Saves JPEG XL image files", /*author=*/"JPEG XL Project", + /*copyright=*/"JPEG XL Project", /*date=*/"2019", + /*menu_label=*/"JPEG XL image", /*image_types=*/"RGB*, GRAY*", + /*type=*/GIMP_PLUGIN, /*n_params=*/G_N_ELEMENTS(save_args), + /*n_return_vals=*/0, /*params=*/save_args, + /*return_vals=*/nullptr); + gimp_register_file_handler_mime(kSaveProc, "image/jxl"); + gimp_register_save_handler(kSaveProc, "jxl", ""); + } +} + +void Run(const gchar* const name, const gint nparams, + const GimpParam* const params, gint* const nreturn_vals, + GimpParam** const return_vals) { + gegl_init(nullptr, nullptr); + + static GimpParam values[2]; + + *nreturn_vals = 1; + *return_vals = values; + + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + if (strcmp(name, kLoadProc) == 0) { + if (nparams != 3) { + values[0].data.d_status = GIMP_PDB_CALLING_ERROR; + return; + } + + const gchar* const filename = params[1].data.d_string; + gint32 image_id; + if (!LoadJpegXlImage(filename, &image_id)) { + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + return; + } + + *nreturn_vals = 2; + values[0].data.d_status = GIMP_PDB_SUCCESS; + values[1].type = GIMP_PDB_IMAGE; + values[1].data.d_image = image_id; + } else if (strcmp(name, kSaveProc) == 0) { + if (nparams != 5) { + values[0].data.d_status = GIMP_PDB_CALLING_ERROR; + return; + } + + gint32 image_id = params[1].data.d_image; + gint32 drawable_id = params[2].data.d_drawable; + const gchar* const filename = params[3].data.d_string; + const gint32 orig_image_id = image_id; + const GimpExportReturn export_result = gimp_export_image( + &image_id, &drawable_id, "JPEG XL", + static_cast(GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_GRAY | + GIMP_EXPORT_CAN_HANDLE_ALPHA)); + switch (export_result) { + case GIMP_EXPORT_CANCEL: + values[0].data.d_status = GIMP_PDB_CANCEL; + return; + case GIMP_EXPORT_IGNORE: + break; + case GIMP_EXPORT_EXPORT: + break; + } + if (!SaveJpegXlImage(image_id, drawable_id, orig_image_id, filename)) { + return; + } + if (image_id != orig_image_id) { + gimp_image_delete(image_id); + } + values[0].data.d_status = GIMP_PDB_SUCCESS; + } +} + +} // namespace +} // namespace jxl + +static const GimpPlugInInfo PLUG_IN_INFO = {nullptr, nullptr, &jxl::Query, + &jxl::Run}; + +MAIN() diff --git a/plugins/mime/CMakeLists.txt b/plugins/mime/CMakeLists.txt new file mode 100644 index 0000000..6f2a0f9 --- /dev/null +++ b/plugins/mime/CMakeLists.txt @@ -0,0 +1,6 @@ +# 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. + +install(FILES image-jxl.xml DESTINATION share/mime/packages/) diff --git a/plugins/mime/README.md b/plugins/mime/README.md new file mode 100644 index 0000000..4b5373c --- /dev/null +++ b/plugins/mime/README.md @@ -0,0 +1,20 @@ +## JPEG XL MIME type + +If not already installed by the [Installing section of README.md](../../README.md#installing), then it can be done manually: + +### Install +```bash +sudo xdg-mime install --novendor image-jxl.xml +``` + +Then run: +``` +update-mime --local +``` + + +### Uninstall +```bash +sudo xdg-mime uninstall image-jxl.xml +``` + diff --git a/plugins/mime/image-jxl.xml b/plugins/mime/image-jxl.xml new file mode 100644 index 0000000..cab9018 --- /dev/null +++ b/plugins/mime/image-jxl.xml @@ -0,0 +1,13 @@ + + + + JPEG XL image + image JPEG XL + JPEG XL afbeelding + + + + + + + diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..b5e1899 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,223 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Enable tests in third_party/ as well. +enable_testing() +include(CTest) + +if(BUILD_TESTING) +# Add GTest from source and alias it to what the find_package(GTest) workflow +# defines. Omitting googletest/ directory would require it to be available in +# the base system instead, but it would work just fine. This makes packages +# using GTest and calling find_package(GTest) actually work. +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/googletest/CMakeLists.txt" AND + NOT JPEGXL_FORCE_SYSTEM_GTEST) + add_subdirectory(googletest EXCLUDE_FROM_ALL) + + set(GTEST_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/googletest/googletest") + set(GTEST_INCLUDE_DIR "$" + CACHE STRING "") + set(GMOCK_INCLUDE_DIR "$") + set(GTEST_LIBRARY "$") + set(GTEST_MAIN_LIBRARY "$") + add_library(GTest::GTest ALIAS gtest) + add_library(GTest::Main ALIAS gtest_main) + + set_target_properties(gtest PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gmock PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gtest_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + set_target_properties(gmock_main PROPERTIES POSITION_INDEPENDENT_CODE TRUE) + + # googletest doesn't compile clean with clang-cl (-Wundef) + if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gmock PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gtest_main PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gmock_main PROPERTIES COMPILE_FLAGS "-Wno-error") + endif () + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/googletest/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) +else() + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/googletest/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.googletest COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() +find_package(GTest) +if (NOT GTEST_FOUND) + set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) + message(SEND_ERROR "GTest not found. Install googletest package " + "(libgtest-dev) in the system or download googletest to " + "third_party/googletest from https://github.com/google/googletest ." + "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") +endif() # NOT GTEST_FOUND + +# Look for gmock in the system too. +if (NOT DEFINED GMOCK_INCLUDE_DIR) + find_path( + GMOCK_INCLUDE_DIR "gmock/gmock.h" + HINTS ${GTEST_INCLUDE_DIRS}) + if ("${GMOCK_INCLUDE_DIR}" STREQUAL "GMOCK_INCLUDE_DIR-NOTFOUND") + set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE) + message(SEND_ERROR "GMock not found. Install googletest package " + "(libgmock-dev) in the system or download googletest to " + "third_party/googletest from https://github.com/google/googletest ." + "To disable tests instead re-run cmake with -DBUILD_TESTING=OFF.") + else() + message(STATUS "Found GMock: ${GMOCK_INCLUDE_DIR}") + endif() # GMOCK_INCLUDE_DIR-NOTFOUND +endif() # NOT DEFINED GMOCK_INCLUDE_DIR +endif() # BUILD_TESTING + +# Highway +set(HWY_SYSTEM_GTEST ON CACHE INTERNAL "") +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/highway/CMakeLists.txt" AND + NOT JPEGXL_FORCE_SYSTEM_HWY) + add_subdirectory(highway) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/highway/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.highway COPYONLY) +else() + pkg_check_modules(HWY libhwy) + if (NOT HWY_FOUND) + message(FATAL_ERROR + "Highway library (hwy) not found. Install libhwy-dev or download it " + "to third_party/highway from https://github.com/google/highway . " + "Highway is required to build JPEG XL. You can run " + "${PROJECT_SOURCE_DIR}/deps.sh to download this dependency.") + endif() + add_library(hwy INTERFACE IMPORTED GLOBAL) + if(${CMAKE_VERSION} VERSION_LESS "3.13.5") + set_property(TARGET hwy PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${HWY_INCLUDE_DIR}) + target_link_libraries(hwy INTERFACE ${HWY_LDFLAGS}) + set_property(TARGET hwy PROPERTY INTERFACE_COMPILE_OPTIONS ${HWY_CFLAGS_OTHER}) + else() + target_include_directories(hwy INTERFACE ${HWY_INCLUDE_DIRS}) + target_link_libraries(hwy INTERFACE ${HWY_LINK_LIBRARIES}) + target_link_options(hwy INTERFACE ${HWY_LDFLAGS_OTHER}) + target_compile_options(hwy INTERFACE ${HWY_CFLAGS_OTHER}) + endif() + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libhwy-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.highway COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +endif() + +# lodepng +if( NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lodepng/lodepng.h" ) + message(FATAL_ERROR "Please run ${PROJECT_SOURCE_DIR}/deps.sh to fetch the " + "build dependencies.") +endif() +include(lodepng.cmake) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/lodepng/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.lodepng COPYONLY) + +# brotli +if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/brotli/c/include/brotli/decode.h" OR + JPEGXL_FORCE_SYSTEM_BROTLI) + # Create the libbrotli* and libbrotli*-static targets. + foreach(brlib IN ITEMS brotlienc brotlidec brotlicommon) + # Use uppercase like "BROTLIENC" for the cmake variables + string(TOUPPER "${brlib}" BRPREFIX) + pkg_check_modules(${BRPREFIX} lib${brlib}) + if (${BRPREFIX}_FOUND) + if(${CMAKE_VERSION} VERSION_LESS "3.13.5") + add_library(${brlib} INTERFACE IMPORTED GLOBAL) + set_property(TARGET ${brlib} PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${${BRPREFIX}_INCLUDE_DIR}) + target_link_libraries(${brlib} INTERFACE ${${BRPREFIX}_LDFLAGS}) + set_property(TARGET ${brlib} PROPERTY INTERFACE_COMPILE_OPTIONS ${${BRPREFIX}_CFLAGS_OTHER}) + + add_library(${brlib}-static INTERFACE IMPORTED GLOBAL) + set_property(TARGET ${brlib}-static PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${${BRPREFIX}_INCLUDE_DIR}) + target_link_libraries(${brlib}-static INTERFACE ${${BRPREFIX}_LDFLAGS}) + set_property(TARGET ${brlib}-static PROPERTY INTERFACE_COMPILE_OPTIONS ${${BRPREFIX}_CFLAGS_OTHER}) + else() + add_library(${brlib} INTERFACE IMPORTED GLOBAL) + target_include_directories(${brlib} + INTERFACE ${${BRPREFIX}_INCLUDE_DIRS}) + target_link_libraries(${brlib} + INTERFACE ${${BRPREFIX}_LINK_LIBRARIES}) + target_link_options(${brlib} + INTERFACE ${${BRPREFIX}_LDFLAGS_OTHER}) + target_compile_options(${brlib} + INTERFACE ${${BRPREFIX}_CFLAGS_OTHER}) + + # TODO(deymo): Remove the -static library versions, this target is + # currently needed by brunsli.cmake. When importing it this way, the + # brotli*-static target is just an alias. + add_library(${brlib}-static ALIAS ${brlib}) + endif() + endif() + unset(BRPREFIX) + endforeach() + + if (BROTLIENC_FOUND AND BROTLIDEC_FOUND AND BROTLICOMMON_FOUND) + set(BROTLI_FOUND 1) + else() + set(BROTLI_FOUND 0) + endif() + + if (NOT BROTLI_FOUND) + message(FATAL_ERROR + "Brotli not found, install brotli-dev or download brotli source code to" + " third_party/brotli from https://github.com/google/brotli. You can use" + " ${PROJECT_SOURCE_DIR}/deps.sh to download this dependency.") + endif () + if(JPEGXL_DEP_LICENSE_DIR) + configure_file("${JPEGXL_DEP_LICENSE_DIR}/libbrotli-dev/copyright" + ${PROJECT_BINARY_DIR}/LICENSE.brotli COPYONLY) + endif() # JPEGXL_DEP_LICENSE_DIR +else() + # Compile brotli from sources. + set(BROTLI_DISABLE_TESTS ON CACHE STRING "Disable Brotli tests") + add_subdirectory(brotli EXCLUDE_FROM_ALL) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/brotli/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.brotli COPYONLY) + if(BROTLI_EMSCRIPTEN) + # Brotli only defines the -static targets when using emscripten. + foreach(brlib IN ITEMS brotlienc brotlidec brotlicommon) + add_library(${brlib} ALIAS ${brlib}-static) + endforeach() + endif() # BROTLI_EMSCRIPTEN +endif() + +# *cms +if (JPEGXL_ENABLE_SKCMS OR JPEGXL_ENABLE_PLUGINS) + if( NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/skcms/skcms.h" ) + message(FATAL_ERROR "Please run ${PROJECT_SOURCE_DIR}/deps.sh to fetch the " + "build dependencies.") + endif() + include(skcms.cmake) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/skcms/LICENSE" + ${PROJECT_BINARY_DIR}/LICENSE.skcms COPYONLY) +endif () +if (JPEGXL_ENABLE_VIEWERS OR NOT JPEGXL_ENABLE_SKCMS) + if( NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lcms/.git" ) + message(SEND_ERROR "Please run git submodule update --init") + endif() + include(lcms2.cmake) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/lcms/COPYING" + ${PROJECT_BINARY_DIR}/LICENSE.lcms COPYONLY) +endif() + +# sjpeg +if (JPEGXL_ENABLE_SJPEG) + if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/sjpeg/CMakeLists.txt") + message(FATAL_ERROR "Please run ${PROJECT_SOURCE_DIR}/deps.sh to fetch the " + "build dependencies.") + endif() + include(sjpeg.cmake) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/sjpeg/COPYING" + ${PROJECT_BINARY_DIR}/LICENSE.sjpeg COPYONLY) +endif () + diff --git a/third_party/HEVCSoftware/README.md b/third_party/HEVCSoftware/README.md new file mode 100644 index 0000000..70ebaeb --- /dev/null +++ b/third_party/HEVCSoftware/README.md @@ -0,0 +1,2 @@ +This directory contains modified configuration files from the reference HEVC +encoder, the source code of which can be found at: https://hevc.hhi.fraunhofer.de/svn/svn_HEVCSoftware/ diff --git a/third_party/HEVCSoftware/cfg/LICENSE b/third_party/HEVCSoftware/cfg/LICENSE new file mode 100644 index 0000000..a9d8844 --- /dev/null +++ b/third_party/HEVCSoftware/cfg/LICENSE @@ -0,0 +1,31 @@ +The copyright in this software is being made available under the BSD +License, included below. This software may be subject to other third party +and contributor rights, including patent rights, and no such rights are +granted under this license.   + +Copyright (c) 2010-2017, ITU/ISO/IEC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the ITU/ISO/IEC nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/HEVCSoftware/cfg/encoder_intra_main_scc_10.cfg b/third_party/HEVCSoftware/cfg/encoder_intra_main_scc_10.cfg new file mode 100644 index 0000000..5f6b958 --- /dev/null +++ b/third_party/HEVCSoftware/cfg/encoder_intra_main_scc_10.cfg @@ -0,0 +1,136 @@ +#======== File I/O ===================== +BitstreamFile : str.bin +ReconFile : rec.yuv + +#======== Profile definition ============== +Profile : main-SCC # Profile name to use for encoding. Use main (for FDIS main), main10 (for FDIS main10), main-still-picture, main-RExt, high-throughput-RExt, main-SCC +Tier : main # Tier to use for interpretation of --Level (main or high only)" + +#======== Unit definition ================ +MaxCUWidth : 64 # Maximum coding unit width in pixel +MaxCUHeight : 64 # Maximum coding unit height in pixel +MaxPartitionDepth : 4 # Maximum coding unit depth +QuadtreeTULog2MaxSize : 5 # Log2 of maximum transform size for + # quadtree-based TU coding (2...6) +QuadtreeTULog2MinSize : 2 # Log2 of minimum transform size for + # quadtree-based TU coding (2...6) +QuadtreeTUMaxDepthInter : 3 +QuadtreeTUMaxDepthIntra : 3 + +#======== Coding Structure ============= +IntraPeriod : 1 # Period of I-Frame ( -1 = only first) +DecodingRefreshType : 1 # Random Access 0:none, 1:CRA, 2:IDR, 3:Recovery Point SEI +GOPSize : 1 # GOP Size (number of B slice = GOPSize-1) +ReWriteParamSetsFlag : 1 # Write parameter sets with every IRAP +# Type POC QPoffset QPfactor tcOffsetDiv2 betaOffsetDiv2 temporal_id #ref_pics_active #ref_pics reference pictures + +#=========== Motion Search ============= +FastSearch : 1 # 0:Full search 1:TZ search +SearchRange : 64 # (0: Search range is a Full frame) +HadamardME : 1 # Use of hadamard measure for fractional ME +FEN : 1 # Fast encoder decision +FDM : 1 # Fast Decision for Merge RD cost + +#======== Quantization ============= +QP : 32 # Quantization parameter(0-51) +MaxDeltaQP : 0 # CU-based multi-QP optimization +MaxCuDQPDepth : 0 # Max depth of a minimum CuDQP for sub-LCU-level delta QP +DeltaQpRD : 0 # Slice-based multi-QP optimization +RDOQ : 1 # RDOQ +RDOQTS : 1 # RDOQ for transform skip +CbQpOffset : 6 +CrQpOffset : 6 + +#=========== Deblock Filter ============ +LoopFilterOffsetInPPS : 1 # Dbl params: 0=varying params in SliceHeader, param = base_param + GOP_offset_param; 1 (default) =constant params in PPS, param = base_param) +LoopFilterDisable : 0 # Disable deblocking filter (0=Filter, 1=No Filter) +LoopFilterBetaOffset_div2 : 0 # base_param: -6 ~ 6 +LoopFilterTcOffset_div2 : 0 # base_param: -6 ~ 6 +DeblockingFilterMetric : 0 # blockiness metric (automatically configures deblocking parameters in bitstream). Applies slice-level loop filter offsets (LoopFilterOffsetInPPS and LoopFilterDisable must be 0) + +#=========== Misc. ============ +InternalBitDepth : 10 # codec operating bit-depth + +#=========== Coding Tools ================= +SAO : 1 # Sample adaptive offset (0: OFF, 1: ON) +AMP : 1 # Asymmetric motion partitions (0: OFF, 1: ON) +TransformSkip : 1 # Transform skipping (0: OFF, 1: ON) +TransformSkipFast : 1 # Fast Transform skipping (0: OFF, 1: ON) +SAOLcuBoundary : 0 # SAOLcuBoundary using non-deblocked pixels (0: OFF, 1: ON) + +#============ Slices ================ +SliceMode : 0 # 0: Disable all slice options. + # 1: Enforce maximum number of LCU in an slice, + # 2: Enforce maximum number of bytes in an 'slice' + # 3: Enforce maximum number of tiles in a slice +SliceArgument : 1500 # Argument for 'SliceMode'. + # If SliceMode==1 it represents max. SliceGranularity-sized blocks per slice. + # If SliceMode==2 it represents max. bytes per slice. + # If SliceMode==3 it represents max. tiles per slice. + +LFCrossSliceBoundaryFlag : 1 # In-loop filtering, including ALF and DB, is across or not across slice boundary. + # 0:not across, 1: across + +#============ PCM ================ +PCMEnabledFlag : 0 # 0: No PCM mode +PCMLog2MaxSize : 5 # Log2 of maximum PCM block size. +PCMLog2MinSize : 3 # Log2 of minimum PCM block size. +PCMInputBitDepthFlag : 1 # 0: PCM bit-depth is internal bit-depth. 1: PCM bit-depth is input bit-depth. +PCMFilterDisableFlag : 0 # 0: Enable loop filtering on I_PCM samples. 1: Disable loop filtering on I_PCM samples. + +#============ Tiles ================ +TileUniformSpacing : 0 # 0: the column boundaries are indicated by TileColumnWidth array, the row boundaries are indicated by TileRowHeight array + # 1: the column and row boundaries are distributed uniformly +NumTileColumnsMinus1 : 0 # Number of tile columns in a picture minus 1 +TileColumnWidthArray : 2 3 # Array containing tile column width values in units of CTU (from left to right in picture) +NumTileRowsMinus1 : 0 # Number of tile rows in a picture minus 1 +TileRowHeightArray : 2 # Array containing tile row height values in units of CTU (from top to bottom in picture) + +LFCrossTileBoundaryFlag : 1 # In-loop filtering is across or not across tile boundary. + # 0:not across, 1: across + +#============ WaveFront ================ +WaveFrontSynchro : 0 # 0: No WaveFront synchronisation (WaveFrontSubstreams must be 1 in this case). + # >0: WaveFront synchronises with the LCU above and to the right by this many LCUs. + +#=========== Quantization Matrix ================= +ScalingList : 0 # ScalingList 0 : off, 1 : default, 2 : file read +ScalingListFile : scaling_list.txt # Scaling List file name. If file is not exist, use Default Matrix. + +#============ Lossless ================ +TransquantBypassEnable : 0 # Value of PPS flag. +CUTransquantBypassFlagForce: 0 # Force transquant bypass mode, when transquant_bypass_enable_flag is enabled + +#=========== RExt ============ +ExtendedPrecision : 0 # Increased internal accuracies to support high bit depths (not valid in V1 profiles) +TransformSkipLog2MaxSize : 2 # Specify transform-skip maximum size. Minimum 2. (not valid in V1 profiles) +ImplicitResidualDPCM : 1 # Enable implicitly signalled residual DPCM for intra (also known as sample-adaptive intra predict) (not valid in V1 profiles) +ExplicitResidualDPCM : 1 # Enable explicitly signalled residual DPCM for inter and intra-block-copy (not valid in V1 profiles) +ResidualRotation : 1 # Enable rotation of transform-skipped and transquant-bypassed TUs through 180 degrees prior to entropy coding (not valid in V1 profiles) +SingleSignificanceMapContext : 1 # Enable, for transform-skipped and transquant-bypassed TUs, the selection of a single significance map context variable for all coefficients (not valid in V1 profiles) +IntraReferenceSmoothing : 1 # 0: Disable use of intra reference smoothing (not valid in V1 profiles). 1: Enable use of intra reference smoothing (same as V1) +GolombRiceParameterAdaptation : 1 # Enable the partial retention of the Golomb-Rice parameter value from one coefficient group to the next +HighPrecisionPredictionWeighting : 1 # Use high precision option for weighted prediction (not valid in V1 profiles) +CrossComponentPrediction : 1 # Enable the use of cross-component prediction (not valid in V1 profiles) + +#=========== SCC ============ +IntraBlockCopyEnabled : 1 # Enable the use of intra block copying +HashBasedIntraBlockCopySearchEnabled : 1 # Use hash based search for intra block copying on 8x8 blocks +IntraBlockCopySearchWidthInCTUs : -1 # Search range for IBC (-1: full frame search) +IntraBlockCopyNonHashSearchWidthInCTUs : 3 # Search range for IBC non-hash search method (i.e., fast/full search) +MSEBasedSequencePSNR : 1 # 0:Emit sequence PSNR only as a linear average of the frame PSNRs, 1: also emit a sequence PSNR based on an average of the frame MSEs +PrintClippedPSNR : 1 # 0:Print lossless PSNR values as 999.99 dB, 1: clip lossless PSNR according to resolution +PrintFrameMSE : 1 # 0:emit only bit count and PSNRs for each frame, 1: also emit MSE values +PrintSequenceMSE : 1 # 0:emit only bit rate and PSNRs for the whole sequence, 1 = also emit MSE values +ColourTransform : 1 # Enable the use of color transform(not valid in V1 profiles) +PaletteMode : 1 # Enable the use of palette mode(not valid in V1 profiles) +PaletteMaxSize : 63 # Supported maximum palette size (not valid in V1 profiles) +PaletteMaxPredSize : 128 # Supported maximum palette predictor size (not valid in V1 profiles) +IntraBoundaryFilterDisabled : 1 # Disable the use of intra boundary filtering (not valid in V1 profiles) +TransquantBypassInferTUSplit : 1 # Infer TU splitting for transquant bypass CUs +PalettePredInSPSEnabled : 0 # Transmit palette predictor initializer in SPS (not valid in V1 profiles) +PalettePredInPPSEnabled : 0 # Transmit palette predictor initializer in PPS (not valid in V1 profiles) +SelectiveRDOQ : 1 # Selective RDOQ + +### DO NOT ADD ANYTHING BELOW THIS LINE ### +### DO NOT DELETE THE EMPTY LINE BELOW ### diff --git a/third_party/dirent.cc b/third_party/dirent.cc new file mode 100644 index 0000000..81015ed --- /dev/null +++ b/third_party/dirent.cc @@ -0,0 +1,142 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if defined(_WIN32) || defined(_WIN64) +#include "third_party/dirent.h" + +#include "lib/jxl/base/status.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif // NOMINMAX +#include + +#include +#include + +int mkdir(const char* path, mode_t /*mode*/) { + const LPSECURITY_ATTRIBUTES sec = nullptr; + if (!CreateDirectory(path, sec)) { + JXL_NOTIFY_ERROR("Failed to create directory %s", path); + return -1; + } + return 0; +} + +// Modified from code bearing the following notice: +// https://trac.wildfiregames.com/browser/ps/trunk/source/lib/sysdep/os/ +/* Copyright (C) 2010 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +struct DIR { + HANDLE hFind; + + WIN32_FIND_DATA findData; // indeterminate if hFind == INVALID_HANDLE_VALUE + + // readdir will return the address of this member. + // (must be stored in DIR to allow multiple independent + // opendir/readdir sequences). + dirent ent; + + // used by readdir to skip the first FindNextFile. + size_t numCalls = 0; +}; + +static bool IsValidDirectory(const char* path) { + const DWORD fileAttributes = GetFileAttributes(path); + + // path not found + if (fileAttributes == INVALID_FILE_ATTRIBUTES) return false; + + // not a directory + if ((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) return false; + + return true; +} + +DIR* opendir(const char* path) { + if (!IsValidDirectory(path)) { + errno = ENOENT; + return nullptr; + } + + std::unique_ptr

d(new DIR); + + // NB: "c:\\path" only returns information about that directory; + // trailing slashes aren't allowed. append "\\*" to retrieve its entries. + std::string searchPath(path); + if (searchPath.back() != '/' && searchPath.back() != '\\') { + searchPath += '\\'; + } + searchPath += '*'; + + // (we don't defer FindFirstFile until readdir because callers + // expect us to return 0 if directory reading will/did fail.) + d->hFind = FindFirstFile(searchPath.c_str(), &d->findData); + if (d->hFind != INVALID_HANDLE_VALUE) return d.release(); + if (GetLastError() == ERROR_NO_MORE_FILES) return d.release(); // empty + + JXL_NOTIFY_ERROR("Failed to open directory %s", searchPath.c_str()); + return nullptr; +} + +int closedir(DIR* dir) { + delete dir; + return 0; +} + +dirent* readdir(DIR* d) { + // "empty" case from opendir + if (d->hFind == INVALID_HANDLE_VALUE) return nullptr; + + // until end of directory or a valid entry was found: + for (;;) { + if (d->numCalls++ != 0) // (skip first call to FindNextFile - see opendir) + { + if (!FindNextFile(d->hFind, &d->findData)) { + JXL_ASSERT(GetLastError() == ERROR_NO_MORE_FILES); + SetLastError(0); + return nullptr; // end of directory or error + } + } + + // only return non-hidden and non-system entries + if ((d->findData.dwFileAttributes & + (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) == 0) { + d->ent.d_name = d->findData.cFileName; + return &d->ent; + } + } +} + +#endif // #if defined(_WIN32) || defined(_WIN64) diff --git a/third_party/dirent.h b/third_party/dirent.h new file mode 100644 index 0000000..37a08f4 --- /dev/null +++ b/third_party/dirent.h @@ -0,0 +1,49 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef LIB_JXL_THIRD_PARTY_DIRENT_H_ +#define LIB_JXL_THIRD_PARTY_DIRENT_H_ + +// Emulates POSIX readdir for Windows + +#if defined(_WIN32) || defined(_WIN64) + +#include // S_IFREG + +#ifndef _MODE_T_ +typedef unsigned int mode_t; +#endif // _MODE_T_ +int mkdir(const char* path, mode_t mode); + +struct dirent { + char* d_name; // no path +}; + +#define stat _stat64 + +#ifndef S_ISDIR +#define S_ISDIR(m) (m & S_IFDIR) +#endif // S_ISDIR + +#ifndef S_ISREG +#define S_ISREG(m) (m & S_IFREG) +#endif // S_ISREG + +struct DIR; +DIR* opendir(const char* path); +int closedir(DIR* dir); +dirent* readdir(DIR* d); + +#endif // #if defined(_WIN32) || defined(_WIN64) +#endif // LIB_JXL_THIRD_PARTY_DIRENT_H_ diff --git a/third_party/lcms2.cmake b/third_party/lcms2.cmake new file mode 100644 index 0000000..906e777 --- /dev/null +++ b/third_party/lcms2.cmake @@ -0,0 +1,63 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_library(lcms2 STATIC EXCLUDE_FROM_ALL + lcms/src/cmsalpha.c + lcms/src/cmscam02.c + lcms/src/cmscgats.c + lcms/src/cmscnvrt.c + lcms/src/cmserr.c + lcms/src/cmsgamma.c + lcms/src/cmsgmt.c + lcms/src/cmshalf.c + lcms/src/cmsintrp.c + lcms/src/cmsio0.c + lcms/src/cmsio1.c + lcms/src/cmslut.c + lcms/src/cmsmd5.c + lcms/src/cmsmtrx.c + lcms/src/cmsnamed.c + lcms/src/cmsopt.c + lcms/src/cmspack.c + lcms/src/cmspcs.c + lcms/src/cmsplugin.c + lcms/src/cmsps2.c + lcms/src/cmssamp.c + lcms/src/cmssm.c + lcms/src/cmstypes.c + lcms/src/cmsvirt.c + lcms/src/cmswtpnt.c + lcms/src/cmsxform.c + lcms/src/lcms2_internal.h +) +target_include_directories(lcms2 + PUBLIC "${CMAKE_CURRENT_LIST_DIR}/lcms/include") +# This warning triggers with gcc-8. +if (${CMAKE_C_COMPILER_ID} MATCHES "GNU") +target_compile_options(lcms2 + PRIVATE + # gcc-only flags. + -Wno-stringop-truncation + -Wno-strict-aliasing +) +endif() +# By default LCMS uses sizeof(void*) for memory alignment, but in arm 32-bits we +# can't access doubles not aligned to 8 bytes. This forces the alignment to 8 +# bytes. +target_compile_definitions(lcms2 + PRIVATE "-DCMS_PTR_ALIGNMENT=8") +target_compile_definitions(lcms2 + PUBLIC "-DCMS_NO_REGISTER_KEYWORD=1") + +set_property(TARGET lcms2 PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/third_party/lodepng.cmake b/third_party/lodepng.cmake new file mode 100644 index 0000000..51aa52a --- /dev/null +++ b/third_party/lodepng.cmake @@ -0,0 +1,22 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_library(lodepng STATIC EXCLUDE_FROM_ALL + lodepng/lodepng.cpp + lodepng/lodepng.h +) +# This library can be included into position independent binaries. +set_target_properties(lodepng PROPERTIES POSITION_INDEPENDENT_CODE TRUE) +target_include_directories(lodepng + PUBLIC "${CMAKE_CURRENT_LIST_DIR}/lodepng") diff --git a/third_party/sjpeg.cmake b/third_party/sjpeg.cmake new file mode 100644 index 0000000..152a86e --- /dev/null +++ b/third_party/sjpeg.cmake @@ -0,0 +1,23 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# We need to CACHE the SJPEG_BUILD_EXAMPLES to not be removed by the option() +# inside SJPEG. +set(SJPEG_BUILD_EXAMPLES NO CACHE BOOL "Examples") +# SJPEG uses OpenGL which throws a warning if multiple options are installed. +# This setting makes it prefer the new version. +set(OpenGL_GL_PREFERENCE GLVND) + +add_subdirectory(sjpeg EXCLUDE_FROM_ALL) +target_include_directories(sjpeg PUBLIC "${CMAKE_CURRENT_LIST_DIR}/sjpeg/src/") diff --git a/third_party/skcms.cmake b/third_party/skcms.cmake new file mode 100644 index 0000000..61b1ca5 --- /dev/null +++ b/third_party/skcms.cmake @@ -0,0 +1,51 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_library(skcms-obj OBJECT EXCLUDE_FROM_ALL skcms/skcms.cc) +target_include_directories(skcms-obj PUBLIC "${CMAKE_CURRENT_LIST_DIR}/skcms/") + +# This library is meant to be compiled/used by external libs (such as plugins) +# that need to use skcms. We use a wrapper for libjxl. +add_library(skcms-interface INTERFACE) +target_sources(skcms-interface INTERFACE ${CMAKE_CURRENT_LIST_DIR}/skcms/skcms.cc) +target_include_directories(skcms-interface INTERFACE ${CMAKE_CURRENT_LIST_DIR}/skcms) + +include(CheckCXXCompilerFlag) +check_cxx_compiler_flag("-Wno-psabi" CXX_WPSABI_SUPPORTED) +if(CXX_WPSABI_SUPPORTED) + target_compile_options(skcms-obj PRIVATE -Wno-psabi) + target_compile_options(skcms-interface INTERFACE -Wno-psabi) +endif() + +if(JPEGXL_BUNDLE_SKCMS) + target_compile_options(skcms-obj PRIVATE -DJPEGXL_BUNDLE_SKCMS=1) + if(MSVC) + target_compile_options(skcms-obj + PRIVATE /FI${CMAKE_SOURCE_DIR}/lib/jxl/enc_jxl_skcms.h) + else() + target_compile_options(skcms-obj + PRIVATE -include${CMAKE_SOURCE_DIR}/lib/jxl/enc_jxl_skcms.h) + endif() +endif() + +set_target_properties(skcms-obj PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 +) + +add_library(skcms STATIC EXCLUDE_FROM_ALL $) +target_include_directories(skcms + PUBLIC $) + diff --git a/third_party/testdata/dots/ellipses.png b/third_party/testdata/dots/ellipses.png new file mode 100644 index 0000000000000000000000000000000000000000..a536988d207bcf8f40e0a1bb912bc00ae2c43459 GIT binary patch literal 7359 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≪vh0V8(`q^U3(d_`gFDw&E^ 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

la;5c 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 z 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

wKw&H?po6ALrN`cKL8mWI1 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

n6QXIAp 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μ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%0D~#WCBS%TJBPV!1d|9bnYCy%?gI4yX!@LW`>G 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�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%0A>5;x}KpYg_FU)SRx5IpcNl|*!t>+G)q9h7RA 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%0ARcbW)R*qqGF7+jkC^WTGNXeV^pZR5@nE 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

4UWF3=crosqp-GtGkmVBVVVwpbi8?By3PGe9 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�DhxWV;aprDLUGKLUD27NmrBviGcd3 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�e^H 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

nC^ob}%eJ+X!rVCt&vjJ$F zuL@!{C4$9_-eXXg;jFvB13VxTgG$zN=2N#_YpCJmGFT4(P%fk^aJ4=e^fWsZRjE>) zSc3Z9GryulW{OC6Ii`6^UxNxD&CoCzXtP2^!OLmp}j)|`F)L>4O1uctwX?9R+ zYO^*0Q^=<(s3VsPXHZM2X+!r`vx+b*=DBnO*nMWH(fSCa(Ykf>i-0R}jzEqr1}#}j zB4|t-oX)5q_qn-G68Etr5m5b{h`1$F)*5yBXxahFc4y}b$(&35c zjYk=3XX~Y`pn2!6?$`u?brZve_y==pvrFNo;Nfj+@GYz5-KaS?AaBlTl)L@;jS%!T z-z4woZEM!D-w;&Al=PM~-5wZ<3y?~uo?STnc;ofetNwMwOc>5dTZ3i&rqafFEaP~; zu(5d6?JM?5Qmz==H>r*!`^VJ(g>wtHx7qJtKbbqngY5h9GSMybi|#1m4708kjm4=B zuEpN;x8&`9Ww*+cw=ocN3u@a6yGfL$L>YkicH>ic&sY5Rrsj`O)RYCZAnY!gZCTv4 zbXw*zZcnud^(tx9X9sKj`oconR9e$=-72(?FsQz|LfRIV;i@*pl}Gh%1{lCbhyl%| z;V|~IXfV1SQR^(K`Mr}uG%maw%8?9o>Fs@qNctK?HwSaDDYIO(aL4>_AuiiC@4UC%#?xI?0q>DZ;)3`N%26}8ee)v<><#z>K%n3x-YkB_nE zN<^ux&B8rhd`djg_!8(9Sd$w+}s>WV}pWRd5cX;Q%TR|{ETunbFyTGxfVbPz+#Elxy4dplS z)Z?ufmlsjn!J%OKl6K&aWU&e4`W2MVPs&Rz?nuI55mh~m7MdecDZ#(cXUS9F=B?_2HP&2HbCif$p zQaeeYOp;W+kFw!afpkU>SWy(O0^JWIZ;ymiLsJkrD1cPtE4HlT@FxTynIh#!? zqE<5`j5d-nSq0(fGWGaWQIl_@WFM8q1b>+Y^@aX=; z!3DY+Oj$;8QL3|LrQOVF-y6^`QoN7lw*`ET?$ri6=*$|@v^j>k<$bdzuXLz)&&UuSf|zv(CzXLik#aqvI}jAVvZ47WGt zY=OAoj!U?#cuJ*t5-LY}$FSVY12JezQ+?(*+r+Q_OEe)dzINzPBK}PNJR-Kda=$&~ znC_u`N>*r*Sv%|jG#vIkw3ePnye+(0Sk|f83N3>WU<6-*Ujn~e`llEE>wElfFX0TF z!UcJ0AxU$zr@h$9bvGx0jB`7%1%f=!yh;f963UcZLk?g zHArKWZHbA#+NW+r7t7c%pBSx7rSk3%!-IG;yxG6`ZvD*{{_c~$JMihyq5uZ)65oTr zyz~8xU%u&2r~ikSaI!cdUvsePr@BC5As5E~^}#>9+3%nD-2*?CaUWJ<`%N=n!Atlm zUBPTHY;g`EcnMeNo8#LR|L2*itmb=2SQDa#`Ul#dmi-q5Du;}v8-O+z*@TWvK?HQU z;-%MlLuNNFS>(oJ;aGSs9UF8dN;$+!I0IM2>!M#M?WS%6ZC8nVY+*T2wxVAJ0uSOR z^5?4a`4s;I{B;|EA3QP05HvkNY6!3Xt?RIM8A-oSMSS`#5 z`4eW&po0+>gybZE6D7P*5T+$s=%i&NhEfVfmLK44uqX3r89tlmlB{IBPF>xDD^jZs z#9?C4DX-hD#@oH8vw5i|HM7i{T%nbDV*wBjJp&hekTcx*kmVG}afN0s@>_zGi4G$l zz-5YHSiZ&jDVrm&s%&}#3H>tl#x9(3b0-s!1V3PU!dINUoN+DJg+)6e4#>MyJ^CPW zR469ffRSh^1j*=-x`GOs=%yFhZ;Z0-Rg6ho`+V;7B8O11Utz+U%V^414)%d%1vOTDJD zuSy>-_V!o$^5qZD)0K1bea&?6X!saMry>iM@)1aeeFxl0rWxUZt9gtf=9wC zE&iAST>@6Sx?#ZN`G>(_+IpzIuW_r1pqNQivtt%Wb~8#oZ{z-g5f&)jW^r9^TYpy9 zHz3l!wiqn6{&M$Ci4SJCpI8Ul6YSvaOlhK7T)vDg!9_#o8*t|xyT5;E{bL#NMHBaB>N^5s&bk#fU zGyLXY>Uh%oRM7qpR9nl!aftv51FytzER|d1cajdzt=iEt|X_|vb^O8 zh05)t9znXtq&pU186IXbX<1)6Zp)`@L4|^n$Hv3LUR4g-Nk~>B7pf$CuFc%XaX z4bAP{IEGR7Z;R=5i`b~0G)jHXi z(7|a%L75bOc}L}yo35oYGJ6+#3S91wwJ!JBlw&SF^Cz1m$hMNc_1uQaI&uh$sS2w9A=l&KI4i+!Gg2I`vdl$p>_D!HD84_5 zn6xZLU*V`%lcti?_lLa=CA5{%S%NDFbjw01?P>+Rbq&mpTx)f@X+-)`E*`Y!HeOJDZXt0gNOsIm zRhGt;Z~~yrYFe!Bwsh1rqufa>rld5RM9P=rSO8NpNi(bJ!GMx5wJ_s|wbZU+Lmf zukwLqVCGAC6B1VmjP_yj1zCT^16qhxrf?j2$5bWONlxhVp&FHD{R+Ci%}@?wM-2yb zz*e2!%K;39aq|KXEP@@MfTI(P0eCAXx~X$gK#4uI^yd?Q|Bk=A0E%fYtBm@#Uij&swLvApmDy15|0ya~{qhg;Hu#!Cx zJTQ#ojKlo(eXg$Fu_9bkhj*+Ferr4^hJ^0ZsRUDLzdkQ~=Ku4v{neNC=`%h(<1=v} zAiM|Ooj+an%c;M->mT3aKc~CgiS%?Ul4Qn2sQ?X&$w>XrZ}_XH{qD)%y~XFJJ{|gG zI#Q<5s1U*eIKfN2#1Iyd2}U6XcuKF}H{lmWkv+pmaTuu-W*LlsKOOpM@ObYKwZ=&s zcTUGU0PeUhot_N|21fgx_67tk*g+Ywr~^1^wwF#Of@kREd|mO&1ze5ku~&~hNbb<5 z)j)Lsyog`N^-%=)5Q;081YS!(lzxOgqa1Voqf870^Z+Ox(jyol7bND;vyFf;X~9$!W%)&mm1>IGn<#-YA)LH2A__^I zK(32C7A)k!!wN^K7CVEJmjG%BylUHZWLb{8tlFs0h z5T%fVPirLxB=7rW&TIezLQXiu35Y zBEKk`5-Mg~T|zWCHtj*yrh@ygTjvDsa`pzs>dL#6zk4cFRf#GZbJ3b$%WZ!*73OUB zU0Tf9;I3i%AlXDMl^d^|XWq>;c|CUX|DRu+l5g*`>|2}D6PqW~?W?sH#YRSFPirk# z&hX}U*j3pWYbWWv0^%(~iCD@Uyj-5@7w3w8L!1q_g!^55y#9IVd(7a&59`Ax?oZp_ z_nU0989C^CH<+XL)D-1kC04~WfcG^tDqTZ59VVDZhNG+wV~jwxFGh1iR)cMahpCan z#YJ2+>u96$B*;d=Wh?um>mZ0qaVa{dX^A|gUYCSW!JUS?bc463qP#;waMXNY6|YOOWJU{}ba zG>wrkC^Lq-Plxq?J9E0w;mr_avXK=S-6RLP%Ns>Z<-%vAd(2%osMxMYDZq-4(x1_J zprs5G^pJZO$57Eq;f3jMZv3aa#plPG{z&L8$}=jY&I22;J|t$iPXA&8or-)KB3o^? z5r+Nk5fNcP#vdg99%E}WDwFK)RU7p9DrOT@^VRvx*IH5bfGy6LZ7P#L>Qx$A3^Zuf$QNt#sHa$@+PGrQ8nfBN zE#>L;7g)v!jB>7jiF?Gd>H2D&uRNGC`ci?;#oLe)HEwT-LnYy{{kS?4$d)=BEbE*I z-$S{Dj3ZoYrBuD|*@U9Wm0Sywh`Ntk+yKm8F7_Bv6KD;@_NrptlVp#}UP+>o{ ziMWGPRH7VTCM0GPbOlMyBf?Mu#pN&8v(YS!VmN!Z%ZiY8QL4u!%)Bn~U#PRjBMRc^vz25aNWz3g=R zS%ezH#%dZyKy4X)&8(w>G>c$vt|N*{adXWw(z_Pj%WYLOt%1y)r{rSBQmP1=P@6On zYXYeo4$6XTIzg&AqvXP^ky?j!fDu||dne#=U%|!zb66#UvmfOqo>fvPmQ}ldo#ARr zz!3T9D5d7{Z+eTK5xGH?U#~_+!sHZUnlM62C3L98#MTEjwa&Z0y}_;AF>CMZDLg;f zKOeo44-jL+!T0iL@;KIE;V`mPtB}I2bvylp$6zf3Ay?O;*%^_9EO+6k9I|C|H(ti2 z8H?NEnRD_+07lFMm4~-DhKL~HU7%Rc)=&$X=DW7@@2sEo&y%;5kF*&yQ}~SJrntPd zw?UZ8B^oQ9*DJ#DNb)~jn5z|~;=hzLi=&=&C-2 z9MF=M*%cmzV>F|d(VDhboGG5B?W_9LQ~;FO!7>a~Rs~HC-1=43WpTaoB2v~1kS z$gUivw?E3B>{_Zg=6-WNy7gbm)hH!B&5O7jrc83+BCbz*|8`z5hh2yl;tSp!`U0&w zT?fH*-cxmD7B0|;c?cK|W)(+OmK2GHu5MFro#+4)(&K{V0L6PtA$zb7qE*cHpMEtlg_V&R0h2h+H*g*Pc>7_rt^#4gwRAxjaBXtE%LIGd(Fh2~Z#^cybQuWKN zc5-vL-vXb;STFA@Z`?|jr+#H0_Hx43X4>)>{lx24jFedNDRzfHu`i%}|4^DC-Y>fm zoP94tCu?Ij3F{o0Gojnuxd)`FXS99bU6Q3+s^}?i77A!G2BmL%Jn-56`!DNXeP_S_ zj33@~EG>%U73;e2i}N2({P7k4`#0e|a0cFmm!Jc74_@jk$tQ3CPvEeB`NUs6_2tRW zpY(X*^8=sOkPfv$N6>*Ya0X7t+3gnwt>6j1gRjsPI9-oZ;T=dbI}ye*15VZf60?yxW3fqOumk6ulvn`{@ygvP zO8jKNA+FUmJFC$%E##8a$l_iW$v`Nw35Lr;o~keONw|IW>Td0E|&NZJP5%PBq|+QVI_GVCRo$@++9Fw*2;i*!CsV%myjKn zC$IpfvMgZIC9G_Y$o?Uj5E15#E9eU=fOdtt1PVt495P&)7)>}$OBOII^MS6o%P4CV zm`^T2d4?_gmLUb+93aa_(qz&#>sq#C2gDL7Sp)GKq);zwNDM5h63P9Egm3`|!eJ0I zKQ3ooo?=s}RjRl)k&HZai3>Qg7Xnego9Dk=Bloco0v_N39>iDS#oH=@Ok`YGUe(cB z{(Sh`>+QlilS;iFAxR7z5B=vHSXWm*)lu=lqeWDi_z~R?G!8^UwLpEKi)N>o4dOR7 zK*tp2zF_T=H#=8d!emhOM@?WXZ=A{&ulMo(H}qI12K0Am_hgK#9#ZmDG;~#EsbJ3H z3T%Bpt0)~##`-7G8|x4e*?|-o5NdCE)0zX^>LqpHB7jcv>jZ*^+HNbe4M~O@ePz3m z>%QepJD~cyC%Fop3zZ!|v6@w47arVqmp+O+pmbhb^xAOa&-PzaMbE#ou3@Sg-e@G= zo=l;r-4gfDIIfRR1|R9D`+0wM-{c)fR&{Sa%O6|SkXN79-rA)Xv{IF&Ndvq)a#Z#b zWxm;#3^N_nJyl6wW}M(9us(EwVT4h23N#dwnuJg(ExJ3n){Uez+StEQ+D5O2A?fE- zctSMvK3&pjV%a!0= z?A~Y;j1VfKsFt2RMySH-v*7}BE+3Db1h#tZnALPo1$EkG?wA=}7~3GR=#2pcH)>jt z0-|H-?}&M=(v;eqa6DS};T|I3A##rSB~PhaRcTz%TRR(A=k2>@<b$tph9tnZMauSOVNoSIkHk*sQxJ9!aOe2uN2xc_~LG1>etcB zM&4XwS4j;@N|$Bc*tlpZ!)=65dw%ywWZvWsYqY9z>^M}Z@!+xuCptE)pY(pD{Wq0d zvU920mPloHmS|qVoAV(R9fB?465Bq+6A$cH(5W*VA!B1@9oaQK_b9_oGBUY>b)>Q` zz6!+MJ^EzB&^6z?8)3^w`d{aC{9#kbBSS)qiheHG)40_@speH!()PwviSrSNm75UlH>xwVMl~l5)YC$N(V3V z@Kh<3br#{0(ejT{m6#{MgX;=U5!m2qk%uUj#7&_}JdxIfA(;VMq(@jW$shDgQ(*qBzKM z^366?^`DO`X~`l{^K&P3?p-Sn1J#q^jCydU=2e^z$Q@~x5+LVFx?nO*JzGN;bcR~JE=hvyq@QaYEvMuBHb zp1iVYw?UKDy_f9gGoFmQ%uN8oTw#?$sd7H4qgBjV#$c|Uo8{rb?9T##YhGl<;<*62 z&w$LAci8fh8P&t`jy>qGC<0u@_Kz+)_gD*(x5Z*NiR@{PA)Bh8%mYXjkomIA){T__ z_N%Eg;dDGH+IykauRd#J(Ov0G<_v}-0$ZxypO{wi#MBk`0P&rMyF zok%+?GMP4(jZaO47hco|p?AQgr-@>3v z{O~*nE7(q^2bP+g6+F-RR|y9&J6}@SUe+-Kig83#?5=pL4_pfm!?Ez>zkS2MeA-_> z_~VnGj`*}PgT!94Q#wQ6uK4Ap_fy|q`t^*jr~cCwKb^ouIB>O0DjYA=EKn-Un)3Yc zSB7b=etk349e(19W~A;QRF0z9%x*=vJ?f_Br#9VHWf0=T`Re?nCV|wWqC|TmI<)3k z?{&7__|yq-Kgx12{RYo~no2CW&#f(eCdj-p4lC1aV*P01Tr4ay(^?o{z?w%~3!m4& z`Og0J7yIkacs`K5u}gfv@b$vC7yfkO|Ge~H-+|NdZg>UW0j&b~IIt-J!4@pRY+w;j z{>?+*9ro#o&xgJ|@afQjuqb3Y{nOI2@jWbgsY_fL%qX7F6>&LUCr*cZOI^GTf9&WP zI*@J~YlHs;x9zZ}V`IvE2AHy=kWr+puyrF4k=-d1yxftbQ~%!n}Nf{Ftad~IbwU_7u+C2ILzqn3qz5?_NE-e0LU2B zN=OcXSZb}L^2ANt)bDQoY#K#DFPJu~yhW|RRhAR^>SbltC?+AifuLh2tFY<3hU@EK zm7iQrVeIvCA@}NZr9%L2ro$l|!aMjWaJb$s_`(%9OkTfLPK41%LlaC~!c~%q?lCL` z4=>=Ia`I(_6csBIU$KHZq!l{a==sTnP}^$>JjH;$Ap&6z4;k=ihpCG=KqH*f8v$Ri zkP!!y04>uLh>I|)njtBqcRWSfWsq}0L3qjR#7N{-eP}IPNHF&$8j?;+O`VaN{fAU) zloDl20LP$^eIr(7Y49>YvS7|*GhDz5fFqeu&dgb$8L`AC^(qIRv8_i!aBG65iu&Xe z_y$~k?rLM<1BXKgo(KHkW9i5?Bpn`0frtmNpg%x=J_pm`?e7N;wD)YMRW&ILQzzu@ zSvtp+-B_??F*jaelMUK?S8QpX?x|^p; z-&@r;C6K0@DsEf(KBRo03}c>o@>005M1V_izc9Au(v-R7%u*rXRXr#%I^7cEj|TG=3y%+)2BF}WEv+BFPEy2>Yp z!A??1yZZr}ZO+%&L!mG{l+Fgl4^5X{eb?Y2bAFmdfQ>?sJdPtGY7nZ*%x2!P-vcg#w8ie~y`gE<*#WC8ik@l1zI5V! zb!wxV_Ok(Z5_hT}ycI27Gw;ta>*nspP(HrCXsJ+Ufpm*-US$f%T2eEx;B@6TV=zts ztyidSV3t-}v`obfwhnkV+HS0=AkTd)&6e(RW81A_ZK8;7#jXqKPVNvY^d7|vsyV!> zj|TULcCtI>736Gf{R$c}yZ^&p*ct}u^z-?3Jsyu_vo=2it~B}^XUn2@d3!ZAa8g{t z&SL3_P`7b2ew;RCOU05I&EV0+UsiNRsiMJEecpR|j%!xYpR{2*f&R(1?~Ib}wxpxo z^)RDH-JW6yk9K=XKsMM8d&^c&;~Rn0r#EZQNp)65BWxd*Zr)gJyFt1}W~#{T)KER= z^`6JhaT8ZLjA+(bM|;s}GqifatwKmOPchli%Tv~wDeNmJ(tHc`AeyJ_ZY-{KG$&CD z-%;JaiUVxWT)9Gxl#%|Tkid+ zPx?KKho^J}bFh~lBdI1FX33r8R|93uWo9N^aT!)p@{i-_P0zZ#QBpfYB-i9j(M7MW zHcPZ5MQ${kGaDgP;8<%rN#-*TElWjIfVJw}yDJot0*_f4!lPj1&8ghm*{kR^oA|fl zfSTB=x6^QU&g&=y{1%bu9;@9-d*D?WkZWcNk@o`GwwAE6e$~`R?W&slSIT+O+yu&| zuajKUN0I7EqPuB}Y--cI;eU*6+hmF3@+7!~3W;B|! zY*t5Wiel~N63jZd^#Oe(F(^O79$>tQr02X-A5s$}nX%q^52J}#T#(jLkqqU@$BNC& z`J6ibn3tCxo{^3;a*tGZHZNEdo`RMmBdO+qKxDpw)qY~-`^PS<&|U{KB$Crw z$AQwhw{By4rh0(Z1TRKDdKa`_uFO)uwL5xJIhW>#ywRnitt)(iG`cH{0^4FjC*-&U zoVY&WIN@LL_>6es@#rL^oOT0At9@rw7G7g+L_(R2Rw5@s>QcB%vTg&v(QAq}~c4h(~q>LjrBHI_-BuCuFgHV=nHxjlRrN2;{#s~e81v=f(*A8@b3CW_{H(_sek(Bzr5>o{hb32TniTv%^&3d0&weu z32fGOeDi@smU#L$H;U&GjP6eUDbYG@oafD zY2tSjeY;m6*7{Hv5F5U!VV2~%z0^Vy8yRaq3AG%SaDO~~tD zH=(aSo~CT{VR$Zl0v`N-e6rtt!H=Kf6JrVR^$z@e;{Bzs-}HAU|J^BsZ~`yjb>cN{ zcr)(w0UYe}CJsIieO~;$_;%>aflsD|35L!hg{OFlFYzV3J6<lIj`-MeF`sVn_@yBb3DX1Niu9#F!agD19FKBSv23dGJS#9nKC1phfUyAa0$#!!_*Uar z;XyDS@W4r>Y=AOrhs8CO_ntUF zAdbp4EYpQ;IsqJ5-6|1D-u2GguugA3K2j19==pKvO z=5AHNb`HFgI#X|SSrP)hORSmtHfzAPt}Q|W6&+O7Z0~Eh4a56ieU8L``>$Kk*splg zE%*PPTNRtm+21xjA4757_Pfo8VL$UNdcF}wclv7B=W^->D0qiQ;=^^-qf2(4w>54v zUcFcsR59D`P;33Lgv()Mz6A_a)j@m@(s9Q*-`$Sw6 z8CLkdWg}^U&E$1mxA|~`TpC+UXf?6LE`kfbQ(t!BlFi!Gg{2s)f1$bY8}8Es!we0- z+?0s34`-T7o9Sun^=>W2k(kUtrv1c5^`&%$k|3nMXB|tfroE>I*d#Ra%Er;C(VLNO zqGlpXwZBkEw>5)5vszhCg@aS_(_MyZL4^;syg?BeG)}vxiA`R8n zt67eUCP=#8Y++Ds=55mOaUIp@*uZp~%ukzc@BD3dN88`!&$hLi8UyWqmS#_HBf2Ww zx-XyQ3aM z5sl!9u#oce^98Z=21=AIvAJU%9+4BRDSOSDDah6;QAoj>VU}SrSX{|HG*H>rb6hnq znmI%J*Oio~4KtG{GP9v!ST=W-b9~vl;v&sxS6nRiFe6$lohn-4(1EM!_MwJwmsP$; z$qKoP5VXk_n`^vLcSVmCU(!7aDgQcbu}xVE4~zg1)S?k|6>$<$F;ewx)n0#vcBt+2xU!vaL4V z)$}^x14-E{r{TVB))4JZ#fIE(eI6}VNk%OqJOr(FYONhj(^JwdBisi%B`58j&e;Dw z{dldtcK80rDp96$rkZibc(AAcZ1`OR>=jX{_7vWc`I{37q>OE1DQ3{`sB8~`w#RL) z$=+brGIwvrE)zY_L3JB)t9u}0&#oFeAsNzchCM|z#>c2BoqSJZ0|Ee8$r3~I?x#S% zxg4XZfSyK?c~Z9fVyp|SKC4W)C7Cx-Y%|k=w348!Fq8mX@>L;BAo<|o;XcQP?#__3 zs;eVu$ubN@7cBYVxhiFA&!Rk{21n`yLx-6w2rk|r`+3lHs8RW#&g-c?1G!nX|* zN$S+9x)w%=cB_?>4n~qU(d=5i&CKtXhi_uKkNLIrs>xw@wkKtau~s04f*Qf>@SZs% z_Q@&>g`1gmSO4j@lWm$en^B=hZF{HYeT_sc&w0K6<~`;nSPLtGR;o3*-Nozzwkb-9 z5|S>u;P1HJ@p$3-gy$z52acqL?ngu`yfVck(1)%}c4Sc)5FPTAoXt0!FPwu8re<1F1}x{Mwc2sT3Z_RNt#K3$z|PJoJ~LUQ*YGak5JqL~s>EU=5WwaG>5Q{hrkM*yf z>u;X+GFT~E}SYLGvy2pgdrRkaiI*e0>FVa zPKmhc{gAYQoG5mTe~ z%XegjuWj6L=(IU|DM`XIk(v~=B{JB)YJjywY*m~RCxEoW zuDEay!%hT;y=Cgmy2+N_xrj~ zPqmWpSWaXD*3I=X*)QsU;`+4g@zCd`hs8}JRhxbYXYf7v&H2^!&2g%loU9#BPRF~) zx08Rq@W+V%;(<%mRXswn4U7h0Sb?i0gZEJSnpqAf%y8)Ui=Q6i!g=9a#D55VT)vuM zYozAt1Q^rEWl6$AmvD-w_!WEy{xgwZvEwdsY!o>&&_WkVIjIljNq>L4Z$0=gpc{TP zq&x+Z3-7`|3JW;NU&uGxd~S8>B*~=WTw->uoJX{Br6U2klq@72e!)D8ONwcw%2_2Z z(lhaE%2$>bo_g)_TtkIJXhKc^Tx`!Dm*@r zV70v-#Z|nEB)Ax8QCL1nL4v2bq6v|GYj!IB6ae{c@f!fw_rM_;_>{XBH!B+>ChgMQw0}tR>ivVTuHL};020{IzvvpnGj1TN9}A^^yH8NixoSZ zyO@vxMp}V`jP6+uP^xn}+X#@o^9X!GCurgub%7Uj!oFH3xbeX8#M}E%*V~Vuzdf#F zov+7bTryl9FbUyy=}r75{g;fji@+n`gw#z!(GjEzcJ9(E%Z%S_&0Vf;dSE2ca}1o# zUpH1@2nk>p7%^2!xr5&5nUNGVhm^}vVTn`uL|G-Om)ZQ1=(YmOy;9q)E~VkC)ia8+ z)U@u{OEFN4v0ImXQ|C08{*g-gZ}DXNIZRw@KST6+KkLtNA;#@F&-0uw(olL+HBA+I zJ5${+iSr;E1kYboOXM%q-d$hUm~7^6S;cQv`E9-5jVMY(H}(s3;T9!9Hd;3B6`>G7)j*nXHTUc;%U087c zol)v5Rs#zdx3O1fW-u;=$x{JtM#?r8QEScwjOV+`^{P&(W@B|#nEvgZ7k*t&x!v;B zz1X?GTkW~a${om-g>MYEM{_kQqj?A*Lcu5)n!QBUA$R9@e0VMPn;4rLK3G$rPv`^x z=g9WUt^2Im+2aXhZCkL~7W3Aa_oB`XO8z6w=%T^-Rz>u*$_RI)j(dksB%tJeC16(! z6M=>i?dCbgjy5ne+^os+Z!wK|eIwPA-hgSf*36!$h`OTP-2gdS9R1 zhAwr%3q=X;7nF%#0&aa1dr+m$$vtj78(7MrLghw;fo8}Co@SR;n?DHIYTZMDxyg^! z+LowE6M6Nbk&akq2^JBoA+d2bw5Gf`j>3YQjp_eBx553&qVm(Sdz;4Wt!dzMBbt^} zPy=|!I*L+lX<#98NJqFaec3)(-EOXXZ?P>y6hOQ?m^r~n$sYAJlf0qfBsaQkG_5QM z?|^Cf0BRPE8fJU;4UXF9iUcZe*l`U=kH7|s=i%4F@EZ_0RVfL_%}O0g&3f&Jvzn{M z%*MWw>I)=QR$cWqlrt%EiLXO>!z$4B_V)gErmvfTynks`DG?;B}(mU zU6}QAhWjPx;Nb|DKkRrvUu=-`H~Dip%0pf-tgWO^mM!o9IFxDu(3H1@Bf;}JpO2@z zXB?C(mK|4I#v_s2Xxs41uXgZK3jrAj(yqA1D-S~&rQ3?xK)JcN-SvE|w6=BICl!zl zq}^QzQEUqmFfaZah;Y@)kgRzCy8K$UV_kvxNF>)}L>+meIf(Lr-Lf7aNt!dR;oW<0 zNuo3jt2TRt-?l8IV|QzJYdp*BhF42wcC{X3c;rm&iVA+)TCUt&v}=|@l}C!BO+VMQ zjMrNYg|W}QtBN@zg7yB<((iX*4_sKyz7e3qSg+OuD1+A>nVs8X}! zT+Sge$yGg~rrihPB#_}qyq@jX*V|}OY9k`K@1Hptn)aw{>00*tSEIL+8s)ds0#QATFTCBWT}bzF*-UZrgstw@Gu7qYqIE~6Pw_t_4k7^!;^A}E(34u959pu+Lc z#vNHuM4H&Peb6k6woIT$tW|r-bUNH_b$7u;_m8k0b>-P$a;v`YV^N!WRS!yJU4H93 zZKiC#i;`D!LDXvA0g}6c1>Mmgc6l@PPSOXNf8NbFbA?qdPdgCm78*w@f_uuq9G}_F z&}p2rZS=xNE6bWXIHdfz999xNT^&(F*b5Hm#5&<$ael_@GoDWz8S-?%4p=IU8tZ4% z0|^t9!+zSYKVfGDIH~daa*ey-t+P>oP640!yTiDo{lyD9kfH2l);A9CSGm>YaaV*^ zhh3-QpgR^5*-r!^2EjjYw3XSM;fLX5A}W5XQE5JKv`TDJxON~bdotcm`<<){qC1NL zI(+pH0Jb{o>&8>ga2lw(?EacE%=3IKtl_jU{cs9pUu#Yw+u-Kfe2) zzxv;O4ZMh}kmNGP^5*$!jyFJQeLE`>Z-(J-ZT+zH#q?%+V>}T{vfcJ=0bPM_7rzC+ zx_)(j4JzUO1;?dO1iuD;z4)u+XV*XYt$Uq=w@!7@uPM6l+(M>X%6hGoX+EiQ;edV( z{Z!6KIj#<@tCFZ#zxyvJkZ0~lRLVeB#;6+N(v`qL;9W(mVU;fk)<^I(YY!8A-s9eS zx^IB>Gn^$0--z#l7fHq;q9y4LAV}j;aZgZ3B87@0NrI%-rmhxe11$@Q$f`o&G8Zmr zA%H70238ojG9oB+1+OSxY5)l}pjw}sK$zO7R-=FglJv4v3}uRBpWQ?7Fmzr|4x&?# z=wjAb=2}6(dD4k9sfSD#3qA}L%uu^xL|C8;)O5Ke7fDn8KL!@{X80`o)l~XpAVM#N z$(Hl5M6@%ZQ;|JHB{buqNNFWP;qnH66g&c_2?#mEU`CPVt9YQcnVPz*hGw1svKD6V-UOZ&7wE~PfwFov zXz+BA5AdPd{WNP*?(GtQLwK}zA4*Y~Ehn`dw$-WH(b|$ zx}N{z^YuIpmod&GK)V2EnLg`?r!Blk{9`;2jw=uk9EP=O_g@RV5JVN+O+VB>cXcb? zY6I=?p5N+Vrm7(3q9_^kXO0jpT@BOvkLmc=jXpgA_g&Y@v6DjfmCVVh&nbP7$HzatUHqo{ z?saTei4CPyff`+1Tip7k%7XL86>YR)?@&?4(CX^=Z`>4%Th8*e`~B)OQ}wVr$>L^+ z-MbrZ@2IJfTA24^x!qk2p7~+i=)et#?2p}y%k2lGZP2~*5m~kF+*b~yaR^Y41oaEf zAG-isIIwvs!Q?roHRc3`4uQYd0*~oWZ*; zZ;@ZL^GCHg7TPt8MMh$;mH%4k3_-5yYDl8V*2Yu@G4Dp;Ezlu4YoW34LJv9EP(eAv zjSj99?!y@rV^N3T3JMbXc%6%7pQLHkTD;Z@kM2i?GVUlMB1*5I+tSU%)H2*z=^7LS+>douL2I#ecw6Y+qfs+nl*vfk793%?lGYq z3GUy)_uYd%|K4*r;pz&bB?kyAD2O)9)-0ik#wNVuS3{X~^Ua_XUWsxJ&ev_bE0We3 zs1sxYO?;P`c_^JNi!;f#fstQO+K|N&?t4Dk^L#c{sCGVy`B&LZzxn$1?0y zt7eWznlM-Copj{dhF^au-rXeRr#?)_aw$YqPEIUVYFK@s+ znPksL^k6EK?>?lP{^|d!1kX8?7zB)^nYla=nUiLOM_Y(AJ9VR$=N^NpcG_fE@5#n+ zpPhF`lzMESXI8I!b=GX2upSYozo4tg8D@1Q`D3-Pa+Y!Q`YvbLQ6MMe+SaJ>i2Bzap#5-fRIQDJb0PqgB2-GoWT;&+9aiW_Fl)yyCMDW6 zZh64zQWe*5!SB*%peq*CWeZ_)Z{da2}6`F48 z|M|+mlin1XgPH%`1ET7a8*7*=)Wo58Fpr|RK%zLaxofq!q&Kbp=NfA}QY+L>`PY<|i5i>6GSTo|9K-%m^(~NpJlz+tGY)CrG8nr5b12O|F+r%v|gpx_^>8 z1tA%xNf+ySWkWFeIeB7-_7lj~R5hv7-&QZ(l9al@fGwPz~4Uk!^573 zo)+2f<6;Qk91(c;_$S35U;3w0|K$?g@p7Dm2QDCNUtxA(M`I1SCv9?3`+=n5r?|0} zA{8|fHNz7TE4s^Wd}@P^)uPeQxjt~7c3+IHN~PKcKG-j1u)LaU*1=%Ip$4#8x5p;W zh67;JoFnRLzB4<@>&n%nruE#kVmRv63wXd59=VQX>PcHlI+Gm212}*O{`CWYcr+&WdFPHxD<^TTc3tt`IfYT5M6vcQY=UV@B_$*?qH*ye%u!IM3 z@DEF0OiznXrl-Y$40(aq9z+kGfz$EJ72n)`5iW_q%3V32^zMszKlSs4e>jB`<*>8G2XXY~zLv?}1DNpgnd;zfx#h{tpu5H5tvon9j6 zjMy6EWaZyD>We@Wo{?+4u@D5pM59$rlHza+%EZ|ZE>i|$5cU z6I^)IQ;XV~vfrCJau!74X+<04zzZ4a2qz9x1Xk8?wPe;HNvBaG5*0hEyq1JxAw-GS zDm8PmCrc&STjV(CWkGwRqc#VBYn&QrCEsvDYW6Dror~opN@Dd1&Qyh8wb=!h5lbn2 zwT`fUuOuUayZERgS)GZVzJFDG;5(X8KSw|dYe8>#x~|u^^ZMj3^Yaib#Bg&21Bw+i z^83(d_fH;w?-ve-J03?x`X#-Tdr$xeN^i}k(tFnT763r-*z3badC+`GPtmLf(-koD z1xtFCQd;b#a!!;O(nwt%+**j#)^kIWRnPBijNSU9C`Mo1#$>&aPGOV(^RD~Y)!G;) zSC!aj(&5|Y6Y@8!^(3mUxVP7!8J!P*>BFDaKJIlroVxe^^K%f3UA_+EW!TKw=5qQK zG|~Fu^YU4BsM~Hgxf4R{4QSs@ok?#E0POm_%bP($bpO)Wwa1ho4gpB-WqcuDfn+wm_CTwc0% zzYqb>Ho*wR&%zHlo_3{!6dllUEWaGYxIJYHLLEf?1uZ-9ONDkh)A!tLCbgpeX6U2Gmsh= zFou_9eUmBG7G2l8_2V6!p3^#pD1s)+!dm}?g3e1K*1euHDyy}B)H>uURawv4>S(Q> ztaJ(?>5Yo(&Nz(({nyIZm>P&oKSHU#s*2l!5;S8Lb3F~)B|M&|h>~_SgjCJ=O~`GQ zhtpl2SJ>_L!CU0XtVMUD!dW?HiOa`$`>kf-cq2K8L~Qfzw7iM?#X-=BQLMQ^vC~uL ztFvOOG!e6OWEx6FO%dfdsCbnww2O7%^he%E?3za@Xe&Uhzr6uEPR!WO;MuG6OiIv> zO)+wCa12)%b08?J(sCxm?YekFdv&|Ko$*mL@Ekn5l{u^Bf3}j7R)$yA?|y^Zwu+*~ zku`L)cRX&iVp|h6I{;g}?^@|Dhno&^Hl)~Ib5>+P1lyainn-#uYptJ z$R#-fmyxryADx;O&5( zB`VTd%friEkY=<{>9MNSK5Z?_TB-L?_8g2#vFQm!41T~GhCBxcQfhEg3)*}U` zW#^b;fCS``Z_tfKrr)Zye#497Edx>|9hm2*;$8aN714`lFU8}SrEF-c*U@NI4L^ss z@s%>?>7SQXwTo?sgWTrWyiL+wYQCeyC#guO&4k4vp>##{DLLQK%l$Mi<&}4{#fB>t z9<;U0J&a75PqAf9`)?LJJ$O~iV+|_GFGoMDTKPL4t}wt}em|@Ldm_t7uadm4eR!sR zwmThaP5>-oJIOpE)5tt0YV`#*!FE40fqjV=l|Eltg3?+;RM$A-+!@;5zN85Rtu-9t7Vog+-SO{YI`~89L{KpvY+q% zgj;V#)b-v3)YAYm188=;ZjUw3X^$;*wdONc@vdb`g87&mpE9dbc5|j@;yq|WLhHKn z^$i%*9c10!0>WUqF_TD_*<}V*j-|~8oXb|5=8-(sDgqX)dQmc3KztlzR*vm^5jI0~_{jZu$bHx;S$gwlERIUu{L5}; z>BDNxt~TZC@Xq?dZp1(?M2YXfUt_Hs|U4=P<*=9+t+mz)wY@8DfDU$E#hR@k^I*y;Ai=a1xkgJ zV2J=+h}36z8^;;==)_|bG*rlFYppn#xNn%g!g4atDbLcYV00t+fHh=ahk4tp$y6yt zbj?kjWoWhoQ5^ER=D1oncEY{1CUL=ExbnYe$K;{*b`b_>Q_|DM`U__=&i1d66P8Ft7jWI~pa{yx$A%ebN@> zjUUDBhnwUp)r%KQn<9i_K~#zv^~Kavx_x-xVRI5U51(G}(`Fjp$}Oe!oR?)EZs0|- zVvPM!0QkUTA)}5KVGECiWq2$chUfaff8xJ=@`opWG#w@~AYR}r@H+9A6MuP+zx(F@ zuV4Mo-_BpZ2HpcN;hngIm%$CO;78G`9`FlS;JQUXjkv8haxmpStVX(xD|4i7 zbFdblyD6X7L(b7|3oHFF+YZrM9q1>nd>uE>!TUzho_U_VA)oz-m;`Da3lf7zkT4Z z-t4gpk5p;W z%y=v=<1#)>3nFWa3Nc(S*LlVHj_cHQ%DpO(SO{aRarxtud{X&QMjVD^SQNYc$N?8G z(L;XW%q9PlLOLT-=0d;N{z8U?_If$i>TB&`dLvV^R<3WW&$p^Q6Q{_12qzem+b2Qs zv-BOVM_lxa3e%+|YE#XvI9x|)MSKbUTIf==mUn6iG&wWN&QvKMYa2!dWJ0@GvioDw z{uSK%DrVV<*w>xN7Y5&pCa?`4X4}br#-A`Z06wh~nd90H2 z&+mUan^Z9Z{h7CYAz8`EqlhMlZky5Vx^6dn|ET+Y@&4|#FNrg#u;pC1eP#^?+ZViP zKYjdgY$f`g4BJmBs)Wdaoi%WF@Kk+keH5B?1!c2*fBtr*2ozE~MtB_}JfI6#Isshp z(_Keg4?oxG%w5f|sOYz$DFBK|uQ!s?fX40Yzt7k8Xhsz`==LgIBinJVIjmYn^Ld-K zf7jqssIZpXNM*%BRHbFN?{0a@Zmj4Q+JLYAd@}=kwv4RO*Iw7G-f?eCq0wn4A;bh=fWP zg?Zq-|LKrn6Z(?4FnW9M$Cm2kn(S#gm7i8S9l4fe++>q#Lr7%r{G>nBK(4n~1Fob| zwz@H@2S;9f=O-Kh0Du5VL_t(wxsw%$lYkiKwROT+J(Wg@aqBA^#)Y+M@a2Y6 z*rr=m$*FkCCXI_)&QAT-Td|pd?90!8Ny9PDd)I9XCOYK=1M^niOwlAsgB?iT-q%C)#{GWaBIG%=xwnIPRXnWPbkux zD%XzO9b#r*$vo>IYboXI^+>5U^)FCj(S>~tJ zS}+&kA4DS_-0xuRh@fJ%ctD3DC)YEYmGpuHskrOh;C7F2R4q(|LdgfmmP zZoL@D941iAV1bftb<D@2nyxL`Lo2!@TPxhjajo42t~Fi&Fg#W@NkwS0fxUg7_lgZC9wrj^erC5D_d0qPS zcHE5e9I;w-f{n8vYD+3pH8fEd+rFL9q)?8mv2!do8L*p&KWGe70<#u0WhS z{9vDNZUtr%Cp_wb^M;AUZ2r@5sv<0>?V1bg9!tP}iQetigdJGkM^eKA%&-DxIDm)# z*F(QQ^!eag2q^-W>=eHN-&|j>_~pXCyBz0*SGMXCAzVaEI!-flmzvI0dHUtRHnmc! z$D|Q!T>E;oB(HNex|g!H9OOglq=gbLTz(T80!_=qZbyRx@?Q0sXi{@ZYpgaw5^#Lr zEy^o0cVq8vaW>bL?>|}FzNzG>@}jh`Vxx6QXLiN8sWsb0esQk(cIpJfe5{zxkr?AN=z4=ZZ&K!qKjaQgPbYqU>6^>V-iqE5vyDiK zz_*KkzVII|;YxW9!49y|UG33Oqxu;lWsVI#&L&F2@ZHj9VgWx#{Kx$8hE4!<`+4(N z&|d+cm)^|Y7M`nwcq-eydY>u&IEW|V#P1CdK3R3vAoL^fUuOf&Rt9hI(R#C}9OHnv zb&XCKe(bZvB?RygedyOFny%10uvE%hh@DT#FwyYeXY!Gg^ zfN!~Hh5ZrnU0U@)Z=xUai0~D`5H5uUWb$gc>|N%L7=y{!9ii;44C+vs7rz5f=vBKU zPUs~b2wwI;tlkUNryxicSmIGIf2#K+&|M&&S@2101(+=?!5xI0yoe*ST?JZ^b`5jX zIe9?=K3In%2i?-(QyI9;C>)BAiIQC96R1F#abfZ``fxbNvtpx}I|3~v1z}R?Wpd6C2Rfc$S1e~Ng;14rOmSPp;S5F)Tp7Kvrw+Vz;- zw7(X7aa&!nxT1qOWZ_ z4Ka)V2&t_-u_xuLH|~^D_7M6=IHBss7$<7nzQX##P?fWqCz5Wp-=sqBRicG%y(wEe z4c-gIsb)6|rv2(aD8|}&Ky!Ih{P%O%eA1oUn><>4$gIjcDr3p6e$l;U6^t7F30=bN zRy;vh4fsWPt^0-3P6rAj-rf3t> z&!hgW;LZZ4yd$6z^r+uP!?-aq2EDo0*b>qZ)ZLb~g;z2J^b}S1x|$!=C~9G;7`Pn; zd*>l^NDFJx%$x-3C{i8bK%>}=158u#2eOA52wq^KwlZpqDm1O!>07gGNim@bGkJz! zyMy@kt?Z}_+r&?FuR`|eH^#0R{+WkVR$z^@7c_|$1SQtr!k~3)?Jba@rvWNlY6H97 zKYp7YDucUuXcZz7SoQY?9z5K11E2$X@>2NSZh>*P!Wm2b!$tO zY%}jzA#m)rtYQEJB^PU6tq2JDZBQjzOOHq{=UB%Tm*RqOtYhpA+uOnGI%IHqRO=HX!6VPh1r1!g;Sk{?RU8~^br*TnM|F!)h<+OH%A) zyS;-Tso$MhSWl@1juZQYS|R|Ts)F7+=P&jL=JU?pxRn3YraVEJHUa$a9Ni zcR!YAhZn_TGk6P)8uesJO$KSt)y6tRGXs=7TWM{UiBT-Uk=>b145c9?U;m|C>pH*R8go|ZlX5>T&Y7jUr4d8_@E)#1Oc>dZ}5Z=u^E72f4L$ z)3AE4!j0*~7;WX(%|c@i!0zEn_W;X>41l)EXR^m%<7$)I6)z9v3{osMvs&0Y#1Dvpw+>%cwWLb{J2c_WP{fdtqDQ&uq_4 zHEw^@#7q@f3rl5gwhsc<=l^_G4m)wB%EStW28#?RmPO6Y#0?|3xB9M)+T>TPR^F_3 zXwfLH2wk_M_(B6ywPUDL8Pu&N-JH@;!s@i4z5or~h&ID%HY@d8x!z3n>Y>^oYl?;^TYzNY{?|*vYuO9fk zu4Q})_QF?K21Whq`Ze?qj-MTH#~FAB9JpAdmlIB0J$P(F1-!_oBz4jOGCJ?N-d$d5 zG{`j^WWduf6NO%l6l@)(q+vD8!W+#ogC)B;aKu!W-j;nmsq2kQ{Ah2jFOxzB9s&*{I| z#(l{YqH>RnSi<7JeBj3ie|X044*amdb zGEZ5;yYzEhUoZT_3FQA$OJ{?wvO_gktPJ#lx{JmKF_}^|#s120EEx5tOE`6VgV@x^ zz=h!V#KZVpd|PoK4ivHlB{Pr0YbhfhCIt>uR$5NVqZCeEwASqN0$a;jOE)W|9-;w+ zzIUt);Ut$3z)SinytMoMKSk0o8LlJ>^6(&ql{Jg$a*&G3YN{EP-nvcaDgE(Ga+pC5 zJu3%-AX6~p5$<$HctkWOLdfX}#VOrd5TRG#tvDY{5oF}bHRb^a{9=d)ijPDv?I+u& zuRwZI2EGH%B1z9OGcL^5Lg^z{J!%=CrJC%yy1Q_wgjdoZl zG4M~|SLjpV%%06&lT0g){BRMKb5@?SDwYm_i4dMW%1jnigf7uc1h0(yYSw%KD^L8G z;|03@P>72V7_2<6Fdi0%@nxnJa&o~g1+emvAVNhtfQqyX3UupOo~#Dj-OtIYVWh~2 zv%Crg!o&lBxsvwzr4M_ov-719r2C;BMye+VqC;Z(U&VIU+zEP@kd?o2abjHz!Ji+9ykOYIDj~C z)O$r(C3jUm=vW75&7dk3&~?b&{y{y|*v%zsZsWI}c-b1#YkQ^KMJ+y6Ygx^om z5yNDl7T&biR2Q)+-n9+AbXmpy-1EcJ2DT7xg9R>PsrI1i`Dp&kZyZdWuA+&Ytvu-C zey@-pj{e;OGNo*m?wez~^`qo$uwhV8TVb69yX5;|$aVjRdCt@8FxGtSaG0En`px!% zx(}_jncW;3Q59ejRI_C?&9=SY?|=2o`c3<*`hg5Z1pGIi#?!Mm{L3RAcG&oOta$1Xy zv)O=vE#c|55%u)mcve@3opppdETen~3zZ&rs3zi_0SY0r+`FT{E~-Pia}d;uEkwlX z4)4Z@>Tooq+TCbQ8;nz}{HVl>@UdYlOgZzt()27pzYq^;-bl2JtZzk$j1;um4qziO zsYhM<6{=#JA;4_^O+Op=VjR^FWI-`yEL5Dg zHB#ZqlBo=0q`;Ygti@x6!zX0kXg%_wpj^b&Ir2`f0MQn(H;B zLveED>y_dvA_|bNkzf*1KcQakZ!l|yiwBU%Qk}0=3veT414ChZ?yv(t$DXI~%4YsH zkAfUjJ@BNw+JZVOu)OPyD#>dI6@+~oJW>^D==>@rvT`3VAB+~;sfo9=`iZ7PV#mWk zH9^ari-wx&L#U{Tt{X#uF4(g%J<5?s?lEyqz)T#B_9c;bg@Tdbm>S5Agy+#B&W73w zy)=3nGw1hqL0t#2o8MzLh_A-tiaW{9awCK^zDmyEu;cVA!Ny~)B;Ei#?C^0{<4V9i zf!{0py!x4>%*Dgia@A$)b-i+mX8IJ0PtBguDregXX-b-ufMgp}TlEg;&uJTFv+(R~ zkY;vyD5V9c#R0{&BNIbPwgDk9xk|-Q5*}K%h(K$<0J@^qI#Gy4^GbnP8Yx)J)>@WY z0bSuldqK6)YmEo9T<6tdxHLgciNZ3==Kdx_X9QM%L*H7#r`}ym$!>dr$TeVcBp-HM zeyw%jHmzpw>m_}n8LR)>QzVCEo_cO7Dz&lbgq(cM_HmU!1u$D4zRUovWBKJI*0R+0 zrlMmy46N0CRuL?%t%apa`wasCzSKS{l9TC0k-dY5kr_4OLxy^~VLxLAk zhP2se5|~x`_Ed@_i?}%=2saI@2p;{Lr68*!XQ(G4JT@zMmpl(wru|NGMaba3vZYw(HDd#^MGnfL4tg+P(ah_!Dg1OBjhhQ-ZIgL!#_{cPc z_1H>^%Tp~?td*;zxY270C4-4fM#{H~Ytv-fX0`dztC+?c1tr;9nc`^Gty5%9z5w@F z)rNU^4q7L<};V@G_~4w~PEY7O><d;-Sd-KiAO`puPD$SLDFqZL6F~-@tC}8mL!X|k$V@#54HG-dR`+57T0fI*XRX7Up z=EA`=qQa3eGWI_n{PN(B5B*?r(>Lh~ygR?T{w(~1XM$1S5+aZg|78UjdpA-gosj*l zYWz5t4fWWlPNPv0w549*K9e%lITmU(s)R}%^cCJDYg9r;KH=0_72npcuK*gy^$n*G z)*?c5Q!r}Ab-PfJ2i59}&MmX{8nTi-#Q_}P30-)pgn&p+bmpTK)Su!EerA zPW{shzr5quZ}tkEz-KF#IDrroPU-#9*HeFd0pC(@XB|+1+fZ@H5N+Rwel;~Onp+xh z_TcXq*WyKe6@I=(ZN$RA?&e4(^5kRbFde3YI#?t&fyzY^7f50mjXFp-jW#!g1)h*& zKd3#q+4RDFK}(MQ$nbXH;?Kr6=@})-4&bZs%7vo$;#DIB{HiBd=z3e($9*ihNyyEr%h!SNAStig*T&|!F2ZA93JxN^{ zJqg~$C0Xzg6}ZH$RZ4YIHX+nTQYOBU<(P1#@=6ToMA_Npl~bf(!9`d?gsIdCslsa- zI!M|`L8TI;Hge09AV7@;pVCi7ZS|%@LHL#Y$@CDHu*9brq0_KL+b0$zYm_?mtWP5G z12JU=2S74|omMc2hvJ*~1MHtn5KhGz_y!z|&*wA~hLK?v>jlY}#?K1D9m?Jo0m6}$ zUiUZVt9B@xvMy@D(CZ?Ra*lcf*?H$3OkyqHegKytgfd7p(SSo&xSe&RSY4L88lvXZ z>w~oJp8tF|t%}(-RyQXzh*Mb7VFcll1upQ304!Y%vt&FdWhYU}8$}}@UT9M;^PY|c zjmT~9ALf^`3f$cN(?}5y;DR3T&)3^Ge?8CZxgIa`i*X(}p#@}F9h94%OM1wEe?9(j z1>%W^!ykz3F3QShRi{z)%0$PpT8U#eF>vC#x72j)abH|EfYo~+4C`nRKP%OiRwqSN z$||o4wit!U5Jd~Sdqv&ZMGe#BpkY)wv9w+Litp#G?_~HDRfbUNn~BomXY$kI^44MGjhmG4`y==|_wi}OT6PbI|{`vbO{mo}`>O(dC^YNUM|2#$%Re|iHLmA2=PtTELUY0MA#>%o|5DFq6UO}~`%-ob z5gNL;Tusx7`u=G*lY{6sqvW*AYERd~%0X^5bTe4A=$A1A6yO!RJ5jRIlK&9b5pn3E z%cNk8Ds}+kZ4XmdSI10l_$&zqaWcK9RdINudSHcUSL^6LLgu4GKA=t)n z)eq6CdDJ~HIBEWkJ)!al-|jYhh-w5_9jwroy}`kJBRSJ}9~`6bl4w^Qt^xS_P!+nubT>LAx4Qnr_lZJXCx zw20D~-G=vkS@-o$=d|_@POwE^IlHmm`w>MhEG63O0e*YF8FbgEf_1BBqks8?QfwEh zL^RD3(d=6-V#!$w$sFwcmt1C-Uu@z~b$sAq>-6ieLqZZ!a_5DmtVIB*FAQ>l%hb@HAUIx6ma{or-gzNHefrj8Y zM`YZ|cI{Fh}e*r)*tBF11e4MEVLQBh?F*fpmtc`b|A1 zDLjk-ik;3s%3hUv6*XE)d!7cYa&ry7US~TsSF=nnOwBRXPSMVu*Tz%K(^cLW9us4^XhavHp}Bk$E}V z1emssUXH*MegX<#&?o#A2kOTPWgI#UA6q8lmMID#{oI?c11jXAT2@mOHO?o}Je=LY zUjvf&vW6rK=56YdB!okV4ZvW6Zj)&_T9X`g4fB>ze^l-?(P(?e2$dja(1$+B-^PEF zbJK(yO^*$$F&xD}4k4kdpG@O% z87wb4yG>(HPn;9*HqGD(5jIAa%$at`L7Ww=eHkbATd08n@EvCw493-z{bBgAqtUb& z&B13ccfM6&S~-9n5Qn31ACr=@9g2tG;NL9%>!shm$QLU<86++BLVqEiPv1|xo%qS| zJC8qa$2-fiZ#&){&qUv^1eWI4QA0E-w6apXv4aUaF8ub8-Zd4w+mmacqm0u zq9KHA=)3FBCx3eSU*6=$cX@ltyWmR^4HyJM6$)_zPxAe2B~uFTF3j;Jj2mEau4j&> zIdWo%Nv4wM3uRWY`$?QBXzC>e-K#zlD*_rX5LGedA=D0SD!Xq=hTlljlnSg&Ok<=~ z7%S7PS?7`8DWGX(*{do$d8Jxb11_*Y;7=}pue6$We6m-kzUA-92k;veS=fx<6R*S< zf;{^ij3cn5hANh8W+jrSPcqIPh{U2o5XpX*NT0wWoY()l%Gcrza_ zOZTd~u~dZNfQT%`Uy*OrhsGbWTPFz0Uxd$yO*5x@WCDVTD8w%l_|(&7lchK$W5Sd? zql$}=^4trE14=5zRNw1(Bx$dw<(WIXkpNXZBZNXD9P&`{tksMfvKHPMSb9Dv2-zqm zcT~Os*%kW63R-FgNO2@?})$xtK>;d0p7 z786+DNO@GhP0zjGTh=d)1OO0J0Z2g87}CK7TUhH{FX!X=;?MB6<1`7;NHhcxY=np_ zV{yZms@p#PWRB-i+7}COfvYy1=zf=rkFj2$a4SeUj7O&Cxic}-$*-WHa`lfuF@sSqM7V%?m8j#))uYNCy=$MR|%>!-)>)5jY^|FZ)KyTUP`;TmeJPr z4bIAsauJknx3AJ->YtHheQflZs$-60=>M!qtz8xl#}nN^bB)q2{H43ouD@`V>(Scx zJIIJ;q{_W;sDWmycmWvK$wD0)*JIb4{I|iy4@_89R=p=)@2hFZYcZX>?jWnHBj^6g zqMz``L%~Ix@ftRl-Yd$vx_;e%B?Aew?iyR^EHeOLRjXW98gQK55ObL^L|vzx@NB)h z(pX<3EaHf5u^9wh=wRCTJ2=~IUbrC7(M>@WIh{R`jku|bUV#uDJ(Hb_;^7LtfT8^0 zL|fV(WjJvfHB4+U{cLt_m`W#wVtx;;E2_(t!PI2>MEiCw zQ~iPLyAwYlwuywvhIHvX9VuYCQSZ6^hU2SoJ_)Ma=}W^=ZB{Zesk;AxMDx2Z_WK@T z-ac6>mx8H&Jg>Y{Ngz<^#~9UAQw};;S&?;JDckygll6V`*Xe4p)+`~zTNEvgE%y3Y z@|3Gw$gr7&6dI+;2Jv}x6;h%YK%)OW+BRwd<<&TEXj`gTY>A1I&)y(NGmG$qiXuYQBD^8y)=RYtWVo4duc?uf_!3=J5A>i+XmpX?c3(>_ z3nm?HR(HF6*as@OqM*egoRUH)Kp3rb90I$^F!XoTUDspju~JbTDL}bVbhGNu1i$F6 zh*Tg&6Lm%2j7Rj)!+D*}`1X4bDf=aMr%2+h*uLv6)ZCA$3QAitEk{)d%=^u;7faLR z8foi?+fOHGEf8m9&QppUOwHBjc}J8zrmE&AK$RX#VP=~?V^ZoQq3z=rd2Xx)gq*#= z#ByP*@*|mm?0Kh`YhRCz(>DZ5DsUu=bqdIw{xQ7NVCFNR&e%&Wf=(}~6Qatz;qZ&{ zp-5Vh)iRk;GReJVtNo5$et?*DNYd!Cn;1xjnU(mNMPBxclg9tES*o_1-ZN(g2mvdD zuGtoA*tC45rJ}{7q>IID#1+joOH`869$uulv_|$~XPZyOC0Zz!v@e6RT+Ip|>@Zz0 z7@cud#-7FUYHq<7lT}M&&-8SBuSNMvq`TT0YW-|Xj5JgCD{_ycXv{i|0e*;g>cz0M zWvn-8rvRj)rNZL(bBQbiyhJ~K92 z@z;A*)(whQJBq1CFfwPaT@8_BwFJQ^n_Z|xxQJzik^L#NmgM;$5Z4Ajv=~Yf*9}o= z@Y-jg%zb3>@V%TyZHIl7D>JuSA@VC4Oonn|u~22~D@tB_%=MZfm!Wb|S=mTZg(rLD z=Vp^Ma3Cb=`6naOWMNmgQMY^zCTR8}J8_k z-CpoxVu)&A1zVkf3uP!@k+iLw$bn|I5|6~-^0Nv@>x!iHRaRj+X*8EVLr0t)*CZ|A zm>{b;UiYcHZogrRyuAFtSTO6aCUU6PA>1#KVL~u@!8(-NuSltUt#-*rief>rL^2u< zD<2X$3hNm#tXhfX0x}AM97IVV2QrHzS->R!vJq8Urh^+YGE$x)IWDP?T^SD|Sc0Wk zhC^`}4*4Gs{FTKgjg@GD;63nr$Nxsg8Q5NOC{*)#iK4PwzQSyXNEf?8+54(?GuF2d z?75OVZiN#*q!ccPpG5e}uP<7RWewu@Y%DIS-;qh{ekL0uGs!mgvyBar$0r~s=|rPx zTT2josKg0r$f@ByYpl?s0QTyjctTfuDh{OBXd2MkyYb0v)==0XRqS805W^%9AHX3v z1h4Yny~y7_@U7v~ilw3&;`Twn3&VN1$S1@oA?|~<9dc|l(M{T7$Umes5 z(NguH6Qrh@3*EK7h^7B<$TtuC<^|t8_;S#+HTIBzIQ1>`r;Q(;@xy2N;j{dHi#s0> zU8%W>%r43Yb~@iS&nNLl?Cj#t!8V~{vmy;1hX&5r%Df>%W(pugo{BFV?`L2SGTXRV z4!`IESLJU+9)@M2mih}o+MNLsOglU5pr@S~vV+*+cp{R8@D3Ac;G~-pv|-j^PxHNR zLcP2?OCh0e-~)Ik->0sLURQ-6pOBx(FG5u00Dcq*@IroqAfvb{e5#JPxp)#+4?x;X zCiS$W2A5UGgLIvQ5|j5|Ni@cp7N|&O3xotL;9#T}D{HHUD^018)b?1G3XhY;8WaF33m2O;HW$_wxk@Sr=k@D#B{XD?lnd0Nf(fJ$gq=)@8@NXSwKyVwQ3 z6S+j#B*>r1h4_Uk8;8)WjI;6_Qg-MeFh+={w_$GEfpBr*P7q5F0tev`Ra2a#MV*mb zrp8t9U!!*-AweQDAp%%{17^Cw1K2?iY!x>>4=+^|GFe7N^Rd+tHberUeyJ~V_obwR zBTMn#OHFn&I$>Z?9Th5|!Y!c4n}CAXjRRnpajJ;ya#E24s#q@J^i3lOS&4vD=Tk>Z zpkuG3vc6f{dM<_sgegS+kuzt<^jYmJF{K9_D9Qt$0?0IPOyj$R263eJ^ z1-OnXLt>F&6G-K4jy#cG3}r^)=HM@t_5uwHIHjXc^Sd1MHj_B9H~hqY`rhiN?k(Fv z7X)Po9celt5e(1GI&D9*uRF0#5VS<)Zsww6!qZK+Y>1|gmSr8^ItRHS8>2_*W?>l* zuTcUGt1{4daZV`YuI%f_a;AW@l6uVGxrMt-d0l7R>B-c>v}Gz121N(1xhII%x|cLY zc!lK*<2dNha2r(|Wsh=xZ-m$_WsR5*94ESpXU|2x{rM9ZohQOsK$^cz>y%6uxj(yG zBX7ZK+4El+PdTKloUDSs!rAJdh{(iiUTj4}o^1x$FYbaJ*__OLW?cQ*7nO^vr&b!n zY}9`&hk*&HN(I9xJQ7DrL}=tt_c6wsq%S-1OFtBK%IJkh8s8uUC9rNMq)Y(H4=sR2 z+BdHTkKTruGz+Ilo9KLKs%>5%!)ROt?Om!>TNEogC1nIwIA*^D?Hq?$b}z@vgvg~E z=u285k}l8EnwtT6BV5ME=(eo!+ETAcfnjrplo3J3t9wn!&PLY~TA`G10~23MXdijI z8V3Z`-hBme<+SDPC(*0a%%&!;;$|v{0@GA9E}}XB$>&+LUp}{9$V$|h2pDVAY1RR= z!rz~HuA*%9LcT&3iQAfxj%Rf$S4CuOl_tQ5@iJ{OE&&sDI@0ukrNN4_n+gJlR`S!P zYp)5@MzzdHJ!31o3L-&R#=~@Dv@?{bFsG1UJnY!+l;TU+8SNmvU`#GG56>C5)Xq2$ zJx)JgkC*629}I|?$aYW5D^Ki#e#mM%6d=#oC1s;upmP!FAZcn&-)ZI~WyPKsl^Uri z9!>!6@H_x`IvD{$&*z>UMI%(wc`UWc*ZsE18FFB|$8fzl@3baS`j-aF0n)18NC)0p zKk2cHDG|-Zgrs|zDMYvL92-cPc1RZpMfTsN|%;y{yB@9s97QmCu&TeG|f^gbjhHrNdqbTKSGP zFJ=QMeOrRjdNbK9zq&!h%cQz%eUgmQ8>V^@0O+1XwD&2k>A>Pcpkn7f(e9!Eb`;k# zm7ZMBR&#+Ed@ak4DCu#3uOLVpuiD-=V@5{nu_8Q`Pq#TVlx#by6_7>L`6R3#1*5r^vKR z`it#m>b=6d4D1P11svEyY@iyxs_`Yq#+-1G?6YSAzd8WWN76`T#2gc5P|X`&4V1-GARQmsmj zmUEf3z(ruVuH>-N*(}A<|Nh`#9r|iKvaAR_1E=G69{>5wuo}m9oKk}}rE;L`(1B`WnvI|DyH;}h>M7?k z8ZFa~7)iZMQ!=dyA{00Pliwclze~G`m z>+eGh`mYu5bU^8j~HA-JJ*{U+eVY8;lX&@ z{2|qT-RfHsl6xn<2n1g%UR4jnLm(<8KyA0B2s|lI;ynIZGx<$`rMV=UOXHsOURzP&8{BsW~$cLSDZ>gsh52>KU7GXJ5678o7i= zHgAXuA#wokP2=@y(_@SRK?HrnO(Ng{hA@y0*yFa_1Uv*Z!B$a1DXLiQqGE!j@Bmd# z@EidFuSM^B;)pOPlmJygs=rLr2!*yGAysHfAc903!OxHrcp)~aF0-u#tBYAlA&yW{ zoLMV3>roA$Y?Qqih}w1*31SgPFcfaN1Bj=VjvBz3jAd413W(0 z2eyWbybFFro8XnlkiiZd2mJL9FK@^5_2s!A{w{lopArsw!%d;!1JpP~h%gcPTMv^6 z+26sF4d-+G>|CYN|{^IH;8Wl^xQT5yOPh8d1bbj=Q zSNu;+UhrsP_pBXVzDuuZHEwPc)D+Pl_GUIzxnQ>rm88&!qO1bl2GLt*qt90_0za(# z{Tw(qf9(ae+D|Qw3x&K0n-8bqhQfPbH96bGXBTo8i4|qTh}#`s1Xv6-(uQ^Sk$v%3 zb!&0k#pL$;+Kg0u{P`Q0@X>5=o?91Y+PqY;O49XtuZb5kH?-*Vd*S2rneXb_J8c{2 zb*9iM{L(I>mPio536~msCO}H$4H{LGL>4@bIBM(6f=yc{67Xdt^T;M@T0@cj_2C{JF;d+nf%|8ov5V$P^aAKq z_ui^Sz+`en6hf4Gch!dH?om>s$O=Yw4|f5`vX+r@R=(!%Jm4Gdgqfk8!Cmh~KM-jm z7wIC9Bs^LSR-rpd<6SK2P;E1go0pnJA^)JPr3y;_9|f$G?DNURB%++V~JxzC2|66POl8T$I zf>s|^!e8P*YWI-dJ%@=#d*l7(VKFOCtuY)Okx*`}77zG|g1%k2osu%t0xJ?3j1 zD8Y>B8;(asoK9k#?n+{hQ8ZIjBm6L{YfqtESq4j*_(l0JVta9q*_x3bp);!+^3iInt7{+`C0RrLES5|3DzLI470o-=UDut?_TL3i@|T6F z2?HwX?$?OA9D+pXVhap`vKQ7|r__wTMWo+PdA5G2>IGx!TIFeA{PAaR-yL;R>kOs_ zO>E?TNahop6&xk6P`^kZC3qqZ2oQNzN8=IMKbW7ReYEzQn_kCORg$pID%6&G3O%4i zJYXq=It%EJ5$MDf#Wcs^x@=a%Y~}8x-^(g$EfSh1La_48vY_pXHmCFS+#A?kZ)f|f zb{W%KtN*j@iJ*6W4#bw4bI&{dp*@T|4yYjz|*wWqGjwORzrE~rF_kr zkjR%C4nJ{bQ|$y?1&|6hFA$`@DyNJvWUG93 zR|h4e<7MGE+#xy*Cb^1ITp5mJ5Cu!|ihuK}-+qd3U-0#cBeioxIDdY_pWpev{(|T8 ziFe0oWwYtP26oDiv>TIAY-?{sFu{`RA;>7V*v?u3Wbhbq;9r^k=7BF?_|MM#0e z2qE>|^(V)7XZ-ms{`iahhd258DFD9}?1it$#U!EuzyiMceDXYjtp&o4q3Fc~e5h#e z#7r+NdLd<~BQi|>Y>60Q55Jxz>h9`FBW~G?0L`$*? z4XP=e-9E{0CcwMW;7&o4h@cB2JM$4DA^8i0BuWN*&JYX+Ns7i z6$IYWp@H1qUP+NB`;5yHD4!4^jNAc>^kc4ufw6G6so<*Us-C@)W*Z!MP+;^?^QwMc z=}9Q6l9UG(3faKU@=!|oivrmn=QhObfMFl6ykQQqV$$-CR26@o8K8>m7J zt08+-N!WE4F8^U|3pj+tL!1 zshYh0Q_CFqdUjI1V%KlZv4XcRnSHJK%elzeShFM!A~rm28**{W`p}@n&LVlkQ@XvY zWX-hBC!h-LEQ{CXFP}vB`YV0!0%7W#hKehFaZQb2qKz14sG8_` z&2n@>b)kVXKV?{@8;sNV6(iLTG0rgqYz{G*m`#gXOSD(_*azx_K?IFQ-Ke-?AjW}4 zdLEYNSm!%jmz#Iu#gLVc*5HZt8n1;`R1;Mjpr>q>&@JZm&~Bd(!SqSrX5GLfESjsG z^mJLLpylHoK=b<8izKf1Sr&hVyQ*CaX?rpz3|;z?T;7>&O2IO?8H}~ADZ+24P(p5Ph5F0-Y+fZxmGt1jc(JJPW*Ey&`Q>bAu=*lzWWe>Z3Qrz_0fW5fSW-wD0HVhLQ0ysNm!qjnx;?av zIH^#NK+7zqSOf$+T%{1O0F?q{ZcDNV3UY@dQ?F9RQebrrfHD=7qRvh2xu2%y?q+)7 z#{GD-%)7)$dXqQ?NKP1g;`_AO5q~h3dxYGGaBoA8vO!SNtW2rT%zdc_~dy}f3 ze&z#o_g+r(mbq*ZV7RI#yHof!s1@p!NCMC?QTZa$!Y&xX6jQ(>iZYM@MNPejlQU*b zOMZ(jn|9x(#fm}1dH{@i7&m^sn-aRzsx?|nZToJLZ1~kYh?Y!hhZD5+(~ihXYE?$+ zmO9hu+M$|%0_g5iNqN^+oxcW zz&)sD%$|{YxK}}}s_s6F-w>x&z)2FYaoCd5O}AU%2Rc0rx-41cG6~H`kiW1jO1eTO zqpPwjGpSoGkQytZTq-@T=*vPgtLSNIcR%G_;cuZOBdv#*Dj!qL-syZ$Dk_SnZ>^xb zN^U& zJ7DUJ>St+jV!k7-tu$ilOGGpXP)EL-K(3o~x)7epN})hg7eBu@1>F>0d7IkvB&Ukn z@_o)WTpu>Xa@1CY52GE3Ob8B14VkUN;G(n_RV~fh%4_~~RYw6;4B_UQ@bm)HPGrMV zawM(xsDlB(NoxtM>qSQa~#I{ycusV6qwB7USRPI{84wjK8 z!?<UXNM=?geS<)ck+i z=o-Kce}hZo9St)T;L@L5IZ_h~wD2%2#fxCszkbMXmdC;pEHO2l!878s<2&bn-i{O4 zDAkv|_aCE>15(3vAqBS=5)h%0;K)>cel+e>BQLqbm zn$!`iu@p(28Yrdy$?%>O1*XA^=bY`~B$>g+PI;LNGpNF(UP|Fp%`Fx7H|b*1>bG2g ze*p#VketiWNZ)uxY9YGTb6MFcrDoTXnMejs6^G)Xco9A1e}2eUFL-_7%L89%EEQ2Y zY&*Yuk3W6p|MwT-&G8;M^~xhjQYzBImHl0TNY2c#Iu^O8lM+$0;|7O{@ zFZ$IhzdrcMDAG2mly~Z;eRlnR^ZPgc=`;V&U-0uAa1x5|7XDVn0Fi1G+!O}hPyTY^ z=Xb%L>JIHGI!n?ldQI#0nX=#Mt@|FChD8W}qj)F&bYYIOS~kj-pB)P27mR~2@F8N# z(B8Ts>2r1xr}K1d;u&=3x%CX5o98n$@Co`{`166lEI7A)9kb%~R84G7qgRPWWi6^VzzgpNv+2_j*UY~0u)r|?AfNC=st zFBMQF4k|@{5&croZoZjRmiK!}4wuvpEuCp&3PV1i5`9{Z^~k>t5&fK<3bn-fg_4sD zQKy_p!H^r=u@xFH!Gzytiwi?cIaqv%n1)x>0rIJv;48sft|xlyGYA0qG2~0dOO&4- zE!%F8(+^6eng%r$AuD;^3pYETu6e42#dbO5qZe0^`F#RxK2{{U+_QtI^D)K@D&ku7Bti&WI7-=hHJ z3*|)%u3#X@O+A1I#4-^;3tF}euUmK!XMkFC@MhV=R32i6kQo;EM;))ZUiLE;NfzvZ zk6A^_1v+_gA-%?mv)8b&&g$ z_;BTNt6EE4H&y+d9r)pXhpGDTW4r}T;iBTI`NR7u_2*P6JA*c>J5&l(Z0r-y>|_FD zV|Ra#Y@C(>ihV!bOGg0@9FCB}+G-~5*58j(%He)^NStzZ<6VR>f_oyHs&TjPRmIer z7HRtXb^sE&Xb-gEo~o1$5iYRSyN#}1F|VMW8cr5I41uGKz= zqR=`ipMR-omxM>9N^}eN?fqX}*+rB^gtpy%UuWImK+Np>o!IuP#+XOKqTWz&ZFk~V zk<_qKu>}bVTLWKnw7-}O;8I^TMnA~Y6{OVf<)XDjEdvhBv?50G-vq~7Ebk*zb1uZc zkkaa$l?KLmL92bK*YIGsXrr(fi4q~*?gvQZq*4Cd+m&GR9GBx?O=RhOtXy8&NT$2S z5rKwPBqD4rcA)(%uu>^TZS5`VTeU#rw}B3q>&P6EAnl zek=J`%yifn?ev%CsIE`D(c2Y=fkb$-PkJ1xuE{tTQv^dBCana5#z-?+mQKK`Fs*(9 z#k~Pzq;#Dto1iV!JIAZs$dbJGfvTp(9|A@UXI$%i<;lqfpf`-07n!|2`RW_+t2A`9 z#bq^kmFr%x4c;0@y|$@l#~Z@^6@pvuDH*O_>R13;R)=@AJ_Vf(siOGo;dJ}?BWX02 z9;=&(YJw%UoLrF?(_pjJn5ajj$|R{xf<1RUM|5(b5|Ij?Kron!qKxzd(!45ifsZ(s znuwa{4p%i35^(pBLQQp!Vvwa42xR$u3Ef(alrI;BKL^9xPb(>tLl-cUDc6XsLA{BZ zq{mo?3u5IgOGc2M8ipNiHysjLXR2bA7|T|!V4~{b%hnE8XovKWf^$C)Te(C?t>mbD zoOZyQj$X@rCgQM)?8@E9M0P}a6`Go7_!DTN5I9|-Nw)Q7B9-OdN z5va%xmtGiqDX!j&ETtks&3+Zim{XrDWBi(=()OgPbW$!A?{GxPwX{t6hWFnIkt{zX zTL-4yp;Hf=KyQBcN&@2THqncAcM2BKom;<%s9H@zBA4;$kce5U`}nM_x9Z}D?cCds zwDP^O(J{!uHqE6>GuAQ2v1U6R3c6Do2MYE3P~ryei$+Z$85~C z^S6(1zCe!<(DJ|xjCI}wka9+?T*|(Fa780^H3!sE0+Oq*DiGakp-U9tZjnuL^$KOv zm`iPkM-v_C8Cz^^S1odNB2Yz2aakg>k_3&&F|J*wR{X3`yXeXGt88DscEtLDekC2m z)#n_M2$~t#Nv+i!!{W?<tMCrH#-!FzoT0?aUs8T{9_&d{WG3{Wrzd1 zU@tH&Pog470@Hhdqmq8hkIt&OQZ_ts$pED71m~RE9(~-ce;N#Z^$D;Y2&5Yj8&m!@H3zii|;@ge`qw-r@6`eYVd_P3l5!p#pioDMnE%OoT1r%En|_SPvYN zO7(yeTu=Zh-m*z6ucIobE0EAf`5{-#RXX0V$1la`LLn~tm3CkjE$_kIOdCwgsI`GU{^5BZy| zi?V#r2VNBi{=MO=g~KA00TE&|-b4T7{BHZ7&cHe3lh#*EhK%omliA*VImYVONeeqX z+pB-I)ZEFoelkQ$sb;=Z$JE8|%5-oqWOV7+*Tkyl#z36dNYkb^!9$@HaUwRh011Lf z!Fg&hw3nk41dNWP8bw$3%xA+S{l)}K8+ZcT{T!wzH_8!ChFusRNNr-W#|&0``sF-g zS>&nWp;(Gn#jE~*UgfVJ{IvM8cq|+up|%uH=P#Szzw`H>@t-~e@5x93juH2Q z?9vrjAc7twX(0q$0wN_Kl51L~NRnhm2i;qNBHw(#0unTbg5p>+ilHe52%yAHEa7X4$9DKBu5=eyY?TEh(^D2Q4QCJ%!b2o9lT4u! z?vftOn!tC7rLOurSHhnIpM)=4bw#*owyLw}w`jkycS-$>0DK3+85FSqRTfpm1qyMB zgBA#c>{O)X={2{gL?D=6L!@*;dJ6;1=3ki9J1)9zg_$uQZ!A0k;zLPP{%s4UbtSwUcz2zCt@R>DwTPy$X>c9 zsfHuHAvX>cY~A6HSDM2=g}%%A2R|xFE*Z`>{W{)2zcww=>vOS9Ef+2+c5bf=t4zsD zt&65rrez}Y&x5emKB&6Y%Mv)pzR zY{<3O?O6|Rjd3#FhZic>M}j#&G%XY(U6SkP?>eLpt8OzD?PGqUYKN=4nqY6NTOS~< z8|=&NmelKFew~cf8CruZp(}^Wb#V5PxnH?1VBYV=B;D@o!(*K8E2-Bv$~MOBc6uL( zZl0@ChE#c0M&f;Zxc^fHo0Q);pYV;?*bd(*^t^O|aBO+N#1<*fjE_};b>ftE3tHJ$ zr&!9r^nJu>=T0>|-6MkBj6g6+BbCUePstNHvy)d%+mEVRDjSr^h}Q;JG@+6_GM7>D zjqn_zr2~+mAIP$!;~3irlCx4%}dbYR3RjSj})$ z#eQ9H;|DYY=J|F*8fwU{wWHwCG_HECw0i;FXyE>kDPd>luhX|DBJbgK$7p zYDkg_OhwmFajFffM<$?5YDErJvwXh^nj_6}@^PwY>aHNIH!WndFi)}(#<=Y9(>^EP zb=eXMJ7%n`AX9UrCV+T$_t(3KQ8L^p_AHws5%#u2yn=}4)g+V|H-9-sVhm;GHlmh( zg>Au9D;A_6tTvl5?a9vu(yvUidz<*Rut>#ZNZat#o1KNNu_0cZ_XS>w{Cc&&=<}Lr zwXltHB@zUSxPU#i3Nml>#%#I!#TzA7Uid_L%GH-39WpPlShGQ;A7hf2JufmWDm%0* zZoJs0o(;LE#`gYO=?vL{mQn(R;j{B;c8Y8=Qf!lHxg9%)hr63vN|T(fH4alRRgM0p7U6l#sjNs(Kdsyh1eoZ2ZJ_1U2Paka zJooUp-tL9hBo#r+RiR>{ndDSnP^D4{XE1HZBTS7&8sX`usiDE*qLebZ3X%`7JqBSq z**`M%Pd#>bfTwSeqEdNOT4;PyanZ9ZS*4k4g=zPu4R)L8*aEstZq83RVvwi5>T^> zzUz%FN%~8^IS-|&skSxcRXMbNy6&O2+I)i}dSk1{`q&ukvZsqB(#*m=Rj2tE*62iQ zv(Gw!9|-j>cXQL7;WArY*HSE9@Vx>xQTOb}xk$C%HLJ=QjkzTk-u%3WTPk>3(^K1Y zO`*NPmZw-B#T28auOzFM9){Y|ZL}ewf zo10I*ffQpjH#r)qXb(Ny-f=Uj*E^_<<=Oy>AW^kipdu+Z>n4YqGs%8aEB`Ipl`N_8?wI>XG7HP3r@0CyD!cF9WS=cQCHcarcr zyGph0@)nDI&vPn)$b8yh&Exgq;0unl;On)jEc>Jq$VHk7Xr6=Ph5jqk8$b=JptVO< zo}8i@$axg)luDOmiSm{;`hzO5guB z^C`YZ3QT8nRFfc;1!hJ%O@l#>!*Xj(03bQJ#xIKR?0#Vm#&jD|ml37Zn?-=Dn7K5H zs;QSrQRq361Mi8Gkc~L8M{qM7>cl3Y9R<^PaGHxJiK=-TiHj&z`_On^ipe@87bKid zPppM&20Ln?Cfs&8H%d9@YU-?7-72pUZ=1;AiqD@=w7{0P>yS%ZL#9JIC*9QWwN>mF(qDs4X=JL{tFSvmQ$S z@JBjos-G#^1OVSDUnsvI0C^L9K?WtjbR1t@!3=H?!N^RVysOBYRkh5xV8vEXT@N@B z>C^}A4ltAe5gIV(v7r=$JcK4<(zp~VlrTvJr{u$YLOOs^AlHp1V3Sro)SUdFARt?C zs%(`law^ZjS+0{=U)UBtWa^xLYF**?`D=R3fQ%Xn5#gZ{Doc4fT#{tP8_IX{!p3Vo zA;^x$-Qg19>Q<|c5Z0*={Uowk2UKxpgBMgoyt=JyG$5DeqqS5J06z-8qAV~C z-bp3O(CFp&GD7OeliTRLkWJ5`gaX0Z8X{EE&+A;BCUm zCD=KHfVi|1qLD)~O1?*EIY)J+D;iR`a_Ll=8-pj;5pAoo1!ul=hy!1MAUA}N4ZZ^3 zLw_Uk84#&(Kor~)O3juA$;r34>cj;z7=?r{W?>>M--I1d;1DSB67iY(o5k-#+~)MI zCjW2A zv%8WEw}exw*S;MdGopyaTyHAjKs8^L;!q{z&^vj3Wvvvc#UnRYXo9Pc!XzG47eG6 zG=-EsTZib#917|!NA*6pq`;J!DpAR^BGW?J1l(v0vQM<>j0Cj{F-R;!wYs4z%)_(= z8#T*0q!jRE|)^n=lPw=CTw#iIyI4;Ntj&O+hzljN}uFLx^m#-S^JJ_pgcpXmx1 z;g%mLLN8;A!xq40=HVvhbb**;IGiWPyUYY4wjvGE^L6J3YK05r6eD-@B2}5>ZAq;< znO|Fangs24?2g2d0m(f=)lv=-t!gsKyc>;4^Fix&M`@?Ro9St8jzgJ9eS%3R(%qSD z21Xa9qE4?}NCcgzB+$yf>DQGn!oyjlX}Q-9{GqpGxb^@BDB;0wGH^x{N2qg5vr6?O zlLAm3M>pxbsLGB@)k{gYfNpm{$(afox-E`LF;^z#wGFhYsaLWNkrwI8C?$8kU{wv! zQebM;`iU-G?jn{;H*&RI)lxir$Vf+stIZ`H^6pF1`1poDdH2n6yllSZqBYnyoHM)HK&zNwTHFlX%;x+F~+o1GPJw@-Iq;K(-HI z$>>aztE%}beK_mg0?p-2=T3KF@YbkQ8$%87ezVoBqf;_Y1v3YN9`V^c(GGZOR@Gq_ zvAGedByA)AZ4TRP%D9@mq%3;nM)piy)1(TkLz0FfEeR4x zcYUM5M1po5&l;|};Y#zHT?tB51XOIqJN8$2yyAGkP`LsZ>D7W2bT=hC2rBzz6ZIZ% zF<)2!S)2i_dOXX^4%g>^;I0e4WE^%g`t!DBlgr$1PUC_LkEqMLAMLspx_Y?gv_DR3 z+H3B7LUIGY^0LpAooD-BNz+tlx@*3ObyJ?`mctc%UVo0n!Zh1OTlufw?cjE?#9S*L?KqM(xzd# z7AvS?84jd9-@jh?D~qoeR)hqe${WXqduNzSF>pvh(t;Y6)Qhu)bsOp> zqUtkxrg0dt2oW3$FZ%Th{{2I~eZ|+W{^f!iw%`dpPi*q(d^*nUKfe1vf99W_fD1x1 zjVDW>LM=TIfz6&2fqmixu&@@)urkzYY%Qh6Ug@YBCNbjR-3d3nKn!z;5iID#Zp4#lDJP#y-W)(AMU z9jD7CHp5G7#vg*e!)R!P#-Bod(mrs@N5YafI_e<2pRK0@9xtD=3+}`Xb{sytNgN7< z&ummtyg;Oab{Z^f>^Gw^Q*rMh;U1fD5)P1-X-@)66E$rKF)D6{Wzkn6%ahf`CX_{5 z);*NUbnBI`og_+Dm8{+doDhZtwBC$VEzO$w-4I$j1*wfl-0JyJ@DzMjF_i$n$Ok}) zHuop03d|XwOKOgkexf7bPA_>bVArx_a@=tA zWc8)I2C#cQV~zGQm00AXmc&88rA7!#2KCRPe-C^eSiuO9@_N7)yg`1Km*;o_UkFam z0~40OBAiNzr|6+#fK!~w&ximngil*MB2B>afoY!0z^)^~?qiW`Ur*Ow0W{Gz4QR;O zg@X`h2!%ofC7`E7cHlILUY{>tJf2VP zC;3h{Q8x=lW>iVJSIP-Gumgu-J0B{Evg?7hLyuVQ_Jzt{iG5c4!&+G0`)_Nc0C2%J zVzqikOFExgbL;)o3?1@tl~TE>%`$Sy!FzpTtKYxss~gocw>dTG^+yvWd%GC90n9^aUDYhRni{r=^N0Nj z#O*-$@%r&`2+6e(-#;ziV<>c2z+hi@*Y>l6wc*}&_?B=L@vNJ$U|aWL8s-o=m&>vB z?Z8%b!zbZ`RPc#?1d9z1XZZ3{_B);koZJ~G1;82AMQ2S{Fb46EyO-S!+>o-x>m!C; zjxXKCIQv1N4HtjM5jDR~3wF$-ynic5Dzq@X9KY}&h(>W)0tL2~x~R&fCPla~zmdR_R{z#dF143q)k^oeIlVlFbTvVZ1pkRjZ(m2s2|%w@TXMeZNql zN2`}=-X%F*_j@ggm&UXgR(Qt6h~{^#wR%m9mTaMPaN~`c82+?rpel*E^67e+r+u&7 zi#g-CV>O*;Hua-u6(xS)|LK$q!rZ?7KDV$M?t z8LYg!kpWkjSQX?=@XTV})GLYviku{+G0*mm8ZCvRWT2n$ii1LXXm;jd2`5D~DelrS zb!!oZV1=k@kD(Enre-nA1erdF+U?qlTgJE(grKba#-|dq-(`bil1-fwnbo9~_(?^a z7I z^<6eO@hl3FqxR^mI*$wd)=8HPigHAvrRak zr+3%&2|9MPNm^eFJJLz!UdA*JS6mIX5M+}EfT9Sb?5|EHqk@$B)&#&RYzUnOk-j=e zzk7jbGY9A!v`*6Mp(xWft8Gn|&tE%~YgKZR308kp1y+ff{96MIuflJD&nJTN4F2r? ze{I5nh}K1EwLg2o(_ma7SZx&nqQhRKH!drI5d`RnwMyQ~=VmYk57_AQ8Z@a{(5Hy> z9*jBMOFyyZ9?)+m69t@rw~j4MDYX)hIGm-y&>oddN9BKxge&Pi?MsrJNAEbhQ6&a7 zv(^;WDe|(ioKeco3|p0VTCrZzK8n)bM8^zrwA7Dfy7(}>$p7#K{^5ad4;)8)VIhzw z?S133$4?t?o8LR$H~;GP=aqB#O?d){ zU;&5x%|rkGz;9pln=kOqYka9R$-ic#es=u%x&2*p2I zd{X_Sd{sVje+-^G-aY<{Pv6-P0M+Z+u%=<%*qUp=u!z0HKGXd0kZ&H8g9g3^wc{{r zK+Wzj2mk=^A4Cq5weT=Lj8-jl5`YJ{!=2mJGrcDGbHwjrVn0|1MPcRY-J!keY-W?Y zUc)B8dFRWzDTca(53ixL7ORHlvtAztx0jDS12p*xoi z9+VfslCnGV-f44f_H=D0c7{9a}gQD*cIU5c1a^0YGkL?jx|%@c-_I|mMh z`UT>bj0l@CB)xfoK>SO{5%F+S4Qw@#lsCa&&@+et`5pOHggJf&egk_k`i`ygPEP~L&Bz#nsAA*uifg&bIqe>xuj{&?l%1aL4$%88@5+OnYq+zMBj z=QD-D{d=c1Y6_U!qAgvqaHU^G9x!z?YJ0{)h3_ZArX>B{qQ`QBSXXkjh*ESOoYAVL}g!}rV)7)Di=nR zr|f}c+Ve^oaSzREodcR*sF7;xfl6nP-esbbc*Lf}F33XMKb+`OrjQt3 z2}G{oA`U1&C~5*-iFmZcr$rBgX!j-Fp`m8$ZsY!jl|_V^5*Y0T*@1setra%Y&8ate zklc>=bw{X_pIg^d~2GRw1&hsyLHB{|pcBhPXo|Hl)=gtR_M6)kwA{GURU&4qPX+053 zLxaZs0Ss)Y7ecC90=OcX3Mi{5bJz-OZvj+QQ`Er304*30iEyKu+V*qV%Ez~C zMYV7je&`VdS5Drbb^KCZr@N~fcqMl(T^S{lWJy}d5n|aKUo2|b9?oVIUBl!ZD0CR9 z5z1`emWV5$D)metN!Z{z%H+1^s%c0yE9YHLJ0l3&_tNFz=iZNXY_N#5Oq>!#xW`bo z+_4|)$j>lU0H<#&hrl{u<^Wl0Pe1cn9d@{TZJoLCja(muC}X3mFBbjm)f>=l5S&D( z1mX!7m)q*s-5#|&o0*g?YQ8Xmz}++NCb`2}2q9^3H(32J+c`5+z%z_;H~ZW<8`1U@ z6`fBLkY1{$TI-sct;D-}Ju`bp4l#%OYr&{)Kx@iiuHm-S5#2+hwfQI4ecLE!T^k^_ z!1U@Bm_8f180*^?^?p<=)a5^Xg%7X?ZRX)g^D*_-uNDHye7Kcn>$%Ly2!9>X z-CFNiU~8f2o`uW8H3_Tyd5}`s4z(1Hqp$YHBZ73pl$2UaA=i?gzf#p)1fzo5STCmb zsa^-AiWmSMVZBcmXx_O*`4b)y^weOcspv&Wd2?%S60S?tyhM&gW=-A7oMch%NtoyI*%TB@Of*rH&A*%mVQnt0Pugg8T8V@jF3Dvxd*@~N!nnAof&&5s5}wSbfxBZ&KP#E) zByWG*1=E~|z$8VneD)WL_OKoXB43F9zm#8yJcTFu7W}j0pHm2*kvO|mBZ0!fa{}*< zMlYJMA^QuA%S&BYQmTU-Hl<8T8@;|?H2zg&OgClMt~ka-!!e+lKQwM44T9u5ZF?uo zuhGs$RxDb+GSwQBX=HR2Ly9XMBtUCJNG&SPH~cJS4s*-NyykT4AB%=>q(3A_jw#lG z#Wv-OdIILfJV~n?XYkDbJp_k*^D6)D6aMBEf4%rgNujF!a>~!HKY9G{OCfr_qtciV#%~Y(R}cL4i~Q9m zeDktjm1)6E(I@3C@R!s7@-u#T!+$;Thg0wboQjbbZGLm@(~Olbuq5f=)!J2Tt;A2`-$KYgUP$vKS2QKq-P{_+nA%rg=yZ zz)i#eO+d20c?6a68aRl9T%l_D@?{AXY9cweOvcKpfs@|J6sdHTX?+@8gpIhI}lbifHz1i)f^xW|3bKzhDZMgnj#R zJ6h&1#9uMiwnOT9y5PhJQnNDQPx))gj}i~Z%5D(Xz4FW?uZ{V8)~OB!H*kVS?gT1X zdXU$%>)oIMLxq7MFo6kWQ|JFswCwT%EqTmt<;Vb1` zMjpWTQjV8KdxI~O_bM<1BL4z@3!H*a4$siEkOet17>-Z_tX@;DF`oJ$9?rSUzg z6v!4C3Leg1s+{Yt>oDkoIe6fBc|M+RFQ1O{UHo12)z;Ps>{RLn8B)Z0%H$xD#tdQp zpn8bWw#OlAF7m(zsgQpR*^sC@&j=uV3qC_XOIJq(aKHff>dR4u8`QCS6>XVPH!eu+ zO&1+CVM0@b%~S2)oZokaWq71jD~WVkA#bO&51ad4chwa_xwC4g>+#mfgwjmwt>$bf zZ7R^Z$dKFbOqtr7AX{ZyWud@$vlB#Dr1>F3z{CYxUPj6v`Kop>z1o{bjRf3ixf|&v zT~Y>bi^%Au@T+_K$S2|U`_1t60rUTXd_vbwA3n8TM**Ai;ji#lzn+@*SJxn;eeR%B zh3@KN$d{f2&xoCsDy!%Uk6|52tOhMzM!GGg(ox0I)tX*N~`0B1sRf8cn>$)$Ilmem#wQS8;wtIv8b& zclTU#0R8%|W+v)U2kDVqOTpPUbLVK61nFqZBsOkrLHXt6ucQINdf0c*q8avMRd)+` zM|<8R6lON$6SRX(K1kM@d>VwLvdaqxE4;5$KrX6k*1l;%?`ohl0C{PROjfV`twp}p zq

=G@{*@(56H)v?E|PolkQKt!+vcr+8T{yTIGEC4}cqQA?ht5FUoRnMF7VwUxkr=G}n#d~)(vDh<;6uG;D~5E%i*8xvD1E7pn! zV5yzHNj`Kv{X}uNv2+D$zi^LbmK+!eMkRHLh{x9KU*r|KmRgFuqrEo(Q$x6ix07K; z;^hFN+$1Zh80o7m!?Qw7Ap#~lXsYg+#S^9)3`+p^@+G|wekL1I9AxUpc1 zwTcp(1vKY5?^>(kgV+}@$+L^<^l;#M`gwr%T;9>&UKB><2WDWB1Y_0xMx`C*)&HuW zCx0}TU^MNZ`azz25hqnqaoqOu2TJ^M=iJ(B(n=AIEOF|R03p5q# zKB$PZ-VAy(R(HP@>x@W0915fqPT@`$rHLgk1)%$ZLBdr0YFMC_$F6wLrt+(RYTXQz z%fNWvrLYnUdjuE8n_UCaq9VC#d-I$r;_XZ=f^ZkrX13}pvYMgX8Yp76?HuSKqPh%C ziM4RCPa-)4RdXfCExhVKh?$n>W<Yvib9q*YP4_Y>dEPN9qSn1@_viS7x?EhBq=@5ih9QeNliJQ7%05G0;M z55Cd>vurl1-AhoZFx3pG&AZ$lOJYzuXWHrwXuGgzzx6zg|BUx{$bubMFF2Bv+MvQ) z)sd>5@^cf!DrFB8Yr!^rW1n^GEav0ZfYxWxCIfVMsoW6qtL=j_<2ui?_m5~#ww9e`?o_(HjbdyL;Si{Ss~9GC#D+&f zWqQH`AkHV`1!nLE4gv<Y|(#|$TZ3#l*`(Pf5ZI5gP{7_ju;8X>TBU(|nlKBN+_2wv(@_Fh zuteE6e2hACGNN9z2+Uv$N0K6W7H&{aI*?5DgX5wq7V=f%MgHF}`1T>+y!w|)Qw`|h z_~DcvHoklEk0<`~Nt}+8C2I@f)Z%7{+s2tMH0daBCNf{vfn(J`dSP*#6CO4!srI!U zzyUn;9}fKXp?~#?Z(jM;;Vpz0r+f~4_l)0v=3jn^|L|09K@PkcX_v{KDZW(sYRT&n zscSij?bw?;vZw!cyqb|&WrOeLNemPBf z8ir4C`j_+!^DB#h016iXg=Yt)LZj@0MO7Ch^-3y7S?)yo$GK>&CX|4jL}!tGFp5>Lu~A6?9c zEG96}DW_5(tByS)_qdBf*2>&B@p%y#@W}DbtQSND>fC@RCp~1P8z{=~{ZGoTLx5b^ zB3xgT4=@nlK!4V<^j#1UYT9yzh$JHgbnOV>JIX5vkqk<6@?{68S{iSWJZDR-e5c_6 zT<`z@JcYW&qr4`9JOhzw9~igfDW48lU_E|5US1xb-e2DA`Gh~QAJDC;(3nA_Vo66W&`@>ZjA1wX7KB zW+UuH_7J!9OBxC0G95&kzu56MAL^D zBp}B5`+_#Rm=p{sH}&Iu$W-{beWY}|C^TH3Iq{rVWpd+b?m+Dac~CcdUFUdw_~(+~ z-$8==k4b&bI9SWA6yyx)$(NOGUqfy2vqRL46p{LIvH8g3NnB5C-cirx+kf1C3_}Uy z`ur>WV_xWJJ+#Pu>4vJc5ri7-30E9v_9JNQ*9b#^JCBA+?3?loXaE3!07*naR0{hP zku<(g73W?#!~{FB)Iwai;m>sv;z3eQIzg`$e9o$a{-$K4I`q}Dv$Yy{Me5*|P)4G_ zMUd1}2vJqYG`WMVR;ki+5Mr2VbR2v0MiZ-0|7e~@k?NKLF>vd=Of2Viy1l>c0c22z zbb!qnn4+4st6Ug2DvP3(y0l0dEc=g1zuRHnsnG-_+~=?4N(x|LqQ1fglG?CFO|yk} zP>xz^2({|{ug+G1swgfhi~N;3ClQtIma95(NS?;mYNe3dEl3NlhDPYQR*@mq<($DA z-YDA$jB3_*Hi?u@3xI_CAci$ITC-BAAs7g#*IBvVW6kDl)K4(YqaN2KSE|NYHV;%! zJJrOjw{fM5Gy8IxKx*e$HP+GBoL`&x@|4gjTIVmeflaqHqs{cRrc=WT?M&G7lJ#R| z5uGNEzU&&3)!=fu?bPelQrk>{k@4pjyDS$Rbn%gqs({jgkp#6PcJ3(f2u4VlSwe*@ zEUJcVy~oSgNq{0_dnh(q)VxupE*0EIv@>UUWr5l8%%+FkqescGzU3=e#8t)6)S_`i zx*J^X2PrZNs$wUr$jO17N1kc|X{xnBXEm)&xmU~mL{vdc?koIjQ< zU2$N5hz4_Yk<}Xl7|}DQC0bbnRRNL1MQ~9j_^=tYHD?ZvYI{LbT;(Mz-CHD6->YMcI4De@skSFjw!ryyk?@6 zR98Z-6k`G_JK|C)b^26teGnLGx9irK8p3Q%YmuM5$EJriL6Iw`s+K|(@)~59Ec>E! z!LEMBAG6~r^F&yb_?Q9S3e3ccTP9z$d4_hns`WY^rEclF7nJd z0Rt=wYC;Gu_Cg@u28MluV?mRB>?Q?d=DVwI*=U9@(&h<;O*Y9_b#US@^Jqtps{N&@ zb9!P2xH>Fnh=d0~oDXo9WuF{a+V&}(k&r}%cmc;3q39J@mH&7vcw)mIW!jd>4KjkT z)XFGSU=4p<8L*w^HOF{PXR*|yOOSmT%D3gV>7P-n(6+`r6}%_Cq=< zL(u6>8{X0G43JdIr-X^>=a-EQyhW?EcrJ}{`$agUikXpgR)@H z&|f0_i9bI1`*-~34Q#?4&wvLu5P(Zzb2)k|g)X7-6Q^egQp=X-u!9fXP3XAr$(+nyz2UQtPbxNRWPP&o4?4n|pMre;P_XR889SmLwr4S3Ba9)&}mz&rVW zkbmc4&J|FR0}@6x5#!1vEy)`bV_DvcKt)X9t06-bOQ)FB{iv$(P>MkhfiXSwtTlsM zYZ*IpH*(}#3Z|JXHQQ9E}QI>TSigdd~E*MVyKoI;E@)D2`4M@-+COB$(iE5ov z)F3LWQ!bnB*D3yn>-Z?gz^DhLIZ_RO4oFPYdKD5O)(-J`;_V62!XhWs4E~LDYZL-_1NL(gc180`4V@%Z$3)&1!>pXTpa zr}Bx|s?mWN`8P;tWuB)H9~)GY4K9HT)y2|u^ME46bA*N^7$1a(y6O{Z^cRW?;3BZU z&^#s<5s=;eG3#1qCdR2-WlHkGu3N53c!EE*HQdpsOs!fXdcn$H+Z37BT|>NBpB#`) zvAXfp6pj^yAroK14ezk{ZJ0i2|G_H8}t9iFJamD;DB;yq% z!E0$XO+;kojR?$i!~|V26kaSH3MGxG)Y_%#;2Zb0?(Qm{BX*R(ef9x&0M!NFfb)15 zA$CzX4!_CJCe zt2bdmU7Kb*vR*ZB#pFmECH4Z)k`#2^nYO`Aqt07QrmY`_HRpHXiWL=e<123vm)zZG z+K5uB-TX$Rfw#YiVJF#uOxtEu(E#4as(jSaf$v^Acb4viwY*``IB{50Fh2vD# z0to5Gb!zvx*8xZLN(O>Bee#4%iu_0()GGt2FR6X}imD01ve!vc9+V7C#!X2e7;7z( z?!Ii9SfJP%02ywS{7^D61^1K3I#N3o43Bc0N}lHQGe05!bge6EDxc5kI~Z!EbIM-? zK zyJET=R1Hx&v-`VHwbQrt7)TA)NY;|WBQ*Ix3e8qN$$OoosvbKjKv9hdQ}u|YnKD(K z-5yckl3IzIDj-5kHSuRD8a@OfT8+**B1|oOvIx8Bwc2t+w0LMPBx=)XmW)g4J@*siXj$gleFMF&eK1iv#s4%Az8D5;{?6~z^Cel4D)h7Z-TrRZQO-%K_wEoK# zjlD&Q==Rfe<#W$hlsB?7*e!p*x_ist57&5&tf=&AnP4v+)y-6p(zH9_mQ__OQjxSd z=)G5b_O2wilfh*|okcY3^P=kACUK*;wN^1pPgJ1$g92(=n_X_enI_fGGnw!zQ$({k zNQ@@+Of!G3LYp_%Ait%C;N>^S@`0TFvW}*0+t_fSDoPruJ^!I%Ol<$!Wm}NOzPM}$lJ;0tf!kH$$Ez|llY+>r5k zGgh2XcC+g4gS~ZkF(83BU@aK(08hk%rJK4zH^N|q6LKOBtOX5hsZ%9huu@sl&9FLx zfRv={%PFqv5WV>dwo(cSqer>E6rC{d+be-7)3(#qu;xU5^=O9B;X~QA+W19*tqTjy z)etzKN&lb^y&1m6Iy~#>V{&TcV{djaF*s(L5rCs9F}B5@)-AAI!q}wX=vvZ^Br)+i ztG8ESjnV0W9jiK)wy7QB`uv})iFW#zRNk1h)@>-OVehh1OtdZ;DK#7i9*U)ZbKoBr zzczUoPc1XvlZ+6jeg-$=562qE)6`h>YK_Kr{*jIFvcDrQW~H*hZl@tMt0YI0dGqvUcqBRM-PPMf zOD;9Z4euNYcZr^s5U1$6e82D5PsBI)M8M%2V{Rb0hCxy|X{cyc88YZ(BSQ&gR7xYY zG>Ir2`i}>`Fus_4weV@-uS7x&fgd*CpYij_@1ODidlF}03kZ1N+&HBqYi!-*v}zQM z8rbRG_r#Os=P3$#;9+!oR+I%SK*~=DofPzbAeZu%NhX@&LqOOY|Yffn}jaE%JsgP{N?5IE*J@ zrpU!i^YDU=9P%^)P4EB>vP3k7Fm=JsVHnIfE+?Eg0|7>)kp~=j2n3X9fSCZX+Hx>K zA{RM@PZ5BBAR%~lXz)Qj+D-&A9n=>UZVQP?WIH7KNtM-2!&vyH(7{o8af9SMzjwNj;j0 z%oiQrc5O-e$>YQ{)@#|5jj_Y7!2nVKLhg@))*XsQs0SGbNtejzRJm4y$h)O-L7VvE>N$=(}Lr9qOo&Hk$mR7jXp5Q~+q zE+Nlr1L}PMa6keob-ek7}7lMgv zv+n@;gmG>>j|^lJF>+TmdWqz1G5&N({5B2arOT?y+l z11ys}e15kgaPp5(V#->+I`BzuR954(v)0|vx$8GE0VuQN^%#_lZtA~s!~K%r&!&*{ zT`b)K%C@f5@fX32#MV`onMbIo2$T|_iV2DzN>4cv1SBf`D+|(!trEZnlaxyZGOYvK zcZN=pJojF)DUx}N(qs|LfAa1@Cpnx8*gUXJzg-wK9@3#4>u;ZiLH-b8qru%)Tsj-9kw zdc7vIC8}s2xkFb=7pgg+Sp(K;&;zuh>^R1ckoFbK$<=+^T_aEAG=xNHYpC1CY@zuB+~UkhV0A)8J$RP1WgJ?asQ&VeGHulCk z+*0@cG@p`H+dX@dw;hKBqiT<9?>+a0Ro&Nw zVl{MSF;HRfEsCxqPbNm`Ty10l2Fj|XaOkFdG@T|X(i=uPmT00LS=-8J>4C#^Ab zb6c)uU#bY}TB?g=bTSb7jsX=s-PP78rrC>BPLf*AbZCtFyQ!?q-B+iw0k&^Il0?mC zw!}Pk?u8!PbfL^8Ypfr-w)v)( zZr!?e#5RwFbY<9E8rjE}-2U04H2+3w&MY5vRfN6m_R$5G z)gFNn(Q`<%(8MW9)27{GoXcp9p4$1P%nJIss8opSoJpBGv}C7jX4ZTdCryxc8HlOv z@~d%G^Tn}OhAkN}q*`?pC7U*v)N~_1ivRK*DU9mp7mT+8uYq)#6l*^dwwB$22P`1F zvU9H^T=9wRTavWRfK3yv1R#J1%pd?Av9P@IehO_jFf*u{Mk)?qK@aFE0NNa}TPzY+ zP*AOD-D{6NtJj}&9?uo~d^HY%o)t-hbS%;+`IVvT>P9jR>%g*R8nlk?2}MC$L)M%e z+0j$2*O3E=lC~7lmA-HU3~LC6{28x6g*iOW$sd43={V9zux4dHKqa`!GSRK*F80h7 z)Sp%u*ic#dB^$Ih{mZXN{AFQDrQjr^X(&9p;)sS>K`~WmNB)Y8nkXsxSIWOz{6+zh zcjCF@bMSrhXF@ms$t7I};KX*E3F~V?9*I@4yYFLeN{SH_l$Hxk3YN#RmYxzTgJfh^lA}aK8^nEyF%&^I8SXf#7pxya0!$h!H0y4`0C*V*# z^nZNeUoE^Up;*SHI7CjjUmS1m_~G<_`6YgTI^Ls&BZ;!5QYxL=3wN)1l&{I7{x3Fs zS4>LcK*z#iI2K-k)h3<8g5+2&(o|uOiq$IZXI^ZQ@$ zzrGXi#Iua!;8Nawv*|0oHvMwR7e+IxC`I0aznuKx&3}Fhc7tU#=vaoR#U>_%FR=cK zi;xO9pd(3C9Qd^4w4n2MCx6(1(9Uz>ML)MNH=ocqhQnknSqIjlDOIwk&?vG6F7(>) z0D&_|KFAlwyAK$5TWv1zaTeEH69#lN2Xa6c%y29oLThe7Gri7efKYT%C<-DZ zT^CfaAgVABCSoEBx*!(N+?_;S2AtUT8d=Wo42f0 z0w9kpa09}!&|MX7Bo7r+SsInHD@hnuKhdTT5|mANv1JHKPB;Wkql2G030{3sGCc=$ zkw={s#R;c4=|Oia)l_;oMKqMq!poo%8fwhvi9R=Tu$%shAzX2R*D_Ew{+ayy&;$HK zZapb3X*i-pD%#Bl0)W2+%;n+Y$q|Zd!$#TY1AHSv5shpkYhI9xEzNd zD>9`dIV7vG@&O;#4_f)8CsTH z^83g?At)a5;`jnQTH>h-s^t_dA}{$74DF7$FCg!MXIGj(5ml(@lnn$7vS1G!=RfV& ze|&k``Oba9Umt5DLaX<0fFqEimS;LIf>~CjVhD{c*r1V00TEUQC=)T2Gs;H77WZau zr4n$1|CYMLPBCDstIK-2ey+epsc}&Jhcw?AwLi)zpL)Y8E74rFcC9((Q%bnDteFVM zj_$x1{>EB>etRj>$G2;de7L-plnmy8qWkqu#xj$ao5`>nH)?08nDy89p zqBoBFKjo>=#1VVdzLPpvU-?zc)~_y8>yIoE+m85Z`}mc@sZPb0IiAGFUE{Wo;P%Pa zPxN*(T-|(ZpE_?}bo;;?UU@NdA1F6okK(e0u;v<@y1VkX^8XZe!ll}(Ldvz+dmndw zKX?RH@em@;K$`SKL(ydAG>gQ}X+miBM&^sllV{M2jbONw+2LE6VIEAII}${wG^I_5 z$#p=kO8$e+?BWZ%`#IU8q@(f-jvA@#K52+Z30VrXY7Ik{;Ay|nCuX8-cA9K#;-r(d z6J`6LVQQI?3|}RK=rw9tHeA@Q^E)3)!mgp?WcQagCkidDmb;Qk4SiIQ*EWr1GvdXCvoyf%IM@@$S?)jUWY0rV^@g|un6)e0PfJyXV9rCc6;;&$r`_rzO z4pPmU7;X?-Bx7`HRP<67EcNh>zP*gOF1s+?AJ#ml!W-FZOV8)}UmB~{8@i3gAp_bJ zifZQW6GVb$%5!NCLdw;#B&`IODOiW9=3p>DHiI{gIo#t|2m0N5cvi{h-pkCxkG0Od zQ|&a%cm-D43o^bW^_sDhNGm%y02Tg(c7V*mDx&%Gj7-ClD!l6v zIfE^e@JL@i;Y}ivKbI7jLa2(uu`7n4zesb4Qi5Rk-o7yod*RZ~uC;oA$=KC)1uQk` zuTGH$fYSqlB$m({j7Eq1a(fVDC1+H)gA!b9uwWe{b>Ut1nD?{#{s(tM0ElgT(BGWfk z?EtuB9y7nZ6CZBt9V>Sb7v0`!&I+BmZG9YH!P*@ACT2&7a`Q~}VJ#qYl&F2J-qGxC zvn{ny1dW#G!l}Lb!QG3r=y5oI6;MIviUBB#)H2rZ^}d4xOE8u$C)Iz@GzY}=T6RQ| zl_ao2N)}MUiMBPo6EJeAaigZ|MLftn%_JCGufsAE>}5-COj*`pxSa~3JyWaclJOpK zsC5SoyH4xo;-YP{#jhoPD48&d!AUYhChe(5PmY-~WXnU3b^v4TcBz_F&4@)bC!dag z=p9cmzeAH+zR_e@4KjeFv)7tRhbp(?ZUDLJo@(WgDPOf$>gHE(^Ndv+gFv_oV5ubS zckdp0%P6bK5)NryUI~@%#CvTS-E7ecBz9#sW4m)%`{EIdX|X9rYTN~9%v9@5%#JGp zlsIg}UV@3HyeTVdC2AcyTN9aeR!=02j0BP&^V*CVxy!X9~%iXo4VR&8{;m2jNzGCXLvUL&P39!`;d2; z0s5S`wJ>@)pbyx(f^g*MQ|?)OLsA77XVCU6iMdqpzyXa8cEYdwCCi^}Va4ri-zY1WG)ASrXd?a)9htc^Iay)e$y z!_wH^u{eQr;N9Z2TDfT?9@~VMI|-sg{UU_8PdoogM`Uw4{r62!(g9J4 zA|)>ff=A29qP%4aF^SuS^;9b1-B678QIwarU~LOIND~RJ3gQ|nzUu$GbD+Tlx@#g! z6e*>EM#A>csO%_{e|+KJKH}>GFXo0=(BOIspHKb#&Obl#A5Y>LcsibelTFWfu}x{f zXbaZfN~2Kpz}`4BJ1XZOfCGm@laVqUIjF`w)7#2Hi)Al;9!rR0_RWiY^##9tjfaM` z1JB%47@n4^WE08=^ zXD`*jF=#ZCBv;tjSe7UI7%eMY`1X*!u!x@mKkUE>_&8QI4qij0mu1ssJeE8b9*gTJ z1$9=lyIl#hZ!fy%dV#_?c{h?mHTC;7)zH0dwZ$ZTdqs;?BGY9L75#6)-wEGDe-du# z>661xkjupbc$0EREThYvlC^w#^7+7@DGy))-#Nf|c>LMptKhdAR8hsDScb#musAYm zP_D&ZAZB6;am#YT(xpWaz>-jhszRui+X7W6B1aSApcE|A?CJ*JyW(v6u>>U>`Vfrh zJ>eq;7>tljd6H){>qacXNCR?O9tss%D$8h!B`r-7q*v3$=Akr%0q(#KfZ|}zIcpyS{&_Xx4$VFkrK2$Zi1PihRXC}A_MNHJy zdmt4?&r8c9A5mrkfqy}K*Qz zrQ*c}8bVOseOdI(0cQA_Xrt#mjOSi-u&N<0al%|Ipuy#`hzIyp$WNL^oeSo0_8uWm zk>4}^E_4Y*xEMkWMYaS!1W=xVPQRFp+zT&*;xFVY6-jmmvxg`RCJD%bnyef*A_kn0 zHwc6W0OAn*Oc3HTAc3o*;FwAtu$TA4|LCuO|M(>PTzf~j#uf(8Iu%HFtP8R_ZG=qD z>*$O8*bqsJ!4nk1AX{i6s8Y86H$!&~%GuIToOuIy81AG7B~UmYiP zz(p}SYB=y~TBz38#<0fu(}68Daadv!=oIg)=R9_EjR9W-%*9Ccf6lbxbq9SYYp!mt z?E^10m)nj4XkHLq|JPZqkIB+(^sksC^waniH6`-{AOFgQl*|X-Yqoo(G>qdsgqtq2 zHr%|n-6eYa2h_@wolLE7OEuz@Nnuz9PuEmAm;S}q--J8Yl2e>f7SsSd`G(xmBFyBZ zBP2QGtPhf-O7U%bu+z(ut~p9+GlvpH2}El@(PAK>%{1q z9No)Xpb^_f4-H6RKq1^{2;8n8-R};vaR!oU)`vzmF0e&pTD}u3)zTSc0|5(pxMfVJ zXeAIgM46mr6H)SEGNWW1g=^$nzl?D%mms8{VFe11K{E}%K32L~6o{?m?z1$NgbpOb zXUNpFo<*!)&2;zo+SsxQl3O2_IJAWNS+0AQ(kR4uAr41$z6atD`?7RlI0QO&($#BT@9@ z*i~`B_Hm#8`dEM>fSck zVuj@H(-pmB=`?~gXLkD^1ELZY$8|vfW2l(Cqp5WYoqG61t5m^;5t;~6p~7P6=J`XC z6xNC}y#KyLf)D_M%U0yoliPQq)5``=E`z?OpUW&N9)|$dTH&$x&R^&1Z>nT{=CYMh zCVA!Q7MR*5k-~h6H!@wi0+H&X+^8AZqMR6Fq{=E9FcaW;o_U=llSEGX&)>Tu~TfFrTeWKYC&<(Pk-Csy7_5;Nzr)!ex!qlT)}R7FrpU}2&z zm4lNaEopf!$rLQAs;RXr6*}9c2XpsKlzK#x3t+7_QBmt{16KHO`+42S?B25~^6JKc z*J8zMH5%@qcDl~TQ7OJHNNHV1(f_r5^v;5IzeF92$V&l<qwrtSy$;+^9MbL z&6E`Pm^p*%^#-ah=CYf-!CKOxrZt3bL8DbNb_Ww$Q&o>X2YzzPqLv=j{qfR%J%F^| zy=c$EDd;)l71dZ$0wYml(sk0M_ADziQuAHcef2VhI)D?XOv1j})|D3+Cji8fMjcx| zjzQ)XFr_I+wVAOo#L8&Zn?=u35;Zb&JSaoZJ-LEB6|0JbBoFi`^b>tl!>Z0fSq<^m z$SH>7?rMT2NRnbM63f~*Ki|QLNTJobIlmt;09N?pFhJ4bDsG$xeS&XL0Sx+zmDH4$ zA|!EV2qf*@4f2FX5il5Yd9>4=bob@h{bj1tAba_N=m???8J1L*crI#vpj|M{?D;Nw ztVo4!BdD`hqR#~I#AfFY4N;feN#Bk(9f;96p%|~?rIP(hhKUU?d(zVgOMDZIHqm0q z#Zt+Hk(3QF+z4{w%jw=G)(=Es3o8XQ3v1y>lt1&dP!2tjhHpbh<-aMAZu@R*lw=wjL!#6^~vxZRIyL4H% zGc8rc0xIa&*eIw=N)lH)oFmRM6GI1}(^Iad)y^{%Bn@h` zh^PZN@Xf;CFZt@=SH@2ZFQABO*e}3&;`y`uaqs`|8F&Ia3cEQIHP3{zfNyL87gi&T z(up9kxDwMoeZV-oSM8Hqm{aK4M;!?4}SR~-@L|`<|e5)k)S*s z-|zVD4d1`>zdV66|1Yq$L`V_X4V^5-3qBq4$>hr;mJrZ0;^#O2;V1u(Kd0rC07(TT z@uomPS1+Wuw)dpvszaqA`^;Tif+VRFQ;gj%LM0nq&cixy3hyW~o z$bN-vf%pNvP>d_=KuDaKac8F=o6&e9V7kka3 zqIj(&@KsRBt;(Q;a;d~B_+cv^luyLBDnHHCC^J$t^ZI|2^{-8~WXW+T<`J=SSDilH z)6)YCUPcnSlwDDh$xQ$MXUJrxFXE+mLGlv7U}n(IQ?+wNxIV-qvl@qkW_O)kd*{wv zk67;E?ga4Ov-QYk7Al3x%G7HF1Eq+SX^%*rm=Jvv{7>+2f~ku8Bz)Zr=qG`sm!3&_ zzCwYOoSp=Mcg zaDs-;q3a*AJ&I8jd#-DGUp%w;+=wx4W^J8?11e-=x1lc5Ij<|#$(NE|`;rq1y=|;r z*TbNgz~?*+hhGP_b8rJ7Z>$$nvZGT(P3j#Iit=v)Oyy{c$cK$!aCs!GaULJroJ^aXhM>^yF~MZSrmJa zA_h5wqS0SQ!pW8m0pe^QuJ%97R(+9_(Re|onhDe$8KrVLcH}cqsl)+tIzDrK>wfO6 z8&ZRnO{Hbgc0EPZvAI1bWlo)0Hy0?!>%k8Ykh8v_BDP#7v9OB?U?a|j!bv%oV2GWL z!HPyu*z%E~vN}Sdc%aK$FTJ_a_nu|0h4Rh2Ce4_S>uHCS=jiGJ zQPZvlbKh`5V-1kyNVq?aPAE(0IYE~TkVQ4Z>&$L6Sv4epT&?c)_0cetsCWee-I(?3 z(N1u{74OuY#Kkbx=gB;*U+>*<_3YKtNz`RNlPM{{JZlO>Om4{X|x?^5Tq#8PY^=|qsxiiCW< zwPdP^N#4;g^z8vsg3+LK$&ou9W2pVc&XV?+3#%o)9V}6RY(EbjRb`E$hAxYl&VKy_ z2P@THWN**sRl37oaX!RH6-87RASx&(v?LvI+4tbX9ljO;X}G+eTL&Xak7RN0tnWnF zAoGmS4cSst1S~$dbcfRlpY7nYLS+m}S{kG($jZ8~wjpUJSTZz4tYx(xE)`u<@nCeO{bMI8iMuoV*PO}s$b8>kC?3a=W^UoJYz4yI zlSl($YR`R^;kf|BaU3l_t;cuhk%x#B2_=J7z>#V&magZ1<_%c3c5JkqG}oE587xVb zT4pQ-{b7%!8d>Rg<+I1W4FXZo9j+olsF@`VjFjXP0u{})s!gBO%iYZ?f=hrMuBwI- zdnK(ni&3R*i8`HxHR+xdUD_Dl^(U%2^Yb>ofQ+X}sPnQw=0&=ETv{!~B;h$PM)cAv zc8%m z(O8~PvmB^*yLh0Pahx`kS<*_*g+}aWEo!WNp34+$v%N$MI&nkE+yhcML{|Y`b(_ZA zr3gp`L6usoEo!f-upzNjm}J8N3DN9OO13bz&CKCRHjULhigQvaRYOIJRr{8C!Gqew?TfbPvakWOfD`iGCJ#|njMRiQ*{|CBC`V_&=B@uB{hb}rZ1T%gPxV8%=GR{UMlcBuvo@9?1U%j;~xcb z0J|xZcIqD!v&ye}V5Nx4pw>QhdF2ZtuqcK``WGvKX?*90ovJa=JRAyE(3aFBXFxz5 zJc|TMyFw>ued{v$smB85eFP03)@e3P0<(IpXdi{Hgpi<)I4YGY;awUm9sHt6g%gp{ zTq(?oGMw>D=QDHBWnV_oKZqA-IQY!%Qu49ZYbC1KJA zqvfxmUu%3yid2yF^DiPlGyZ8;FY_OZT_g6$p*pT+236)T(J#tVR^QT|11guAjYN3; zXHBdRi%oy=*_rx*sy*cQbQDh@QO8>YM}Pde&}mxo+iWcAhHz4 z&{2X1{^Oy)J@B>h)o`dnoLk>diIBIGe|pCMIQjki#yfB-C;(DGt-nYq!^yS{>~#YM zJz!RbkU3mE=_to`>RE6Z-S^pT6ureo5oL z9tIJmYKm0F-5(-wc6k+pDx$`f3C!@|e>~#b1y}y*Njw8*msb-NCYbYDpJ)ZbPojs( zp*&QMN_PacKtwLzmd0CIT)tnvOdm}Jubeiy_6WI2^z??VMlZ9{;AvH#{0ewu zc*rSjx!F_rp?Ita$RqINu+j7DQK6#V@1K90tV`fQ2>9ZB*}4RWlsGe&ih(L25-8b> zD8-Szc~`I%X)KkERx#JC9#Ne*fivL2?byIO_>7`a@;0ReFRM97Dc?c~B||y&Y9PoU zzP~L{TT#b16BU@qQkv*en2IP&X%|x?4g3}oD7VEv!_0zs67PTnzEk`{QNy<)RGezB zE57*sJ_Ep)q6h)dC@Z*<$aJ9s!VpAxCUhH;gz7Rw!`rcQ zTrOp1L1s)Qksw#s$SmC%<>&zQN5L!Q2!PapxnT8}lh_oBEYQdRHpM_E!zC=!FQHRj z)~H37iCJ60AsJ{zU@5{S6f}lmyBtdhJRsjV{s!`oGP7hlLaMg@Uidrm!O&2Fq#{+#+1@d^qJRG?|r4^ygzwseuTnyEt5{1!K> zH!W@^aNX^Eg4*QPd{1$B3(BLVW|!D^$eU3J^@Wk$Kh&?ZsNbE1Dp)jR5F6FF5CSIY zju+*h?jcMUajUmV-~z8d9vJ!;iAzp&XMKiDx*D)%46-m#yf(Wu)#u85n5(bA*_4@#Bjy{*C_Od-W-)zKFD`^-y1xy=qvVWG`0wkUy~x zEC&r?KdyDeUSSo_#7HCXJc_DDhfjd?Or~%!s&meVtC`Q8uHhC&`bd@fj~byroBtBLk-6WYud^-oTE8spji`J$kUQPwWXn zXHvlQS+bqSlIAIGnU|4?6z-3vc|Lfk+yIE~JCru0(gOb2BA8z_suwaKK>NsL^L0Yi z^%bjYdp_Wzulj-^XwiAkNu+yuse*)JTn7Cvw9Yw<=Dw2bplNQkcA<-f7$oH>S{ZjQM z1f+_~HmOUL+DFtHBu&j*HY;E6UuidKe#OF+id?;dvZV3%p`?19`j`(lodqA(uB+ob z8Sf-quf_Q?)og@4`ELchXxHl4YVyXWJA7c}SzL2qTRP)K3B)q~>P~y#DX6-{#!mCk z7?Gi%sn!aRm0MFm@+{R+y_0K&V<8)$l1djKnrM+WHcb9LZZ~kG8|i(A#@4ccE_1wY zPR+YgfVii?U?s(6)w+BQ@0v=gQaU!>+#pPs;Bx6qrBs@xZr@2kWt0)ifKDPpO{=09 zqpyhVzRd2+`}VCO?(X4hEvKJ;=9}buCL^UH=iUI0wM1p_%_McyL}jTt-NXWsB-XEP zm$s{9Ad*dAx&S0I*W5EJe9?@chkN+z@#4|0^0#%YoT3;|3zBqG%}uR*`2cGJ;Vc6- zw?Iww^sTCzMLBRBi>VHbo!67?7b+U`r3S(oiKV9~DiGO}kPokPx)%!~z0nMuOoezj z(0*l!{}c7Rm=iqX6>k;$HSp5aE)_^McZL+Gqer)86kC7u&#OD!h}mgA!%ofapKo0e%j^W%<~3}sTz9%{ zMWpv!&OXxaxO2mUT#T!T`Bj-))n~4vLuMzVDODcjldd`?*o&sj8YnpoL#N$dQ+?JQ zZ62Pia#6dTLzrffCey2j=>yVqrPB);&ziaF<{-suQz1mfvt1Rr_gGL1K8n9%^4-wBdk@%RW{WYo;RtBGrbnd*|Aa-T#C;2hfQjv zi{;xN$-FSRh?!<|XI7xn;beU6s+D3oTWdk7a1CBq!jwuO-5jA};N_0eMO96YTyVQI zt*nPS#E$PlpRTVikMOI*tF=(M;iYMzyVsR#pPia2NAcT!-GQc`SzKVg)r3gtXd5NF zTAJ|8Yf;r)vMLD`T^X}!VYJ~2C@SI12wg|KgA0Pzrl9*qrL&;XhuEBdQTBRrJMz`} zwr$PufB{U`xD+mI1R)z?eOz?xvFS_GGpjHa=_5L4x@pJxC^Yp(C(IzPI9{;wxqJ8d zvsr;Ys1wD12tK4wCFM3fl6hCf;Tv+aja6!f38al*ZD+!SuaIFg~-kn8zKp=OYaZ0OfD736TQe@25N$rM7mjkh07k(~(%59gGj zTxTg`uJNDG*Go<(xcT+94In#S;v%#9sG^i*Dv^d z;InZ(;)Rl7DtOwzJmbfA{&>p&Zxd(WbUcX@-M8*dI`+2^fus;^wZjs@91Ocisk0xb z2-L6)hrqNnQi<*7&AFP*^=2hUfQ7P}{)boj{U`amukaU-{gv(mphy5a@MXs@PyG1K z@1KIx@f4hbEf-OgBTxm)VTNU&4!j!HVn&a?hkyAw{^e)=hh(IDDg+>;?fZJ<)J_st zxq|M{pPf<-2Vt^|Ujy$a|8No~vHN91+srCQotgXBA`ihsOtzJ?%?TiZux>Zqo}c}8HD-Rx4`p|qM(xL_FboUkVJ583h~UP?CM=@*jz*u zCbB9?r#8amofT6rjc+3frO>nKk96WB&NgfNnfQeO`HrHFz6-pqT<4}onh-fvR4IUq zoZy)OPr-1w;vfbt1Tg1kg{X=dEiyn*mGPCsfuAU4>KnZW&Bv90WAdy`(}G2=*CsAa z{}Ys2ErURup=sK3cpR!LmrTu5ME6yaoA7`ML%^njP%bxe4g4E9SI}5zQ9-E8j*-N` z07EQ6b}11a_?q&2!5?IBf2lI2-d~==mk<+bgRCV-2^`?rO-n##@PEXwfI*F{zKoSMZ^M_jniO67_7-H zYYpqQ(DjJLc*X{#U8x&qzy5i*|4r!9ft9^hJ+QBlhjJ}Ol_3ax#rOsynj5KH(+7P$ zwJhIumR^)DN@g&Z`=N}@Y+YbMw{QMi+PFUtBb@=R%f9Ayvm`U8E(t+ne=#h=t*-k( zlFfzQ-UKsY1y@f;|KwuPx>ZQx!-WlZH8t@4o)p5fafLB=j9D6wqmyx&J$9=q$sz&$jZvg& zQi;7#c5oMydhKRX$NuSf=1n3abxyCLJ+aerAFEOpghgyziBmF9hD@Ups;LxVQ>$yD zifOH;O~vJxRa1V@%oTl1T=D|oqBGJ{Dt+EOLbHSTLJupIrSXxrmjaSl8o+mm(q{N3 zDE_j;>BuGC)I8Upd$hsTd0aV1Ni5Y?eHmKj;oUTOA60EVPzD{z%(H6{Hyu0ZmS`O) zgcK`_w#o#_rpqyw>`v~yf3xb8t4;HnLjcWRXECGgub3u4HSTK+w}_{GH4%xYYx)?W z`K5w=NmWC+ai;H3RL#~Q9UTKEDt#i%BK8V7tB&)JFrM@xotSvd^cxVRNNS-6P1+UqVpuB5Av8yX2Y?Y`Rfw_Wq=l{3qt6SW zH!C^q?~(F|3WA|!1_xQ~I5&_jhr#yUK8h5GkGrOn5UTG|GI?bTc)`w)peI?YV%yK- zc(`wZl1s7F&B-98RQx0#BO-^bGu#o3cpML>YZOSe)OJePJO9ZUX=j)7E)ljg$~%20 zA>44fv^*BV znOZ;182&!LuX{r@-Fh{@UVJ1J?eH0G50*)U$$CnvK4&Gl)BHEgD@D>$xOWO&tn+lr zlyxqBY9vKR=gdUNMP`Mhe$D7znimpoPW!OI%~mGZoeb#14hAzWfo;RJaQS9)0Elg%HR#lesUo`qOPB_NszTB%fTlvI zg+i(!UKHT?Z`RWZhP$M0Q&g&?uBRe(_(xBsh%$h->FA1fKCuFJC8b5HTYU*;%*;kS zV1ryFLqovJ+NJ>(LK-}JTa)dgj%2myN6KeXtv)@cOv=vk>$H}l9L&b?3EdM~Dl5PI z3?aAhwn3u?x1EIZ@vZLXXqLD#c%#ZJpoB^Z;1(+POWGPy`1ddCx3B!#;t)Iz=n=1g zgznI%@^IB)cu z_wrk2RxPsJgp-7ch_P@7L8$C1WGkn`i5-YwrjIELIO%$h3^;|tqXcqH<|;&V#indZ zYWbdlCxd|#c+cw%9GUY(IkVBDh{8k-*~eGZ@{$`TFN=AUw3N4Hsw9z>mYwN?V!P`mp?&XAd3QE3!Z>bj=&?=?ELxy ztL=2YQ#A~UNXEmdLdmaOsF|=9KgAtD4xqEsu9MGQH5lE-VV5~)4uJY+k4xprctkBY zLM-;sL%rpui16VwfeUtE33qFT1&nj5k|5g##qo-h)Mj0DiYHr^afpECQGhN1yL*$ z%}Lg23UY`@2qbkIOYn6NAwPm&W__!B-fNT?u%Iu0e)!k1HTHXK@+tcah9dL~AmF=$ z>=^+S$u?W}Myv>wXG8Q-RogDhxLrhcXSL?KvPlSBVN5{;j5vd4KyWQtAuFI8wx}oM z6NHqYd;{4b?~q@}G_r0|Y9z}*hHZsFS8Ir~aLxV5Yv)RP>KuO7>tPVldsfX*yB4L4 z7tPH&H=`k7YDaE{GL_KzvX~Nj3ZA^|WN@{%QQ|k`Yv8s0%NDpX>;G8!wI!Y4cKp2& zHnr!pjquc3n1x-t+KI~;cBbdhT9_UM^#0YelDA)}WewG}GdswKdK*nL-QKnebGGC0 z$2uKypHmd!dC(S4dv&P~NmP~e5oA-?nWT!=* zO-EEA^C-$ur)u#0h-Mcq1C#YCLcO_gy_x7|z=DtZ$voq;rX1Zq50Xcx!jgVl{NQg=tN>-pv zCNJ-Sxq%7pC)AkePT&6q-2=BLGPGGL9*o%nKQZbYIn#X1wdVBQm{LTfVxS7>M|UDOk|5WLPHXciedeK)?Dwhs;p!L zFFi-Xn~H=w^V8cXDAk15;GcCgf$V_|3)P93ndp&nlM2S0$SZ>MG4U&d&}6~$8C?}c ztpBjO>tDcn z8(dYo(v*~{WT<4Mrg(&@6_}i{L-YzgYD=WUj+9j;F)SlFP4GFknhFw(4FPs|gp*X! z?PsoEOIJiVBUy*Tj!}z7#E!yGmKX$nTOtzKODo|tIou4A4=wcDlTlz4sUYR5wajw4 zPE$k>DaI10Al~rrrF&yfc0Ulq zhP2V8TxHj7U#~)fP%CbtcWn`=Xs9B3b0@rJB2dvZU88mT2HLW+d?Uv&b~YuMS$b(7 zwN1`yPL$i(t*r#Aif)<*K)vq*MqXhj;EpyK;`hloKmFBJZruck4@T z30^LbQFPI5F6B&EYo%jmUS9sK7Dt_%7$PFm-b_}mVw@_qN*xK(L^RhvhC)J8c}f!- zG@7E3C`YO{ggeI#bS@uNtSXg@|0Q};#GP~$y@?aM-HRycQ*OC2@*6@f z@+rM0yZA{!u7Xd|me!*4z3vTzQbKaf0jiSXl1a@zusjy^JSBPqmuoB)H#tT2Kw>IF z6#}~=oeTcU&$B*pZG;!MgcemK{}czzOJ9Ws+(}3(a;)!&+;C?A(>wpEaHH-W=mz{7 zn+F2Bu-Ej~%M5@EVnC4r{DNoocR;1GE8tzqvB7J(NQ?Shy`@PtQdXz*h2&AB%c(Mj z8`NerVX3iGXuu6~D$5s~DR?>|3Y?Hfg)?b3CUqu|I{1V;;@Qq@$!r~13+n(4v9Jyt zh6DAB3aD3NjVaWzQex7C3aw*@h77v{+0v|rKr#%VZ}s5an)rVH%WgeHfI!H1z%Ptf zr-MI1{s;S32jTqh0eBKHUX0@=qRAVMG-|qQ6OKteO+TZLeFcToWH?3?_SUmTuMs8v z5wr`-27ulC$2F}gbYtH@5N)I;_yI-D!8zijj8CuALV_1ZMWf$sm#RG>ucVby+QTka zRPxp+%{J~=;8`lVu^VTTB|31?fJy{wf$0@2{U;Ax9ZYSYm(!tyM~XwR6c51?*iL@y z|EBWIl5dXqyr9N|K~YfO0_Rh{ocQq>|M}g2d~=-97DJAcC36V_qg0HN;xJrRZ*6r3 zveMD6|0VtnNDl0UmxYJat+O#6?8jP3Y%NlhR7VL+{?7RMu)lki-@Wp?M|`EeAR-#j zP5H(3N9Ql^{OK)zJb~SbKvy}|kbfRw3{V^fQ<}k)PB{6?$sgYFrwxP_wp$}yuAq(@ z5ZcirJ*sPUWzjG>uT5a`odLj?jqkVM^wJO2$d|0jnx#X}1^FwXIqZl7qNGM87&zof zJ;`VAxy1o0!@0A!%qfn?EQ5Opp>Aopg0bJpdOufK(18+(2b&6pysT z2PJ(;BK%I(5HPBU4f1J);am2A%$nV|+dAf}F%wm#iG+kwLg?gA!S`KeGv7J>lzj%s zsT82rqVW_cG|``S>Dhrte(VNBwxvvnAYQ=a^F^pkwG=FEQ$)qW0*CM+LdX`ocA45| zoAQ)i{|kNYrw@nbM%>ECG4;AC>Ri_ISd)GQ5UL2g@~*R;mqBb|iEd?C!U;ks5EEW+ zT8+Uy`dH`3KwC2jg*-%DL=|h{5n`)=g6uSvfT%2*BYw)++Nn01IGNK(S`uHNNva!v zY+uiu01J2_78uZna--Vg6QcbRc!4VMB^XX9a8gB=aAkA1WDaAwG&}~r9Y<43{2 z<_#Z>PCDjipNa(Z<-a}r--mx4r}}ekhD2~n1Q=?BJ0a;bK(Z}yZjHCiH{9aNskK8@ z$8mO|eM~PWB|tS3Mg-Kv2?QLnohz^w#IXzn&4HlieT@)@JrPgp57~rL%kqKcEooHX zEXqOpB9&$Ihi(i)cga+pmR5nlN{y64cX1MlAB|f8^#h$#ewgR>VH*P|gJ}(U?6tm*Q$E{3mRmXe^ z`f*=!pAXvvugkh&)AR3ldVXFuH94;3qW<>Qhjbk<`x#v~fwwF9m1JtxpJvXktCc=z z)ak9A#=F(2i1#!h7r4P76AU(;@8Vyj77*P`e8gmR zVj=?slQM>EzZIw3vPcIdt5uM{p-uvi5er4pzTQf4KV z8`V{|LkS`cXIYqY`$KZ3L_tIOOp<-Pup>6^TQ0e#5y4hUwH6BXR6d+|>dq(VG$&o6 z(c2#Mm0s4>({yb2HM`IY2!43a;cRPzYmG(zf_sRHs(M;sU+z>j zX`(DNBk3-%8$XMgD`&;#Tp|2A-La+J1yy9(^6=A7ftaY6I6ZXL16bz7JcuYAr>{%iF56sGTxwLsK{~lN+bs&2UZaq%UqYxeAl9tK~eqySh~cBHt z4BmVQb7zC39aJBr(SsvmW1k>kEs zCoW!clCDeJ(QXI~T#IkjTOq^YAzIYNW(6>%RTpa!A6gG&JkXMqQwfjFG-{jpz}^bb zL4`3Ta=lMpkB84F&Z}DKw|g_s6{*&uoFO>^wg&_XJ^idBkfgO_iBBmoSkQxU=tALu zX;m0vI>NHJkw#0^J;KAxg6;oER`)@E@vc#-?JGIeR<0&NsHVW>f_*D}wAdAqd{|}{ z?yP!DyhW+gWRz1bhfG$GlC>22zQx_tl6hNDJz}9^Il7{CU~B@Z`R`ggqBBd`)0-`{ z?CpQzLkqi;8enl{L=9?x3H{PCc>MjKYLL`OP1lk%hnIW#y|$LvyFDhoPxzL%w6SU} zx%#N?ikB44Ew*=iFia#smvG@ra#9_iLEwNL&`w~=5F=3_jkdf(RwS-NV*<^EjIBx} zQb!o%QGR!2k0!9)=emW34$JXEr3>TQ_3x5h(ovoJ*UO}@a6!B>y4A4RSyO;KieE{G zz0zK!q(;m73B4ND84uk~JI$L0TaBSR!rOK)jrCGp*|9pX^HyfNi$4VF%Cu1yVk zx44;6m(X^zGQta$)<#=$R5=g|)juo$TrC+5B7|cn|I{0iMt6fWja2wcTwkKZPHNrQECviI^~IF@{MuYn^;W=hFQ6ryT;CpT8cG(Bv?jEVIbPW>7SvZ%ZdE+Xj zoi&Xb#gIc#+M@$_00;l>z~ccidsTd8{Ef!9;)n2nP}M}A)E}Sm^Be#0#y>v!&*zC} z;4EUGC?zJVlszu}DU`#%H&rV6%fg{~C zEhu!#my`ec9(*WUOjo(C2Qm>IsBNPH2d<*Q5GZ>Dbxbg`+ zAqG52gS@ghhvEr^$sP)LW~Vuuhu)%b1;zkjA%w)34k)Myl@N)tNMsXHl(xSjp#mdK z(shvwc+)JM@SX$4@c_j5m&Sii9f_q}vh$e~Ab$=#l`oV

70XX+9|sRf>vHUW{J~)MWGG1hSoCo@uEH5V;K*B(MY4d@}|#XLSq|$SU6p zK8qY03{3(li_A;~6{gyb@1R6zhzDH6!8gIizccRP`t5S`R{6qTqB=@zLBzcdvGg;0|qwqbADf^L}6 zLN<}Wq&N^sra^-GHSG!AkWG9cPgg`1b&f4n(^hj;Kov5(fYpjBUE+#Bqg#<0`454d zU~j%EJu;_D06kgzw@Q59JXWr#F;R{;l4ckbSoejSCFq6uB#=>nM@}4kM2cj?#;;7#jrN06&R=s_C%Q@E=J95ipV(a=-%aS9QptE6gp?f-X{`6dOc*_}XJf{eJ@U)h<~xL;Dg zMlMZ^3fkKx+NkhG|Dd>-!B=i~1J%Y5(rbR=9Fur~fHPEOR6C_f>$7!IO^uM$HRK1R zFIfU=aTjM?yn7S0axDxju}m$+V{laRqbqIWy6wC@fS@}U~tdk8VHtt?RrOp$@&@~`SdE=hq`J6-n@ z-(!-kL220~5bn}Omtr3ErGxw&i3aZ%rq^# z0OZG2BRi>PDsskFC_0WS7!X-vXKWMAR2mpOF>UG+eeUP7mBW7fCh%Bm$5xfo_cBXH z_E;+-JR+x-W$>7Uil*vLGeIzzTO|~ypQeh71w`J8A`4J#8L#xCA&XHsn81pr;B5DWC7O1zbha6hxPT%5wBgmtL(E<<^$ z2fW+ve{p8ga*>oryL&FD!%p=qZs~AqRhu}NPNA#WsG20=oJ^B7 z`L%$As!99I4TCV;!Qe8yG%7{HV%gQ71wwhIsEsa(Q{))FjsvnhmwXKf6B7mW^62`?-WdMd%T$gOp|u z%IQJYR7@4oI09b{q1Bz$j(I?O=ymZh`FMb6xa0ErV@{!2HL{^2rF`k;XX%3hu{0)a z#jdkWZ*P@BbBWnVA}Gao6(~(QT3N$q6Pfq5FpJpjrX_zBK z`!Saf9%QbQx6sAhvoUu`tTx)qg;r-*0FYj^`Fwf1?y1z>clTKl43V|SG$TA)mNMS?SzHwAaGT!1Vt}St8ZFU?va{Y zAAQCQ-K6wXE$Bj)A2i(s@frN-$_Tlf7z#sY>n*Y_s|MgX(Y2MF_;rlW=0RzNO3kcH z97zMQqxi2Ox-32D(tb?M5?nBC+rT8ZPI$9=7kZE|!5g+h6uzJf@~lfO zTR(#w5QA-m=h}VcY55b+grQ%`Pp=$tx<(YVE^4|vdscQYRL+;q@E9y0Nv8hFsf)_r zE!-hMLF|GSI!eq<}yT>HwhashEsc6>WOK-+49Kq|0CmybWeXxjV9n z$yCne(ZgZDB1v79N5J;uEnT2leFodCQL*e|qD8|0VwEE$}3sfgK3PIZVdT zlWam%FZ-iAACp7ECsZsD4&NnN6N4EZ3omI+X|TAxL&JYS0*n9Z0U1_5h-$Y;^AZ?Ztu`kk+3l0P$zx z-@;5`S~ge$amt1~1Mg?-O*(4gglGmPMFvWYuW4%#IHPWI^yP@CQ+7 zG+S0!alaC^TyuN~S|v?E>%rc?PRzK$_bQTpCObX7Z0zH zvpn2zy6nJqo*TZY4uZn0k|=})Ba%BN+Wy9h*A{m8rUx9zyLuAOGC(LX6YlnVfDjAN z%(!OR^eT$zkh~9GbVq$;tsKGE#{O$aZ}I z-+(_#&m$epN4}#-;60FK%cmR%l%1>xk}F_4;MVn~U7&*56u(5MJgI6l##X$j{6^)6 z5B15f%AMQrAV9AbKBnn`*T}gr>N;?o;0qB5r#=azm@v&0Axux@P1)obw8(QH4lop( zIP&N)2Au3Yr#Bc-vL6FUdk7&=(nM$yM?Gx^UPL5vx=N0&&mvm@m7jxOATDA81>YzJ zd@3xQLNVbhaf%r51ORU63qXXknjInK{%ha~9N-BU#R6~6v|@G00z$+Tu2Q;dfr7Si z8W{4T4B#7;FGH48uf`-@5ICTY<^TKR^Z)v39}MGmZiH{RA-I>SS3O&$BjsxHs|fTN zqh+^1QLR*2k#H}>tokbFJe0pq8fp$Yl~jiicyN7!MCgXS zDxQKj>YJYkg;*0IEKgB7WMN6|BW~3};@XC>3RA3!m|{f@<;I%iS_znT- z|23&Uyx~QRH9IADMhZ8I>!Ofg8R}(Nn!64{uocRHVOfNcO9;rDxT>^Od_u;dC3eZK z1&Np@ys9jt61G*UO%vITnoe0a{JN}Z5>@PamN~U0xGeHSRnU1DWOv`z_z(zE62W`uc14kS?VWk?1)rtt zY|#HME$TaFZw}Dt4TgRwH9!qYk+k0G+;O6ru3}iOh^$^B=W!?XMKtZyUN6!RaFq%- znzJW=Hk8+RNOQ&l{@;6= zTaYBB&r2{pyUz!6=^4`lMm~(-i-)eR>!>LDnnaaB6=RCUsL-;)#6GO(Jp%Iy7lz(M zZ-a02y_?LN{)3^-?(^uf2Au6;SioC)SvS74<5m(tNVQYmlx~6-P1n%D(4~D-Zx_+p zk7O~W0!K0<=fW+kRUaiTEUi^P;(E$C#M`_}D5*wD%dZ|wS=X0ima}wGJsGE_70=B) zaRAZNPmn2rs-9-i2wzrSUrQ}1fsD(_el;QX-piJ|>oS3g==<|ad55Y>Y6MA-EG{*r zv9p3{+8?jbweE80b48Xz&EoeaSHxQlfM~YzF7xO$ zE*GB);)&b01y+mH(4zOWz_r+03D%7finces5kSp%DSuRnHmKf(`NCTrOlSL$V1<~wc zo9RMTD~l4d@QrSB0NXdC9{t5o*7W-fJjSPtX@nYa-Rn7jUYO=U zic#ZVRD^QD{A{e4W)#3v5#UAlkErh$&`5M)4M;jZ zIhxWoWeQRBNju?qg*h1TjZ+}N**0IAOh$ATLV-_BI7z{kBVVX*(Q+uC{D8D!EZV(` z@qjEWR?0$HNuX*bh8wU4{5WJ$iQd z*(CzQ1&CBuaI{xm@*KS-Q%cE~WCo@z9Uw*kD|JX=5UGpG_5IF&SP9e;>XRWjrJJm0 zbs3%*z=@5bj{L+q!w+3|&ro3#Fb&*@K{w(Z8{h*gX=AN9r3Ohk;Koj+ltEdhjtTEF z6I>KnI0#i7fx;<2(NIJMj#>pLiqA zM9{oBwmx3x1)Bhh&X#S}G&|EP;H>fEsj_nHe2$7`I2Ilcyeu42CS|?(_gSr#Lvs`? zfsXus$!{O>yHEPvE53cir{xF3Xd&W|H`hPA{P2t)-r}bx|8(mAr1h*}U8EO4p9++c zNhx4>QM?$`h>$nprwxB6Ha>Kb|0?il>OyJx-7G2h18KS`9{6_$zBwR^`x$t)so=G= zqM^^iy)lKl#1@f-L%EbbA=WalP^N0DbF*~nt2kJ!QGIDJ-PpE>0}GTArQwx? zmn+-~SYVZTNN%=NU=ixnj0(GN7qwH5Mh9L|JY4nNrb?PKQ^NLCs(@N%lQiHhkKI88 zRrs?(T^Z9H(5ldwEst@$_QLo*Za)^W?LLP!Jo-T*U zHhCnV@7iTUgV$97OE3Po3e7U}6j+q3Dv`SjPpx|L32_0T8B-O6QrL}-?7lQ7JopS4 z^M6{<#h`*xkpYBIGX5c@ghJ5c3TdduJA;;UL@vj1OumZ=XN5~4*E5<#z(gv7k7+CxdB@$!A)$YEuMf>in)l3X37*~2hG(^gvmZ>pF|z% zkU!_KN)E_I94Pa@&vXZDfUc`vXUXAXpPalqu z$_Gs_t7KALwy8j6YjS@k8qcd>xKXo1E?z~^ugIZZw=dI+VSJ^Cv^rci4^Wf)er*0& z-e2uvy)06Gz3ct@C-a`!(>93b{OPMoBSeYo6SL;Os;2ud&LY;$E6f_Q1?blm-Zru< zv}WIz?zE^tyL*pX=R~rTI~`ENvEj9Ry)S$U5fg^dwrmt|dSkgrZ}<2iUK8BO7Viiq zk&sAhF7SO+)TETUojIRX$%C%KNFWLIwp2(az^1OD_x2tYnr;4#q~8Drp|I{$&dVew zYJNx^6f-*;H*FS~9T??q(o?%GPBJ-Mvd1HvwUbM@h{;h-RpvR+ncS`I$d1pVLK2^W zAi87qPECzk=sr~j-l^VPk34xiw>!ZD@WVq6#6lEuOb<+|^4RYW2Y))+i?b?gck23#zC@ZOlBxc5DIO=1x`|Y&t z>Ka{15OXrTXEA7?rl+<@_;kJpTA@O=7LZgGX+~5v8{6+|LmeV-vsK1mCUrmeSIR&l zar@SsaPfNJ5GnGq|D|O^G+U82Hi^8>wv>=@c2Q(zNA=VTN_bFB88J~_`RwbVw5Us3!zY&% zK+)84bf8paEQvxAQ!N*`1XQEvSLH&D2H?i@%nEib`n?QR+*GrP7loO3jMv&euSrAZ zq^@#Z9iOKXmUR?gea}4CTqJy9od-~JV!a>H43YvL)zAdxly-p7@*0CaD*@_4= z+rG0GVc8MkqLw3BOqMM;P1hM4EN`JIOL!|h1Q+?89SMwt)@Q>QEpo!SS9moHH*!f&A0B3K%cZ?eOinrhmp*ZWRdN>fgM#9r-kt@;#T(b)LCcLiYd_E4nXZ@<-Nv^e` z1T)%3!Se{`wxu&CtETJoUD1e~-M`3%E>ZjcYs$T(I@6?wOVO3x60I?vt#CE>BXf7A z_2%lK2o^m!EJ}qBm2GGexU*>bO?BJ1UYJ`{wq9_e2tO={dGYRE6=aReOb;skY%k)G ze0wVKYp=YDf4Li~Zr>tmy0&i=5Cao!2Sx?q;Y6wn#aa5E9o~ga@3nxcHJ}5w)IIa7 zN-R~)b-Y-oCW_|J-qM^Vfoi&z1hsl<%Bu2`;cF*|%@~{tApgR>Q^DjKTr;Dyib|NM zRZ|xYsXa-gJ0>djaEeZ~w(moPr-5GSxT}}5`&{}NRVPNo+=BAQ8>i_ipzi0Lo?JCj zHJD0j>j&Di0#lU^tWsyNYO6GhHs~j)y2#+P9fgZpE}_k&px0UAYR0AN6$zp)dH*dRr+W#})fCr)j={#n^w*1=18 z=Jx?yGI+-83u<8Z`dp}bVD~<3#5jmfKFmzqs4u48!h*^n*)5JoSOrj&ZoTGxh}&XYO7dH@edFf6YC z@`9Cw+^VlrD2zZCxS{Mw_6)t~&hCoX>0ux!UP=f&OL{^r!NCvgI& zo?Cv{{P7(>Jo(2b{_``%L{e`S(y3#~f+$QXp=tq3zEgQ!SVj{ua(jGv_kVh#NUS=X zG@%=}VbH+vjls>Ap4Mjpm?28vj&jvY=D5(hfUn9Vt8Z~z*5aC@Px7F=1m3fbxvYeG zc1OdkrG^=U#G0}tfr==GPdZ_%9(uqPrU;ZqkZl(!N`skXlS5FPIGqYju>T}ZG9o*K z1si)KPiWP(mNDrHCrlWC8X=MZqsk0)lzmFFRvRJM0wj|Y(#;9VFi)Q)>tXjZ~qLuS4CDKZhI?r}&4; zQ0i!!PXYc6-KwF0$O$Orka$uot>i)?TOPYjZ4U1uRFOj%5EmTzEEYkrC66?0Oifrf z{vM1s2#LE5Nk((Tj?X11R^BtIDa}2s`Y>KGJD#8!~Tf%TWM8boV?6i4nn(aBKny!&{7MOt-i8qQV z9sqy`@RqrPga9d7J1Hy8?SwCavm1_2mdCI{-h|w_!05_@iBJ-@u^NI4;^2b_#b?UT zY(Aqn29}f=DTsf-{GT8EjIb)BBNzQcr~u?Wyv=Y^ln^3mu0^y>O`f!B3(lcaKs2gX z?*>WFSs|PFnDUN<*!#S!mWQ}Z4jB9tYl%j7ug3~8$1?1bvT&Rto93{;0N*J;q;xoI zyvUM?RodNkV?1C{^&ZnIS=-ulnne{x_;FDzng!>C^F>ub6SGQ(_TMt8RD(rt)(vK; z!1x{j;NpLl4py8PD)xAkSyE^19n}hZ<{mK~zSO3B6GF_0|-UZZ4UVl|S zx2BA4&bzhg5bgYa>&5xN-*gLOU-ot7tiJo|8R|)Ow-cPPRA0YcLwUDlzENUz#+kKc zlvq)FG;!VDbTXG-LaY41DSTIXs8LUrEQWa!*xO}^X=;} z+vty=qNl3s`NC-A0-6Ds$vENvnkyWSuZFB7ZFW$_%8mJJ|1j3&U02ecJaVNk798K< zQlkapz(D;J+v+AbubJQAq`%x>+<%>1g|wyMv<{1Cc)%_J(hR3jm_*A9aBLuNj-PuhTPp?iiQQ}P~TJfviV`Fspcu^Jk+%Q z>vXOWzzv|31Gm#OWawBC?7f?Fz03~_oh+EH&1-uyX*7q=B_}OlMg=ac$Q-sTC@(Ex*F7D#;9^Ho1Q2x7&^y z%(YVWnvLu9&|vS3H=xD2vV4|k2zd^#q2BON6&GwfqD=CsHcWTXba(vJY{@Fn()4b&VTpHC0J zZYJsBkK^IK0jQZk&hu$z+s_0%%~UB`(N++E!L^oGU~u&uRQpRAKJZv;J3YLpMNtS3 zEI>Rq%9hCwTRXOiLGXOOli1=*)zYaFfwb?lL(;7vuQMr9HK#{}Xqh}+vQffJS>Z)Y zzB;lKvD9gj$qFLMMqcmoYh`*3un9^y>RXO%#MVng$icpEmLYr zN}26ph`r=l2(`?YsV$773F+IFQ!8pmi%H2D`e?S^VPO)(FUBN=T+y6#wOCYh>cywj zC5>dLlKrMFM!I_@M}XunhUeOH?Tpv9B53JbZUL(e;;M0`G}NP&ZYRIl62|tFC}Fis z0W0F}Dr#v_%I2$bOz5?oRfX2wp1EO1ghIVnH_Sgflv1t*s#nSWC-S4afUB0JEJ19@ zj84*H8ll!vyvJM(NL4X0k8s9Pt4P3J=~BqX8NR*rXR2Oxi3*%y=`)jjTV^eyo)WM9 zHS4gs57h`DErG-}D+{*(E_qXGo0y&ksBV$sGqW|OhYQlRqeN^Cptth9h8JT1mbe=k z%*I~^k<+u#%{5x5nrYU6NWl=D~C& ze!r0`Rdym;6r2yrea}<@DB3yXa&`0{6 zlel^|dYj6X4WHi02M#U1N33X`s#Wt+9dyvJaHDy=uhIG}eLpiVtU04bb&A(TAKg@& z{#{y3n`-yoW@)xM1|az@HvhpEqV-dW5)@SPW}4a?v4?|cn4ay+7yIH2WI*j`1F;Qv zaCeiwT^Us9JCG2PF4zl>@oCJ|2EF(*nNI&Gb|F*BWCLlXh~_Wn{5Y~>r~OpXqA>Loz04YFH+2c)=tyZq0=Um!rf&o1vlNwwT8A-r*r{pfWT zc{UkomD@CUN3v+?6T83zE#qYw9+cbrd24+*&Z!pASb+s50*mlOZ>^nX1A@5D~OK7|88 zJf-hXeDv3v{|xvG9!*F2{4?)~V@FdOY{t%D2abiu!o#o3wf*1? zSoVK?$X|bw-@WiRD?Vu$LMaq|7kP&K>ba|FDGKlyYNK_;UOwQ^`OH&ut$nty9**0 zlk`EI2aTG5vmA;**#El9@c2n9Q90d}SfL8)3MA|jNo z2!U}sR3weWO2H-4g}7u6Eh_J+&nSR^Yq%c*fiv(Vz7Pn3T2*?R)d&(8WAz}g-nsS9w;lQG}@=!dMs7qAllC(au8J_EpLX)_4xZ@P8 zfLlec-HW@+D}qis^%R6iHVmXqfPmgLQMyhpNCEyWj01Xszm;W`0^1;JZCps|pWU&J zX%sYpEipkTOjI=%;RdIi#?@v-U8@bW2ZHaU5l=#e?|}_nDlT{sPu5fV0xV`PZVjl+ zh5beN416s_(6duRz>o;BO&2-3j)Ua!qSVt5f|LNiCVnA)>Y3D~DykTx0S7c-{NWLc zk$b=a7eXW!35M`QLS5N=h@k6zm(0^6q!QjQVF9L0?p|TYEc0lVOTL1l3(oI^bHK98_t2~Qb1BvL; zjwxq1thP4IvPVzsj8NV1by??KXnz-|Rf|T8auiC0U#7uD0RSY7f1lsY z=n{JDVQYytsQ0I{DzsmFN^3PC#n5KAxj0C}wWoDE{KZ({W;NVzTbIX@k4CLr{mPHC zSnlsO|M`)Es=B2oA$#rqaB=fKE$~ex)WkWtCT$ZY}-2)Q*lS zxkJ^bi8>70jV{?h?w#MAorigxzm}W!1S{YoSD^5VZ0ISZv+NTdfpol&z`$P2GqjXz z&7F+tlQ@GJOy&`0P13Jz`8_#HYLAm6<1%wQT5c!NUD zQvSu;6vnx!OgFm1KbTPmQr?d$5?W?% zlg+^qxC}@Fpu(Su+8O+G0I{;hN=g!QqC&BI(^fuzg^2@^9;@Xg$Z3YSacR@?KMzPQ ztH3lXLiH<;0b>Kvvu>5PuUM;w3we>RH!H##qBnrC#`kqVy$CAYrXFs*2y2w4bV`~l zYG!3h_lO%ERT34ds&5t*+`Z-`+NNlj*88CUc;#%;4=5wSZr~mrWR9X}Z$$J3Tu0(w z$46sVatB<}v?h*=g3UL+*g{}(Qf`a6h!i!I>zkSGUCCR;1F1w%Kh8}S)v9duK-DKI z&Tw}V1CZagMl9+%vS|bN9pJK6OA{#~X2v>0;bH6P7xP*GAYf~GLorle^oJbmSw#bRj$?U!LMZfTY=C zMWU$tHZ>DHVood7|S9ua0sMfX0Pb2sIitqgQ9@o(rMrr*x~JXn!nC8!D8*Uv>ui;c3lKptyEaPp z&Y*{@S<&oJk&&_A%&!}MnKd*AUOL2(2vn1TTq#(|mZknTrjEHa>{JyvEe83{UVR72 z;fn&>YgDO}GE_5>NRSm<5w@9Ho*tcKkXEd!m`d|Hh@h!LlIc-Z4c~0rYm)AvUC>Q& z)_Tv|YMnRSIrm@Y4B6i|7xdhNvY+ufMiq4|X$G2soPu6hn{w-N$}YcxKEE^|lf!p0 zqHT@@+8$gvJ2Ho<54%H&-Fw6Ya{q!;yYxU3DN!Lr%0{LhsA|1_)O`BEf%GPv3Q?;R z$2zoiD`G3pMh88)kyB$y5@t!%Pm2;i7|^#YstE0?%qmaub~KiB!!Jr{O|Ku&oCJ@G zu8{+_5o0Ka0-dYrHBXy;mY#_+-Cpv5?d+}vPxu4tfpvl>e8X3PE=4os6||zIj*DY; zg%)N>A%)V3Qxf1yXYBw;y;S-QrUm@!cT0kM>{bA5&>#!C@KD&s&cmw1$1N(QIZ5h_ zp}%29LZx3weF`7NxRmB5MO|7dCnfGhEw@LXU7zWG zCP{&$uZTjA#;?)57b?*wA>*C<>jU;E0TTkk%KF-Xzux43Hfykg!ukSSF-H+$ECFfinn z*5pr$Q3nCv^}Jj<7;Kk0B$}(3J_DU0oh+S|Vys9hfoVY)kv4jx?()7ig{D0tD%6TO zVyz>!>|$HTltOw^Bv&VQS>eXs*baBh7N@H`f7ZeSMGBhYFdV>PcMQ*2Q~d5F z9^wiLB}CsT@2)?D{Cwi4C%&BW^X3HHyiFP9s+JEb1r=I&2IytY;fTfdgATj=RW5_bs(3*$N=`Y~IR&j+3o`eOS z0zei3p^KitCU$U#qzoZ+_MNh8CE-jCH-fT&LkR(`JXaF+&>$#V@Kn-6Hx@;dDvrKphtS65{dvF*dkAH?{xdTu_p2~ITb-65vfUXx{B3gDwpt#jDkW{C>b^x zLKUHA!6wyc*e<&S6$B!F39We))d^aZ=O`C*^>Dxd%8$fXfI&V1C-GoGq|vEZ&~C5N z6lsZ3js=v2!zDb+Hd*0#J( z~5k$ z)pgh1m^(A)i+%MrUBsc(?#7O}qgS=eaeA2Mp>daGKtg0GZBr{lCGs>U)x%4ncN1~W z^k|1DF}-^2GmN2a-?TgZ^CZW}yE;Ho6`G2c>An*| zQnYO}B zpi$C`OPlw>l|erDzqjXdaqI{Ts+zlZi2H5uWY$yzGSiQr?rIo_BNzW9Bq*vHwV+-- zVGUn(?n8qxs#iD#hS_ZLCx3GkFtq@UgHvNUMNu?rBoz=|A5pV{lsw#*Pg2}F$XlR; zmUY)GYiJmviuNtM0LgBK#!!q!?A3KP{!`w14O_V;&==m^r6?4}G$@LQwN~8JIV=o9 z;4Ufxi}1o)o3Sf=EIVN*r6!5hDXtm3?=y1qQVynvUc*~8dEG12Dv9$lsZxxK%+(Q(UXDjC+e=Q@!^5MCj9j~l_{p)Pq7H8{V4=7*)YDTm zsR*&PlDJKQ8iKxLZTH7I&N!FoZquy8j;B9GR%OYH zm`8;piZI8 ztyMxu2}Z+8(u;X5AvJ(y_A7}96ZIQ)RD>uwo?g;?Os~B|bkExiYf{P?z0qk6OV$J* z{QTC-QByA>qSw{=w2@q2TvX7O&Dws|R{Zc05md|FHXtS3>^2rD_GtK2L^j`;b~RhE za0%2`<3bf_47K%?q_SHUl1|QYDD4WHDjPjbZVScMOos%gC!n5U zlxp*0CFXvj{2{PXlg>i0F=3 zW=OT8stBH5*>@2%Q-P%VHJD*^Iw=>E?bK~b`6ZAVvJp1*Dl9W^&Fo@jZdGHkwN(b{ zRnT_P(?gJB_{*+4*rYBwA_M8o71!h_%i!UarP?&EqLKZHBiEgoc9ZbuUZo55rTdGj zf}y&o&1>I!(8g6SpY2t*U87r@ZPGI3Q<=iF+i-HBE?}AC-8XAQy;TjBDm^D&q5Ik7 z*X4bVPrc~X{8AAr3;mN6soZV)2yBE^Xbl0J1E_!k*@y)>pnxPK-M^bkoq}Cj&w)Uk z#U25`f_f2Mk(!&dILlu#NHa0_2AZPKknl{KqDVy|Nyl83OI~*OvzUfJ_O2Jbq*10x z)(&rMmI8AYL074%yK3YT0LP$Dv9wvPhX3XbrNP!}gJpx%sFWsZOCJ=Je$TucC!C10 zEclX6OA=_JHKav3F@D<(IF)5w-nPKfzJUfiV3t%b7{CcVv59zx1OAF5nScZK4nN`V z73}eNDYvP*3Hko(@8t2H?B%xyUf@5<{u^KaQ_uh1dpL!ZFe^I=DOCfJG6RaUby0TZS8p1K^gmFz`fbcgE-sLlT^ri~oSO4B+yPeZXGv zSTMsvsmgpKqC`u6Uc#Wf{wn3Ii6vCZ&K0dUinn=K(uRZ+E~Uk0B}%fN*k?In)mJtg zoq=>H4#PvR1jm6_`^yJ@|G;+-`}By<%au^z7YE&b+Whg!e|(Ss@i>mF@Vl-Jq0Qlgp-!B_Pj7P=w7xcsp3{V6{``QuZ5-uUy0GvjS3AOo(W9; z<`6Tg;JM{bJKq`DUb2$BzN8p`E?8lU8hgp39pM(4bw2VU@I2!-Uj#S2(m$k?BJ#(KmJSC+e%< z9e62-wKBpI5VSm4^z$^uQL-~w0Dy@au^4|7_K%Nr;S1#K^ImxEcKSkJ5{5Qw^ilfC zKPW<=l(VjZXF6_Ht7TWw)yRq>UV;qmyo17(K?*1P&dcqz`n?z=Q)nSfQCZtV=JqV6 zB$P~nC>QVmBtjh!c~!oId>I7)0{ZzzD(J4K;++LtzEp{W0Nx=>@MXaLd7yF?NI`s0 zfOw(&wNQ5mi{gMl$wQ?gHGwc$AiYewHt`oCKU->gX4jLFl@9@+{IUSy1M!&wxP^Oc z^;7%-cC>rN(A27T5rfJ0OEykcr_@ji(*0%aE8Yi%T-}&`X|rbJTcsdn_c@|4ten** zE<~Qyi4$CenV8{l*`$RWq4e;D^U32?^e+(ZJGg$d{ZlHK3wzeG0l-<*R2bCZS$!!i zUgMYp?Dp3)bmGJRb)ilr7< zt)`s2APG0#NN_nJUsG}#6r?|{dMoO!t|Q05=u+GF<;AUiF@xRD1+@3zMLEl*i<-+D zW$o}O4I*fk>Zu z6&N(rBbybO%MUqjgHn*6Bh3 z+W7s3zBi|Yny*@#n7XCbx}Ovb4HR{Q<15p;@sK>0N?$_}7zGq0klhhhC~e1uf5p|` z2|<60VQT7WwUQ}eR@3tNOm3S|JqJxLpal)81;Tje!3?|%>C;47`y#PWxzCmMx66)_ zG6O`x&;$RK=q+#EK!=FBKhvjN_U*bYE~_Gjqv)MeA|PR$zLnnf84 z%v+InX~-x^KxQ5f$tzvHrDpR@=@f0IQ!ww}K`N~tAcNid+}f#RopUiL^;MI;`58-> z6k*+YC6Hiw7T?x^5`PTLd-`c=r=KRqo&^Coy`B!X*Qn5NcXv@q+rSYN5_}ww^E}0r zz`3`r74DDY;o-r6)N-8Cu;0M6*qRRDdh%vw=%bWdiEbTZT|*{KCtM=7q7s!40h z_x)~WD!OBzekOpaBDq3&p8OzY>)acHLncisyZpseNtjw7%2`xodxmedwHTN<+p@K> z8Pv+15fOOoZ2DKx?R(ksmYrwE0E)W$FRd20r-s+S|eSnoL_d6c8 zl9$@yx%<>k)T2vOE0im-ce3O{!5*t}pUa#cf*!*$`t7N~Z|y0${!B?wWfJ==*tBp@ zsB4k)Xu-y;7-^rV=@l#;>4clmLS^EO%FGN2S?A`WYQ4-39DTk2eE1Zvm!)2dpgclG z2zzy^bhAOvJGpk?dYlvCRvj#z&Q+9nlMZ>>=wfvg+YGvyv3K60s|&jLP;8d!3Dk=c z6JRnrxpJZDYNzDJO;psQO!8mHZn+tV?JfX(D8w2@(|y4H&a*4!@JVtdjXSMBt$H>t zUX37Cbki+mQUEx+eo=0@ooB<~_FbZTmPdi~j;l@8jt^n29XZSq0#VTrwxW%u*;-Ny zSoTwcmnW=8Bombgk*V(6=GsgAHH?U>MeVLs3cA*&rIXgz>`5Ia2`*i7ca;8MM9kYq zT|`z2zjM1|7rs+TL&UUPWjO?(h-p5Pl*+lN^>MgauJC$WnJQDJO-$N`vM1_Qpytxn zRf66Dl%$l>guBLiig8jARaN4YU!|-n0cVLHHSrFu#!`w}Qb$q-%mU#E8|#70_G!il zwS`_X1UWdmn(Q?z!c{0Dhb(U5f(SvtiW%~B3CnfC+f)rep2Rx79%O9PxyOf_Souk?|_Vri0O>FKC81sjLK}J_Np2)33E?wEW0kA@|W%Z zHhrC;5W)q!&mu739o>xEjr@)YHYI@7CVSqlgVr##fl{rMmCeu!hOD-C>$;Lt8Ja#< z&%`*Qt@$%iIZo_^%i92uN$OZ>!mL@kNv}yjAs^^OLvdqw3$Q+5$vFii5NF}yYDhJ!HN8*X@WWcHV~s zHS`9x^i!*56DtlWo6!+PQeJ3C*Ciu6(;=m-k?qQ!k>1Xp-i-!<~4Tjhk>bv9piJ#y3|9*G> zCGhm}>vO;f51bkElN3;s1bKO5=UU`bl&mma7BBM5aVEB4iLO*2yv+S?mZkW}&edo; z`%bId7YjSctlGB^{g1EsyHD|(SA6@3!@@v^LUcpFZ2b9@@1OY7Gye3%e?J8q88ifs z`d`f;1#WJWNw=g@O7VNeH-^K6KzO{p#}9AeDJ9HD91BZ|rK@ha8QVD%$52$SeN_}o zpn&0J$y#ucFYf=k0SBJMPFp9mJKI!ynJksaAiop+y~^i>hjFP;B~*9FoAZ6g&l}%w zK2Opk#9k=&fD=%hkW+4B?CMUCrm32s%2knLy{N76;#chk_W*ti`2!k1W`FTo$(xsA zOE&M-GV_TzwzQ?El-h5;bwY|+q;|%vDMH{1q=1vLDQfgBRHS3~No?{Ao=uF0k}9fr zs)#ULRj>d`Mh4?4>6a<2)?nnD?0^S%hzBztErff?K?q@8gO=}R2!cjAGY31DW~mz? zkgtS4Cs$x{E<8=Vd?kZhUB#5%11kOKsMWxvf}^YF21h|is6xXPe@OSvc@WUTd0lXo zF}IxXH2z$4lTZgny)b$?qs#`4FJ~GN!@F zDH87`NGl+I10SZzOq}5o`r!9Ye%Yc*i>xXjz;VDWB|U^7K}j{HJ;94$EB`j+e`?c& z7$;8wY$%QIjo(YWNc;`L;ESil^WbJs${TIBK@hIY@NR`3+GTu~S`dLK1NPqK3cy46 z@Kp(2{cVCqT{9YZknu*5tM}i`+#}5{PBK(nxk(Lba0l#^FyLTpn9@jW#)JAA_Ah!? zn^YwX@EFz+P2XN^<$4RGl9zjIOjZk$%akL;1$e+7*c zpqT^o*U^u>RCMIwNYDG)hFh6eUNFtXdGZR8jcI$T8Q3jWb04DH#k@#(iEiA;L|Mncje5+2FZ+&Z9hbLFrs%_Vo+wR9Y=$mH~}B{GGf=dn5;aFbud!|_Up zZ#>~b9;8mvVjA0{0{?p3%TpEsGVfT1KGY|&SCwn9 z-nohT@A?9umR}Rl{VKJ$7K~V+e1bifqNNY)x3{{_w`|k**2!F~p_yhp6%r0nNf3}t zd8dPrq-&F;7g^izFn7!>79!{qoIeUTxsQ)^jG^Q&Hk_C1oVaD;TNPz;?xq zdS@EtBqJf!gPYDbVB&ONY-p5aVA?OLoUpbBkLjqp@krmTyt zgh-5$rIE@|m{FWmGxz;Dv8M{JIJ&XKl!6(rNMiy z&E=vA^88ccB*;1r&tMyow7JS_+rGsV^~x}4(6?_Ak99a)B>9qvmtV^~gwR9lB*dzqXqBc3CA+sKH(qTujTp>KQU6F=Dd# zNU|o0`B;w~8+V|)w=7YiTnn{| zi;!~SnOQ!L96x4CfId_zyE#Bo{XA1R(*YUTs)$jtju7hNLnA2VrUtYeFgSv|rVlvs zj3%l|iPe1r^|pFzfRH;*J)tfbn1HKl(Q7uO8SC2qBxOjNZ4mi&*upXq0OgUE z54Y=i(Y7T^H_l;|2KaBBoM~@6W@*w!YrO+ko?>Hsxm0?EBj)4qC~7W0xSLu=l0XIE=NMRjVW$z7Xu?bkz`cb7)rd6gy7AK8^kdhBXB45qn5ugX=g7F$qbx~F$% zucbUzZ$h7sMK&iV5F~Rc$wi_5y^5CN+w)W#HJ^!D;4~4CtWmfvdsr71 ztRl13ajIu6s;UFZu1za7OS2>LHZiI`=!`qGUM=^!2m)K$5tX2gWHV@pT4MrnuW#x}cV(QD_1ci8+(&ZIv(nX+Q&l13R$*6Bk#g*-z$)n^ zneO45s>(4B&MkRrk))|QJyd3?R?Cc}lsb(+P(N<;^;LV{^(#>| zMV(_;b#!LIS_iqa1Rr&PJW&lD2HD_7JQ3Cnp#(QJOSn@I|86Nv!(@p)rFdwLh-oCK zRvZBRIgPP-H-n5NIZ;*^B(oJw3vDVLj=$6w-byxtv&;!W}8yIIyh0 z%Z85)(Rrb_Vvqx-1^brXD6h~eMqA_q=sc%!uq+l+@{G|R7%cm_Q~uRuem%P;#+ZP@ zg1urruokS3);jP?fc&P~<@nl;2Ul+43-b5K8fo zPr8WsPVrxnv0f;~MS6pjCICuEm;XMLPiJMZg9@rMzx7P$e1eeHLZ#Hk3a#kFYgrl+ z9WbUPAgg7wg@8KD5DN!SDoX@*OYWJ$Un3;Do}YN;2pJqC-xEz@;f9hNZ70aap`?JS z{9(6VK-dnGlgfrGa2qH5S(JiF{ZqX(G!Mb?z$e8g{a;_~_g~4k4}3mE&5g0)2i6Y9sl&iPtOdJa=ZuL16u%Af>aQ(3Y}bewD;ZmZjmTFE52FF+(|>%A|GcBv%^DIHn+ZXU zkdka>fBJ2`Wm_<%@X-WI)Cdtc{@FV{CIV-V&s4o=Pe8N+i2&p~)vuPkEZ$>p zjbX$sbd>OO^K( z|GG_Joh>&TJqx|RHHF}Y3%4>U4Kwqn6elC32wbw!X_>iFXA?z$Muf_PfN_(jV{?m@0A@BZL+bdKzUFI{g=cAAlb)6+#P7 zYr3I&Yp8%+yUpB<@j&~4Z#TGG#fJP5v0Pq4C}jy36kQYohfvF_8^+9`dT4H6^6!-Y zLcDc1%dNcwy?O%{?}&eWi5L2cZSK#AM+6YU5Ck|5U(I`zjX>VI0S~)yub#8({CSJ` z7Ib?xAe}!y!6zbuO4yyUIbYKvKS@_z3aFw%x)8)t)!-(U$O=JtESM=^Rr#8dL%Xf;sE^RDg!4gY-%M$Y z_t#r(A>%jp<;M4LR$Xbmaz9h1*0+*=Xe?|IFIDRgJ?yJP&>y5uwa`+!jA@m4!!M^XOHotKibi*S@)N>6xH z@+hWbZ0gebrvopA<>yhjzGLTmiwG}bV)njEaZoTP8>)C6l)NS6eZMG%6pI{BftYG;RsWPR`D-VSm|z z)lfEwqn^p&1GKpd`@LFlCs+F{8ZcCe8}fB6-9x&lOeCC0Gjl28@W?o&e7}rWD3HA# zRzqF@5-aTQ?&TbrVAm}Pn^2Sfnji9w^5Z%7Q#mZV9X6lry-~faDJRUQBEIrmS2~pe zt35(xisW9EM9GNoI}Vo2Uo(`L{`BhxO6gRjBh7M@#7(c(U?St9NeWu|KzGgX8`2d* zhNek&$0N-$dzmCj>mF)X=53r1MU*C1FOd5fD6K#eyn`v{vs-Dr#+nx>TdIrSp2w7k z>HtvDqJereWmM%uMfHg9J-9mUrx_a{Rn7O7+tq&j^+tI=!rik4Wn!7gI8M!JXE4iN z(igTZlLuW zTS^j#01sQIp9Eiym+d=!m4tXaUINL{*iY=M|ndRFS0OzrgT?#q@( zEHw(u)^%gUmzjr)iHTj-IE5j$VOrj_g zj_;L3|H-L$!?n8)0EK2>Z9|*_gSY#cz;ZWx@nVWU>=Z4%ZR|Z4AWi_K~rtcmiHQqHQ?G8QjMHYR3te$HDbPbuBHTbx|cQe&lubqGK=Gm5}u{H(iorbVxo))N^+g^IIMv7Qp84nI?b{Lrw+DdHYJG4b7Bbws$*4-VxtS`> z3}Kd}rlG1p>A6W<-q6y-ZT&`yyR*rn$n6@{@01++pNJPa$Ho7+x0xpTDZrzl&AA+9Y_u0z5$n7Xsxg zg3NF_OP8ZrrJr!#EX4DqC(WZ|7iyM%Nn8yuY6U#WF8p0GeMjrK$QC8AJ1C{IZSvq< zm1-1>UL--HVBTM94$S30bQ!+Yel#aFg>^ryuG^J#Xi7~r1iivwVv@9m6WXnjVmmSNx}Quq^0@R&(v&A~Wiy}yqCCZaar3>2oh%DJbipyAKhsaNRBh$h?aGOXq*QbLqaeq^^Vn=ZZKunED8Vvim4f>E1o zv^6+v;ppLNfC^p?e3IXPrGNFwe)p?}2CF z47?NX#Iv2X$Z7`92c!liM6sf5`WOhuZZYCcB>2GUJC+t{qOhYphzk3juRL-YR#XFr zMwNo_D!i(fzhq@rdJ+mA>py+gzxu4d{mgGO&4vtUKpFB&$Pe%Q^IQD+J^tIf{xD%h zsgc?_L#K^;RY({c{K}CK1obzNm&F&AR}naWIr;rFzU+R=)yUj>pj=UsnAB8;O$#;# zEGjN434xMsd71AlE?vnFZR{qSB6&$T|yNI+~te&{lqUD zf7tlGSVO#{dxgMAk4DT;>_x98X&Nogve|!I?y1R!Di+jez0kah{7(5#A;rVVWJ>jp zDt$w)La0|!M|;O*Aeq6X)CM&m(e&Q%8T?Evih431aAAmql^}*LA>7X0)^io&3JHa# zeXPhfp))Naa#*H7YYfOp2C*q`z&mi#>DWQ1ER`p@AxB1Wbfy9nF39>Rdsk0T$uy5y zBFp%)@v8EZRWL~aCm9saBxi>hA|wwW{n(19PT@@7CZM&)2KrtnGo}xSjCuv(gMOY{gl9GX>VUpWaP_Q7x0uJIE;UWU^ zC*UpTJ5YFiLNKy`01gX+559oI#%p8r0Tmmo7(y zX|=92kuI$UQzNq#ESf%7Vd*N!USsJV#jVh4`v)BeVRrt}7{o(LcJiPE zEM41v0>8k36JZc7&1lvqBV8F{W`YSNuGGvrCX7pS*PIqb^Oq7Rr-SF((FDnqsoD(? zQt5gh82+BWeS+v&CNp-rKH)_mUF&~b8{h4$ac$qY=K{_=ZQw6iRkXGBbx}7ON&f9M z-}ox@_sum*X3OYO{1UbIuGC=kFp<}VNwZ?Llcww`JCovKq}ZKjEIz4PJ+fR8Lb62P zor{@%)Lxbbbhu-?MI7PMy;Fh>BI*EZqLsdrO{)w*68b4_Q>Is+f{tD45sQvq$dfOp zrew8>4tI6UwShbue2;#%+2mUYMz#`YX$S_psNZP0?Qni34(s+xwU`J#XBUOc!bhq)XTcf+bXRzhlR&RCAWXbdw zjSC1H_Xdu3VaJZN5Z$>7VH3Vgk_^W#R8s2PJcD`pmYN76KLk zo%>oY+1)*#=(i#!XsR^C1)>#*=Qmmia}hc$X`)qmrkr*V5xKN|OionVZoHnmr9ONrQWX;ZnxoQXY>NPkP6Nhi{<@PM|Js&(nZ(vSJ3XKWcFQd-bBT1s*_ zlllhtN=K~ZWyLJQ1HR!l5o*2d8`!(&3^MW95GXXlOCxn(1W?^KsN$!Y+bm{^38Sc* z=#HITPbrfnAZigFq=yS}9FOPuE+U(oBm(JWCn^ZZl#t_iL^uTLZx$WnyHmilR=Drj zAbdkjgD!xW?btcjni8(M#Z-5ADh1!q_hsu4efLu#PB#d%WzHRf)6c98(&d5RvbAGp zid8Z|r=J3ea8*h6&f!8?x>5s>cxL{4;`Tx~LrescOp=H=Jr6!5fu5WuGMiUM(9L4LJypwzoP(@G*X`7Dk+tj^YclBy#4e^&QeQC}lXmw< zb_p<%_FL1>{AY^TN`pIY3VGSV`FFWBc7PpQ*t>K049HyAAl10>VYS_i4<0WuPxgQ| zGM~bps$%Z)**>PA>kZ6jp?9dvjjNjku@w8NpmK|QBfCW807Om0T`JQjhL=w523z1Y zn_z8-?N2-PQu)1=Em^&W9_+sg?*KW@{@6`mV(vSeBbQvuP(GTJQ*j6k_Wxt_c1wh( zg1Ae-esr^oYxTd?pJ~r51fcI}&ROG?u-E)~887yp3AHr4r?n1Fq!-D#O>*rMrkNur z1ea=e3UqPcUM{p!vM@ka+gUmyPjLzf(qk(mn4O%0>p3;tUM0Ki$jRULqyN$8x^5w9 zuu$rVnAX+R@}TXlk*syqw<+pML`GZbQvgUnx4(wP_x>GBA4ewR&G?%dtGO7tc;=2+ zta+lCHMA^uFU7Pr#vl={wxV*Xx&b6&w~RW4WXN>U7O7Pvma0T~wf9Eb1RGQ=xu`O} z&rW1k4`2vv!BB#yXn?JcDu6&1Y{O5)f-dN82Mh-bXN7LWu1@)6L^RWqw8(%k=mWOP zwf|xQQ0IBD*(qx!P+DpnrI<-;com4lSIbbOf@ex@@@y4UpITY_B`Pu^)icc)%`qDQ z(}=9jnNWi*=+OXcqONavN-1^nL#M|g@xD@4S!Dv{PsCtJPgW2~RD8V#WEZjzDHb6k?5mX8*a=2xSppt`Gr;v5{pN2E`%CJV#~a?? z5!vYP@H@8R9_N4EZ+n0J%kk;AFAK6LvI9S}M;8W0&;4cratnpZ2YOAn+iu>viio!?4tyu~WIm(1rfmlVZaN%!vFcR{l#Z| z{tBO8_46wp7Ow&@He^%YxBjyE{f>V;`Tu;ghG~N< ztPyzW@ zf)~R&5Xyb>&zt{w2Tow;fLT~GczWVnE-5#kN%py>)IS|GVgi$I4sMa}&-h0N_FSIN z=6|J0)t!ur&7nM&d^+USc&rRJB0`?T+llANx6OavQS7EjCNPPJ8~ubseo00j!$ZuP z(5&IGxokmO^5RMem&$1Tjgq-aKS%tO>~+e~wAtT$=+UDh{|xcqj*Xfl02oLBW5TYI zV9o`Efa2c=9~2`uVSz=4WPe2+BZWvbYzLkW1~zGc2~A`bo5Q0QBnUgijeV9bcKAuW z2~YA1iNIR~gDbFTx*ci|)9&6c~L#Q1n8CYbRp$YQNyM;dx-#H#4CUlWdK0_Xf+ZV4PEow-9G0!t?o1RD+Q{`2Kx@Ns;5-y?4N+5fZ z@?q@E2(jctIOUNFmk1PMJcC=ICbIAfZpYUSfIkA)gsMl=)r&$W!gJiBo5IMZqh3o# zQE@YAB>zl(fGZE@9tb%pimYWRCXulD0!TS=2pxE(Kp28Q66c`Xz-6i|tFX^_{ExNq z`0BgYQ}?;{5xPtc(=%Cqt#Erm2g;%#$kK3k;!95QI>%+*6+S?)v>8TiiorCWFu7V6 zwZh@5zpf$0@-V2Ah-igubzeO#H?RctjiiS9j^i+F#KEkwl#DId(4@zIfq;rIKoBfP z+vsGm$i^V;5KCUI7+J=YX}WZ-((IC17J3n}jZna)>^J)?QSV$#vvf(&R11MS^8>bYoJH}8D?h7&Rb6(4D+Pa3PI zX1J5Y9P4;oC)QH5)V_vhFRrkhV$=J{WokvAXD_F?q%vxrwyEemZn*^x)e%q|MapFB zgnpuFlQ}OpD|73VCR@O8(KcRvq}@@95B@3qfKc`(n4+E>&OAbkcbVOgQ)oZ(rYrPZB{;xh{!%AXII z!wG7_m5dxA<7+3?AsU< zBiMtp?&p0W?eQiZS9TF0B8Am9b2$o|WC|0a$zdGTh%k<4+K^_^8ZlH>52MEAZla$4 zYK@m>AYvz&7F_#^EUI82W~E(tL#BcMTkc$`ealKXd0-ea;-T!ew7yooM~P% zQTGX%I;5J^NA}kXOjjN;W1z2-*PTPpWrm4(*->jL{E)M|{CIm($mO=zr3*n5lSv!N z;YblPkj_}LQms3fuA(!4YF5gT*3+v0f3p6yS+XQa62sKY{Y2c%dUSPHPd8@B*~J1Q zBtQ|8|Nl4e6F`E(6)a{U*xlZl=~q26^WKQ#X37ut@N;W&YO1p`Z$z9p#~Le=X6S7mK|ww3At)h4J?+PO z3@r~-;gFfcGaSI7<7uba8e^!==X$6PY}e=b`WlP2H)yXTY;W`Enr@gF8r|EmvKIRi zl>zg>Cg3ouM5VnE&r-2s?|H*yZi%dwarVY?W09k zA<-V{Ens3wbaUkI#Wh-4m<7bjrjmk;FB+wkTD0Q4DmB7+cXwAX)vec!vy363vc) zIK8NnK1YBM9vxCpn5DgiT(XKBcSZ*rPD{`r$Fm;>}wWTs7eca+(hcN6? zS^>M&F~FPM{lrci?$W{Q?6Aj*(4D0?&jK{)N~A)o@&-fMD%)Y{(jb!rXlM+(p01&E zY2!*ZCmXUZ!yS9p7xxCx4Si@#xB4P|B z4EM7upYfWg-pX+mO{6k>254hUdSjhZr?;~A1)-7whVEUsAgr%6&>xiCCp!~I_6eGS zim4eDf(0A!#xD-XK}xlie8nA0y+ng-m?_<`6Sgi& z3(boRx31nVbYfh?lXCi`P;9#b#y^(Kido_Qakt}h0i3D+p@xeM+`*ZYRmRK|RKXc; zRke|NLAtG7Q|N5^6?EW^+rsiw-(W94^V8eo6VA6C@gik2!9bJzKl!Ea zrT+*{E6J^J*b6tsqA=tBYtV!SpF1~8Sri}u%MY?kJ1rJmOREx_~OT~V;I4c zx_mqwZzq3#J^%WK|M1}F^sf^{z>+zh3uhcXW~#f=DXzQqgM^Epk@6q{d}07Hb9}~Q zih0!bIDraX+sbz5*{Z-8BspgV=#7b?sb12Ri}V17V<_hQhmYg;U(5F&@tcD$(}l2L zq-=a~|M|`T`h|aa`2X)s9t(KZfKP+XQl6!Vq(+yQC?`%#Qy_|ouWs_;fD9Pq=VgC< zZSY8Zw`DM^6|hNx#*g6J!GB!c-m_OQ$^+yFyln17|`?Pe8VeJ5L4Ojs9>QcAkkaz zh9JW-bjr8tuOcs`!i7=!%l8wXJjerw8BCL*XQNkzF?qQ}hJ4N2mN1lpicuLzTW}`5 zM-UoTV9R6kkbCJ~(Fm7ANOgf@Iii&|`=TmaNt+@c(DZwiy(?3USk;1R;7_1}(_xSy zyn~Y>;0MTEBqk^j4b1?!1O!75ri4;B@tAaxPr`ER!jHg5=N-Jem_rG75r-;;U93cn z%oNnVJ2=Ql86jUFFG7{HQb?f9ujFD20=fyKiEu3p1|LX+IK+wNK1`O2x(sqTju0nm z4R+urI7LhV6IG~V3WwkZ`Ka=(jz5hBQD`M4h49X6*-w0sTTpI}<;ldyLk&x@(W8+o zdRUx>xSYs#f)XQH9PGxO@PUa~Ax+3(K`;Uk&KNNj2FW)>mpYZms5X#JlW3_b&rA1c z>=tO?Un(+lESp2&lk$;CJWle5-RB5^X?+ZW`qqhAvCRI0NZ4L#RYaq=ycl-ot}Zx% z;fepas4FNE!5-<(IAl1*#T7!{93K+jtOGz+WRhEF6n^5E=O69m<@TJnx8pdmo)`}~ zhlmWRLkY6GKi@_YL`30=36MF`kh}o0Soi{}EPyEvNO2f0h&=Xp?aM7_FBqvb^w&7- z7K9X`Xzf6;%2HW}h^UW9Wz=Op9qO`-X1`Q+m_`M7 z!W7V~G~iWIM&WwlgV`cEgUs4Wdc(M&tFbtz|839Qn%&iIiiUpl@^WJTzF*S6&+%1# zPU%zGFS)GPyh^Mj7vug=#6t#cK_dfD&{8ydn zb z7^($xjG@{8*(i^s6HcJ3-A&wDnLHK;)|R*3{6BNRQCQS68^3kzxc(F2e;bvH*W0<09bw>9#x5%T?jPjS6m9!%A z$SsSdSctlFKHI*F#%rvJ3|_&j$GW?>iy>=`V?w<>(vYrkk!^7*wq0xQ0+!9E0V$^sbGED9AFS(rcsmN6?KAowLY zAw}V!_rdOoFQ72Gij5Ay62Hdsl}$Ow?7NOTdYaGl3X4o52N_bQIdA8B<{X?OmJ~M! zIU>bFEN0W3)7%0Wzuk`Wd>+Tm=@Q-c_5J{tnW(6$ik=L2z7>~bzQT1df||@d_7~*3 ztxk&@GRIi%bIxTJY|zMDW*H)n=i@jI!edYcK}Enk!trRaxN=P#tE*-FP{fUjfnx_PvB~2z?chlodO!%-mM`` z%uCBl_Q>oJt7^7V_{goz=~&h#Z#Fq}Q76|TmfJ_#QAoS*UPys#2Z?)9M^I!TbkSAU zx>?StX=$5>kQfl3m`lOJ-A5ai5G zl#-p+fT)(XlJa%E!fkF_c7iGlJ3`OGRI-4MAJ!TW(dR15ERp9Fw7{zWEM_L%RY!BD zZ4hY{bacj%EGc>Ime%fbiP*NG8VgDuYgvs9WhszvwAxkwaa?{r!;ne@T9M{U}OKtwgx;^@mr^;K`EMKwpiCrF87h6*6=b}hpt z)!#jVB!%$#DoS)<0lj*rWF+SI7ZD)bj-o3tCSy>~282KU6KfM>c|Oc%G;`=;!5>%w z*?XmZlp%>67(t?}0{WjYfgEXLNERCus)F)E+a@Q$A4tNZF`K0OA1_%%-N-3*B<>4N zPH0qc#n{56JP#Xu)`6~eyUj&ioC8@Fd49rIy0{RKa1^{HCl^$QmdIw*-Of*xa{rr< zSy*kfZJsbItStl=cmPPcG6eJTi!5 z8jjchz`ssA^!(}nvp!d2X-t#~K+1AB^huBZ*?z{uzPF{0KSth9P;vP|^@-YEd2Q#_ z7AvfaVd;tl)RbFo<_s5iz7n9skP$1ZoTf7Yws5ETMO344x6{@fDwDC1+MS@35M-p* zW5Pvl6<5AWg%-<8JjZ8#*1BOf3s`-%R zrg$0u_R!xR@~eYi-Q?xA?l(@MLe%91-WI;N|8(-tulE1`Y4P9DBs=fqXus&q&!_e8-IS~U%&Vdul~nZc|3sC%@3Y%RTVqM z`T%H8v)Zo*Ac86XaKpEEemF3PzS94E@;^Kr%dvu>)m)Sk#+T$-*-U_g+$K(aZzEi1 zq>10F;7_OHG`!W7c zV{k^hj>>RQ;ONf%hQBp$$kt@^(m|joA`noD9F+;IrH1+_g(SwxmOTxnZ|e32mfvPf z3iG_qoGwfSCkZPU6txkELga18U1W5blkMDUh=U1@6p}_Pif8MZD2^~h$P!F)RL0O# zQ4N(90ftT~xNmVR-~>(qMeYg(R4{;>@POPRt~Dh+q8dVVAM{0K5M0p-EBbbm|7cZ{ zBpb>vhHvPu@(>vY88X8=DSJvJjJ{ZzGm;x6lpGY$)iHrXc^7=E_#v`tdY$vS?cuH& z{AiI-IfkVuCdd?=kR>qSNgc#8%pvYH%`7fNPj9?PsDwgeF&G1f^G3K}1e7KH904mv zY6~lot723L!S{QaK=#aynm9d&0rEjAMTjB3B(_+X$jH2_IC2R=7Z|x*CdC{}p-A%{ zVL0+dIyDrWh?FvMRi6&0xciC-2$4n5IT8V;7GWxw08}_anJFd!IDn7DVSc!P^4B)X zqg=;8KBlpuoPFTP&2F!$r8Gsc$nq6H!6M!WA@87IOpUxTx3DZwQW*z$32? zCjV@ZXAv9Sq9mKKkZcRPw5B4PTl>V2NAx~b6#!5iLG;ftNMhi?a@<9hQ*L4oG3vrp zHN~17cZiGJFm4oAKIor#Z1cCB3VEx#S0OX)!KxU?sI^h5r1aiBV_Vc@h1=0ldgG9M z;!+)uwUNOe7B)o+f%BU z1APaITe?2zzQ}#q-TKzo-@+_zPwym|EP1Brj|p=HH&_&qE5%YNuSdN~Aq$HDFv>?4 zX`2T0B!exQO?RPdY73esi3ZH^SBxoukPcDkD1~cVF}i00dERyN zMv;&OQQD9ch8}cME+KJ_joxb1p|wzEFulS0qZ;48TvF^SJrGhu0tr8^ba1Jz(<)rF z>}b%Af`Vvj-gXfxv}z<(aZ6$#;SkNsxMEiE5rG4CI?)B;sWX0o?+G{8u=gsdq!32g zzHK$ivXe8XLR^NXCQJhuWfAHTCjmlL<3GF49s8jbF+e)04Jc{(@A2||YY9#57oz1< zJ@aY^w0fzC`&wP&rF{W88H@QFW0+gQ8bO_g@T;N2-6Ib-&j#YwywsD;uGP4M-iv*X zo4vF)iKFm?_f}c9;7ew-tpTP!_J0mpCr& zK6!GF2_78Q?nFE9T5^Uf{6$zTDsKxUGAIP zRqpctxUhg}^AjL4$;?PYHJX>mkJaJSYeana82w{TTPp#5)estv5r%2e3yv{|nFT0c z;8-ql-lRBtD(*(_Z3hZAt`~QpXLqeH*CMs(^9r$35kiVN%ixj#`bp1G1qdT>I`!Oi zADZzT^we0FK(H<#heUS3#6cr(soOv{?)O#|0ww^(qnNuzhRYCj$&4a2azzdu%N9X% zAF6r%NDYVkD(+lt+3mPL&&L=lDj|sz6&-5JBsrO}Y7IQkr>LeES;nDOej7<9ZMJO1 zB9>l)B7$++DI!Ca&Z*P51Ti?5>Kvo`$m2!{rVf_briF!+6Y6U%Q5~uTLvEqrNoY3a zAWOP>X+1dfIB^nku9Jjm-cKie**E}*aiNyqCaf%`1wC9QQKJQ^{SlWr) zhE&9)PMKjH7Ze(+ODnX+lB}m^^w~~t$pTZA;26U!J4l+5lH#a(M5{^&*qYOlmX_oX zXsA>JG#1fB6gmsAwyH~r6agvygJ;!OQ+m&`T$5@Ui4`MRn8>OuU7$N0I<$%ZI)YFD z^RB5`!mRY#P!%#88hTx-WwqNp!YnM=fv$o|W}0Ht6u7Zywps|XMyet|L`-Epy0_O} z68BDiISur0_IBnplt8)KR@#8A%c}U6afLHRc3xZ4ht)xoWl;9tQ6M9U!P*t7Qn?f& zn~<~I+Dj-}%)c;m9b2^%Ga$%ecNfA_WW4(VR`x9T76zBcD(mA+X$L zXaGYb7}-vA&o-rf2ADcTWED-l+y_*uBZ(DS{66?J;+le&z%^lJE;j>0LqhFv#TeP)~w3Q5d`dSg1 z>D^Mk++%dN<7J4|s7B7nn(DJOE)YPrulGC0ohT7KQWTlzjF|V{TpB%^gZtRflQiK#)016Fdn5*&uxB2^qd9t%k)Usyw#pC%Y-d4dUNNh6 zF~gej{9ot(T6VgdGGsBA!*d>_M`bghqHa(?%zzx2H_Yg14M`Kzfz89!P?9JL)Lx3l zYf>a>vnMbsH>(pe+ZPcj^s#65Z|15m>GG#HsP6D2DKf)DpcCz!n%b0(H^^_KjS+RH&!OV?>QSboe&m`iZaB(2laW`dEQ)PYO~ zFoD58x#MYgtFWv1-mySwxi#i24o|#@7tyZ|`D)@gpa*?KLo6Q7x5ZcEmy>T#&loMr zcSENL1p`ps&VW5E~T;%A+L5h$nA(!$a!_MM( z*|Erpa*qt;B7ji_BXKpFoMxaT{6c&ZjKRNiIAkaz8f=uB5rRXhqC-VS3OqGK;)Nho zo}+MQFFelpWx2VG0V5}2M7kYkD}+x5*1A6phVKxfKK%x7?6)5syIX* zkQ?}v(#Ckj?=>Zc>-dohx&sDtnMEZIIzWD+eggi6I4pIf(Ow-V+-j92k^MP~c&4wU z6h4O|Y!;DX?EFB*6qw36#D^}YglQY#6bZ(ZLKO^($Ph+IgFtl102Zhi0~R4g!_>)_ zD$+Ldl)RK=Am6S={uQ<0>He}K*3dj6-$Vco;!W@-@(pn}!$XlCBUr-hz)_r0skvZa z3Ct?=k>R4r63Et&zyK%Y23>FS{^9mApZ9sZjP($|jk)}M(ucrZyj%F{AQ#A^GeeiE zV_I?T3}YAO*SsS1OigB`xw+bV>pahm$r3UK+R`TuVwjOe90mLs+66NQU)858$ly!o zR38{a#u8fwVmSORSfY36N8m58Wu;z1B{QXqH2}i8h>s~YF+4Ym03~&lg>1-_8>?cK zOx(tOX@=Y#T15ioAn6LT@7@KXi84<*W%GKrx%_Z<3+}4y(r4b|Fwc3_(6Hl<`h?Kb zRI}pos!PORev4hUz9U_wdKb`p6LTDP!$fzXwKOUW_wtxf+VtJYwBV~qsW9U66i<6m z<5;z`8_qN%0joKN=~$py*=kCRh=oPLIqCcUqlrqYb&pj0Cd%k`@#A zX~3ZGP^wxSPrv(; zL!y+rXF)?cI<&s=#d$Hq98toD3QiZnX>R#-3sZI?g_0^J$EUXsEh0S9JF1Cye zh4L5F<~7sVKe>TBmN=~F!MKL_7^#^h$>oH1k%Kf_O=Yz;U&`Pr-E2iaZ4E6;g+!R4 zX-h_P5(21qxU%W_m5TqX3cdU%d%<#67)&_(g}YUb(Vl)qTqX3(VsozxizNdzrTvndi5`Hhl^z6T>sq37W|1>=thErmd^(l|kEI0Ns3TS_Qr+BD#a8GI zbop7ju2r%%UGa7DKBRthnFS0t=j_StwzXVHoYLn!061-oNcU~-9FO&w^MC-%aKYF# zec3!FC_`05&b4Ch#~_*GFk0ZlQ%9D^!E}!?bv9fdcSb}!60J(oH>zdCDx2+n`v=Qw zu2l_*B=E9VdTiosBT6UP3uM1AgB3%zMNcxmhMcBAVq3-bM~FlPD8Z+)%L>L5AEsN<73EW~fPy znzyx4rt7VHZatbGR2HkC5P8xFgqG=`kX^r73lO1)B`G@0mQrP?7u!^`aW1Rn2TDW^rjMX_#h3MeO8^>p8YE zneg7+uh&?;VMJCFl2A#Ez!E8tN}L9&^)-&f$Brn+f{)r9#)Xmcir|XmWT`mu5sM_$ z;RAA{cxC${9*N3z;tsWey5i&+dj^Co-66*EBgeu$&Q2mm~lL*Bpv!GUIrbK|*TRJI>VvxV|eMET(` z935j+H+r&QO#)ThYULvq*mHc+RaA{06>2kweHFp1m5eSG{V|8SSz-u0`4FVpYCU33Zz@>u#b zKb=q7!ya$v^XbpOt`Gm|6Y(~%fTb7zF2f4tR&k|~jU;ubi=VMh_C_jHya?W?xEG{M=W-1%e>Nn`m?{meJ0IU#_vng20$>WJ{{E2v=7W6qf?wVIgK&lu7spBY zyyT}h{`AE^z3JZE=5V_6e3QR9^s6Z! zZ*n`xAz{yUI$jN5jBkry7Jpo&Kmu&gl!PuFq6X)Hd<}lu^=KKf@v@-#1@dN*d3o>~ zi}nfT_6ibQuIlb5YP-Bw7&(BL_Hu zupG|kCmeZRCO}hS5@$Di7cYfIpPQbl@t=Ea$){94LB9xo4=P37*McdR!4(e?6_`c` z0V1{`Zr30bqNE~1yhMeE$lr=ShndP>LTYKXc$#7sMxteg**aPe3U0v%Cd+8It;53j zQT_;gM;wqPm`b=DHKORkCibH9*s-FwNi^OpsvaeU64EXUamx^mi1tA-;qa^l=+(oh zGDsJULIdkr%qb=?;drP|SJ%V+MtKWPV%wT^&QL8HmVhX>Zs|fIq!y%Eit1$%nDVpB z=hA@w7V929?=HrWr)OT2is@+tmWT@$JSY7GsxWum!|=d0?RhMyFENuEG$2l-Wo?-4d#FRQXOw?pJXn+fi!(^Pm_ZWWxPA=F24~&_icUi1j zmZQ*AK}AbbZgv_OarL0PZkg4)Q01u92AYIqx2n~jU&+fRkoK?lq<%0%QJHMgO7{D7 z84SDZj7?I;5Br6<;$BGhC-$1&M+p?o-0(*s$nQRv`;zke_~k3$B8}<~Z7Ha2byT|T zMUnYT?`vCG`6M;PZR#6Rbz<{*$}01oAC^4ST^zKVuWyWli)La^Ste`Ecu!w&kN7zz z@T4k{%aP6db%G-!=479ty~1XKC@s;Cpj_`RhDHh2f7z&<06Mcn zynN9^-V_@RSzOab>f_?I{rWOdytck_JSoPjp}epS>=A_cE=G=u+llan>Ie;{#Ji>V z9Yb(A{5Nv5IhBHw61KD8Y@&g&*t7VvE*)tRI>%h+lA*qeHwu4h62u%w=)jgO zl97r-LZRl>cPr|?O3tCjoXac)i%`PP=QG3uIbbBtyjnbsn3+y>_c^Ay&j86ObB-`= zW)NLq!1Y5En&O*d98WttttP(v%W-#iuPsBenIP$<&`JhUJOaqYrBf=9aCd!Yq8e#W zB_vH%Vo7zwz^>yEfy~V7g{7C}8c|-|DzQiOjA89HwLw?oMQ*HBKVSB#@E=X5yV?+$ z#c+<`iEUcC(?61+i z6jh&8QCbw2>PS>vFY4P$$q~79w1|u`xE28CUKVn>D0=EzMRZ$4FaF$hOWg9Xlf5iv zB*+O7i@Y3q-m@xdlT@rqH*S}Trl}rw7-r-9X`xtmHX4_FJ1I*_VXCAdI8<1&)80jT z@sj<(FeIvn1n=Urox3v35s=WihpEi0+@>x*V_b%5Z)~cWNf@PANI|0xiDSKtI#YVI zHT4__-FH0iKD_isU2ev8Dop1{RG(Ds{n;)QWkXGf(xS3LhO6pRRJf>+BQn79>Eu-G z2d`ten?Q>#g>ETg7Y#XfRnv?+miAg7-~c@{}T1b1v;B7K&NeqZmiUYJ6NdSv=l zH6VaU^$=e?E}||;55G9xB%hQ=&C(L(co^l9U!cD-55pS9TtO8~=meN1W;~!?RLw{S zo*Wf%iaxO{eW)_h@By2k!w0;Gfv_eT%|&60TB7F>$T!TYa3sN&_8ReVH;nieXOSPV za8?M=sQrrNl@nYYkugFey!#4b;7TURFHNSq?23Sy37v^G>|TWsQ5{Li0Oe1(3cH`s z8zz#bitAK@q1K9P4)W}{#Arz}BGOVM03Nq!e#umu_?;ia-v`i6IHc>%eW|W#SGT^7kLd_g~AeZ~pbcuMV5Q z6pBDh?9?yD*C)Q1{$&0W{jcZw$MX&RfD?Eqo`MzNaJb1;WH$wRWJ|Y9%*aKvf^v$N z3co?-@&S#>r5EzGI+T53JyVaxVy`o!f*J7@#(jBKo{)h#F$ZQ-$PI|E^hF$sLx2CG z|Mo@y=8oUo@s02hcM-ZgTz_`_^uQ0F?dMl~dG#M(?T@bm=STt)OmweXx^Ph&u=WY5 z2Qh&uxXIt#_{|NGPxJE^`8(l$`jH{4p;wx-5#K3l?Bi{gdl zeUQQ^Msg($)<^S#Um=HvT_K`DKm@bMWl_iwC>&~~_KDTVgkNgA&WhZKh2{7Rd~h-1 z2^?tOP==kMkl{l)kqo1|N8ujDa&H*ifN#UPy$Fks+4KuF3%}4#8CUV}xI2z+2bV*t=MQ!X+MMQ8;Nk zcj+ThhedTjnEap^!WW|mZ_ra@roP70Jv``1VhJQn;hiJoSfgT^vX9CqU>Hds3{lfD z%{^Sf15eEu)M0{|Di9S|buO#O0U_nQkx-`zb%MGO6<-ogepyRVCz@E1_OZ79aEm_6 z2yUM*nEt3a0r4jhgZy6j!Ff9oK6wt&12L>!VH06fIp`lr0e*-v+1HRor&de+0kJ>M zsdhhZ_xZfr{IG6M+96{FY_?CE*a&hS1`2r$enHZlD3_p-hqbP0f6+-zU<_?JK;D)p zGwX2NZD$C)o{&7prBo}-Gh(oYtLGj&lbN?C)SIHSYN%81X>#Ls-|$;1KPNQJ$SFV0CPY?@DZ`UyX|T>a!) z>c(s^a;Fvi!91zu?eU^sHYHGQIg!>B=$MX52)ncrV*|ao9`4E)D;xGVU6fLo%{(Q` z(zu$`zp6D8`P3q2TXHk@gx3=~SD3Mn=W$up%UgVK?P?JngqoE|FvBmtEcuO6fsto) zr6&fLFO1Q%{Z1;titnm7CcPwPUU-(&U!^I~h6R~$ftCU*lfF|dwM9;y6cQM~R+x!^ zaQ87H!~61$_I+qR2_VOu;a`%UBsnUEq?KaC1<`lh6gF6w)4|*V)yY*R1r}^|O`84* zRMp%xEdlp43rrS~=Y$v)8dj45hNO&dNZ~#EX=?$vEW#`zlqf}PN#YgS${TdoG2+RZ zWi&{4siRn;Y%*XnxY)ynX_iBT1vQFPk0x))2<@WiF3VCvoGQCdq7H5>+q#%0WdgRT zR+2h~o2AWCjX4pm9`)+_tw=Bg(9|Id3D~4*p1g)0; z#0Q|TI{4#@upda_CQ{2n%{x(*5Eu~cS_za4B0k4+^tJ$+r9?^CIz8C}XBYN5xubvt z{h(u(=^Snr{WQri?FAsQHjqPyq!)4VYROI{`4rlNU_nw9T6AypV4aoT5K@n!dfF0| z`|)zxDFO-NC#b~G;$YA`=;1OGQz30R-Kk^5Q&3S+lWc>a#UY^#qGE(ElVgG~`ka$w zwWQ>92H@rod#H-TREG$|nlA|qq-@m9!s>IKHWdyz#&IU#!*yDov~@nuQv|e50D`SF zy>$A^yuY1qdv8)9(bi%^NAv5=KUK(?sL0f*aSy@MLevEn4Nfr^7Znj%wnoGr6jdtHG+_m43P{e zi~|4N&Wd+0@2dO4u4gMi)@H@dqP$y^T#px)PjByo65Fx7FG55lzl$KuGeaKg9unSc zVTm~VzD84+N=h;=)?{gnn<>dbZK@ilIrvl?FgOC z*sx;|KOl2WObu6%nS_Ip`L>@^d`bXpmx)wDqDoE@l@PJ!W}Dw8SMzwf)p!%@X*-QZ z{nxg_s&!benn=1;M}^Kg#xm>9rqE{M{SQYcR8u)x13q^8EM1GGs*<_~Xgs>MDT0>J zs%KBN2$h__Z508;AV~JR*83>gv$?YS$N+#AU9Nt)hUydPd6S(7)i#y;K{cb5b7hR4 z(3-^|Hd!%|D#%&GB@A97L9sVjDUDR~6gkLL0+PRt{c$PjfJNy#RT zbZf5lE0<=i8MU90RFQo-t9LmP*+LU4CZXpbcp&hYyk;#otrCUkLf8)Gt{=3GUyS~9 z11NO0Xr&q6U)m#fQuPs%w+FAy3$-+fr6e zKyqCzJL8Gf(NjTaO{idAkQW&l7^Vdn2N0j}0}HmWydu_~ST8sxMg^f=&4u!8(sC}% zXD8Ly9OEz`Z=SFdRz%}b+8~Xn^++4K;pM=K{KH56yBFN=^6`bYo8QQxDiC#@E()jNRI=vEcfBI4ylli z`uPY&3tT(WN3_fgtBz>bpfMqWF>wsciJ=vo7S5c6IzneR1c&~1j(>B=?_c!$7k)MD z$UR1lvR^P!ao2BZV8(OVFd)KV95V{@~eqs z@aFTc=kxOy{?{+Say+q>c2^*ezku>PWkdipjgids43jlaQtG+NTu}f?GeD_?s_UD_ z`zwAo@a@3I19MVGtqmPTA1W@VU#HPD8w5% z0IQNQx^%pMYy`msSD2~}Le=5P8|GS^{IJID6o^Q?g9Ri{nbbq<1Mn&Yu@pTZ@@aYl=~m~8Tbf1$%o-4w1p*jE*T1SxrHFC%t7M~ zLdjbMtC-3;)Rl}twIg}B%2Mi0q&6mH_P&I=F6;umdPu4$*%PBcia!H4;ol5V;+bCL za%+1dQX~&VW?c)$a|os)-Z}%YQ~-mhOyw~kE;dNVncG|kl29ldK8CQO_wqcT7$c;W zLJrcSJL7xEA2+e|ZXdnehij`)7clh$joRn*Ad^Gp#hNh2~rI}{W^kcWt(>bea-JGY&p6(_!1+_I!+ff}O25ndOtI0XcB zi&#t~+|>i9b(KSfi%|L@{(|`e`Q-9RAMgVznej=&XLKThmU{F!Y3PB&U=g5*)K%kJ zipaJt1@^+bk66{flqtBi8L?5sfBlR5-_7^@VmJx+K>$+o`C3>a%dH-$DDVVbA>;B9 zJwvZea@X-J88zAqRLiHXv{tVy0LnZc#101yzBSDZS_@^B9d*JM>@5E2#MyfNDD$Eh z3WHSg<|66>4)`QQOe|`kr=Ad+&>3PWH*td-&7*l=CV+gaAXL&*-N4_8d zB9hE_D$=1U7eaBPF50_UHMUB%4Cfv6deDOEzORRvZ-RHES9Rb2vIC$osca)iBwc#K z*OR(#S}eLHZ2C5pSfxi0SVhRSp$mJPNpx8&BqlDo*qVA|4Z6kxt_6ukEh$oH$sT)h z{Yoh7(G<<>M56-1yT!Q%SDFMi@K|B+715M`6NsvLw7+G^qNPhysL_}% zO$cMH^sEAwtq}GRoI1{R#*z|?qz+ZlWoP(cscL1ebf-&2asp0w_jycPMaq^nm_VK8 z=AgR!m?OUX)DaGNI+WBgr@IH`XJ#s8;?i%hJx$NEIu&LMjjRE53YB>YV zhKT@VK%2jcE?aYq;KBo3Ek;UIRVZKHUdK$Yqx-y{c6Q-0R0SBKr>%HuG5VswYf;B@ zopmYoG~Foa2Uzex;36zH{gH7$U??>=zm2eZWI&t6!fhTOhG*R{VzT?~ugn_HFz+9r# zTFh)Gy9S@8H)8G?&ru-}9BS|inI!%rQ0cBtaPJ^G}$%F1t)#!@1ZNMec zgR~!3@R2NoMAo0^HZMZWxS$o2_0hb6U3er<#>602zcix7Zt45i-e_%Aw_9t=olFEe zF|EvAPCTDj3VFf2W44Q+WY=?idVZYdV?t!-l!{9LKH(1-!GYtzm^c(_Y1cD=W8x;b z$=4tBfBdN5f56APkLfabh?;P@*eR#MPX6#_|MY7A^cspNfoZ;~WMvav82asNu?vv<1Ct? zIvzkQq<5f-i>Dh!4&5Os|6S*&=oQzdawqBA}eXU3>`{wRWu*6 zj-pcO>cLAz&Nf^rH^;9jubNEJ5qDEn6hkDuCx>zhrxK#3*$V{+UEmxht_26XV})~L zD)0`&Qcz_D^$$!!G9rm14E>OIC>~v%(p3Z$am_}WsrXLuPvK1Gh4A*4Aw>{w7-pyT zp#h@>wO86zEaz~mml5X?a+3LlUu z;sO^^KouXkFJ(hf%pxxdt+x&@Z{%?~T@gBI2XQC9bNryihe~ZmsIA{OCwaLc??gzO zpe5gz3YX^y|2cb@<)c#|RFkf+Mt+3U>m`4c=>GM}zvXsXW`@c6#T*raJ@}v*#eNK!jC~W6dL=Zi& z+hj#9y=w{Q>qTs&(vVq!FWEG`;`prs;*9gEYNpc(TyTi+fEkWMIN>+0uk7~YINx>{ zswRmV1&@^J^qkp1kI|yAJ`E%$WF~@ zyN*>mT%n|#_CumnPHVX_OD!nglz>{ys$O^lUbYCQsOpgLp$b&Abx|y$ zS|MKFc}Hn&eM~iXC&mVH$K991Terk+Gr-{RH1f8Acr;;A6%b7=7wm5OdNQb!eRQvq z^mG6VlE_Rna{;utxBR{%A5g=5N&W>!Q3a#ytPwB(JC&>Pk%vEeoh~GO({M#Fo0gDE zauBe;)VoPU(s&sy`3607nucW2$;D!#FFQ&IP|EutM@`%s zGCk=Er7Yn4@*rgIF0mjuIp>Jfp7`Qp4lh#Q5v?Y113WNsr;AD|PM9c}y9dxZv?3Q& zdsSg#%V>}|hNepGoTbFd$qsRMhaY;xV;Z7y;yQFLUoIFjgyw+JR8%pHHpYzpdlU^8 z0IV<9q2Zr2r|#?i0&(Ko&7tPL%%%<%J?63OoZ|@l#1*D~z-=5Xc6^BDCX(t+ zk7YKFdtf-qI`-3qgDNW32@zsY>`|OcEE;7+8_FR$D)xje7BK*|5mTwQGh}=tSvyTS z?&k8Ge;eC(vDBM*?mGEmc310LwN00^m!$1v(A6kxA=!Gb`HRhV5s>a3iT_MBOS1b} z^F*ntQ$ih@y2ChOUUK8YJS=Pv0l0bBnYF?zs+prGm?THS(X^TCUh>>sDDtj;$bE?o z=(lgD?jxtvM?qmkvvfSUrJh}zFV)AaqmUIZU6v) z07*naR7qCPqp7UjDL}?HXO@ccxDh3u$3-1wZOY#*9@Q3+^_C?8m|KMxrGZQ>%aUU1 zNW%fQY_-TWGjC%L`n?gwNM#mhU9iSGHwT)nh{+b_kn<{53bK^mS9FGIkP&PCi0Q=c-aY7U0r`t!LZp$#2RUJH{Df7H8K#r z0Ymz`i$is?o)-~&#<(0dlpx*0N#qI-#)a&rTC3phWbt}>KviIQq*?GIMLeN?h{vo8 zHlda}3CGd#dMMh8#@W8^RtedQHi&Fw5_7vNsJfn&fLUQlc04p*(CtCT<6A19M>1 zaBU_GgaX4LgaUK^?OlF<)9*jvH!pa(+aW>`LYB#^%cqln{o;T5!vFmNJW6|Er9SCY zxguEK(^0Kp!-YF1ME+KB8#V=U%Fkv$zxn_6aJ-^I(R1NVWm^^tezDT3S&%cg-&vu8 z2^{!0hks}OKR+GMzU81g1~ z4LquXFT_VcGUW_^EZBKh0DTHXA>DepMg7%SHMSR_!nzN!EyiSzffvb6uac|ixJX5t z5qWn3Qb1(WQ9&yb0vHfVhQ*K-{^0mNBV4!u7iN2dgmy@#!YNJ}LmmTP2EIWC2$6;K zLKJccMI#U?>#hhtqKXSa9>fRXZ;3bHC9hbj3=R7>9vaVu+I)f0jAPN3EZ}TLD(o@i z=^G((4xFiS6idBj7TB_lNQI#9MK>A}Ga@FOZB*6OKTX{ZoN|%fw8~SbpVUqOCM z`azPlDmY;=8(N>hp}B7_!&7C9_|PrtR&X%Or}A#aP2^OiBI)V@OvBk-CW^s5BzjQ_ zV0t-*V`(l~UMI*d0J`_ZEQ;kN!$pEsKcIJ*GnA?hOSDauL8JyT?Jhb{%0m%wQVe-I z@^I16pj{Fk-TVeYGq`ew(JukP$Pz8)Jt;gus?nGG2g6S;F1=~iqLjb6!2PUKP@M_( ze%loex~HUmri}5v5FSt5OWr>?&P260D6UH@oOc_HUoSy`r zgmB9e8V(PoXS5s{7P68VQn^hIl~TIL!>yhq?RvWx*y_7#&HcO5t81zYJB8+oP<3*b zD;HX;i&$P>i0&bfVx)Sj>g}|2PFK407c?p_SGa*jR+wvzvAp=sQuW*sg1*{6P@-xn zpM$96chOa1cX#*`E2Q&hdNQwMs%~bBob<2P;u*KYhf&<8n?ihGB+o%{W8naUMqhLi z3+_ZjC7T)3{tXswtbtFp^?2R5K+IcwzzohQ5sZY}7{Z1so0_9VwPtry4z*jS^;Tmb zmo94d4>UfGX2Nn4Mm|<%`gCW*NHt5#`;jqJ8-~s=QXd-}Pqz=CD4@8IF$cE~UY=XB zbKCR1u8pm!kVwr|<8h5Nb&F>q_!o_fZedN`C@;dP$!k{gvFthYuL&4cO?j;#Wf^`c zSAuRt$YMMLZz?JMT2w`=YQAK9y)eR3HrN?~9BD?E6<1!Fh?_wQ--M(alJo^+=u~n! z>;v=#2kWJ_-?Sc3;)A#_BB}Z+_Zd#Ex{ z*}^%jqE-?ji*H!Ks1#(=iwfP5(?kOLbN4VstU;2=D205YgHubwm^M;D=Y(U;CctDX z9*|$BQiW;_EXcH>;a>P8iMihFYUI=^Q0rvvUnthI2|y7|)$*u-`RJ5jAk7ReDk|8f zqlkCzmtm|I5<`G{B$wl2&u9Rm>dR`EL?D5YycKt~VpB4xe)G`i&h5R~C)!()GiZfb z5p1>EToJhrbpu+RUSrJ7%gYmyrAN00Q3-^*T1}#YfKy$u^_RLulMtzS*S!#uy)Ql9 z@)j|r*q7$`gmwjDW-6jGdTU#5Lx$5AE#{l12X}q_M3{x}B2LgaevsK=|ZaNSCktvE>#+ z&dFmOr=3QhbCB*Api)I#0q5xpljjzOT98QQ<7~BMMDP37aryv0ED<* zRgT6|Y@v87IhXGk)z*QOS)Vq}zq=WDnHRA^NFmSW*}(0LAevH}6m6DaOPX57J@2Y5 z#?6ZZ64EuiE*p?fzMC6fk471X4)s`!vR%V#f4P+Ct*PBSbT9abQChPrmR7w8l!;*G z#>mc$y5@LtUG{W8&Q|-xO>^oPbz!x!mxzaCL^2`@Y>`R^>F`Lg42IO=u5u}MhW}E& zT`n)F+wHzQ56$W22lpxH7o}V-h1u6oZhN&eBdhJA;bXD(vVWB{TjOP0Ds-xQX{#q|nTV^{@uBst9a*;}@3o6dV(3`4on$&xUJbWX zBdfxQSawo$#LZM41vZtzpuj_{&gq4k=Nv*V?(@KwI`wlY z)+EukF#dYNf~%P2cqZ^OX~x3kfQd065#tm+k`sEu2n3teCVLJ%unr6dC**`2>58TW ziqW#MbhM>jkwz|1&#rvd@!L`1Jz1>OAy(4OKZcIZef*A@!I{}n&CQ5k)`$XU(J@G) zi~(6;JTrMR%;W}|Kg;Wh03pvrfsT*Lo>Q-+r2!41s1Sc|+@YUQ*(Se^ zUJ&!byVIX=6+?DFk*YPaeEDOr6!M;&B7tIGX26Ii&N?i`fiiHG{HV|pHLchvTbVR`V)#Fx$7s{iAcv5;mMTobOX=DmQ5OQ&!BLv$k*vSdpwhSnZ z0UdtA6mrMmaEGGCWc*b$;odM0918}BXZBCTCfUWqEl>Xeg$@WX97AwZOivZs1Sar7 z{;wb9zj@(zcYOS?rpOoQ>*DLf|8nwA%l^xQcmfZ=LSz#$Q2@LeB7l z%}^*+j2J(0*_H2=zG?su=pDx$M=Rc#;aC8~(h_H}IL-reh6CP4i()WLg$~T1;u_Aa zytj}s4S{+Ba^G(9UP(`l{Xa?yefZw^MC&0&!@w1@dynYLoF$%Wt661|I@o}F23be zKk#w73jW0m58`wz&BK|r%CqOPKom!#Sptz?4f&|NDGu%K3_OglCqA9_d`h{Z)$)~{ z=)6;2;5S#QI9*)!0>zsCA$?|PR#;Q}T?`38-rZjW)BMjd&VAJN@6xJ@d&BAJ{7J!$ zNyPTei4hgUPpW$*o`^bDUBjq1rl+JVE?>ojOYou)z=S~#IvGw%R%%KGE!bwEw1Ktc z_CJLR%<|1Eg=6qk&6+}>-$c_QI2rp#wg;Q=E>IEGFyApNYZ7saLC6R2WYI7%7!k_0 zSSp6-0Ef7WD-AkAN|5ritn{*EKuNypKDOp&1!kuN1T%t{vc8Wi$pFD@$X>h!p_ zJLOyDPd!IaPz!04MLq}x!?@3#%VhwIBEqA3*0tN}@~-)@cype_0=@-)OjRhM1JiQp z_OgrIo?T29E<|`i?5L*4CK{*X-q+xu%)n$6Yw*?ZQRI$v9undi&B4wE&P7-F5cg>4 ziIW<%?8AcweWnypIou5jnFDl~kRsvEojbErQf(XCp~@sEGhCI2fKHV;^fuwn-#Grn z_tf~i4vz|H>*v>V>za)bH<}KU%`#?nmrUdvq2Pqek}+@qc?g?w%+&sviVliHiJQsq z6#vq_GR;h+L(7CXwoLZ1~oW!CxT)T)*nL_noA)na=m6lC$qYf!G%JLR9H_Iabno5WL4$`2NS&}sO+&_tIOVB zlEE#usgJ$rr&irgtR?+Q9ZBys_*cIYa+V0Ac$UM*WZ)sgg-?l}+D*Ke4+9tM&tc0PiZxD$1tjI- z7(9o$l4`jb2nBLRBc@=%C@?Z!v%v37&>qj0#hHK(`Em z>#=lYbjr#v%;+++VGOBMlmL~wubG#TTwl%|`95_!fb#S2{?u)ZmjBrRLk>;W5_v)X zFZU#0meA8hk#qUGkkFa=7>P+qgOt%`-Z4KYiO+^wb4zqOJp*I%`ih#0n2_=y%?neN zo=zyS*@oUkAQ;*a-J9po3ekyG*Uhyezysq-EF>1Ujy7=zAwv;J8*6(l*hhLWm0_Ci zsBbu_0tw)+V3cT~u3{knPtr_zW3N(;*@bbV?_X9Ef@t_2Lu=8<8v=qFAcfzt%55>Gy%2!;TLLlzpG9|gxFk_IUxIFNvFn zYLg-!t=eW5LbULpYjRGs(MX~iQ{<9EsjW{aJEFiydx^`^VlOlVnPHirF@q8#y9kM| z>K?}QWOBongLE!3pBxzqHj zF`;Krs(DyO+{K{h;1^9^r`sKX(UOK=Y#Rt^BSa(w8}*TP>G0_9-Z(r(WMu5$XneY9 zNH%Gatsy(YCI(>b_4yNf8?JL=1^$%{TzdQpL;ut!y;b-kD`S>E#|6TC^wH>`+0Pd_ z%5>Ej!ybbvoOm^ZgvCoHvHN54>DRJAhE3gCi)@(}Vq)fsnoVN5*Zg>~X5b(CFW%W1 zHXq&>uwndE=>+xQnIPM6fYW=o>XJrVEgrhA-b6*Fu%e5iO0V?4kn4IeQoYKkWSUe` zSY<+*;C_#+D|1yV7t@3kMjxT$(`>IT{6#LiW`1Hwbh`>f*(zp`xa1Z-OAQm-vlsP; zc;JN>c@lIoDk85do*1f{2Gip1xdo*OtN^)I?rt?=4)xA@$_}6+!#t09;YHdMRBxKO zkGglc(B};u!)#sK?i@_*YDb8^b6gBLRBGFkT)eb1B$c^;Uj?QB-4&&&*YBDvZH4!+ z6S`-UNOp*5ajo_CxxuoC)vM!2Y>?wJKz1A)c6kv8rQFOqJS^r+{;W`;%_QMcm^j<0 za2MXKy)21U$4C`wlVb1G{7Bz~>;kZOHlgjMnYau#^ywq1f_RIz7YoZZ5g^WZkqt8| ze)%jUsL2(S?q|Vo@R_~kUVqg+^_lRjTiViYX=%P^;7fw$sq6`bjY3Nxg)4>DfRt@- zJyd^XMj$lM z1xOLxm<$M^np}?5*I_Z5669HBkA)*#1PQDM%(8!WH{Q-lbu!q@_oO7nI1TQQRgcv( z*2@J$;H#8LaO3s@SB4#3K%|t ze?IZg&-34ZHoPvJgb}A=X&}=8;45`DIpkvJ2|Kl0D%H#o>M_xjuVr9NV>&@ClnqYg zUCkF9AzUk>nkB!(VJ8||M!h}5GQa7yqt(ejEz2CcK5gJ{R%@@oJ1Mx2tmpImSxZ1ga4t0{1;2#xyS9C#>M*bq#7rTk>;r-viC zE^IGQMarbSh=iC>nl#}5Y3Rp+WAM-{XPkT^-VC3Y{d5wB8W@H5rF*B0F!Xee#Wy2; ziILXd?858@^_aicTA%AA?*!y4fX1J^7~d!%wHd6+=V^5Hd}5W^>7kentXqoP>|*p9 z&rypn%vw3{S%SdhvQSL?b;!qf;7Y4qt=1b6cDA96#PVcDLvi<-#0DC0hOf~svPNf%EvaH$MmF1Z-g+3Z}Q`lop2jx(FBe(-b{G674VHZhEQR7E~z|;a(efh2(d5;Zpc7OJ5QeQsG^L3%Jn| zpPFC!9vMJ|+;D35y8tk(?SsKZ)!8JtlVT}QOo_IWSOvy3f|ehmp^|h!%(inEHn-GJ zSVBcoz0cW8nnrg>w3-+64v-PPM#*YO9h6aKZi1%kSfoWY9&fo1q?XURdE8gX%kSe$ zjR+AzkXS58k*ySBVl49hxgcd2abpVv_x&bCcDrsfQlAkIH^>26ag{+MFLQg zSWPlD{lji8M*(Cco%0xG6-t)RZucnXemX*^faVO6yb-urt6u7L>Foz#8xnf;CaKUU z0y7(PCg0aqhnKH>Y19|3&V%|O)GfN`ZVTp0rnrVK={jb%^8*(uqW6{x^vVx%G_{bo zHMFyp2~y~jWD_JRfJ$qAdw}8S?%|;x?JU)iT#O$&Oe6=Pw$jVqJ^f7C4i7-4JTCF~ z;!naRhaw`kF;82y(n^0sctJsCd6i(U|gF^6BQjA z#7L0TWIRJj(|!EpP&Il;ZGy2LW15+i&XZ-))ZfjyVcS3;jTXYcfc{mrH$FOD9poI- zJctbiZHxhdEfbZ9Zn+)v`8=EE^c9Se09fa#W7rBRE6{h$<8U`SQ%M%A^_at!F1$Pb;*p8iKF3^UF-qZn`pW4Lsc9kY%@rjn^YHW=Ilro|C+R*^iFDzVXi4kc z`yEBW%p$GjFH{_*-Hk1#)GMUZa;@!?r)1hwGxXJU9eBRhUqzIRO9Aq=kCb#IWE-lN z9U@79YEx)QQ))7{u*B0NH`*j~RF{A$H%cv453`NBv zn=eKtn^D3!GxVCw6;C8vB4YVv2^oVa50l%?ATa6v=BT>1WlO{^^=xZE-*G-tEK`LF zRVUC$Kh-jmYd$FFO;Sr~7dNvX0>w96ckXPL2rV{I61iO1$k1%2E28%y1eOYwXa= zkw-bwQG(4XB>@)N|Db}9#D+)YP_)~yko^!fo5f<<%~LBv`BdF4Nu`d*?CLD;ea8A# z+GN=q#=`AMsAit0XtmOHJHYiZO8NFm79Yi6vmSEY7T_X=$p`IWee0z?tQAIjDwf78 zJ9JPRPx}*Qqh)fxQDa4|qpeTsiU+Kz0?`pU{)f2;0w!aD&4m2t^?VLy3ntBALxcy~7d&2YJ1}k#Kvr!rqr5mqF%1S^_2xOYA)2VVUh<8c=_)pZ zow{-}UUro6o>7dmu8GpWHtF&f2-F~)MFJAK8mx>=*_M*+z^Xvbsq%<#S(bMqX}Qcpsdn)Od#{p z8*AW#hjgsXF?qduj(V0eV()>h|Ly|dC;l7x>6chAs=Nz;G1HephKkvz| z`U{B|NsncE(g=OFaE6a2!5)6`APhsS5CNTs&ZVp?f><7;?u(G!dV$nesL^BTw6W3E~ip`q8LX;H^sP0zd@u#&x5Y>B-N}KDIPdp za0>xhSx`1ZK%`lkbz;OQDH0Rp)8Zp_U`Waz7TJMC0J6k%pr=S4LmIsfovI5jWkyDQS^6*!r;K~ z5BYH8c?ao#`C|XmXUCgkHCiU==A)N(?;f$**qr2ww^K()F;e!m_2|>> zJ$J4213Vi`765M|zX}PWRK!F~>@={<2D3piLP3-oFaba*iYrY23MLDoQk1~vcsNhW zpM@jvM~Wvf&i3e!9v)%Ra*uSBH}?w$)oz5{O%5u7tk0VZLIyug4_ zWMvI3DJ)4uDJB9;2C>V(f&P;kRKA;{zz{irLHQjC@D2D$Ln%cwJgHwnZUes+{Hfz1 z>!5s+r$fjG&eB6-auYAiRW=3zFMBQc7k*= zcK!QwdO)GhQs>d~4erR^+yv4vO#qBxZUGj=IVIb-RT(Ca61=!V8RfpIoPZ0ah${~f zAJhRqD2oaX6WpjDD1VIxE{02I;ZJhpOpZNQgIO6EVFMtganI92?IzuERpx$P#msKG zsE4&VA-w0AHmFfcXf5?D=nwq^a-jn+Z(c_>0Ex5k{iwtH-~3W#)qmw_32-G`6ScX5 z^Nwo8ub#P1%3RrOWb2Y375{1Qh4sX_V9&4D`EfnQgDUP^?oTXLn9xRqF?n=78y&t+ zo1*PqMoR)Db>$v0V*s=yEP#f~{m=^#GrDGwN`2PsH`|hek^%%o)!Su>C>fuO|47&Y4nb& z6vq(p<6B=PJ@le3&lwsHLRaV`o8|NVC<njlLP&MT6LqeGWj|!GsuvK~?dRWrw93*yQz7$I(%>%VnV=QPjaY~2OU|>;MO_B#&lxm~|8FmHOs9vZj zvPqhq)RF+>X8U@mZ)7an(*l0!F1T9Uu4X?C0G5zQYN<{-gr3RWl&t&8uU}h=~R_|jtzQmQq|h74eBOaR$s9C>}8Db397Adv^HK*nR9jOX+koJ@Mk*Y$QJ^TIQ20`-`=;l1?PshyGPPy#cB+++TQ7msp)sa# z7Hyr(RPLpMF-C@n?zl^#Uye!nTmlAj1WzuiT8uFus&0lBeVIppN_%O^-3$+hj0dIZu+NkZ4+AGSX#9T!Vz5 z$RM3%_1h&yN`RRluxO}Tl-Zfyc@JI;QfC`#Lc(4vd)wXl}&_7JDR} z6VMMLsKEhUm#d1k+$#Ip3kV2oVqgfkN(faM04a1^ICUo$pni-etUh<6S6VtGrd^E= z=-6619htcm6`||73AK7FobBHbfDI-$!F2( zIsj$VmiNlxmUNWY`$&CUo5rw_8oRKSB_xWU)lT|h3VlF`mDlARH{@lcjZ28P4$K2n zvp;JnhT^99Ah^jt-01M|1b`lV~RNST>4n}=ZF7KZ-y_1H^)=4!s=}#Nl8Ff z&gGOzWpMz;l~EMd6D%SVh;2DYApH7{P3_2nGjF)Ck{+7Y9mN~~;;E$r zN+8unVc7OQ{h(1lFn}SL^1I33-SC^6e)}Tde&9D5n-ncaavHxp`SUCO^o4(Z^Z)z` zJS&YU=E4Y{z^tx!&jN#=+qFtwtubX!6Synx`nMnW@eUFB^V9zDa6Ew3yG+?gN>fl? zUE7jhwpZU6RH=_FZO7oRlvw_s9*&1sj#SB{klR<*4rt9v!ce~+avP$PQ-L8=3Sf?> z;d%PolbTUdHJJI1ryKyuB|`G)?#9y%OEM}($|SIC{m!y&*)dcqsf(iez?nOPoT;4tS;jxXujCog-Yigr|V2ZW?!6i$%`vB=Zm z^swis#IM$w<+l9-nIc0#VnxyuG02l5A};Cmi>-i=)UOq=1Om=My@`nO*A!QLr~`?R z!&O}{a1$mOY>JzIN4Y^%lN~;#^4NM`pn!0}BF$hfVwob{;bodd3r6u$GkB>*xF~R- zgod%UWM-G|n8w`nV^H~yD^thQaSRwRYUu#>(j7P60g8)%@=+6ze^eZxQf5l>7=Pm~ z<`R79AP?+QB)n_VJR7LWAWSJvC_UPTNxHiFGO2`yn6l21h?WkKna@g;s+dFX6Ham} z#eXaOlU-{bYyzhXf4QnWaS?oXZPRpxYzd5;hFHu z#3UwCF;?Xn(El0K6bEDi$Eo+{e0e(_V;%my-N!@qfI4{6ciVT4)`qRGsWFAf$Io2O z$-a-iE~0>@{X)4vu@L$^D_*d|ev_=3FF0MSYw-u51Zq`{;Ry|~exl@-fNC@?*2##c zmZ2Yok{-2YoQ4sNR}dxiJMc4)x9DDC7!Sl^TP;D}vRZ*?+9~IB(5%VR>bAQ{%ayvx z<9JD)Og9)`*u{kpLe)I`=+)C4mtV3S)uJADy#LiS(&bqebY2%3AE%8B*LC&<)Wz)% zkC=nMfEoHfqx`T^_j>VCPpn7@jU1`4?|b4rlc_q-`+DAvrdik_CrnMf|o8Mh=tDKw=Cy`t^x0Hh_!#mBGaey+!tFs*tf zWzV(SOtWcQ`sbU;bMlh%&LEmJ=1(UjPIz&-_1d8BYv;0UUv zw^sZ8Mc=J$F>Z6NWHOwz@WspAtjDD8y`#TKJ%>bn82*0!VWL7KrSyCon~Oe|HoDuG zUQ=nwhBg-S~*L1n2rCzTYyD;MhnQL>_>6EY&}H7=&f5y^lX#jXwuPBcu^p4 zPQ3)OF0v%ZhC}PFI(u8nuM>Ak+l&fD=>@#aY{$PM@0)_kIg+>{_x+Th^rSTqQr|AV z(TY&0WE}H`MAW54sgSrRy!f{8sZvqV5#ul@5GSXo1XGd=BvsUyT|24NaSw*Z)*^7b z-PY;t7O6wi$1iuhq6^nom? zG6F9{|LW=Xu8~{q25f8Y<}lV?4K6J`Nu^gC3N0s2tHG+bqm&rMC$pA!FXk-BV-IPU zr+@WshG?!7uw`L@S$$u1EXmsY+V_)RPJ4##tprV!v`DwM71lioW;%irS$kP7o^B4o zP_oi&fsMMf{bK&5(8MI{DX+Ak(Mk?#22;sYQ-d;A08ILx(ePj=5x8ddPl|Mw$v_y;-kD(SIfQCqoRSxAq zZxSquylOQ9N|wRi;Jec2{h%z4c!{>-a2{9!P)Q@mYSKk?9MzaY7UE>= z{&5YSmVkTD?Xs>8^B_5AiH)L1LyER3?mAZ`ukRQfTXWa4%TN!(vKZ?!87OPWh`ENU zC~~i=l&0Kfs3k8Hz|BYwis#g2pE`7m_&zmIt5_FarH-f~DfXDl#LfaqCBD1wyr|t( znWfcrf$aYk5e$KO`)IZzl^(^1kb)tW9`~shvO@N6Wq?)~1RK#f;eZ?v05jDpBI!xR z6CO@<1B!~_dBIf3E$JxE>}5t5c?Wcqx?-hy5fz$`vfmR07zJbpB5&}CJKz&oSehXRVQ!YQXTd_y z!@>)s`Oc{23MIMt$YWV{tL|bC1AsBcLK8Ytd!CY5d z*2Nc)y|RLah(rPmlY9{z%$@#HYv%Wgd?`4@mB0^qwenZt*PcyX85t))sQM6geZGk0 zcoNIvdWGZxoFt4`Wv*B^{R8zD%QpeS;bPehYUBbY0F9mq-yYXUzmqfymvqWe;Q~1L zAVfF-LMCvdKV3%9cdW27UCcN-J| z3~&mj;3*KHQU)=STGb+<9g{F%UZCo|0cz>zYGBC3B$fqxebE(RPs4Zz$7hj8Ld+67 z^`C*?QJgf=97{s&B<3_%FR?zmoUPvAC{u@W5L09+R7KP^1*_@`b6FBir_j6`I2=Aj zC5UmAS-jrBBxK6+MwdX5Kci`ECm^-+im~BlmJ614h;CNknd;HFopx8SB)S^116E5o zO31QLbdpqkP+$@I+g*vKfQmUyfJ3o-oS>FL5kiV6qH@yl@B`r9xWyp`)ht3J^isVE zgo|>5bXnT*N-wc0Y7{Yf?U>vQ&-H(Ewcn?uf*?=@)jN;tfxQxU&K;7u;Yc*7KWn9&5qRVGf?+ zEz%5~dPa${vLP`Y!$W+5KF7*8t$WjDmJb ztSzL2%~a*$e^>-;j*A5G;zY9(!a9^@9}iG*xawjtXDIWprV~aM}vHqmv}r@NtU#^nXv7F>PU;)BoSB(SKQJT zhoN1LdR58%^6Sj%Et{!|C_r>abLAb*egmk6M5#KhFQvB>Z5WsOz7JyaOgSk)a#QAU zYL7qFu@g^{DP&=2a#o9M!-a((wHFIUJKx|Y*i1`)V%mr)*Q?+$&rT9nBxHW zSWnLy5h}v~NkF#0+=jq>iO8YHY3I~I!09T(t7+IWh9ik0PxrP&AcKRciJ4W+?S6B2 zcc)Q@7OXFX)9CIxbRN@IhOJb73fxu;2}Hzf&Eu#`(sru@14u8!WRbPj90OzU`pdbV zr-+!Z-W+MJa}Y&N6luQPRP;y{5seg;F~)hGu~5wsu`*c`4wYplDjMG~PLZU!<+H0r zTcdRjxgI?2Jaond&N1kI=@uI2y}C)c*P(- zN=U@$bcjb;!0n|UqSmS1^-y{vDR#WZgY*WMDUTOgeb4Zyy~m(?9qOZ07;ujj{^}4L z_kr%3I%)4h;&L9yBSwCxG{4TOSd_IaXHS2me@PNiq2S#>eE|=_NU5lKGzdynWdtO3 zb+!1^q!v-7g?3$l^h~Y3lf1&-x8r*1GWDX>+WQ}?tS^#N6k}xCVeF1(nGv-mV8t{<-w)ocRKc&5 zF94a92SkUOTh59Mdvp?`7GP!TmFtB7V+;T*Oy26k zVh3p#=Fo3y?fvBT-%4z;T2~P2v{?NEqGTpRrUbiSwfH7FbWg%70(7yWmg-NG8WVfi zC5al%&su{`JVlvBVT)|Sw5s-UsLVkgRJ@GIiinH?ytL7Ajam^;uA+bc{ObR>eQrn) zf+3^Uos~RLb|Wzjj zm?>|{xngA{7q|xVoE(un7MdicGA4CV=XQ3AAc|z3y6u!D%1~%`@j?+Mq?z7edm#A{w1Eh$|MKR zO#I4=9NA0G)@*6|%Sok1B|8!dq#h?R{Z=mFZ!#Vold+ z1|g)UO7j+BapOC!N(TmEl`@sAp5*7ISWS|+8WXeR5$>gVvIM@HA&WbsSO3Fe$jF_K z9TJ#PPe(@S()NyeF*8IWfK9-p+oTsXQzmA+q_xD1g0`I1oUZlzQAS`xFbAe!^8YgV z&5#d=-G72s6W~t(7P;H4L0Os zOk%(teqzPq)rnA%@^Zs%Kwog5iTU=pO&QjEjg%LWpfw1?vvG8?R9#ZvXVeu&yZ5M^ z4gm(C`nv<)-Q-ty{ng#S9z4_{TSJH^@x}4_kRKlW;l#gx67kQ8atFN-&pVC|o9Wz~ zbWO=1)rR5_9EuP4`w#x>DNg@k@gLtD56PUHM#{3wM3Z>7CT>gX$?7a+F{wH*ft%vs zSL&bN91ndb0Tf9;MmFOvsj7@0$ygB?B117|Ms$hDaGZ;8Pyh1dr>7+SlC_mr1R6q4 zswzLc<#C ze25E^kt#-n0}z;DDGtJ_2)X`ZiO(o+jimb>_XYAn0sJJDnBYm?C`Ot~XhhL>GC82A zNHl;ug&azAsK^jkV1Uo6S|MuU4sn5ii?QqSwp<`Oh2k_(5eg2$sn8)GC!RI6T=_8h z)EgPEYj1IHT}X2Si!}`5;o>iP0EG1*U7Msa>V676LCw>jo!RV2b82$UboNXagSxepSH##B4UaA zBV>azaEQo)3pGO~gm8w9ugNmw&%m=6$#lC78Lq=6yI~5q%s(uQJd3efI=A#6y{BCG zp)B^QC;~1dlWY|fu(kD3SMTtxD$ciVwQEATd|V^L{N?z>`+0i(!*`aI7iy|6^_TUt zLe`~GCMr)WRrCp4@Fy%fZ^FCp#Cc+!u=9Sdu})b}ECWx{hdkV^@SQplOW`1_&8)Jc z3?Nm++$b<#8A2SDc^v)H`o>9D7u881)1hQwH!LQRm-ke)I-@h*z`c7q_soV%{Wv|lIo7^rM`Rd7CJ6rC+~wU!qhIw#lp~g5la}_ z9c9gzo;1}no${1UL+KlmH?Ig8XTTR3Yky&%`>y9w$wF7O)}EP%u+!WS1Io1Izt^rH>cBKB#(;MdOn`2A(+d(kK{a#^*l*ZNHTOz85SdS zw{grNx@-|`pqfEJPWrN(Ru$k99Tl=9TjW(@xb{k{b8L#yeaS+G@(3Iz24IZwcs@sn zBeNifW$W1H3oSy<5V#>O~YNOq9O;7`IYl@8(e?mz5&3i|bmupo(Y7@qnW85Ls5T$u*7rKBFi%cc?Ah zh^^KM2*u&GzFtND=ru5T#J)qlA4|@~-KkS-DR6nC#&~0$>2?H>d{Q+NwbFKy#AP=d z*^K^$cd?LFXxD<(>SSD9i4ogsobi%NNh&Oagzq-3*Z?R)#45dMxlf7XW-?ZhGCJp2 z%f<*_S4loeve!vO%ZMO`wRmw$(H{gdXOzU@D;IVx)^Rrsrv5IU!lv(H>vk7pFT>Ub zY8Qa%p{(T>m!l!wr#I6n$w?x(lL|VjcD*=Z?nfK&Nj7A4Kg%4$%^@A))%F5aObftV zzam^Q2(4rNfSFwY#|Qv00P+QW?|)u3bM%(pYCVyBp0gF)-@; z{-@jLE{lqj%f&x;N*1q+iXQFyDPyaI$D#Np-$ z>cH2B{OX2}2Zt_6R0Zxz=i7;&Pyg$K|I0&ghWw;2r!TUY43rO~lv!L_#uXhE$@4>T z0Egfv|Lz6f-1#!`Y2op(w>QJ78?&RqiW6nIl~sFTO)lS&PMPzen2H0q0S7)a)LlT> zpt@GJkf5v59mCeMEdTcgOqDsPC7k%;cw2ZZ{_~O_@_h=OFK`7Sp%NS_=M*(Og>TMh zgP!2lAQGF10&=Aa5Y{Dg+B8m;;)0(75{;JH(d1tksTDe=z);)>0Zt}qdSaWeX6@tk zbZVG+Ed|p371zB6`5>Gyfy+8gpi98%&8HCQsCeh7b70PzXaCm{M-7>?q01$ zF(H8Ky?n-FN{dtsVRb9#AFr^jb8x`PMYxlW!?+;l;3D=2w98_p@>%5}pkPL3b>P{g z5rfh30RUysNGUM5$aDeI-97C_C?l6a|4IBSLMUGaAFXuq4*VpLzztQ}enDA`lou|i zfa5dd27nSQ`68#K1`^eLp7UG;gQ3fXa)?l!hSw@ha#YO7D^e`NCp0`QE&08$qs(P( z;W=?QER0442TH7bX+#LeKlPLh@`-8)eF6h=q|8&Ez$DBGGc%>bTn+_8#UmA$5~Ne8 z%T*?^G)i-flovAd`;dI4lfyyabSh+V%y1?Mx=lm`V^H_u5Oq|{CXaW{xc_4#quy4*91`> zDpdLupFA8~R0m(&jd-2-(F98o{I*Vn!)#VZEKlY#!E*%!gK%VKSX3`w6=1{H@%!#M zct=)sI9an7O-;$Wk(OX{FNaN%6+Pycao9i8wRfyMu#NOWchQS_P)%RSKF8OuE+MMg z?Ajx!DX+*VFPmjq@1msaiFIPFZ%+H*Pd!hyHS7eRG#5OLmN*nqeX-uV08%d!5OD_g zkjB(fMJ?bLOs9~KG#X)^E;Y`!QqCns)+WB1rjdJxOVpLz1qSN$+fOL~vJG#RSc5^3L)J$Ul4E(mc+{t&Oe)~ zL4Fp@D!wH;ubaz~nxVmYSoG^Q2bRMsptyj-3%8Q=&ao?UP9VOBc8_coj)AZu+Ih=t zP^#;^5@_~A%;%Bp{_;8%)qPh4z>P--w4RaIp}_I+b7FAEWyYuxsd{=}o{ z553;x;tUcmj#P;7px<6n^}_J((Ti12T%Tv}badPbonuI+xd)Cpl9DWm9drEEdsH+- zJzS>HEeN2LJorvm+4K;S?U1(8>D9x-45`CAN-NgmI?qbA=8qI?<;+PktkYsd=m;n_ z`&dI})Ch7Yii$j~^(05+rlWH9BM2eXZWK#_?r=o%GF4Ptn*fo4>N;kHonF6M@{~X* zsLYl?#vF7HlA9hDh}i+3`*`4Q!5xkfzt1c2oL{|aH$};$!0W4Cr(rybqKG$889!D) zPWP51_t?93bY$rWFw+Yv-Z5jmXhdpW)G^h&moC6DBImEjWzFsNfrKZSS|dDt?V8*| zHdBkd3(hHbIOf(9c||E*B4dzacJ)Qi-+Y}sXjCqXxxbGTE6h>9;8#~?6{L5qYMBXGYR&$UAQvCeZG0~hf+ zR1xcE4k}wxq(>Ay%e1Vml0wwtl!tI}OzmMdM58YY#GDgS!mk)*BPAKJfXPT1jr)1K z-Ojb5iJzd#n1T@AI1pr#s7@WoentW~^f0=ufs7Sm-wlh*Z88IlC~UxOVVC7WJXf5vfk2>>c~71rPwggih9x% zv`8gCcIr6IRK(mz=UM?Oy39m(@=Y!43??>ZxvD{q#Nr#RC1Bd$?cPa^oh(&HJ&|}R zSt|Ytu`a8`ijvW(u|$V_yw6{CbJ0Gk-PcqVXFr>emxXfH$C#U_jIE)nzAC5od84+Z zw97;obJL56x|Efu@G!B)g5#Q{Y=|=@GR8VXSF2gv#A4A2vDwU0;=Vi=_ih*>{ItR( z1LcY1b7aGo@DS}@NKb)nN8yxDWjoBe$H42Fz5E*i!rkJK;GT5U){tK2MV}2W#Ui6O zN{Tm5nzl5dD&pCjjtNmxi9o%Bl8FE(PpkT5N5&SE#9a#9S#C)o)C2|ua>9z;L3!@+ zfC^}^0af6@P;ezz7OIOTA;R=55W#4Z$AX`*Tar})j$=SCC%CRTf7^h!D2ZFc5llYJOX!n6 zek>QY(#OaJ(N3yHG8QXDLNPcSn0ptTgL=LRnROMK&mvbIOM;WA*5)Q-g7Xke!6BK* zG5z<-L-CbDhTp|*sv0a$OCbR!F7w*4%sZ4s);EY5Z;Wbs~}K)v?^ z4Rw*HYhs+@0>4*a$iF%8?I9oTe7V^S-!+3`Q1l7?eB$%zKR*3W&oDDAQ#O1+(NTqzS3b?Sw(qzSM!~xv#+Z(?+-~+c${-;;qA;~iYb>!QUzmAuz5r~>!G1rQk zLQ!G|a5%*Aw7PQHc|waYCdn7G@>&FyZ}3-;7r{*v*QSV(PvhI-=O=zJ?DU-t9HT?d zOf~ut40v_C5kFoKlKd8uJt}*`cBmDvvjq>q4LBjER0@F#j>0Q}nli}keCUR@13)|- z*1nf0N5on{_C!W16ge+8Wd*#(g;^+}_=$Le=6I6R;l|UY7&?Y(>f!i-@+Bx|C&=YE zHDM11!w?H+YVRCS7nsllh8}paz=%l+sRRq6MUI9-^#qL|J#Z=qzFe70R%nyN0Fa*t zzpZw3HyBCBbO6sx*Q3pp%!QP41bV9Oo%&FGH1<%r^p#; zNzf%?u{?SW545O}e{~F@P+o{9aX1!b#=$x1C{PFoML56-EV@=Z^qv=?qt^6wY`}-L zmgbmrSpBU#PQej#ZNRw;5`@<)&?HFS;gFsrk@k_koDc92IeyO3Ni8cT2b zhsR16$SG7PB`J*DK=rsQB9t)OT0}IVDd9paxe3KU$do)>#caTl!^T8znGPer0sj)! zg49sY<>jKD1ou(1Qb~6H5Gwy^*6JNNe$U66qh0I_81sHR9&S^|a2t@5mJ>hhK|~|ux*^>no-XI9`XiDfnzlD$ z<(1c13sd5&*$NsIl!`j78rZLC907{enpda!IKeTT%n@zZ$;Xb;Y}HvPy@kUEf;rGd zL>x}~P@$6Ggs7`250$y-N;e);!p%UI>vSHFKgonFtbt+KMUe@tWfH5S%3y3l=yK-W z?v8!DYMQnidb&~X-Oto+Azk_W|H=B-Cd-oKNDNdn_j4nw`h{))1J01crR?&{itPXY zAuB?PaLMHkIm8SGKx2UJuF8yi+)ej`nVO$$6iOOdkr6k}!yo3VrmFo@p{=B~v$^W- zQ=zGY_rXX#{;{)pozLX+0(8b{w4r}j{jK9{rO)zX%eXJ33|-)@OsS}Rsr`xd_Iv%s zy03f9{T}NcYZ*Ud*Jha%h3L1!f{WXyIZ!SsY!0BWHUo*z-I`YvHiRN{_hYY7Q4Tq0 z;N)4x=<;dYK08hQ-Uro00rn5wQq?B{D?)Jq`RNq_lDBz`QUu+|ZeQp6N4<{f zJILvg)fDG~G5yvn(;)@BDPe+3G>VEIR7aQg%2 zEg*~$*cFy&1!-OEe(`!eWWJF{7^v}{0I3}a(c*uwkpas493d0zk`ODR$3wCiafg`bDIMMtn?_p!&eW zs6f=qL4%8dRd0c6-HuB{_a#(iTPvG@`W(vg;BCbPRpc4~%rPT&DjFW^z#%?rZ|z_i zNdU#w)`phM$de&g5ner7%y*i4FHv_!OF4>?`;sV6>Z^+NZsR~Q$H|BXeYmz|1yom7 zoIt1aIPK;Y;3)y`QXJJqL~;GKB3V)iU8m%(o#;h8cDIP^K{prz^F3Xiv`+cdb%2(0LPxHG*sG#Uas7kE$-L0zwjVMWLsU8+QD7PvC zdW%FI!aHFMUG-Z3=|MG-Obw}Cs)hxgykj}f37#ttKMU7lD3=ec z5h*4%Ijt1EuIkS8oaE35q53uI7|-Fg0YEWY*~XP^acUduWlN{~J|-76Y9rOM;bZE3 zNOT0T!6ppAr92*%a3sYsVZZ_>tlj@y{f|+R1Nq2?Jy?y#J$07$giZojAWAC} zvi0LuMiF&ZZzMWXqpTR9{E%vTkHV6g=@ijIn=xMn$DffeA)8~e%#k>Ya3elRgWg=UMO zaxErB+NC(=MgZ%szalCFsiT$9IDR0AmC~Q@qpc6b?+@=sbtX0R&kJ*6w7xA}{&RXX z=D3fvtHuTZhg~J;ad9Czm<#BCfaZJ<^8ljdR(VIs{GoiG##k{@W8ei`j+f(u|Kr$yWgOtQ!QntS&B5{B_*3lv?Hza%H*p8< zjrYdV1c?V>{0Pj95DQ$WIGl_a3Nbf9tiY|ARmeL_D&ck#{>*;o z_;7m-OfN@*MRMWu=GPP+Z96DDd%MoJFh8n$7eTDeZ}|WVXKc{$&qNh03bWBep6O4} zB0h*a=jH%LTiY>@_hlp5vKS*`fdt;TC+w;uFnf~g2eqkSp`5&oqk?4vpn76kUyrJk zQ;$p*C!SiC&%n>YpLdT&BJd~hcK|H&?&!K#9nuT-+lH^scY?E#qt}AiW*JXQLs+Vs zkVY)GGk`Y&lEZxk|M(p^+moW+@~7L|EC~f8Z_>jMG;)}aO%GEXkCDuvxb8B}BZdb+ z8{h}*TQ}#C2&~}GgfrZXZhs@oS%~FuFX^kZI%@y zolYIbE#S?-H}OIO3E69x(lu_v!>mG}YX4>?W0{R%lk@;~c@>Vp2n^s&d?k#Sw1JS4 zgtlXwyWvZ4>|ELn#&GzAxnIuLZ8StVt@8Kj`_a-f6QTPUnBO`3ojRwN7d@+NZ1MK6 zXg1ei2=IRg{@cJ816OSLH{&w=1@Ja|J3c9%54{Cz&{8Fw4cLY8`qTLM#r)Xw+Wvxn znoo}jzsWq67@Qo(Ps(?6ZPE$hCvwa@TjwEcYT$6-}Y7s6Z)U#w!Yiuog*xkNZ_rzlk zW$jAWz^J<=%3SFqJ&e72aHAKb9POj>RG{u;iC9!s7u_jXDW6^qh5*&0DJ-QMks{Xi=GcC?+{SIs53PL5bX9gKpXAW)KM)9X%yb7f%*}`KXg> z-K`cVX`Wc%sJohcg?zpIp94TUNK+O zhsWCP9(!lAPPG>#n^>g_^3OzCUxuksXsw7H^EG3~x^Ek9DCJx+SY~T2A7hwv`1|v1 zBLUjNQL{>&KJK-a!D&9mitRouvsiqr71<6s##r~VVe(@%0N3@(V5)4{U)YTX+Lw=1*d!+Gm6qbuMq7)m2sc}?r;i-8y-jx!GTe5sl91x;K~;_CSk3*kguhxM z#Os*YsltS{QpKaq#N9OS9^5DQU)!9wIa;21`wRC zF>Ixg$mWFe;qo+xi=}R{M0vxa2;*8#D}KnzD!|SLf%CDQPdut#B2*@srZXB&oFZKDQ5-VVwuLl&1}zIj-Ab1TGt=BhT* znzqj;xie`SF(LTSQvTRl&(`ANmP#gE^-8tSWp71yn$&uInm*FEK>}^qV?-i7sZD`+ z*`kqDr_s9Wq$2h7vDNh;!a#h(F)GY8*@t!ZpH=Hn->dD^x~E2`+*+-hIO;8ypCi+6 zTIHL17EX0gSy9#)g${#dpIv^|&5+bQ85O#5PRseu#k;6gM}3l(>gn>zE5Jc2B|Z+M z-^YDL^;?>(2U@3}7vs|8hExJnGn`0EJb(xFJw-&s7^8XZQ(8sPqUr|IS=3sH?M#KM z+;j4IRW+G?-)u6qWz0P*vNs&{!0RPS+G(dIR;x)J(Q_n|2Ne|zRXt+U2lk2x|Jh#{ zUqF=TBo$F9jRJrRz64lzywJqBFsPGiey0ra(4@<%Y@z%f18@V7bJxU3pnstpS8Gm* zntH)?s;o4o7bYYsy0NV!HRTJJh0O-u*qy+{M=8}~&xNj{&}0Jm#LAGWAJuJbz=e@q zl)k{{eC%_w!#B2C>#Ng0KXzfWvPeqQiNod8X*?e(vGranUn+;uMl|ZW)vG>>+cT(3 zG_UW*MqS>{_G%YQZ-|L8VN36^bytrjS*jcT1i9Vyf(oRzBfv`3hd}h>(Fmc#7Fj$WsK>t3sVDr6Ij?1Ege7~H9ad6 zF2D;@D&BgS*>@Z`NX;lR5BV$d$BCyGrMzlIkv^nFUEjCr#egyXn-_omfnUDj%M1rV z513*84FBBtmrwrw>;C&s{%`N}zT;uw+%YxRO}T5raKuHKm^}5HCy_NBFT*r^;I}W@ z96ur6@B4rGy6`iwtcWPIIhP+9Q-`}Gby{qFzd?f&4ot(9dX9~qF*%xdNsyNV;bRac z0p?;5k)z#S!+*GVW!zqt$v1ww`MKiTO)OwhIwXJ0ta7#f42+Gn%hc|l(%3EpAOa5% z&;&sH(c!_KVJAP3=DffU;!hwr4ols{EtDL#dIDQ9ZDBAmot&^O(WD#~LBs%djgtxZ zh8t{xfj!_rq?46ZzTh^{ZpLTC4;w~o+Lrt_jGx>7ME>)FGyw(LzJW!+Y#QBSSfjkK zFeW<*EZ{5b_X)WQ4Oj-6t$|^<4Flof<;!R_-iM;2z7Y%Y$0Y5NX>OxJmc8U3-F{)U z`%e-KZo6r}puLi>2|5cC%^`16N-2y4Rbd7k967p3L|_qtpBTR}I>uL5H!tv0&Q!OF z*!26X5`64Ogqj;};11gxdlP|_8Ew+SkWVlvfiU;zvA_sFb>q zTOtyYUM{q~Qy%g7UyXc19Bod__&N9re92PFusy|dwgWfqGP_A*?oyG89JdNBG-mIC z+VveUOvggTj&Tr+7Bs`Y!xn9(V2m~OhI6b+Xo7hRrc%v~FuM%A^V}97eni%T z_dhTOV*aDQ{>{AZart=Jc#ZL9q=iieX4nx)P~}iVfCueKC?{*?K643MY#IEc$&CG@ zIOilG@;WXwG2XHZ$|R z)(j5V+(68CDnke?GlYRdEK9Dp6Juec{G7`I8ZvWk;wN9FSfVC-nnXP9mXApn?aACE z);Zg0dlGkLJ^Lp>G?h)H?2Wh;6C)BY{OgVJadMx@ix~(G726bxInVJ*| zRL`4Cq*6`m(D6_K4?=#roZ=j(j#!+h_k zw0me5^GeXPy(bCT&m*ojzYe-K_7M?D#aKiK$$Q7`NVL@ zFwx|#t=TO97;+;DQ!ES)D5wV!R0vjn(QAsGAZh^R9!7O=mz2jU#Kp|M(=Z{SEhc*! zu^}>s3bF15!qO!*CxCh5`?0qs-8mfQVDsWxQKZo$8PF9>9)B(dI-Wfs(x53nSna<( z`K_V0tk9!SRp`7}E3G{~yqAo~d<{8m+IKW-PfIh|w{@sT!O**8QU{VIzGqCW#arju znD)x)Z-0!*HrgosVTUk)(o`M2Xfy z$LM#MZzS(4o1RmmWbVn$OSqP^e(YUl8QJSiY5357kGhq!;1|BSOXqQxuC%YGva1T| z%4uR&B8%9)mgTdp+fLuPMyr{2jwyKXkwG<<+mOEBP66Pl2@;Ho$F^e79v0rnp2mpJnb?@ur zDqc&JiKjEJnHe&1D#)Q`9UEq5uOF_pcC2WKe6N*l7G_p|@T#)hN@f%Y?(l0waJ`pL zPvMA<>T66x!7)c#p{DmKgE#Lw;en*((mi}I7I*9c0NfGjSQgxIk9n>8Hd{XCUd!iP z_X6NH5-ktp$h>Pijua$qZ|jX-`+&=@_kNRU*^S{NtGJyJYq8xtxxk&TIZqs7(4%)l z_nf>t_IJ5rB68$Zj$S!yglyI#@m!n>iA|TnBuk1~)h<=tsFiaje&|eo9pzIiRus3k zIT>r6^{RRTR28I#8|XPGI;xXyTE^7P3cX5|sU%g#%hrsd zVZ5RdbEj_7=({c$=@aXN^2DTA(X$8Z`}NkUrpzhiKJ4tchfU3SmAfgQLrK58MnVx8 zWi;8og;eLuxeQeRV4w^eYi%N6d62GPJ(Jy2)lRV&0H2o%jxec%t4_+uT!!1oP}C;e z%f<4$y}l}fJ{~|$$GLjf@hQrHd6>V#Mw%oGU0cOk<1!INCvnVi~4r>cU9WYxs zRc|og!YW=c-IfC2g+cK{{kR_YHFJm#O_i%cvs(1V{P8Be^2y~L zX<;vYSEef^tK=BTon!@`Qoq!1-2%#+ixPoR9M#0I0{~^HyUVFB$I={l4P0&6hrhn~ z+p&H*V>>*?;`~Pbyzu+A{_`6+a+Zx1rJLEFU^?KE!EMne0KM!1`w2UX-YC8&$CzYQ z71fqxa(VJw!q- zYb8X|;f87d&oBI|5Btp*{y5`>?KJa2`X}wDWk0>~U%$nF`LzEv1yZ|>LLe8d2Y=H@ zT!|?UyQ*1yJcrdB4d5 zrqE#;F5pEB;tqTQ%k;A$YAreG7B5Q2kdq=(1k|gBU?4t{z1ow%&NJB(A=C z+EK7s-{Zyx*22Ev8}7Iq)5=0{FR^85pO!2|h=%tp1QHRT91VnlLS+>rw0{MQCq?M+pGRwhJVIDwn?jjBRVcc1aBkgCX%_Rret~pf2O<;pNYz21YQPonXnmxgZooEwVHFAy45@Cn@sm zz1>}%{+?0Y&yrqe{;2bPyP#Ny)k|)oYL4p1jIQ>f&eH_O4Ov`yA_&z?hQxXZwU6o* z?9;19YyJR+%%!z-5iA0b2sdL`rWs%XW7y_B&4%-rv_EAKnilS)TSl)i1nik40cEzmH+w1T39#sjkT~A_WiN$AJ%@weg!9Q=Z*blgkeYO^=q*? zVfAVfMPua!7gS^d4)F>0pVhD!%2guEy#qoX;t`u~ORAGdQ5GxFfiZw_#nR=mA}-7spUeY>5mgG_ELNK}6EsFja9|4R8vX z$i==^hGX3|X{Tg*vb9yC96Rf`O4*Z-G;yNqn)lj8^%X#wZTa@yNazt~ z)juQ3k|F?31V}?1y|(wQ)n}-wy7$3^&M4=lY;n$yIn=VW=nQ%MVXkAqYTMHE=2^(1$z!nF)W!``dM zZ7l(X1z9fzOr3E)Iz!)rI+*k4G7nZ<`>4z+J(j*#B|=FEl7xh{;20gmc#yXA!|W9g z0@<08qgs258w-V#@uW7IL=_tH&5HwCuF{rXu~pLBZU0~r$|6(z&+%0MZujTyyqmPc{1*~>C)q#hI!G#jkhvN1-EgiP`AkL$koUTa=2 zAjX{ceebom?J;bOx%T!Fb65%2+(?eAqMpi?G-D7oa{=7=nz+?|olmrkZVW@7O6k&8eL-lA||$lr(8& zsR3H1V)f24YbvK=bZ3clkjMIxIhd0`E}BPOBcKlGNe`I2XGTop`NG|`#T2a&WVJ%+ z2;TKSpMx1t=Z7jmR`p91Cfptg%^WA414+Q$srz-6H;0|VnV8hdk76G*Vf#n&%9%30 z@62Z7Q6iee+}V+1cFu)K<=|l&4DoICL6%OVne3GApk+cR3tVfl3Ph%;>8;YXqRqgm z{nsoDtNKRv0+I8ZJ~H*y`!CHfD|ChZsmi8VTlEQ+Kl-fAigt*{YMQ`Ho-*srWf zOY~03){K);N7^G3`o7Ea(RR^C-{z}o0~5E6ejF5mr_+J;9*`X)GIeZn)!N6o|1;8> zIvYl;_}*-=?S2q1f-qHkR>@C!4S@Ry8L)N_Xc`BcS{`WOFw$uFL59_mR1Uve6fC~` zUp$^SoZPWDydrj(M_1{p3bZ!ASv?9fhVKNp&)SjC*^i8X0JmKG9QMLUR%QVAgSE1X z8Um%QNi5^4j?VJ93q;y9JanTl>ySk^u=Ukw8KY5XQgJ#+5k`yR?q{imlq;V1nBqS`JC#X>{K^0Zy}gget;># zabjP^sbmhhwQV4(`#YhI;N!*I-r znK~QC%Q5Y*9lx>o<>1Tw2G+7~%ih61Ec|{2-hum#H!CIzDAmFOHtH)(JrfA3jP)Zj*0IvebGy?a) z;0c<_Wg&s8JUO`d8`qE-hCz5>*uS~(Hy`#_U-0WM@vGNj-jg*P;>DAXESA^gD~JQY#l~t~qhT6eh7p+lmoJ+ajPb|M{eONXR^V-* z$tw)?D_mrJ$Qm@kvZw{QQZBNLoCBBPG7Lj(d=hV~A)QP`G(PAhVz8bI;&RL|>@Vl| z&C7rB!Ved|OkBglaFh4K9r#lUo|5@1$lhz2>g_cEaub`r82?1YaW#x+EsdfY6uJ}( z5&6v3<99K#YiS_SvV5wW*7tbgl)sJ>LBwfo-?p_q`+M&sFOZ1GvDTEi}QIZ_tjq z%lVJQ59ANqg4|(|>`ubCX*h0{ME`K}fob7p4*L=KLVn``Ds)1DI8lu#QySc<6a(Ri zBy@Rp-VsB=A7mzpkUub{h_!4#vyoopPVtwAG5nNnj`_us`^P$ps*;5Q> zKAjnE;Rbh$U8Cg-_>Few7Eazox{^%rQY_O%=22N#ahby`9SWW1%ywJ*OY-;N*L?C8 z=YCb&>YyS@I2!kVR4=(Hbk1U*Y5$BWpuw=oKc%l}+e^CGsrnvP%1`zdB&3^ydk$b? zj6eCDW85~(#Y(B^y$WtMxLSd6FdhAYIgTG^D!RI`M@CR=|Ot& zeB1L-QX}(e3H}(a=iB)B;=iWQdSL(Auizs@Du?L8z7aRp!hYl4*vUxkANT&{di%QP zdi(vh`?gph%y#Sr!oWb&Jjsu$MYa5zm>8C4!~QW^N6`j*XS4oWan|# zxoVNN+5$Ums7x4j*l!Xd{}7KZ*eAx?0C!G_a<6xswaRR?x@Ja@>CGAd*- z^+QdPL6Adn2{rB$SSSVpBL_A?S$VmZ`x;=h#7Xi|>r{W%qz1%mz+_`?krRB}b&WC? zINrJx!Qpy!K48x|Gt=L12_?z3zt}*YuSFABc#C9J^-oR|H?UpX40bs?_USq@O zecu2AHpUEYiXx7+#+*3t1ggrGCpP_PSp8UfJ1?z`a)r?^5ZpHHM`Vw&2c;rHELD=_AQ1_0Z(RS;bfs%b3S} zReGAlMM0TB=7Y-S40m^o`>^w~~#5oTA zUh*(#lu3I2m^jQjNN2A=N~isJ)|tDnz0_NSooK&w2X1n1&Hec>6f!*{-HLZ}_f@(} zx_WS`HzcB5ZtHN9Z#Pq}b$$_DX%f7{u(z5J*%LFLPj3IFlb*JcmP+M4w)jkSS#Cn3 z9HO6fu=kj##Xmv~HK{AKCIH+#YZ=ByZ^Om@Yu)fn5i6@99X|c>#>32aEK>wmZT4D) z3jA{CmhwsDV7`xvUlbK-+FDEt_~RN zC0UOt+$h-{f(}d!fK7%*exy8BB^aw6MEF|k3-d}5oHB6r^oXJZN!CfmF_A( z%K9;m3jcsyyih>ugU%d6*jZjKp;e~Rp7N2VFzXGXbUz{5FLiIb0k~$%W(8~2LFdL! z0Z^WNr_c~9c+M_N6xVo$S^n(AoMIQ+(hx=D_$&*%rl5wE9LI=Z7>;Y=h2LHFnm#W6 z(qn>ianSyF^B)=i@JZYo>jH^Q?2UI|^}rD3o=##ZQ{yf*3B(39v6O#R&oCex1!Wrb z0C4!FC@FOoMGbrHO%TPJE^M`W~p9caneKgH635@-~AZBdBrcuzx~txpMDN}-S{+QzzbD64uBdrR7K;KT6(9QYRa1m3yS$h9y1$K$bDLTgS>Qa4Qd)ysZ$`LAF2@#4#I z8LhLif`1C2JE5?P0D;`5tSKUUlNb4w@uz&4)Qz~GwKrw2#RrE@qD@X40?zGWYw#QF zr!L|svZu*5Ho3+k+QB0XZSXV<5!iPbji&7+f!oV4aGKnAfe84(1x&9G$uKhmR^aWQ zFO;y+xoKe*hX0sPAX=J+V*WNk{F3-#;Hr}hge&$KV5*ZQ@z;TixM?5Y0$}!Xgk_K* zJhf_O!;>nj?#aB{Bt_P4GBPhljOH{relY%kisCX_9x88gf`dkYW>M&V@g61*9ji`2 zy5`&`@h_C6Q#L*1z+=Gevl}MH!Tt_1}@`B zGonOt2p{zA?%N#`@j={yza;*aamcwEAx)?_jOBirGT&q$8&%4U`!qBWn3!9xj9r+c zuU69D`uD#Oe`QYNG~46?UgW)PPA+25i)}fdQRg!^4q{r|u*(_7+i8H&oL1Vge9|ny ze+PzIM1gDKMr2PgdcPxJM|^_pX1_;xQ?2$3z>)@QG( zZmV(%z|I-sps_s3E_SR!+PfT$0_GXepuRC55nE{8IpR_Zk50_(x;?7PzL_Zl%!gd^ z%cZCOiI70Nn?1RSD3c%0>BANDI`@UUEe8W5ANiHlr)QWLf9w8tVO1p5zBXX2-kM$O zar$O~;}EWgg%agME&Inz@1dG{@M!GbNS697f$9w4KKR!0!@vKCKAYI{yB-JX zE(m+i>iwlZpO4=nOI~&xQy0l0vD7U_D!*a>u->-TT=&@9csrNHj>yh1GheaMe4>;w z=C<}mh=wFt2jOrVe6SHaQNKX9|QcYlF=d$`o z^OIe(Ltv8uIxwmbY*a(B4#x_7OHs(#e!6Layd);n5VH9?^68F+i(P*oBxM zEtr+jF!3jLJTOCQP8q$Oj-BNSYV+jozy zVGYsZT2jATEvw3*c%Vwfv*Fy|?JDJDSk3RbUn-N2^)S`=dmz!QYVtJ++f7_ol=vtc zx};Ai>Oz4ROwy~n(u19bF6#7D^&0(|O48)kK7GJdkXHc2xhZp(t}AO#G?!LWHBt|?^2_}*WtXn*?S2JW0a?>LXI?}tI}bcJ_?%5< z&a^fV*4Hq1H}X@6ehsD`ux@~NT12hGOwvUdaZ0@^d&#=_w$S0d^ z?qOY8nf@#&$rkQg2eD8ObUOR$_m2pJ*iYW%)u-ArRnBOb2FL+(o z;$~z^PYua29mM7aFy|z=V}ZEl+!kx`rNO@d0Du5VL_t(7GfV3^8;%G@tk^!ziV`B8 z0Sq6E*n9h&Im>(ufcp+}+bgK11RrjO`@P(q>-OP3#$GE~t}%SQSDK_T;A4!*ofosh z-E4U5#dZ_b%53hzJ?8){Vh{J^GuFP%Z13f;>$=vuN0D<%jB}6y8^cC&kjW()O5na3 zK_+MBu2^^b-jED7e5}0*3<1TpN0^bEW9-(RKKxW`_V#{3Yz1p*F@ChI7YXE5639v} z7mM6{O;CBtfKot`!j;@&qj=opN4v|UhUq5#!5N=Q&#U5nt{b@|hS=X;*`hQv>4aAw zRm?*--rHji7n5`rw939IV>h|HmV6l}b&rfJ7_$FBkzM^yUT1e#M1;>_;Lbd%G5Y(> zkRFWHw2`Z8t0%j4tQYymj*yFTnbX#s)*An4j8J`*7Qs>0vbkdpoB7FOj>8t6ngE0` zq6Ov*O8Lr^B)cx?FiGfkfnNR)5z&EQR%W!#uk;cu`UhnkQF1G~>P3RXeODK1ZJs3W zwQ`Y6mVQ6NIlIp3wOFxFIjS^lo6w3AlGfU#d|^YiZ=h=2F2Vaiq7%3G(#(g*T7JP@ zzy>Y-&1T_7)*uf8D}Zck zgcZ(UzmgD0mmfn%|G1y|GwY&w(uFaiS>d3egWyxH%b)c6BZ?s z*sc&bX-BXT69%cZyrOpsz1;-@)-XsiFHc$%BeUP~#cf1n*RWtjeNF+6db!EWnI20K zx*;yJDTMsN{qP)XosH&c9$`WvvmU9{(2ZK)CNS`CFZ|6H``w5A#h3Wy6+a>vw#^B@ z!+u)v(J>bF&xwI1%LBp z_(yzO_s`z~Ul(p*0XvXRWM^fSuq^Dkvw_mpQIBN~%z;-1R&DHsuf%6cc>+jAN(4!* zpSr1z<&a<2Uk>|W*e@^qaK(>aL|Xv94KxP-7&;cpDh_+pkJqbb&hZugb?!e!g)Hd; z{QKL~e8Iz+@YGlVI_U|Mej5JS+Vl^AJo@4}fJr*Z?!lwu`ZKGT34mnxz#F2gJii<`Dt-q=Qd z5PuQ=&%Vd@FE9?c+hrKAi|};iHNmYa)le#&Y*I@qC?1+Qt1Tc$n8Uzdmh2gjn^&s) zqAIQI1<+ui5?;UMf~>aTT3XW|(uuG) zjiukhA|`N~W$Kcvhik&N$Am4nX`H}|{Mh^<>?<^4^WTj#a!MoR#hMJ+Gg4gC(Z}#n zn_9JrG!EWmq)=m2?-BkH{I`L(#b?;ZwmmU{709z~k|^7Oa_7${5Kfzh2~3XxbH^C) zf$2a*W`QRaxl&dYmOf`;)!F%?t4M|$Ck>MWHZ0Sx%?9jZ5)~WvH_QKs_&FZu9CQzX zV14WV?dkCNCl`cV=Zgl?^Qpx_lrvSi7pSNT*3?ds>ZnuQxvYjWUva`N`1P~TFFwpJ z^8p*fEtW7#2v{v($xvDW9mo$A(brj%q7C#j7h2zWFs5P%3`!aK4*7a^AG+i54J>Hk zU`&S*2|~VKO`}-FiC|=EZ(8b$Cq3uhz2#17>{xw;O+J zn#KW)Rqw@bER=hyb4w1{HF)ql}KbLll-kkAH8OQvd(Z8;`c@9LT4W`lS0bYf%e+p+5)Vl4K_VJ1;Mz z95S6+;fmk>?)7U4QBOA62~EP6`N$zRdBfzx9K*Iu@bUt@bhnCBumYA*Mof(>9AhO2 z7Rl+Ju8Eb7BbMm#l%P`y4Haoa>YjvLhrG-56`GpRi)JW=L~JQ3^S0Icea1No`C%fSQ8~rz%pX=1$`65vKuM3sqgQRxn{!43Qtj^aZ?F2zBl1y!XiRt!+ac~y_sg_Kh_Y5tTMU=Au*&3Rx zjpK+JYI&*lH`g-VyAqmYJ?;OE`4;$AsMy(q7zz(Fb$CGa478vfPAYovCx+#+xx+Sd z){=bUmr7R@wF$Q9FqbS%Lp>`mvJHR>wzlH1wf+kPZ-lsdyLsN}W523~`&c#6L|%Hw z#*{^8U4qt%w&H{~&dOB@dx%lK-yRWUhpZn-YX~d7$YL~x&h2NgWR#QZkX2~ zVPnKv8F!b<^te)A<*~W%Ws-t^a|W zkCDD=o@%f$A`w(%YLOXho7=Hax-_JUhQYm7Nn`*9lfl}c5pKR2{$T6xeZS__WTRGi zXF9N%r>qJhV{$3Wm1@NzImQ*0MAaPx0OKsqHPIsTD3L4NDUuq zkG5VAYJ0HGx~U~PZ|$KrJEmpTm#Uh*h4hYlZ3 zk^HAbFno9Eao#o9PomDpwLyu7f%W3NBMfPPCh7{PVtWJE? zwG?2HzPIQC@$L!JTO})KpF6DYr=hJ$-6# zo4#GbvJE{qFJalfFg79~E*u&uu>%+9L_PCNr7ED@zkf9*=RFpnd_X9xhvN^C&CuUQ zREiZpvGhgjw$?g*fbA=|5bt6>fhR++HFPLFW%xtt9!rUid#XBZJ5*up)jz~UfV(vrW z#fEL{gyZ$UL^;%kh=l+@H5|F;+Z`UAiHf?vPl7gu~#!hmm0S?c-eu@1W}0>|6dn=`+<*vfBeRO z{3PB`l$46S(jUl4toJl*?2Rq{zTQs0bjJl;z$A9cq@F}n33RkLJditc*gl)4`H#bX zGwqiXKMa2Hj4&gBl_`k}vGHr*56O5iu%iT6go5i(=sfYCz~2%7L?Mz-t;qqFps>Iu zJMbMwY}|;q&3nY(Z9YVe5Ha6UE%Ef|pgVhPJ%ndQU0|X55|VjMx@a4)uxHxm!J;O6 z%xsg8(q6oc18?H;`wKo`X#}{4Kag0&O)Nt^mdY)e&Hz&n>?y2DRxoKhTVU$Sa&%AU zFT&oFG+fihh?n^bF~TQkW}}?tSrH+IH+csmxQUgdixRNvN}?#+pPWA!e(f>Mx5Kk{ z%<5LlnU%CK(idW*x*FUp3fVLHey)jUBxMGkY;xS9A?8Y-7*R=7ru|pUj)DELiH{n$t*O56@%1zGV*NeXW7xF zU$lTGX%59L$z~4ABxZ6N7BCwKk8QcJEOH08B|B!BH{?LJD>~@JG+t)6b9ni5j=`~M zbxO2MXHBo`K|#!9khiO?CE&C%VP+g9sDZ;Koo-iIczkCx89JWIx`+@13piCSAjTi8Nm7iaslVcs!Pn>&W z$NfEN`dzkf$P-!Fq@z32y)e|xGNa$zPHeX~l4vz^UYiWY#1F&ve;@e!<==8m3&WnU zUF{J`U>7x|JM4wYpX;_p$l3S*wNk2ULOwQvex5i3=}=kEP2pdCph&qh8vAqj_+oD{ z$F}h?m(OpB??!L^-h9yun}~(A5pS$F?oYh;*!#C@zt-FCJ>te%7Te>tPz#2Upeqi? z?Z8cmoSB&wYM=b3dZl%d$5y62^m^DS98I}F3DkROJgpi=mCysoTqIM~f=U@NE6S%M z)798eNdc$t6AkE?q3MU}*UFAwxLdllh>6L9?D%e#jSmh=Tj$xD=RB`dvE1=+UN9k4 z6o2*{Ue#-Cs9>1Xd=X=gy{h37nsD_D3yDs~&S?+&xu_LpW=UOuwFl!#fL2-p0A@Hs z-*5g^>@b778xdr7%IBTST}p>*vw9s!k1s}`GnlM znfj-X0&MR{GG1W>6&Cauu_Cq}udE3_hzNxBQ+-aidhRUPn}b_H3UJ)F#xpNX_WPxQFi%NX8cP+R{m@C!j1A zYvrQp!FB{!L$1Zwo+nnQtH|k`Ru9IOY_jTsA1j}}_@9Kli!JWBpuFeA!6VA%(CoSY znA{^1PHcso!i2DsQSbpR#jymss}_-Ttl;__jz zd%s~$#;`Gb?8L>%Ip^MMj7hLkMPMs}!FgRd5n^qWqhn>hP?IY*0rTOZwgNNky-C{0 ze5M+rl_3PP;aI!5^4Rn_)@S5aR)|Ed}d<5j*yr*A6Bqw#l6KT6aKTj_Y3coEM1KcvTvl zD4QLXAZ74~7(Q|nPtq~S=_6vB`HmQ}{+naw(wOdWXZz_6oG6jL0vD?um-C<|06pj}I;%|XRJm&8Ta$cR^v*h%8)AB`6uw zrTnL5`c0HLI7DwrGqNTTl*p85St5ex3U@3LIm+S(oU)EMSqnB4*TpP5>LBX>`W}%ftbPUDzzpH z@~y;sj!fYo2=)xTTeRbEdp|HhI*AJyGMSF%@c^W7Ryt&s+R z&35)U29c{qUh=VpJu7TKD3qgsWzC`>vd&48*YJ~U6QS!QtqhV(Oc-h(653F$Jq?Vb zw!^2}ESoJ9gOh#>@gVTDd8c>b)29)Y6Ze$!|M~rDoa&Iv^nWmmy?YwCBBR<}@W__b zsZp!UF2Krtbk|rmiR5uNEeb^$o;%f{PSre$YgHkj-Y7G#DA8Ju-F%qaN`jIvw5=+%DYIX{bBk`l#mk`6Tgysjp zf)qQ|85>O9jF*@uufI^6zN%DB-DUg*i09~0LGX=jxL>$Bp67)b-~u)*u(MYs5E5Ng zr6>ovTCyGu%YoCl%8bQoxz9#ql#~=pb)9|GXda>_1ZvLqbJO)FX$Q!|kfH$lJ6lB6 zhKHl_c5-ZXv&c9YRJCDzZRK z%(AX_RP0GonIcoI z-q+526h{o$z$B1LL6cOaPhlHsp%pKc<1oXMMBSC4&BA_T-Ac0=aKJt=FU*0db;GP= zQW*m_?T}AwR!IR55Lex&*Gv|nly;fO<;@KP8U8y^{)gbN-2Q6tuP*uRln#-TzZD1hGG!6VQ#$r^Tsb;z~F{&@TWz*H{OPYT35|P3u*ruWd{qV z0+=koMeguB5WsERW^2NLD>w~f;+mKP$whf!BGW+$VgZER3Om>gjQ3M7)CQ1F_>-0*T8LI zX8dc|KX3dIsP#4s%U}zOr~IRwEuBC>l&?TI@jc(MM2~-{u8{{~p_RGkk?YU;kn?1# zDOf+!L$7I-@9TN|>%VF)${uf@vR>$IohO+Ufy}LoEN?cpT>ExhasTD|e7!$yy=`SX z&PIfB8`Cgor}v4>@p!_RA5HI#k#|rum-2;B|av-V4 zA?Nhmn4V58vFbo9frFLt;}zj>*rxMX#H~EIx&{q;K9;W_FwEB4CV9;R{p4X*;ek9#815*M6QU>$u{oCD+`4LoEUQ+3MOmTdX!B8exI!( zf{Kq8cybp=B8aIW2bL>ZXi(|T*8G-k4CvSWXlM|R5~vWF3jC&kR5SSlS}vMHOZVwW zW`vEIA~XoTOW>}0*&j?ktKXeIMt(z*&+#2I=SN;p->(SxWS;Wlgq$T<_pG{cMs-0z zUfte%xSQDm83k|$qqelzmdWlY8!J6VL45h%skR9`S%iQ-$@r}0~8Gz@mS9^Dp zTaN^m94?_Ssn15h!-&-vM*4CyGye``-#x>HHFAaW@RXU4RIx5qO zGEw#5Huk!yY&HrNPyK_ks1f8fUJA;xGPAW54}}lkd(%+g6R{naUD;|94EK?%*d3ge z0aNZ@IZoVaniuzgT97k!3}vG#zE-=jqU!oy(!n*{JgrD$Zx)Gc%Gv&Gj=Ab67$ri<98k{XP(*rPL&$`kj@Pc5yl4^K~YZYG!+FNTEm- zc-<70k}<{E79o9(6usFD83pHP7a5;a5xP-J54zX!;XWy)QP(3~IfWq5Zh13pgi+ct zeS@dWkdU-9>k_fwSq|oy>xj!*<-D_q%)Fu@x+h9Io*G_;s;hXOcpfX}j;_Uyy|P1Z zPTZk(!;%s29V7cb+ErO@3^l`#ldR*o0f-?)GNS|YynV=36QBAv81>)y{Jl#uT zz%L9hnFtL#{ok%~*nz) ze_?X^3VnS@yRdcG7Uf{wWgMlWvL#iDKR~D~B*zJOs6z*`4+}QB*Wh%NCktg-`it^$ z$&Vh}Vp2rbK-DU}Leh;s;1j&y3FW4yD$}s?B5$nxsRd@+JC*a)GK+CS!jjJ@0@4eZ z_Id6OUQa1B(tHX9)96Cm(0L8HJRO@L{vOJ1zzzE21?6!#ibV0qAbZ%vhvQ0W!Ty^I zznuJq?O*01O5PZscl?+8{?BXU{fR$)#W%$rWlU5Dl`|uto(xlnED6LL_riAYLo{bp z)Qv&HGH5JO>Nkjp5}z@WOptpU-&i-YChqoXd*RhTr7KCvS|x#5CCO3zQxCah7usYY zpSFvq$>EojX9>}i)HP(6=lG4`7v{en_REXEd+}FS{5Uv45J3;(^Np|X{m0vWUikUu z&$s0E96QVB+67f7ej;ZoYJ*Fp0PB29PyVj-jAoUk)*M@>8_ZHy% zZlOcq8{qMWFaDbkV%j(7PXVmJO}vS%^>93bo!s0x+=lbzG4mju$+@s)+?g~R*tBI_ zPILRgflR*|p2^8UI5EOrj^Y=y4J~kkwsAAw#D6`!vJ%mt?Jti*1K$RI96&Y!Zph4$ zJ70cs;wdDyoS9~3;fSy>gctk~fkEuxnLC{s7h)v~O)xlrLAcw?Y`EQ%f9>`US>AT7 zN_3yZbX*aOqhb%MU53scg0;SegKiw8rHC^r=ISj(0I{)w6-6V}(jmq-7n6a6?2cNI zhHaQ4s!RFTK9F`8I1}^Q3kdOB=W8<;PMU>YRuB=k2UplIRvE=A%GFfF30!7t;uZED z90Om5t+0>aXV^F3_in!d=Hetj2%|CWl4io{77jXX+Y6kDl#D6uP5^wO; zdN{ejt2IZ9Em=8F%|>_e-h4yIP`6(X1Ph<$#7>&jT1WlT3eB`ah(^j8dnL@eVt)9A zF~dgWYG{vrf~rd;qsiN~_O#!f4e z&2mW3179*drAAtHj_kN5oi}xUa~n1Ukyw?It;AucF+F&lbCgsuo8D;E$5I1Uu||ro z5Z59^i0bCdZBmITBxz$F>?|K~lCM_fPxU?L#q&#p8H?gDw+Kj{_E~_b}`X18(mXF2gnZ4Bg9NzuA-}Rw)VjT1# zb^9Tgk$$kLRA?)w8j5Q3@80KbF^6*?h>hm9vVt>&p`zj0#M+g{wsJ;Yz z_XNdx_}zXjA(Z86B_xA^98@{L2>O|slpP8sX^0xwTCaKnHf%@G+)8AYLGjicc&U=u z$d8t#>dep6O=>Lqub5pZE@a=DAWq=uAO}bFbbviP7kt5p5uqqG^xYQlswA6Yk-J|w z0n;Nv5Zc@0p(n)Y09k>5bEP%*yqqfgc8=Sm3D1>VpqwhHZ>O1~8oN98{+JaJiD}YH zD4d3q8fknCWKGRpAJDAQX`r|6e5>AiZ!rFR|40nqi4BGVRL&Og{n-jp8qZG+Gj=_1&Bo_0;7-<{D zva#K=x=rocqv|Usw_v>E9>TYCbPOLoybWA?d1B2vp3X7j(fyxdj0T`c&N-UStljOj zub2!ivkuIv!C@AAL;GW!=vw+Qrc}tX04)Gn-7USVcH5TDQ#>?tnxExvp$H|k^5Rhr zBbSgvt@g5BgG$B#D5Ci|t5gR`?p#&ph$(?sUNg*?NuA4BDHm2zR>ID(wB?YrE z|H2o#qaw9jL6(LjiM{ltWaV5IBG+l9AP9o3#A$J?aOPJa*&hs37nYG!mb@xd^RI?`t-0i5Y?uL4p;I}N+$LF{dh zhK`aZg{%v|3p_;f_B*AQ-StpU!=*5<>h>IrfY^J?nHH$(>!?zv1I7+{sg2gmvP;r^ z>{zmtuM=qxbn#)uui`O{vxxrAGNS9M(=}KdsmTqzXPWq@9cL^N|MeTmi zwC|Z^q}GvOW$l#bm^c6d1#!Y2Ps58fq5Stw=jldFjAw&5b)L6NQBa7@R?#7F$~@V}bz7smIDSo{{;Vc$0Y`1$#_pMm!$zAmi53fz9Q zH5{-P{KClt_KZ00h?O$v7+y@3JrGO>3_rO2cG#~z z@E0%pI2YdQdo8sQ3%@xB3Q?d@98o4Qv%#)`s49W zl(gk68hpH+o)r2O18K}~J}3Pco#on>>Ru?DFbvin3bVZXaywu9e)~T!IPo>`(+(`+ zHoOB7Fvoo817L6rGancp7dWk0LX^v`Efxw2VjGs($Dk2&*qCNc6UdU}$f~LwvfaZB zdt2;{6&ga2Z((ug2DQ=rJnWYf?_mzRw#nb1TuMo7mMkfY!?4`*YVhkI9~bbF>!9o_ zx7;0bYK+5J7!CNCxOV2~ak}4fQEpuIu`XfNzwsIV5iUb9R9zm1B6e$>n-gX~{MZTJe$3ag~cnw;c!#KoDD|nh&g_UdEx2ALOVO*Pn^sfDzbf`V$!iT58)B zHeHW8yHLw<6)b1Mm~LrHcnz$Hx#7VHy9)ice{sX)FE)X&sPvSZPH+Th#gJujota@` z_6g$!GjauHU=6s>xkPSI0CaoZ{ds(s2i$|%xuFSlm`nS>{9k?S%8yity6q}nM-1T$4;aL z3B8nRVWe3m*fZ#GH`}Pn{h|eEfiU}FLE9+W!jO?<|5KU2ix<3z{AQ_npRT0)WXhQ zlN)Pceg4Jg{aW|6)>vVCZ@wAZV;gsd6S4v^RwW#m#DCSlmrHB{sga&Zd>rfI7^NKX zhFh!zdt^Q-psczkDRjF*1{VBYU8I?~U`8;ZmPSMCmFu{dYwGby7J#L(n(J01VMC|~ z(@>O^=pr$UkJ%Q59XA711eMv2aJMl$nz7SK)xJN6W)EmYo^Z8DX1{Ma6WZP(+CXHw zJ?FGWYJNYwEj>Girw3Ow)&)1cVZ*S{?u$p%B|E{0 zh|Od7ax&O!Fjr=T{;C~^`t?1NpZiwSC9AhtZ`MxEP|-H*>w##DNAo^pM_J1ubJKTM zLo;HBms=grI5ZfnTkKn`&_p(s(eND&skxB!XFmU4jZ14n?a!woFXGFF>ppVxSJS{_ zETrmX?=W{g$rgCYVF>`3-MWnt;Ph4ZTvIDR(8thTkeO04zI)vAfiKS#$^TG`T7Gyi z*`{DTW1`TkZ3!%-%t*2*;dq_8PyW`S<^dpGoN_$nBNgLvl3jWdi!SW(Wj#Yr{+zH- z(^W+#*=W2WVBWNmk?LqKK2hQZZA2_l7<(b=hhV}#UBYEufW;;yh-{NL=*fZU#h2s% z0H4Vaq~lR8xu*WW#})R>3in}#z4MhoNibr&A7>Q7@lRS8a!w<8hB}(xax+@NztSmc z-><>ErLNoMXU#dT72Ck!Bkcq>mz%pGwNQCEYcDf<&DY*5Vw;ch<|Xg7#vHlc?X~iS zrx<9jHRfoyv>mIS6*#Uje60Jf0iTF}DO758+I#yP22{=*Y)p?Gd&QXUVp_@o+2lFn45FBSxmBFe9B}8{2H#+_O(&i!U7J zxxEa>j+k>m3Tc&#G_&Dj@5QI1-D5Q?7FkqFFg;GHG&`~cjW>s-d0^EWeg0y9Ke$^Mtmk06krjn-HcAPFrIim!N0LcjnS=Ex zGm>$(XBHsx1)1W_^0CYey>jzd(!mA}hJWm8lN9<}2>g~T;A+peHK|Ix?okW`XIC9t86tb}EDd$Z$ z0U}*gkbS>HRqeot$BBhK(`xoTXJz`l=k*H!Ba{W#pYFcP0ie1y+vp(8_fA(M>Q{;! z&Y-nklA`*U_L54eBhf}`GxOjID#ot6KgvV`5Engsq)%=U)*+i74bVqLqyR~s80}P} z)qP`{RmrpsA7(+orem6TnoLV7)1(1kh*fRH-JjEP7F!Box`0&&(e|d(C{gB9%a3xv z?Q=@EP+C%DT)_aYlQNY^rDmf3ARZ!h^@+I>MTVuCDq)N!In;Mk;+PD?0xQUbge$Y% zl+%OcVBWwB!H;K@=W0h zIkt&DK|AL}skoqct++68$SN#34JvmEtjBI`ok#IEDDs1bu|y3zd?PY&Yh#CGNmdwl zy_a32g&FLHk>T&+u|lYHu1TfH889zLyL`W{j3pXuVPy}j9;ptICFX-T34UTV6VtYh zqDg8{8DJvDGE-`l-`yo9ES=G^AAhDs3jcocx3)eU-yHkq*Ug{T{=4_y?;BqiK5yKC z6$L`4CM+An#|592Ri5c(4esY6R8eh?an+Cb#0Al-y@I;ZJ{)%8%D=jB<&YSnF?NM5 zL4VFsO=+`uVUB`|nvSYaH@zBD?`W&gpm6)pvn}p$6dyLIFt#3_hEWkWrF{{+^;^R) zhyC(pzkb<|AMs^kW*r1y+kSrI51;XmU-9$8AGRBef&X*^+X2Y=GZb&r+ra}+I}n@1 zf?T*aZs-c18Un+xU%&W^i-`U6*Z8MD2EGPvMWKh4h9f_`tjutR8MICw7TIw*=D^GG zfnR)CWAFd{E3trO2vaIMzLPn+PBkuettlDDUktn)uj$tZ?W*XbPx3SHZS&jWw`HHi zPyG_1I;X2N2u90GbzqxtXTgO~dwVTSeRS*B$5UdY67GuOw$Mb~V~_5&G~FiUlp>t8 z2V2k6l)vo4!E*@;t<_BNnH}TA`gI!{)kWb6%Sx*CY52rH-2)LoU@g4!EfBk-aSqY) z?*=|5%#&ooa1Pb%1a2sKDiAO?rvLabd~w6D7`AiPgb{cfV6qeiNAh4n-{!%eldWMc z#qIkl9M{mpVG-i=`)!UMyCAT4Hs=hLO#85Vwc~~alVIRb2;ak1U)zWf; z)3#&UZyjG?*Rad5+{esQEBV|PvlDNT7+6!3ZrUkjn>lV|9GB_bJq0>2Ue2AE*bR?vWZa0O`TXjk#8 z)2k~uIm$U;tZwS-ZG^4hUpjtYc>1II{SMYg(VV38(|r(7U7p2BlLw|5s+_IIq6#o@ z8zz6{_-0xO2SG@M8ATPRlzGgb#%s-Mjk*0}&CAyCJOOO;#Aq02KSx}agPR1c7^u+Kc|IK?(lS@jKd<{zU|*RAY^s=M<^$&^V*1z<#~ za5{!vj+oT$WZ5vzaH#Roup<)EcxMf7T*G{3vVqx0*k1yl#6KLTpq>pEF15&qZ?7&|AYP8W`6BaH{zY;~&@G5hWy5QDn02Ec+5!7(07 z&;bJUjYTzBJtZrY`G7APW=+ac3#T002zkxP;l>_s@Nr?isq1y5MvCHk)s~QQBK99l zaB8xW3E~xU>fTS@Y7s8Brc($^UVH*634l9NL}Dkkp~*HDI%$3!YZ`k`5Z3W9qe~f9 zP3F#PC?r^AlJ&l(=TJ+BL^esCqVc-sziTy0BLR#@JXovo*M zKD_2!xcTO8iGr2STUfc}l?cHNL=3+ib^vTZlfUl`;NI^!MsTN`sBqW@EzdergT&{| zsx4xrkmBW6#0JbOTg17egs^VcxbFLw5udrIdN`3x_^|cfIih?F?(pfDzE_Mn*L&H3 zWiS=t!&7g&*G@JlXXw6{rwF60aXr>X1)d;w*l>cIJA!kLoeof3qG)#Ar)11KNbL|E9S>IDpWIMK53eF-Bo>e=e zWR8+oy4pNOF@O1%X-|(zM$$!X(mCc_YgxMa`3U6|rGlz5R!Tjl{wEPFE#5fo3x!lm zNq08uM>m7EW|pTUwYDUyBJKc4k}`7*gQ@}IG4A!JgzsY)U~a%(+f-eEkCMHi6)kbK zMVcj7R~xiLY)d^>3K&p9UVIsVwM&QB8~xc#kEdeQiJo1Ll5!?L-4eGnuvL;Ano!eC zrt{<=&!$n_iZJ%(aZA^=H;UQ_0F$KWskk=OGP}%Qi9jbsnKnN|iBu$8>23;Cry$e$ zRN}8Louwud$58zRy|cEnpot;^l1o?UlnTe%YipwB#q{=5M4}x|rpkoG8%m%T^+a+o zT3aUi z$px`rN89Qr(TVTLtpsDIKG_M?pYp zBy{)o25-cL;U($V1TTzR4q7b(O0fQ|(dL->BTc~McRGAYLgcliYIOv+uw4;C05Dy|^B7R&y#R#mU}dQ5{7} zwL?xOex1ITax%2Ms+?1LSw1jE$w$l%`;sp=uLqC2bNMCOmcihKkyKxbTEy@b!%8E5 zW#g9@e)!;zAN=9M<(x#o_l-ZiAW?Q=qXrO(vL z>%zjFdP*-Aw%yVl|M%&?y7->>;ivffulx_6#M`h;>t0J;2p`3HH2haXP7z%B#pxK1 z%kW|N5-;%YzXjgJ?P5mHOVfxITU;dZW7~)Q%KaC^f1LKZY+jfb;0WR!_-W&h+kOgs zB|ZaRDZstnY4^)(3TQh2HSrJ7EhGh1IbPK zjD88m8}Nt}TgApZnuQW~($2Suvl!s`gX6>To4_Xs@CN{}8~7-%Aov7{owTXIET`g* zz>$ZXWTe>PKn`01i%`kzo>{TDBl=wveUFXIhN;EUnso)IRh^0ev^TNZP0*cKwsLnsmmVm-teW%sAuJO_Ca zfkR`i#TmfwfgcPXunSb=Rv9Vmz^bAaIy~95qWeurdE0FcT-)x6YjXv@5G(L&*n<6& zCi^DeLbXL$z1Sm&saSlVUR6 ze2p1Eee`iCWbs=kOum??rUX!6ONFCT$}khPaI|tK3?bd+bW|Aqs$@`#hm(?gke@}FRMm&zX%M&$KKqL zu{T=oY!LBYpL0wiDwNAWVkdi(&lMcWNHxBl1%S`k%g2n^?xQvwBh2<*kw^b)&N1Kj z8(P8cz2`N}%rBRfmz(=Fe60oS*x|!SHt zx!2BjO>&G3z%@79JA8D45kT%G6XGamHX8%MQMPXtkXAJ~0JD3)bLBC2H@{=Q%CaK0 zfgE=2h~d5%AQxlWFqE6uy;l`riGiouhF0-@*(}JIKAw$GIsN@~H5B^`$vzkrn3P*! z>|=W#j3PUpxk84#yVFDOMNe~*VGFii+s};OBI}BC#tTgJJeFBc7-M?tt?G-3o*PlDl-dUh%i9yR>yJZ zvFnshoN`gj%jhaC?<>fft{riv9Xc7T@?JkRU{w)qY|-xDwI}#Mg@qm;$v>Mg-_e!* zl2m(@F6TkGYLn*IT=1w7DB7(0(;XV$T_yH9Qq6-~ni-N00t9r~&Io^l{VdGdJ z7DBbkOvlBrR0tFf?6u=N8LL4}vV0H)WR6jyWNO4g(`C4>Pp{_P|Tm&Gvum*78b z;tuSE_r`nUedB%O4s0R@_`)^nHYv4PokVY}jcxE3t`}a3bH8x4#!gxB`QnmdF};ga zagD}ntnB_uzGgL^Pb+bUqU9@Ch+QhcVi0F3r64M?S+sFs>UW)#5m{n}hPO@3raG*K z90AX(O)ZOII^Y<)uBXJ5_BYPoUG^6r_NxzmT=6nH~&9raF=#F z8O^o4eA`TSs!9gkg?zRke=zq_cWo2H+GyRt;Eyl=>o4&a7r);8Kfmq&`7806?1jNL z9GJuo#Jh5{rUtFrHHu7ih|Wny%JDJq0?hxXb>kb$wU6SM1-;gD8@+Ti2B#|5@H@jV zhJC#3_2O%CxYMwRZ^558zHWZq_!jmL4K5!GFA>S5B`W#F-xywo-;n zv24KlV3f&k@n!g}`H#eAeA52Fc4Rs)qkgaw%k{iz?~#sIzY;wFmN8Y@wdaq^f8fQdYo73xdH)lZuQL z!Ut`*n++_3`-VKg5caCfIO1JtUfbW8%%5G3tQo5UBh-)#Jg zwS#dJ#w9)ynbP`=?-`dq7jCGhM|g8T;WK9mj#T~r9ERjja$=4~-J*K)BD*vuuJI4+ z_3P{UygtTy?S0vv7LyTXfsiRv;^*azZDVAI7Ru$KF2xQ*Ro(Ga8QHIKjPbBEP_^!U z&ggw+IY^vZ)ca6SXN}PTA$8)dLY+coMVwGiPP=67Q^7h2xHER^AyAnyiK*F&osLqB z*lurNj+(DFE6CX=Jk~vczrC@t$^}pVP43jY>rYJ+f6T0BX$pJnGLJ=@kJ*iXHK^DP zO#0&YEB<3ySK(*cQ|bbqpP&__O?S`Y9|9n(DC(>XvxbjQn$#Ohfl~WD?|ZBju^r3s zxg!kA`R3jZSfTSIE1se{1W(gm1M+*Pe^dhKk&+XsR`r_pG#Botd{zPUAOlPE#WyCWoGb7_Aj zGGyaiO)VH~SWA~GhGDbCHia(vnF5q4P3LvBwOw-@_UMC7R;hc^%|z9-xTnv{CFSAp z@|bXM&!P^E2O6cy?(BL~8FDQgOLKs`v5{uGD>%GoP7107kQAI!ypiCl3Z;lj4M|X- zUvYi1>e<8`6Y5PhPefGL^+6$zJkE-a7hQPv=7TnfBx?e%20-H}`7^!HiMKo!N$i++ zbmz2nqDfC|Dk&r<)o=@ONMQRqcJ5LAZ$yMdKWH{Vffw#WTYw?tZB-5Jg+EZ9a?dvX zL_m_AtyRX(GpTGR-LD#Fs;wZ}ppubv)$y20G)<&L;gAo8vfORj#YdeC!7yr_=`MDW zC9Xz?R!FGPp#WACLRj2Y@+%|V?PMU7lakUv_2kGvV9WNxEr!Zw-` zQD<&SDs53y4KK6Gc@msnV{XT;k;TT<<1A;p98Q zh5@$sHkLZ4ve(RmJ7SaB@qiuM%SB3xy5j2#@D7d9Y z%6yFbz1+>`*!Py?l-bsB-z!CaJH;i#fym$Gb9iM7mBh&`qhSK@z4qpsbCPs(7~E{_ zZ7`c-m`Cijml7Py>SYIwvG;PDIi{u$gKtLitiwDaW;qk4Ihp5NORv9I{*^rnrY?u7 z#i4eSN7ZxoO8;K@bgHl%vPp9b`D*$hu(Z@$+4pIw=?zM2S2dTY#lq@+sg@R*`4ndKR&@yJ@xU#h8yO#$v~qlX~p+mQug&UOTn7SR4e??o>~!YDh;} ziw8Ni1{Q^R(m~A(mK|zkNt_SC>Ty3SEGmldQI=TEw4G$2)wXT_+UFsupbWS!R;*I! zsYj{pFBkC@;gtfF-YrIDVs+1oJ^UqA!chxxD{YP>+$tZdcR{07m}-+)T`J~4cmxxw zspNze6=O;HwAPkl&rR#p+E${)af+ZbBlsL7Ra2fC5lw*Smf94^*=ZhYyvC&tgc@gN z#l6Z3+FcZG2y4RXHtWQk=0o=Ie^_WN|fUENh0G9((tbaiNO!Qe#c`;QqGnH`Wyy z`m(cc+6yAD?!~)2{aPD@Y<&f=z`!1Gb;6m$COoOEC1AY~4VIQA2_&Rqrl}fX=vu<6 z{~A%y#>g&yqg4ZJEHvzREN382-qmjUmnkr5&l@rF?v|$c$pI6{eDuRZ5yn zR*L0YNC4P|T-q`#K2Q__m52_dmv0>md0~X;igePO7_2ZJ*RO~aW2IV4l#&_jQeMs| zq@I))kUijXF_$7vfk1S4z%Bqn2X#I3T@f;M7MkMH7I@*(BI+lX1!hN!)`o#|Vp!!W zjVFp?@cr*$UjpX%X~+M$7S_Uh;|}bN^$nj3ZTJB?>mUuCwcH+cX^&YL$n^^BU`O)F9P6O(n#UmyPlAz`Mec>9#fL08fVvu04 ztDcFsGhPAJhf6@eXiASLVk|?In$0TB%_=L6?1&rwKY0Ar9KZR3Uw`n+j0*wS3j6tv zpMT~*{fvLS$Nzd8w%YJh{jdO9i2}R1;b5%l;_m@ndcGze5;2g3u9qDLF8l94@Wab) z@aud3zdsj#2HuXPf$CA-gKoGC7e(&&1=vGg$(k(Jz=z`lm;i759QcM3=v2FU2#m8z z-fHy*Ro4PNdcVx^i_3rU1s^ZIhEKOm?%?N*Z=2sX-oziHlrJKxQDf4C>{kIrz|Ad; zAGPB=zk#*DRt=VxiH|2Q&y9S`zcc%h9E`Dug&&EZpXI?>($yCqGS;IVi5t5?VT+T? zf8>nXX*crx{f+!o-hu4T&v9} z+t`59Cd0OmkndQwyTQ`#DUCdxb{V$WHpVnxypAcrz$BKHd@VGPGDUAD&m>U0xl9t= zSOb&T4b2HE?C*pMp~JFhtpGq9L}cqLtob_{B1J|$#epy*n3}6<-Y`qO&^7EecuyO_ zEBLXo0>1?Q4eaj^747$s7b4 zI;*T{X)KsYoo-KT%o=1jtH^CMl>TSSiO>ff4=cmDZtcMs)VU+AJC%Pf1kHw7&;~{@ zT}?ezonMy!JG=kWZHp+yKf14L$`!eQ%qo4OCySk(OUI2<&9Wy5A8TX(|3@*#$C9oU zS(_lf|EIU4N5=zN!9V2k{SvDU}BZ7+{yu@Kv1 zfh)M}M9CMk=#C2XtzP>K*hu)B!7(#fo3W$vrFQ%Cbj?qVsiIf}dGujZ?W95B21G?+ z2*#D62t@5PFWLxuS=k8wIY7=i)Jj&Rb*^wFHvoHWWEX56^L6mg!P&5F`7J64lap*w_pP(@C#~s#m6t zxtj@c=_7CtYW2?6Lw*WYOiM;REB0P+bS=fcK%RT(5bGwtdXcF1h9Y80!BoIPrZ*i@ zr#^yv3uG2Q+TV7tb(SSmBDN|DX{p6IwW8>#rl6jE$3j9QLXe-(Ie(&%Ap05C$EK@Q z1!eUjeDZ|a8VCI-jxqq@@YvBvO-BayN|KT=%Ei$jkz#Y4h0<;}w^7(@835wE?S2JK zxHffHiw~tUI8vbpVO;a80-uTw2;tfkXJJJWK#3R-*vQ8!*+e6XQgUq^u3Dq+WQg2E*j z>ZoY#hyl$3DIzR~aj#c-ly|K(R+2cj_(Q!OPdCW?+ntGAQam*Fl~?FvFgWHs2xs*c zcu%sPCS|F2P;reBoTZ>3sVX(LJz{!85TmQt4t`|5jW?@d*{}W z>pWn4?QxBDZ?l-Mx!!9G-z(;&{E=j{l2u}5!ljPOI>l;-$p#j|aSiSW2g&<=>#H6t~Tr(e_(mU5e;$I zc%)e=s%!XV0*@YYQdaf7;+YAjb7*0#uN>tQ#XL2Zl{-^?B!*x>bGN%e@vu ziBLVymfN+jk-gZLzuE9Y>CsOUwFsn_!^!k=kJvVBmTpuojc3KuR>EMYXD+=ILreCN z>K8?mYnkmG{mu+kjLKrw4DuW8hB+%wMh!ZQL)__$b`rhhWT+&!ytd&WQ^J+|u9+Hm z)YL*`=SYo%?o!ETRo-@Ih##ufMzb=;#C%x#%FR}Hak?t3o^$q>XozY(+lvc1Nox_Q$U=q2Li$i_++gP7_|6s!ldIprc)2Gc{IZrv5tXz$R`MyC1 zFN_WDO8zzlCYi_~i(-b_o2aD4$=M_`G#*(~CWH}Xwq_H4;Zk5yZ8Mjg$MTLgI2DB8 zz{0*T3jxw^*TPlLVaxR9I$N9Zp!5Yc0YkSUgT*gr-o> zc-R}e(SZq{vI?}aLp&p7lvU$+L>*YGJsK-%DUSe9EkQtEkHGr+oa8*V@}Dk@jx6fJ zvFZmTGt*0HUTQL_8Fyr(-1$o3VfF7&c#qgQC;mP8$G!jKMx|$`Q{R5e|1Aplq|ewR`OYQWxcNcZ86mkQt*`)#g^S<}IVeUQgsASXdG?-4qw(gyaA5 z@_%!U-(L2s4_q^*k@TDRy!nTp`TL*nU%uf#-pYm{umd}=0$T#C9F_GY2p#4PL_N=8xC}4D zlJ0iw#0noYVcj$NKSU?KAqpK7&-^a-c!RhH^rJHFihaWplkjDNb}Ei z{K2+H(Q*uV2;(tatCvD*$jLMKbAxz_MY`?jUqS5=zDG$-Xbk!~?Ozr+wD>TD;j-k8 zVh|ZJHXL*E3cHB4aZT)Pn|KF)9Sqt(8&|GT5AyXV@`tcxaKqbSk%5G!7$w1Rjv`}{D#IT%#7SYch5F$>g*wN5vtEGbSkYI@e+chcT+%UPKuBktIc~J zvtn`}+vO*c0-+&k16B2{U=Zn_%5f{*OZQ#5-d>@DlOo06tQW- zLZe_dyr{AqlY-q1GX?hXc)BLdToc7laFM&kL4InyRW3)q8nIA$uTf8|u&*+8Y*vyu(J}?!YOyVW zW2Y?`5ugn()eAxP$gkq&QKeXM!eqO2)&Pl)h?9|0Dp5}SL=RT?l4^Rvxq&rCc)A#51FNj3w3b+24P^Bal-%SHGB=SEWn?xqzS?9Y9P=b!@&)+F-PSr zir_T2iiR2vBK2olEi^dXB3l!r9b9X>k9mz;Oi1p%U)Qy{-7n{@(Mh)vd(SbW_#tF; zQ>s3Ok2wabMTx_P!xDYXmi-iac=oBtgD#81(JTl#>~s`&BS#WT-{Hoim zv+`}=pV3Rz!H^_V?srLkST#IXJ=syXrX9IYGN#rt={pHC?w#K@q{aV^VClu&x=4@> zo4nj8vGvhl*~#cp!CX5@0iHgZ?V~7PrkMgQE^(4W7f?qDy+_q;@GOx}?vfC|!RIENHeLu`>QtgG1K3R>p&})^Mvcr^XN!A6M-$5$!`~G*bw+hb zSYMuWLfzA0-X&NiD4sEW^n)nFnK~AmjojUUl{X?fGpp0Bb% z9;?TGsyzbEy`B|SEfQ2VT|ImcZmamPbMQiS-pTS!?;z_Lch4kh7(RV13;|&UE^O?n zW@LYyDqrvyCUhumbTz%&Mj`$**b9S5iy~5nb$A>*?fA$82l(6|A4D{fc@Pw${1eZQ_bY6%6BRT|&b z>zGBWl*&q6T@bvbB*Wtvf3`Fb6(1FZC#IIyzw|k#@1)oQikI>_rJC@^ppN=OVv^)S zWOaXvFoBuLGM1lD)O)PxX|1fqHc>hT3LNR4)||tFRMS2#Ns;P`O%KD~>e?Jq;bG&2 z*(Bdfcp?RkOa&TE7~lbP#D$`alFT@wC_bQV5sSqs7#Q*1|HpmfedF`Sdt)iQm3=~( zFoz9b!oHNyN}-ns9;{@gs%c_exMr#Sun`P!Wf4)B7j%XQQj4kNqp9CM+eFpat^z28LS+f1RkXV($2hS13DD_ z^WR+f?d5;@vfqB-*OMQ!Oj!f=b@7ki@DJbi-+lA{{2tgMd^^T`H^zQWQCbDtDY3Uy zy(!C#9E!+_;(*9ME6HY4eHQTX+sl9TvX6_eiOb8bX$S6QU+>tT zd@sEBliK9rT0o6SbvwJkX@GC6t^*rV`3|;jt3#6zaMzF!urMiJg!ma;#3M8=AJ?vN zlGMU}EV@u$xzOV@jESc0Z&P`mfFgrw~5WGPS*)Dv4W;tobP7ftl7IHy>k6 zj=|w@l!1B{_KWr{aBoCZyxkiF6DvWa#glOXMohwq8TiQGk%bEga=g=YieJAQu3C+I zOCo+nyl9tU(mcu!e3I|9K$Kpm_H$x! z4cag+gVTdMv&A8GpYBIqlxNc2v>@*khN~$ex|=9P9ERaoNk>g)Se_4M(v)a>W4QWD zHa0~cZY)$FpGwd(icIYSXtA=_rqG1zsq~vuKMNPV!oz|Hx_EWpj!_U^(G|2rF|#kL zd)21iWUXi((hBn_pY1#38NdPIOcs_QEKh7DKP;eqU8BL08 zdKlmZe_?!y>)XCQ*8O>{7srcnXN%&4@2T}C&tINdC=gSnwDaJJgH8;kr2#-6s=G&o z1?`@-PA9aCE)5yeyUJ&t!%=yyr#)jrjZx{aB=FFtG`T{KI@uO3m=-FuCuwhhdKj#B{$9YLsb@Y zeg3K$O>1E*&OdUIdThiGvZ+hig9SoO1K(qu&C8sT_B5HqW2@+nrA%+lKfke(j=Hhp zH?cnU^1a7iw%*_#yvY?@>Q1JxtvdDBQ5ooX4^sraTdvqONef2+gxcq0sor)z24oTp zP(#b(A9H&?MX7wMfGG<;?H8@9ftY|gBZ#1QaC~%c$fZD35w9j>{e2AC8u-ll&F zx@YYUIS6y#O4_N$8^%zC80d4vpWT92=97ke-9wHIf*FgHh`47B``q;>=1u1+@hv zF|hV_b(ZW0+}9g!JeA511JzR%q%R_a5$;2=0tLQ%WAc##o%`!F}N;Y?@6 zGAUy(yw^1|#>w5*TE$eYoj`u}BD7CnmUixhrJ5G81B4HA_r3Rur9dwbv~o@h_pxJ< zyyk1T5gg``brb_-Usc};^?$S(Ug}4K>CIjZLj=~|Bxi-%m^MN4a;2+r(ab^br+hH( zXO9zi-@HgK0z>Tx)%jG8k=hx2uM_~jqW7a}nkjM- zl(RP}W}iua^cwB%NRjs#om!{V!1^sE3+ZxwEmt<2*`^EllwCl_qHZ z8!VQT-U&xSEQL^#hN_c>%c8THEJlJzIsjA{;!Xy$%PKfx&d2=PsTYe~igILUU|D7K z!0v*m{C}OP1R!oJBF30pFngNP;WvLXKV0`^>=OgOJGAm&Ytm&zWYOxb@&gx@sF;}$ z`UgT}AeJGXVm>qLu03VdyQQy~MR;7$H%tJ=YHlN4PaHg=)IUlKjMV8=fRjl|R;;Y^qO%`(%FC{TUxR-)yW1xY8!wmx6N9#I z(tdr}KP0EMJ78hgGFJ>I5`hJq)f~R~KO+pm8&6RntK8D$!T>uqSy7CYz%>Ff2op<9 zZZkL8POgw;r{^A-8>B&BAhax^B;OHyO@2`$>1|8cD`A&=CATy&DLqNQJ_RO9;4U zq7$?6%ATww0Gc?Q7+#Ijb<~4N-=9f3@aRSi1_-kMUUg5UuDjKgm(MrNryV#0nN3$}2 zMMu?BF_PD?keAB;9RUNIlxQ_GQ|Fd6xWGq!zBR%8SDAGx`D7?mWhgF*!N3DZgx;Yq z$1~TIX>MJnQwn(EMZ*^Qkb%jGSJvXiuM~f+Gblw5^!Q(p-kL(KO=3~~a0c)d{$sWe62p8@$ zR8Wy402u>w;t^t|<}m~B4il{K-vqwEe+WEzVCQzQklzP>A^3t&i?0GVjnHexOJufv zFNNF_u^eN_n8L$g zB%+QMT$K&Jidc*Y$zuczk13NA%`N!52hDB(BDT%;<9C^S;9zvI(<{?ROQals!PUl1kUU zB&hCh>>jp;*)W5x^hVA)2oLkV7S^KnYV2MBQv)xF?}U_XT8}}mki4GPG7({mlt{Za zDVDY1PZAl?kWFll_l@|}t(Hi|o*b6+5`@)Kw;WgFh|{>Q0H>rJ+F4U|WY_v!V9g>F zG$Be@ExEcd{D^}8EffdWbIII;sBMjHnAaeAKEw_P|{3QG28x{9ek4T z=xZ(sE!q-G11AlN)_*G4F>sC47!{3F90sz9E1Pko{)qxIO&2E`we@n9#_@_;QnR$Q zA-mdUhQaVzpYyr%dbcl%_Rl6}6y$qyLDbI8R;Jk5=M|6sdT}*odrsLSbbGam9FB7XsDckxG~1P|eH{ zgB`Yt4vG{;2SW%{#kK`Pi0BYCH**syqoAQ`VM9j1mxlJtT-RLp&BJa4JcH8mJIeYC?&S@?mdQ|itrelFrLF8jjiQ32=mB=&DMMy0 zH%k>Y&oWb(nG|Ucb5H$QkH%}gF1-S=VuPQ~B`w(wjc{nmdCM#<19BcvV&Q`HaLa^2 zqjB%j^-3?K_QEZCqT0KNu&Yz5l!DEK>Q2q}b-DXG;o`dOU~+ink&X)TYNS=_Cpt;a zY(eK>xIuV1vQsHr%T7l%vLXCVj;`cN0GQ-R4?FkSyP8ucTGwTE2=U%IP|d6r-lx-Who!*S{?of=`yFYaKtDv z)2_%Rc35F?gLDi%9n{&KsFEPo$JA+BUV=cB_%wUR!6=!^sw?s!!4r{X2oXAjze&cd z=z5M(S>IMHPv1PAid(7IF*=y^3C~GXm323Bst4T}c42cWx*HO*I5v1RmnX)Ym)Xh| zcD$kls+Ss*p*RXL@`c^Ez?O*%ReqtOA{uZv(x+s;B^Oz5&{M|;Qp$X`{(~bQ-C0Vb z_gx3aZPKR_32I^|sn$PNL~Yl+W6+9)o|P>$K4S{z7;CMX_nxNq2IT+6rigbNvz#@C zEoFus5RLE`*6sF=$x}b*s<$&cWwR(L)~egTRz;-1FN|)Ul%1$3vPr~zJ9@8b(oxSw z(?1yEhCMzp9M!7l!UPMmQ9iKy0SH1>;Q`B-iw6nd(kb* z4n_krQ0xmIZgZQGe~nX_#W*LNBxiwW97(q*m?>NHqOf40C-TuA!if4$^=!~>3q zA0uq(E94IXEXOnZMLqHL10OeTRND-{fKlU93s@oVP~Fyoup-r}1D3;*7+^;(yy1Gs zqpb*UtQ?RN90#}3z(8?bDm4^IZ5GFXL-9t6r7&F1#r}&yZ!R3Be}mn^@)ynjq1erB z+}9BQwP`Q{XzZB94b`C|7e@A}62XxF?IC}A;Wuyoi!0uyKgx1n?vM|c@1OpMul8Sl z#_Kh(1{ZLK$_4Y^|I~XVNN#0FNiwhNg)*#3K=5b5JI)o@ z0A;@y=#p0&0YJ)xnHy37by!px{2!YkKL2SfJ343(cXn|67n@d#^IAe;t%LyNj(uA4y*f%W{HJm^=<+b7Uprg zIO3^zn8W}SFO{oqJ&=+IN5&=>DLXyMYmgSdnB(7r)|QW}_`D@pQ?cY4zz_>8z+4ci z=9JB#gPbJ-u0J8Z^MrxbVv90ZGS75sKm}t+%!rjitjW~EyGmvk&7zSw$$#yxftAWp zQkkB^7aiy1T!aCyCdwJkH`gKp;3a7=S>2%nZmuJs0z=^sWe=I#Lm~?gO!rMp#YN;A zSRQlAw3rja@Ce+7Z-9aE5dRSTDDjGu>c$U&AAm>ET@VXM*ewoBjTo54@`(zjiVUcS zI9%lo_}1g6(|(yaVWW~ekL|j{+WV9^`)3t~aHG>SkdhZF+nim_mXz*zay9SsA^`jZ6;Rbj|~|d(WlUQ zl>{{96kYM|-Ex7hzU(UgLdV*~sV|#^-#jE+o@BrGgLI0%05QosuN6v%iC`z^vye(s z8H&IifzghIjUhV_5<|u1!`>mU5)qCC8&H8U8OWmV6ie-Cje`Vgt})ZR9=jK&e&zS1uW!p%79u$_tix_|5ZHD!IIX`7f_gtK?DR!_1x*=#tssEeJ=p<#61gFDUei-S7qW|Rr?78 ziMtWa=qpza0HQd_F0-jw5yh9!Q@!40C{_MB`8Z zAM3xM$h|&$D;xkFiWNYwi4DBgw_B|W-N$v17OER`8YiaEs`e?WKT1we7Khar*6R!% z%T0jl-N-ml7DsD=on)CQjeSE95f9&1u7`Wo2Aa8AIxHm56@+us zwX8_K>JQM2<~EEv%lD%qaIW3lKD%!vK2OQ0ZuCHLpX@wW>JV61QLfNsOS6G0N+Y-s&tX zLAd9miik{2nm@S;5t(!5k`rY&S&;zdxR%}SF{Ta~%hw!N(>4G{K)An;aV;}Z88TjW zUt{JX89GqQb!2Mb+_3I`jmzpp2M{X5!m2?O;RW`F1XTw+;vIx`XBhOw6&e^y9)0Bu zycwpZ!25I+ftRF&GzE?y*_9;}UF}GcJ4nd5_ldt{om73);$eYE5p|(E1WAXWZN%5F z$dkxb)WeV$=R__`<;F9Y%L9k&X@1A4%B^AxDnua|s?5ZSy*tS1JZWHNJmxA9N@$nf zaT%rgMe*s?ncqY%Ja88m?dSYDWv#sXk8yp^my27eKiV zkKu;0oOW|W*@JCY3T~mGBs#@ZNK;dw0w#bPF^dBmPm(M27=|O}Cc{2i zLK)%>GB97TMW!d8|1K=d##WXj%$+)+^vmH>mMd}+rt=-zoWni3LPBRg_UOpqU?ov}1; z+N&#Sk!U~-GXC+QfAhfKT=>OXd~^9j?BP(TW8A>&#t$$5^T+z#Pw{#wo?~k-@*NYV z#{n)2Nw_lqr2MXop{u^V+RxNd93-t#Dg#9tm;L=E*ML0Z&p)r<{b=|Iyab+E?_eD0 zk_(EE(oD=roaB@zemL@z1y_74>&wUbx1R&gz^%+NNx)QGLhUl^4h!8FDR-j(!;oKG z^38>}8E+Z`5D^Q#mw!C*^^Sj8_>%!X4p~xNJPT5V%P;%SaL3hcquq!cYHli6asSTHFzMoJOI*R76#%5mcm9syx zeNiLnj!3UPt;V+6`!2uEDHRPsejzd>F2R(@J+~^0*Qe{taa-IA%kgsj#qpE-e_9eE zKI67J|2;4qx64GeaWT!%C~1@*5pLBFx!$+N&0(m1U82ym?uskXsLXMU{M_yG$IBa0%*0x;CL-jkq z{xa5cyg%N)8FuS>jrH>J@?j$!Hlm{cL3n_d@BnwL2;eCE%i+$@^HB6yk` zY(-$oP`QhJjuan>yBOp`-0}wGTqs;hrR|wS1x2*ygyK*maZXRC$1~Y3?}0uZI@4oB z-PbW*%K8jyja0*~(P-nzi!!n7BLKcPevU|1LV-KloZAyRgx?1)gIM4>)#`U>(>h_&wwBEo- zH`Jk5U}7SgkFG$-U<1&zp~(kGcbm_uGy_X! zyXtz+c*ChcmWrS4c?6JzYnsky5%3-vl86aUySdV|s{WF65Au3n&H53GBi)+A`@W)e;zKO)U7$!7?wc$XcqcA%41%pgq>{V1ghj_e2~hgh4ZVWHTb(A90)RxZ{Xl`6H`-19w%u-(MpXep z+88E}o?qw*HpbDxB~6}krGF~Q&o`1*PG zT!9!m8W49_K2qtbDGf1S$zOVy%19&h6y{n~&mj?AW^N%GI>s1^h?Sj|Zp)@Ypk~0A zMPS`4ifM)pO@UzQ`~CJYhMCRBB|un&Ie^#e7Klz=_mU|R;_h&lIWsc5SpY-ib`z1c z4C4;Yp8x=W07*naR4I)DmaPN_^8_)=60Y#<{~}Qxryyo41K60?x^Ee(s`u;8xfpW* zF!wMM6&WL&T3PrgH!q*#y8V@=ZBwsYdFhU5ZbPf4T7V&A`8~&ksJo#`cF@*s0;r7C zh7!5epl)CL=dXLDL6a~|Nq4q918^;)Z!fRN2?bEqz?2;^O{vZ;*5MNV{5 z(T~$~oYb-@63H`-ad$yUt}}A1Wm@j0a^!EDYsq86tt7>H#N;`i!N}c}@~n*6zRSGW z$EQhgZDFo=YE+3Xr_$Tp?-28-sKck0|6YtH)2wEUn-THMIQ2rpy7i$-!_ z)k-TTxI#wuYHPFHJ;a%NMdUuOY^FbKCp1Ml=1SbBM8JH5+vzDehKk5C^NOKmH<@0d zQH{Q2*D=*ff}}*3xf=C-q^!y@I8;U|F?Anxs7$&%(ECop-x`BO0eOmp>=j6vsE3`0 zwluk#6Q=5Jo!51~>k;L5nY)&%+sqe5$ES@|2eh<`K2IH_v@qXAv!B~vG0WRmV3%0B zOLL(*kdxrw}sm`;q7D3%3A|z$eod$;Qbe{v82fVO%7uAvbk}Oq;&h1Cj zljP&h9`KJ`(pv>tataEJD0bdZgANB$+%7F9$4TY9^eMQuU@UZ`h zB5Ycd;hMe6Sua`3pwg**5gjalrp~FBlmjV{$|h1#zge_X6a^xDU^b@PPqZoL$>Puj z17u@+Cx)O}m9K?gVXc=7c1PhkAS3p)Rh9U?l8xoRp!LO(;Zi%pGS01HE zMR5r(VBl@yA7{M7-hr3NbK#F}|8^T*h7ZSU;dSG+Xc;#UkLqex>_;UoU}>A!mdFTlL;`-%?@LpO>_L0CRawT@>KGT}oy zTPQhyzt)Xn0+%LW`hU6f{Y_2n4^R7ld`;?zza1)KA`H$5AI-nC5*!Bz%ys7^3(r+*Q@*eLqeRHrvo*o}he0|1G z#YpW`3fkNTVfrIc*&~^A+H8}s#-K)9x$vmK%VX{n0cmjklj^q;mxm)>7S+ONe_iTY zdR;k28@>gfCutHxpH0q47=$u|uAVv>tWfJ&x+MRIR6&oy3|UU`a!~M*yBl;P^g!TE zBzw=*WkL|44p`{zcp6?So-ViHN5@YdF#NN}--gW0xf~Xl0rwb&TSX|C9ufgrRrDWE z!JqeZ7!0)7vx?;Ei1C-Ix{z-pzKeJ>!0}VW)A0=aCZGaWKoQk(hE8C4!OA2vEqw(d zPMbAs*H&eC$)Nzluf`u7f9qHg5;7qQtca&K{nOaPnFt6Xo{rnFEG)%Ffe10gln_GJ zDltW5hz!UO6}(krp{V9pn`G**u+Lcs*LDfZ4!5zU`m)&}bRVP1P2>`tJzP7oep}@< zMIpME=%BGxgD5-%V&Q=o#I1mcU=u?~j7S7PY`{X6VOdPqE6i6unmi?jfSN0*Bm@qR z?-2v?MZ_I2T2)_TQ|T<5MKkKynd=a>%te@P_y{26R5g+n=x7?R{KNy`J;B#b0Ji;{_Wf?+D^T zhtokjfzW#|HXEqY@Vb0f4XD>`o)F@}!LTNg{@EgB(}%v7mSH{ZFqv)& zYu*k`!`4Ny~d2Q2CC#%#%d;k2wc?W+dKR?c8*{NMa_T4E~ zm}x}Sz=>0woxe!wxl6c&4>v_XEzCuhv7b(($5o+AL@DYfo^2tr^*oCaJ^Sb-hAxyq~9!#h5;aAtQK}Nf@Kt+Fp`4`# zN-9lJY{&k6%r*F?DH`D01Pu~d5?+g;x__m+9o1lFuB5lYGEpz?Dk4I4cv#6j!JbJG zl6`ehA%k8*LFGw@DK7J)=?J9S=cEED*s&&Jx3~+^mQBBfhbN8C`exneP%aD&OY$Qp zkwwx8k!;5~A-GxVYDe50SPudfEGTLq_jn6;_ESp-NUx=@tSv@?`&t$81+bLgT2K{I zP;5lxRPvM|7R*a@mi$s9*oZSL*wLl*qwK|aSWyxOvYru;;K9+$GD63!)(>Qa1bbKJ z5!J^B>Rbcrql#qV?D%G=>SvE}>(DA?jQIIm8ZgM^uc_xK97(G)8PBN8Yz`Si-8?LGN{SPDBX4@R9v%%Ih>Cc*tF7C{HS#JJfR0gN zt+0C?G9tn~%zS2w4!UYq7dpx9b6wRRIz!RAO{%Aqs}8f3L!qOLTjzBxyJN+=mkyPw zcCR^V)w-2BS(A}6*D(VaW2g+1EIAT0pJPPCvRgzfeCQ1LGS{*WQ-dLUM^&JWsbjgj zFJ_eqTqk94>Qgy%nZ1Z)Jjrx0X%f)`zD`P&5}HzFbjqaVEb3{#>U9k{?=p05(@HW^ z_z|Tcp)Nn%ybtc;)brGU7KSNWS{f2vQn8EN6^|%TR96$10g?4G%0L#)h}08oR-H*4 z$cojaySOV9huV2~mcQh>JgYYtO9eR*Qk`);HVD9=D@;PkBUZ20PAMh#NaYg!Po5>r zmwc_L-Il{;2-$9PsLA3n5mg1!83_OAh*~Zkr3DB~4_9VlyNYC|DJ`b~Ndm;C@~BhU zx}trwWON3B+9ND>n{7@J?=w7brA^`X)QMbUSk&j#6R|Z>5@{ew!|tH!?jg>o7?}ks zO>|;^9f=>w&a1W9cI8)J#E`ma&q%Se(ku<&lhsjWt!u7`v|G3BTiqsAsKYJdr(_QY z_n0he>4eVZey}X0Z<%D(@wn^kayh!&$uI*|c42Elhny4CuT(A6K6P)atzY^i7HIT+ ztjsRAg0EZJ^464b#}S^m#v^w6GQ#yyWLOr&&y$?blNEFEa87%yJSS^{3e| zE-f$JEfbnLIVz^?7q2F)l{TeEDk8#xrT&`hePCFv>Fkw4(nzxI!r%*@#MOn7?S-_U zk*a4*b2%XpY;cE6T7-=)3)tKV8?n@tb?jo>ScnHX9!Vo2g49wDekD)_@e~1OwI)*E z^IZl$tv7mLMoB0}Qb$kR`MwLDP2lOhRt{-L@8wLt%IL4qFXH2U10R?-%n={B0leXo zf&+A-n!*@dFbHd^#K zNo2D3OdI#{FoheIVF_O+_vtD&cM7Ir#;+gxx9^a5|K8Rge!lUO;pR~6?IxdaxBT$5 zMcR(F3)y#Z4qS>0c*H-v>ubcHUXE8_0XK2pJZd{+fv|hZm8u_}l$I+1#XrpP>r1{o za3#+wVGwgXpZIwC^BG_BD|}~fecW!6BRWDJr(pu(LXsw5w%9n3V#lWF5<2X*UdI9V z-+(ulCqgYQi@W`xASJ13WnLW?T*Dv-pS|%ksv%HMK?g1;42ljxNAsy+o`#P*(&qEe5r42~Ro}nCr^|4-Nema0<)VS< zut;(!BE%)`2wyOhpCG^Qhshn(&8`qt)zQ+xz)wpqV7ZIqk9DO90bU~SM7BgQGpi~K z!_bga?rFjIazTD6@qMjN?_OST2jY z(1lW3<_nE*le~V2s*Dk1#1#3TCH{Xc-EgV@LW=m;0XKe^SDj-WA{)1`WF%DKAxp9m;Ha|j2$eD5E^iu;aOmG@ z`~l~R;#@Zil`1IpZPa#>4d8*xAjL`)vi19KMa2qd)Rinj1!J{5bvzJn@L$IGqr81T z@AvsOt}pX(t374C%pTWp$?{#;M)IKejGePzg35rfOjJ&EyKZ6m+Fs?DLCMr{F{xa* z-AF1<95CoMrx0QmyPjH=?&aQIJ*5oTKt^|l+9mK^7Re)By23121*noX9Gm_MsXE-0 z+-41=1|(VY3;M?E_7S0R-g>~M535N(POIu zNGZUvB};{qm+mtB_Y+z_4ij2~Ho{9qh_(!6+q4iz63X4mQKqb@cISl#5Lq2R>Sc+u zOZPVlGL;p+BThY*O0!}Wj@T>*2XIz+1v7zOBK;S$n6*$^vWfYPBtHsA5b{O|uhHzM z7JLqf!Kh^s5QwXedHI>tTBWg@R^tXn9Bk%>z!L~U3XArW@49fA_$sZ#pl?Ua^@LD;A2i4$b4dU;M6$pfT_B(b%g7pAo`IHlEy%Qs5Ndj(ma zzxMGfjZu3MHP4p^R*<}HAQ2@&O+3X;0_iK%bWlkh9$R3*|Lkj8M{Me(-Wf#L(dgGt*Lr?&>*2So7YZ z5kAMrfL>8=sI669ybfBWAArR*^_5vCSgf#Ws_#T9OS4Lqip@Jdr_P|diqp@uGEv0t z9~jI{`|t)YkeR8s_i;WS8Q|kHMXHtAq-e=;}8+?ZFDf+|uRGU{&N3 zky`TeF4`55LFmf?r1H0mWr7E0lpI(f+5ZsxfG6kd&bqN}JO}`lY-OZ22*$YBUT+rx z?YJOSqa?EP%iPM+bu^XY#PQ;3`uEpSfQyn9HHm@IaoDr1v|y?Gd-zYdXh!E*eGLI( z6+#3e-3cS=30QzWejV08sK}UAw92Zpm!?Lr`!nrpKmae;RSl5pFEvUq_{pQ1ITw(s zY=SZfYf3fOi3GA2+_40B!%R8u1qCjvE<1X3#;P_j0=cnJ4BTSPzK~GCg&E~m7ec%y zn?C_~l`GvGrLvO4lO#tw=^`Oeo9|c}Cf`;61>g$g!qlQhk|KhapGo~gUM=t6C1)Yw z;S2YLLE}CNxItE*k6Rw31NvZVu7Q~bK9su^e=zy4En;I6iy{nA+N@#$^VLX+)t4?9 zzgoLA8yw;8g`mYuq}ELoGa!!9etn)`6wF}q0U4@aHGa3&x(y#UzCQ8s6(0c;#GsJw z0t9}M3!E5HJ*CDdWT!AqLl{;!JjlZK8@Cs8;|5dc!1ad58{P)S4a;I)t6_=cNG|Aw zG3v?dSaWWRsG-;lUxUa4vsXy&AnVQw`&&vqz}ZZRNikAH@JGtglH?2SN=InPS&9z4 zsZ8QJ%qO%cDD#?njsJP-ch~sE8@`$HcKQ2=_hDC!oI-So-YdR;+FyQ-fBUfi;{&f- za3{DwPzG3JMiG2d5|ahfqin}=1P}Tz7t#LyL@6Cv89TPD<*Kkf=odg65J*qhS!Q^avT2aO>|{ojNiG! z@e9Snw$lfuQ<5N!(n6n~ERV(~PA1+20v z7pRD`pPH7^E2b1jHzAYzCjaQst71o(a+XL~OS1WFpiE!$n#@~&|gg?JlG)n!Eprh^NS zhFx$^6s+E!_F}sRpJeDBN4M&!PHMGgsaggEASmyXqeDX$yz5}w8-fwFMX8vn6 z-6W=pr_bWi3<0WQ%ldZ{?c!=G!$7d@Qz3mvfT(R)oE%T`XiZOdQbdCQeA;@9g7H*h zWn9j92v)CeH#WK>X8V)sgk3m>6YJwajOJso`#j-5`Gmj8w8!ECm{me8OEJcnEHZ%g8Pj|TvIiX(9T{z#nkZ)54 zQ(f-dm;-;O$e1y(DPwj=4nPkpDwTejZAKKW+0qzwl zRXZ!ft831NfIWL6aLw1#ssK_96CgUmY{Rt&UKH->>m`SuRd`f$o;($74|R`=NV{Hl z2q-ALV7C*%Il?5LoS<_7Zn}dm*g?EmUh=*<8u^QCA+ZGxwbJ}JlDr&oRc%!T=$gWLqNEcAyTNL6KpBP6of|mRA)UJAO_?WbP<*>;-WlT5=&_%ZEU3({@ zS0I4MPV==ZJzQm=TzR?1J`)W@H=!jLlYpxZ7KN82f+5b&?d++wg+!ychec;T@p7ks zsxDtcXQmCy?HVcp4<9l|Q5x(q?{n%U0ma0NkkB8>%w}g{jtXRZ$&oD^t;_>WF14zj2I# z)X%cZQBJL5hyYl*1|nSY1;}j&u*~L|ncazbGO5DAm>_(GZm*Ll5M!v-&3FHktD-Z# zI6{v%%**ZH5$Rkll@NwY=>i)1E{)etNk4HVJk|-$cgi3p_~SuP>2XyrYce* zjS%wg;SQ~2OjN>>P{;?wm{X5RvMdOyY+wiN1pev48(FfjyDaU%&Y9GHU&usVbrW5v zVhWVXu!mkf0>0^lI^I9Q_f#}@9;qsTRa#XATr6tZ`q`dfb33aWglAun(5|IM@n|3U z+(l)%ib{rDRCAn?ps&p#yqRY>*aQKNiK*;0&uF3ipT|-qn#q~9G?QetGc=Xn97V%K z-Q}?JFb@a6trW^CvVCL0Sh{y0Gh;17y!!Cm_jw*T{q8FFv6Y^l=+ZD9I90_vA<&)3 z7pYSuP{JX6) zt_W0Ba8W7e$z06dcDyI9C)Jm$&a9F@tHKFQ=2DA|@8pGeh>8aUCa@|I*k(=5B!NWK zq|aNj4hL$P(t*m0lU_=#&wyk*k;ab4r>b^M>#A%?MY1QqR16`7?-!Qjtg@GSp22=R zQO9dFPuE%OT9rTNig5y?H{GrtKuWtNG&{0*>72@cug%s_wV9E$fkH2gsMirI5zh$N zfte=ETI_-&78dr*Mb9EdGf50Ye%hqJAE%=n^ID7660YoGZSaBN^qu1k!^!!LMjNI6 zVnR-lKhtCe3Y7*B@3;h*e2W2%V&>%NY#;fjFRWE8Mab?zpo4TmHwl{pCQapUU;J_4@gW+NA5lml3{ zm7J$#vVXxnIXytvgnhX zeJ}n=TS?aIXFv^h3OJCCFxfuM z`T5~6gB>%QWOw)A32OBw@Q~{f*O0FkAFqY4fh90bdMzixAg$#r%oZuKc`#IBVA!`q z-Y*Itk)6mp}ByF%zd67>@`)i0h=Zg{0{8f?3j%tx9qFqq^(Y2&HR zquFcks8=yWH%%#i<%S7DDfnn)j3Ig?vY;+ukQHHprtrE1guC9F3#8 zxYpSO(XjxT)kCf}Y_it?6z0{IFB1|S5dX8_3t|B;i7O%^EMmX|L!|5>TvE6VK-?pi z=CIuYg+$iIFh;1xgghc%F83U2>$)Yf3ly?37$a@|TA6i3An=p;Fofz|RfvFz1~f}v zwEBfX;u6RZxESCj{}b?g_}6Nn1+oaju;6>2_MEWHw-N(zRP)m4%~hX4Aoi{g_EW3{ zrV!xD57(bHzGeeU*w=Xb<$7P&Yh2%4j~DC(pO6>4Oe28Ej8)6rssMW5Wm|=Cdsgz- z0@*ihQ7HrZCK&pnT_<-n2;vPB^fCas7vZ&_ZwhE96$VKNBwDN>0Vi457<63-J_8U* zx6(~+(V9DdC6a(7y%WS!UTC+5|Wfk zdyr56NNM2YoYr0+%@uL|eDa?;mcbri#_i5^JIxTV-x$NrUvXE#t4f!6W>st@TA5Be z%zm@hclUbSHWwyb!n3m!JVdN2$$9H9k=fBigj|$rR?X*2ZYwha;wr@Hq&_W}3`z4& zLa?bBi(FS_1IUT1BDqZ+)p&I%XzzX0;}oeNUHMB{)n2htnw7-KS%AAq8j<$-uX5%r zFwcHVKviBUwHFx$3bzyF9iMwhBBFd~0%MHe#^6)876Lldjl4)y(9oEypCKRE2fQN6)Q_9tR&slS4kdvQ#{OF1#y%Mf1UQ@AyRM%-3<=pB0}1DQB9D zEJ-tS`EMpqrLMX~P?0|<(FQ^!r(a5hQ9V}zfLk`z7Ip(ct;K3=Q+$2nE2*w)Rwja& z;Qvsxn*W}w8cm^{Ee}XvHrCj_M!ngcbGVIcdgv1*Q}wFYm)>q2Omgu+ip;APJI?N6 z{GE}4HZ-yXeoOt&I3S>NhZG>!@bt?9bYUXg`bEmIkvFz#iS2$@MKnkRRs;2;< zJ4kIBGFe!qEm$7})l3<6jPgI5QmP=ZE3hF5Sp=o{NGJCmU$(AJqJA8%@a6PY3dX@65=Bz z&ttK;s`QrhT5Z#aS=n6zVH&}I`n$%5RsmNH4|yX8Gy-uOlL6 z)@vr*L!+UtTH9sRtlnmQ@Jq+->oMjv9_BVR#SbRH&>?7tRdi&iW7xYIYE$g0O2tz0 zfj%gdEJX?n9yA48B+(0~&z-IT|{Y=bQv6JUH}0%IwA28H?+#R3cQqT+E_56ELW8lXNaFld8W#6V{zN9kGR z`CJ$=4y)B9ZlsCrw8KUE8>e?{CI4L#l8FbpaD>MqAtl*NQ|3FZx?v_zyot)zJyn~b zp7*R*kbut`m{tz4+Wb6&yOzc2T_$8ENYAz}ZE_nU+(0M2-QyqwJ+)Gt)ZK-TEK!wK z<@-}fS2m6*0N=?EuO!M<^|{WXVtw%<;S#WHOvM&tf0b~w*C9GnA?^?*H5Ekp>{dFx z14Gqd>JUru=1~D8beJW{CKaih;>y#ZvU^1)-*r%z3U%9Lr#DJ2Vqv=-k1qZd)^?%q zjj5sDO&Voq15u(S?&-Ewm1;#;r&`7gCIXu{D$zZP<*kwj5zWFDl9=96C!0Ou(+!6n zfI7-l7eIuL352`%^?0X-3;{e!xj(~22f>51j$f*Sm&Tg!(^6J8&XBQ{D|_d02`^Og zkqE1cU?iP`QH)MKxnQKaB}2`HAtg3~&}a_DW))S_i*42QRfrL%vd7X7Ey=C1MK)Wg zl(ZI2b@DD6hkak*la*8cTD|%bCGepaG`7Q7?UM9FQp7^M;DHszVcUAieAgK%! z#Dr3evaiZd_ye=)qMKbhut_*b3ArjO8wCZXUlULRhZ&=gk%Fc5bMkIE9TeZ2y>(pC!s4E`cf>7iwLm!yt)v71ka*=7{# ze0$-(@w)K>{Jp*0~7NLzP#ftdqlmk zQYzP%gn=<>z?gA_#8srCF8h-;JuoolZpdBBSgHk7DP62uNm&#gpX6~hIhW!^a?GQf zS0yP`SvYrS#1k z`tia~xBu~H`{$qicTeC}EWm`@L?Bj1cAJ3+etS}DT9UX}yFfbw{W9E-qLaN#`&o!B zf(cAu3Lg4@dzW9o`S;7d|L}i(%6922qA0Tp2Lx`_1}vXJnFQ3w0+KDw2Hpg3@!Nqv zz~bfj$?&t|skj9=3e}MuKZPbCdp#HHmgDQc2z?B_uK04{nm+UF2ksTGCw_RvPj~!( zS4J?YJ`5SqYFQF={)8Z83nlqR{F{&=o;>*1%|Rw)e{kW|jHh9kLq1i71^gp#3cu%t zf4B5i@(IUmo>e)RIhEd$Rw**L9w3jW!Dl0M{#UVmA4yDei-Y$)v!384A0`{8O<4#J z$yN$0FhV3?AxS9&22DqI~&6|k?sJ*-9}i5g*o8Tv}|2@fQgvUp`l7%3gQA%^`su#*U{U} zl}x7|D}fD87#WOKXO1P*TlZQ8;8+3;OzP_Ls0w^Yya7+(t-`LW>&UXhteX;nAl+^x zWLYyqq5?HskcTqEMT|_NT<2wPx8QPr$q=aH0)7oP7|ZzFl&d&QO!d!!FCGtzp&=$x z#%&@aDmBaKHK8IC_=2S2;y(=euVA%a=O(>ex#d2y6H=8oPL3*4NUf{}P@3`bbEH4Y zE2iaHKOIYpAr^sZ#6UP=-sAo2<8|3HwCBSy70|H54lAP?U93S zLU~w$QXZl09ubcNTq;1K+K|jS2>TbcldB}918E4E&(dYx`_SI0U4x1nL(9h=V*uW5 zw$m6;n=VaSo^3WN3n)ipZ$i@9ZX9f&q9SHqL~nWsNx?Wu8_cdP;M8rV1suaJdM>!I zdihilLQz^|Vr&^|9QF#_KxZ;oEj^Onk4;=vA#g_HGJ`slY7lh0DPFUB#|05dwo5ST zBeT+9s#6@7ilCKi?1IvxKrE+TAPc5@K?%Fjd^$0R!xp)ft~)$p9o2pbRrT^J^2MDZC(>Q+4t7oJ*(n_f`Gvim53aE=-N91qh-=Pe z);6Xb-XNgpP2V*x=zUBd-dLwB!`-_hU7us+3|8CFa?&k~C;mSBR0k{a`o%LQa}#y| zqeWqO6oDt3VPzlsY6~YJD{N@Cj|u@+_)N|NA)-^qvL%`ds$t=9n61L!M`fC3OGMl; z=LCHPp%MvoUc>ICLor5V3wcB&`{J&uI)~)V*lQlMOPn*6!a?uZy~Z_@Ar$g(S?I1q z*Ynnq>|2afP-KpuZJ(AUwC&4~EY#32k8l|!Qb{kq6m=w|`Fg!{l$j2B3|U!ErUM?K zVQxd!+=prrV*o_vn76N$PO=desjP?=wGS1Mc42`dv>5AML6dhJ4=>J}_<5cSQeJj! z(4*bCOZsA1+lD)i@H@<{iZjTmwhH37Jr`4wgh#oZrxj|F#C5ks#176C5VCeI0*C4} zgFx^Nt4t><&-_Hs$L5vuft{VSDrZ*7Qi;mYNTci`6WL)m|0tz2Elzcq6!F04Wd^ag z7w?l&PNZ9op`&u4U0qbo;e;7YHN$hn&=To(ie2o3=!zBLW6X;1L0Ysf3%r1H1s=hS zk+~wN?0FB5z!Ww;!Vv_0A6KylZj2ttdKE2)G+Ki8_tl3q@{mUwGx2?o+h(Iuf6$un zy0}uzZsxO;Nv(j3n!m#bS$0!E)Pe|3RoPZxoZfwPycSpwAz>JCTmcTHHuDlEqCxpD z_=3SrQTceD+kr4MVo4pB^TZ8N-Lrng>%NBdf~d}x<$>FSnyjyX+8sPUQ+Hn*4@!gJ z&#%@=8E`cxbWmZgnnv|o`cpeOF}fQ> z@Dm`eWqu%iKoJI#t{}?P1f-pkSs6uIln={*WS^%+d#L2AF5Xmm=PakD`MFDkvBbW1 zElVbQn`BTJq!|?hyR}k%os~ub*Y@{|Byw8#qUE5aZ1_}ubW&=h92o&H0E3Uh=+nK; z`W_y{(2)+_g_q?g3%&c?O8Zp`vq4>ybNeNimPn=5;CJgL%U@j3Bp5gaiS>=x<;>Pd zF?o*NXqq%0qFn4`lxPY43O79Yww&1kT~LLw0TlwV$~my>XKNJ=jEQk!q8O|^pW6zL zMOzrjX!f#fgG9lIqC4_ad5IaKKULqm>@3598C)22kgPlmwura<>spt_H}*)G>-@D%6@dS=$0+m|Hh4C8x}Nh%!vuiXbLnz_mA!Jy-ll3tC1 zPkL?F>J<)SRBxN1H1@ophBU#;pU;gp1d3Z=0dcVik1E-*fJNLZ%)*Vlnsh%N!UyqJ z@V@*6{1kD8WIvRDQM?@R_>JS=qYsO7VH};_a`1s-k0So!Wd5vadvUFVK$*J(dko+h zh%1p`MQE7B*XoS4Ngs|jj@?%&GF*Nc+3>4Y$v1*ObpjmCQS)668Ocg}ZV$M604AQE zpoD`3GQ&cQ|btdume8?HJ`(6;`*J-huuu&(>jMXHA?#27<(tPx zyuMj)PrDyF9$`<_Rh^%Z^)1LzYyZZT6p>r zaRn~DvzAT~YL|;l(J%4*t>KTR*+&(an5goFq7|bO7@YHX#sS%7QX3-JmqVNsj`MHE zX)o35hwBRTD(L$>dPQ}uLu`?~oc$5=gR@3t6I9>1&sgJji6!9@CI}G=Usd^^$b}!K z>SAG42F;fB-8n{8+{%%L&j=OC))c-XiG+H7g^0Sj4tA?zwXKpUfFz1U8YL|y^T4x% z_^fU**}=YevZe$Qe)6VHt1CDMO$t2SD~$tsT<1%oloK;XS#POIxL8pOa4MO~t%Ss_ zn;)6i$;dRJMNW>Tq`t5GX@R94ML&d)Y@h^(m6JHD#jCRsn*tq;5Qv`$%eR9obL>U_11!u0uoNsAiL&los@=ak70Uv%Ss%q|tu;eNZ@1jIG8$YV1bVMhEFks=@U|Dmt5e7LbsmAAl z>J5fw`}yQG^(d+%n#YD520x7#CkMRyrA5giYml9rqCebF0<2HyDt$`$NLvcWx)=DdVJ-OHt=I*LY6@5jwd>kTs+DRvPxa64vQaUlz#>(1Bfy?% zoM#odgqRn=LJ8pTa19ZW+t(18GV-{|-Y&28Qq@RWIbQItEd(d-YPx*&jN>!w1!S z7A9Vu=Q>;s6_Fw0<L}_FmyXQihH3y>K&8Lpz$#jm2h9$O4EFW|RcJiYYiFqrVR1z@O+;@Bpt?^1 zWe#2mPdXDSWi*}fR8H5e@2JJejn<{@;2TLbBNV!!@~X$9kYUr|A*v_};@ z*1?el0mr#kM`&ufq#I0Xr8LLJPuaJYF(Jv5I0j8px|DHa*M4n;es`ZCLm?D_G!0U8 zaJb5-E`#i7NT!J1#lCZb>W)pkZHxr~%2pUPbS&kHB4esbNC>Oe9w=YA zXoCfYc$71E^;_xmRI1x#FrPLZbP*+Gy$}A{&n`OQCHK>E4(!WsCPT_oFn_T-z634! zvU!0}#mSAzT%qqp)a-jhQUXM}8;3?z+iDU^edva;dXCF1xVFj5N|X@LD}1}>KG_YN z>u^YB-$lgWSjfW0BEz8LKr=Ic9M$?$fAjrm?DT+&`&Suo|o=_tE41ZtBVFYQ`}?lG?sZ_jsoR{ zm-dLVcN-0?Xek@g+{~I<1F`jtx1?vXdsDU!k_io@j4n}cKi*NAJeZixOGTmK)em5m zs>uxV0Sqq1ow^oLS9&I?9Ievm%#Dfh)nu(I9*^_@jAHVtAeulxq_hS6SUnng2_2B~ zMUI|T<4~E@Gvq@K4EhHK7ZQkqi9o#A5v9x}r!v74_=JAJ^@b~ou^R%tVNGE=$%grf?GwfIz%_Ac0s1l^ z6Oz)a*kZel%e1}?kT+ZdbCxIJ(CRpqVb4?c$LjT<#H8ycO3gFbhlz7^0+AqLqXl%; zcxKbJQ3ibvmEUBYr0NtAm7hhY$h%-l%#d#tzf^oP@MXkf;{DAZ>X&%k_%K|67#8y5 zjlVqOFVFRtr~e;M!$;tDJOe&31}X4fUn+b6@v~&)hr1 zxJ!wvK&wFTEMSc_Bd1rW-{j3PZVg1`Cx@~XVm~J_%``=v15+`DoR%bwTW>M6<^}O{ zU`4pYOZLEFz%rz|et9WsaCbbjFqEiz-k>J=F+vD?()offi4{>&o+?PG9^))mJB#^vF9+C; zBFLUZs~`GCmL^99BYFr66cdjCeZv|GVB)RSQ#Ro6yd5z8=IhhV8%N#ydArYBlDQqM zO(E=+e!*kyVU)z)%& zm@5oe;U=&MgG-m8xGpO65x8oc^5cg1R zHUh%_D>&SW>SF7q!K$0OCmp4Qht~J={fUs~@35QI<^u}r_vkm0L+sLoR)Ql=TIfWU zYu0FP6M)susy;o=iqc-R)oepIewXi;-jZPB=W6f4hs=FgG(b;(KcTZ7`qsoaF*vO{ z(Q52Lzq~|>BNB;IQj)^%7D%}KQ`ZduNKsMZwkO54S8JzaNiib#=0143w*06TXV$%i zRsd)O*DqE6j`2y#mSBZy8UuPGAvlQ7zaq0~!H0qo8^rH;L%YbTq}Uz7x4`|A&1pry zG6*fVn@pFL)JPADGWV#b^5~7fE$oVz>;dZ|Cz~~pNfIi_P{AJoR~4!E0*>Y@Rg+M` zbAJfAmTF~R%Vu-sO_~={9}7X_-;czQ}tgoZk`yR%o#23ctB$f%NM zP~$dC#ER|6XW4TqeP2ER-~EK4s7aSG#dH-(6Xpp{#!7_jthIunV5O51hq_4>?HJYwodID!!) z%#D;)!AG~PVBNz*FArN`O5XGu(<02)7-PA+`J7i!fo!`DhQkN|&^g8&UOFfDct>q$a*bzA9K_Py1RKG!aT!cvR@GB7!n~m+}+)Ws)g&+@UUee@aH|R znM0FqsJ;xMVd=PtaJO6lQebt2n3<|#h_5y0Re0((^9*Z|V!oF^#uy$^bSW_A6cph% zM7z>mWq6nt;isy)e4)!InWeE}rn=w zJcfc9RkR6h<*?CmnmQ9%)xi!tDsWhk_ zuIq<5Vd)nab*Sc`r$YIRItP`NGg0P(W;W9rp-UQA=dj;Q0B0A|66w=5OV!#KGatAN zVx!5KHWNsReG*hj*wBfB{MHW105GamsTU!{>rx*9xf5to;VPAa;AoVU$C9m;+i%Fo zUqnRiL#|XUrqR6d6)B8COHS++MpZ^$ZapH*R8=XER2fo^kSyAPs#!+V327EoqG}Mfu#pQO=<}N# zX17YtBYdNLb=IZPb$V`^TY-a3BIfFHC;jHp_m=J|$&udbB?xx1Vpuro>bXA2!JDQ#2_`5<7S8#Y*H-MRq*_LiGr9* zOrW%s(N3^v?$ltY7MzWMjp9o(_-EfB7>@LZFlZiNPb>fif5Vku_rS0c3DrS0sGvF` zR>qaOU>~@44oJ3Z@*8$xUhoIzz@SH>j=E##C9*t-qM&NIaFLwzfoYVI464dfq4mII z7q#}j1E2#kFjk?N=vq{U2PLIccoW$CCX34H_}?`RU8W}#?i9Bt`@hAjjly|H6KO#a zj}}vy4t{OAmCqvvt8G9=K{gxFPzV&jZ^rufpP%@6;`ze!iKp+jF*(8`eaw`uUr}?h zq%72`@b-`yVIUOa4dV+QfF!Jzu7-E1!Gx3>2E_TB831w+;*;59f~1MphQSrOHr<#} zUei`_79%wTDy5Nt#Vlvv>mKCnXNe;V>O z<%`BpSd6EHLRRRl{)>=>(7+Wj0g=$;VrINgyel4>=avd|hLctxF%@IrO>qGueuU$8ys}A^z_l;!P+(46 z2W66E16z^Q1J{Ca2!1v6eaPF0p%NMav54F7H2=x$hnK*Br4a13u~Nv@7$#>w_^;^` zIK)(j$Sv{b-N4Dpi?U*cBlHuY1yWo)`(-wiFA_(DDRzyl5>hFJs(4jenC?3I<7;y1 zCMi{QsmwxBnG~&;>is1YUmlMNU<}NO$HXJsZbh}lGB259GQNex$wvSYS>0)l*YF1< z47a(NERUz-57I|iA0pV#f7<%R_$e?VUM}Bjz~taNaU6D^0e$K1+1>bdG65y8CVXW{{mxC1J6Fb|0#q8c(pW{jyT+o7c?tG4y4LIWCQT`SQz8iWhJ ztLjLzY{|~Zc8_&&paLn^{D38KC1)A2p@-clc@T%4o2~niyyjLuKEntKUjOr;Z zzD)oyCC0!QBA4p2@shWRj}>o$Z-5u@6>X7soYW2FN5JDp)3+gya4{%kPE0tUL&7v7 zWN7%10X#x5VFI`KE#yzYU$eil6V%UH4Ta`AgFRMkb|kV+1(^uRF$F;d=K=j-_>{i5?clov1VMh$v}L)z#&NKKOEdg64E2;URe~lZFCo!W(tsjWUbb^B#7z zfXFi<38ca6WK~{Dt9%l^K^09a3k`%tv`v3@dBkxYNl8cf?L;3`XA?})9oHsDRvj&3ieCoNKgq=5u~4I^N1cuvL{V$^7?a792@1Y2aikYmWob-(jg7~Qb+qLaY=0&;kB(`1!&1KA30jWuxg7S3~ zkyh+}+udi&t6I$>nq~EDPFMZm6THqJPW?wmHdU}qPa^Uzc}K{`Kh&Dk+$2K&=^3%h zloMAi>Q0lj%|$f|p9z&(kW>MxcMPsls82y{?^zpSEY4VwRaQu98l?f z)hJvL=?2fHKuFa_*UJ<>kUuZ9YTWm>^iJxnT6iWUmBp-znGq?ht>N44Nn43^JbSc~ zxH^1haz&&_IPcX_ixVB;JPD$Wzp|>kipEqUVnChnI@{QUNXm#g*wQr|g(CtOV;D?T z6FrL*vmAbp`^4`kR?cp@W^00)n`a|=j;njKP^=Cqyfo>Mq=7q{XRjau#u&>i5NQ)v zj8vabBC5p#s_GoW&C{vK={K0m_*^8Z|E{(sK)PoQoyR>Q1wC0wKgJkvC7^z1{l(QeU zJ=o9zx7WH=HL0LL3?0jtBbp~k1ITDn35pvY7ZrE z1O1f0A7~d>o7|}rB>)kA8-XiLTYK0&rAKAmP}7{Fv(-w4rIPu0kn7p+7jkKajT@~ zY3-}lS~Z-JB=b>3+c%MLD_E?~i8ROb?N=+RUMO4zabGSXX#GunipUP?l$bp%`TbZRCA=4PjM1@OQ$#9jVH;(hNdrIAQ|LbjAh zibdFcDh6{D0^iZUNY-k>U$6y#V7_6dj03xPv!>pu3`@#tko3yWU*!>melOI&u5=3| z{g5_;S<5fPsrmlI*}-OAIKWdrQa_0cMzcg+_j*)bgC=tIuL{*_*OY%W< z_X{5jHyaW8tgnuD=milN;OQcmBEKk8#3W5?l^Wb&N!<R|cqnE-@V^ZG#fUGeZzD`GhurWx^a}ZKH}`)td= zDWREwj(DiN54=}VY97#Xhkh*i{w2SE`5!*)fBy*FhL6C@@e;T|F3bU8vn9~0ih;M5 z(AMe!Fl4*wBo!LL2mlu*xdAyy^!(o;cnIG0fBk~rz9X*t^@IHPXW)riIjMp&!)MKKkZ;tC8v9MAmgDEUrBCbA>f#1*v{jFBgGBbzF2F{n4plNIp{{N;?$Z3Nek-OrH0TyRHxAMtYDT(UXc zCjtOf|AFI^mE3AN%hB+GyQ8O)iU6>ripCo`Ai}tz8u?_~SUp%*v^!0IPatm1L=trG@6s zc9D=+NbWBz#35K%1FA5Ai`)|Fq^ppLxIz~27-CZ}RUToE_rNlF0>6&s1A%o1b9UWc$C>ul(@tjdy4XY-#^W(Wde$ou55xT zDC_LnxQdjyl)3j?Skq8QPX{rb0=OM@g`|X0YK>^PTWvVGX>cmx0vQqGKltl+D^>La z^TxW$(jaE#HaJ6f*9UYC*^7aM5W`%BGdl_tlGW~N%X4YvEx?Zh0_;zjr&1>K72ez2 zgWn~B0gBX_QbHi{@4&CbFNAAAmje?8gE!_<5N_Un>Mrl$*;w}|Ms8$GBYWV1^Yw|{ zBtRMk{g*3LZJz1}CR#qPiK^w3jHv{Zr6Jv>vYILrQ3M97h6^7l8Z&tpDx&hBDQ;k+ zoOJ+6_1bB&HYgEDFtycSU3fmyjKoNh$syN}8pa@~M8G*K(o2|H>IzoSf)XEs%Uuec z_K9S65(q{a`!|Och2pW0)IFPXZ!?MpsH336x&~LFCRDmiMCdb@y#TCWqFxg2ID|nJ zEu|21fI5~mqBH(5i{-U>rUDI;>a#&So(>KDL|u|C*h>@wtPqW#Ulz!wdHNla#FVF zrtzfa*WO$a|;p)7Lqv`KrAfD#bQ#E{Sp)&Xx@;$^26(ylHE+TsHn5(JYiKh+#~-2xWA6t z4&=F)duPyzs-862$goo8RT)8H%||85iHA2MMOP%&7SufFu~jwt(S$)xN*b{ij7D%c z(n2ttZhIOY>1kHnDN8JaYYdkmds28gdUuen|;B2$guEvO+kP<`Hq*9lKwqn3)XGF_Dcc27|*^1ZHR& z3W$Uy0<2YmQnAI{HF-R82$>0t(5#Ax;_@R9;z`$R>)qU>y&QsR0BCY+T)LIJw!nZg z3DAP8Vd$b%A}PafQS9`2?-dg^iOCv^wPs@&-Q zJ|cbCFqh8+*g?8cG%Fk0%%27}UU7hAeRnm|JW7syQy8W?8sr_JiV4DthRDo{tv%Vv zl~GYBDLaC2rS2!oz;FaQyVe6t4ndV^6mZX$Ty6Rp*=D&!QBDdEo5apo&J42w!e zk-4dGhak#PH?k8)QE^9DL7PaRn*ml2q+s8pzG+GK2j+vg8*^psR9|SySevtCC!6p>ag*a*gn855~|X;U+qA-Z&eQ~d?wMP7lc)lhIqpz2f{n5mp}k`!AMl3zgCwI)ZLPjCf1(U z6}o5uqb`9EM!A-u?}PQF+LwL-`sS=qhs|;dD(Tq)?PZN^%#y_*sSdBcHT8~ubWYBx zoy47$RMQJf0o$*i?SH~x&R#?n^x+0*ky2F)JfO)F2;rYnYQvJ)yAcCRt3i}gt8v^x zfc#!+(Ym$6PJN?jA#J81Kfx(u%HO2C44(nh^o6TYLXC5x22SJ=oT-8mnlaM)A$KVdR30P;( zs&fpDe>5>KToYG*BLH^6kw7eDX`!D`QEIcS=6X2$9z_uny_IrRH0IvQwv;eliYdPx z@~auY6}e`-DJ~I5e2sXSJRJuA!Pmc7)}81fRN|s4S)tY1=pd6Dys>6PDNY^( z@A@CV8Grl5-{0`_6MwoFz6S0BH>(ia40mfLygIIF4xp7DOL?`BkpqYcjKFlfZhQo8 zEl#>CO9as1q5=X~zBwCMLCE>gmbs~X5qzU~%$P&Q2mlto4EKtA#nT$HPo_4?lrm+V za{LjvwR1x$przyZ7~xh8tFKIvjRvbgp4?0Z0pIz_2ED;& z@a97JSDJ(tK&2c+vk^|RI8~@E2*3naQ~s@vZ6Zf7P?mj38KO~F#yZ{%3+}Apa+q9- zt6p86KmfPLN^``J-_=>ob({FU9xL`pHh}mxKIRHOuTP2=I5uY$ICABZbA$HX=8Wr(Q9>iP)*6l!(08e?FUGBkRgqm(U- z`lNF#t!_Hzx$<`pw%#8(_?FaivDCj0`Bvk0d3O&RVt_>q1j1djcg;!(TES_VjD&~} z)m!DIFNpXT_`5&f~w5v#ji5DO)8n`3WZ9w%Orb{R|216%A;`(hU(J| zLt=SOH=&QRjZl#g$tv=^|2prL4le*mfGDf92B}rWtvQ^!2D*7-tP%Sr*M}a#{4$}= zo(&*^)@Vb6H2}J4aj^FcB<)o`u^Pn{DbpU=?^u=igo&_kzalE|x|1ejp+o@q{H3g_ z`cU6D=X2uPsIYLi<67)&cGY+#(l{}Pi#T#2LHumaPIvy|CAc2Bc7L{SZoX|xw)4+o z|B-u0oD=W~6eD@Ft7*A2v(I_^;6>015BLnjk&bddLc(6i-QA}?suy%)g6XlwWx%Z$ zZkg1&cl#w46ZZsp?ZYH3qP%l*e6`DA{rKwUX>zq>Pk{n(Afvtb3O~?VCoa|GyYi02 z9D2Aaq13XS%u?puWn*ZuzXT|Qo{d3)V67kDP(eOwRg;s>pL3Qi8!ZJkaqD(O+O4+( zS&B~sh`is1dhM=25MMrQ+0p?|X9IlUhQdz3+GY8xSuZ4~^D~}8!Cr<5zE=5i(MKL9NbJh^J|>D*(T#AP|I~ptRF|zRL9T*$uNBw?l2j+25K4}6h%_yo z*pW+#&>!{Q9PL6>O|yi@43Kb7=|E|QNXOPPJ}d-<4t|1tajEm2l_G_(@tV*-UP^=Yw6UuXQWS4y3ToLZMY8FqYjO*_aIgmRWer#{&YvA_Ua)cyu===no3Sg}T zAcjI^jv;`V72GPBIw>ykWjbkA5!H;QhABXcQbY}q7F)M1e@K1uFW3zU4KoDwA|)pR8l9**Sji}I9+xP1-i z7{kLMELBa8lY>YpOP$~XaZgbK!%G!9)qm)hTknqA-7Yjrt)xs46B(q8a4M zsk|anuWB!or+t>2Bs-WzIXi*TJ_Y-XNar)(M$_Mp09Y^&IS@ z4Hc;h;gP#5iy~A~DJ94oh{#agRmP0tJHO|+7xy7pX88_U^8Cu<`}hhnUeW6=W4(uH zX~X+bgWj4c=&pm@91_&8v2p=D!kcLnlDa(FSV_W!0L6`URSv4Uvs#-=as`5M02!3u601Zi+0&KD*C#dMe1PR<~0|*+A#_(m_~6AewH3*+Q^4QS-s7v~Ea} zABW>@1n=ibrn&&C!?NtcbNsitCpWUA=Kk=w6v+}571c4^EKr(9R<$S)ZmygWc70{d z4BaZaz0HJTH?Tq1_1|!X&vMim1j|BzRnAf3rFp8Q%`a^i`-~#O>PYil``l%cy>?1R zNGtr^!U{yOiIA40DH~ZuxrKmsC3{bjHp@Oz;I%QeEbU4cH+=Hm9&kPS{C02}^wuLw z5f6-kF^b>HmQnr9&C}F%+K&;Q)wgC7fexF+TZ~!IWDbHhF0jjHDJF|@N|F8nt-^CT zWl!Xsieky%N)^$h5@^$?$4d)GjEEuI#vZ%~Xm~hFwU8D}qY1EP4gvrMYu^9rD`H`> zU)SnJ0t?IG1DLfK(t=3|kLqLvZnn9T3LNe4j_7qt;{zB>Qra`AeEA z7P84qNt6Oe#m~e@oG!bU*>-#Llhx=&dtqFdvm{y41O|sOc@dC1;T;@dFe9!lY#c=k zyV{%SVJJkGctB`315*rth`8uiF<3kdKSiv-kMN%je~9=w=vtMf1gd|3$u|lL)bp55w0Ne*B0(J?-~jQH<2f@p9Zjy}}@e1Sm}@yUZ&p?WrX?*i)w{ z;a(>LN^UPdZIHwEXpk6l;7#$Z{{5Hu?!wFbhp+bk`5^gJA=P0wo`Kcv!3;)inUW`~ zMlXO)jKNMt4hh_buL~c*5|)ZsqwwHAcj59ibHQQDYhE5P^9)7tju*sk8=NW z0_bhTbS^?cI217K$P$fg?lZBLL;;;moRj4b_NL0;3X~LMD5hWt=ENh&$TG=lu1*Ce zRc#<_o#lcN;-V4~)u+q?W-yBnlRxt)uDAANJGhdg+b{Ne$&VS7?BH*j06OxoZ75gq zPca6r^wks=aPY71WDm8FJccPEEK;G#(yK^@&S#7Pym@-&;(_?+V?cO_bDQlw7z$)? z5{J0P+T{YMk_+z>6ZLmp>R1sC;1(EQf;(^_%wq8sGha}Z;drRbiK+gr;y(-i&s^9E za4Y5Ko%h;F+i;yXv>p6~1jJ!V&dItykLM;&WEU){d`6Ku!}pGX_;3vKWjdC~6d!fe zK@g%Y;aVjF77!B^SvSUz*TC>li8K+BkPBhZ5QK`)v8KfZI7Wnq43A$RRRivv%~%5mM@Xz?kXd|lcd9c*4EXhj`S{iKzTRHzevb7V{(`^6r^u_c2HCTR{6fc+ zE=Eqk6n%Z^Yd$v#&(nlKgE0r4X*YCPa2A+xCleMZRaRfbA%!|rMDdhi$*0SICQ5=5 zS#(LzF0QA)lk5v|S&-3hyo%WNjjCLfFovvUgB+Wy8t@WN>|RFTa*+jv4!Qq(MM5o^ zy8t$MWUy6Q#Z_ke(Pv6$gTeT~`AuU0?_7l?BRfEKX;j-SOdhFVCfU^qM@amug_mI} z%R7{zRvhjcE{+LLn--C^tkLTX1JN`|00>nnm{t2kC^xz4sa{u6>`PWYYxmHWLIK=- z%u&UJdlZpP$oRJ#1x_-c{M9!jg;oJG2j!GOsf)r7v zDZ3`PA2TBVE(CU4)`N3jiHsVJp<~%1H;JS|D};D+AUP4~4cJ(J`VHI4Vd%iCWKMK2rHjGuI-)&U7 z^d}KUFAk<}q;59mNNylCfx+(S^>~L7Q~j09;aLkrwxhuAU8D<`S@xu;{YrqU-aXpN z?vpO3m&d_$9>L8ksz~w2EJ{Wd=9heDQF$*^g(sx$@nQQmG-y3D7A^(Gqd_6K?-f> z`T>O9Px?@YBd}53qA^bIO%?)-IeZyB^?Cu&p>P!MWuE0A@~EC+ zEsiX|u7R`;94KM&ZMU04>6qsskhBxbyu!^OGSif=PO40?bgF7`B(7sP0uteI&4<|v z#5HCB71{wrOjXq@`9HEpwF`y}07+#{j8efK>PZjQ(~wQ9GNywAPVUN?t<_pkmBU_j zR(V>n;L#!wu&zF``&3!~71XWAH8b11>eFffJo;zCnV>SnGgDRHi{X%>FbJhqtX91QlQbKP7%<=rSL zNGfPakk?rp(O!p@34m(6qWW?ynB=5}n_1INA#zKnb?wormM5Qco!xU;aR1ya?4sW`1_mbjVf0O=6S%5CX+yd5Ol1n>>MhQt1I%R6u@6g)CQ2U$LO9&0#pGSE#JI zwrkpj2OF_^0lrcPvp__3K-6rpgoRhx+z+0WY}7C@SKz>)#Js8-R}^H0okE=oWVq)V z?X;Wk$Woecm2tdw-8pK{lzh7^5J^&YW=K)_n2}%9bZITyTAaDn9bVj6W$+Q<&=Cj= z_GGTa$kf0{9e^wtZ?4^109~Q{#1dR>g_k*O3X;6E9cc37hFut=R4ftYHCL8;UXXf$ zr+OfIWKD#3+BhpKn?K9V&>z^>W<3hb`D4jiX|rD}v9CXMgl)8|Jm``|NZdSyPzj?e zI}0X(bQd7&3o?PLtoa%}tnzVqQyT4J_Dd%5NLCH6O=)X8<=ydQ*L7S{z*69bRIWqi0Q0121gH1$L|EgU16C>Tm zS{z_xcYw|^-C(q>vsK_P3^pMT%v_>(N%s=6RL~0MM!awJmeq*@PP<&F`~5jg{s-)VeV@oV@`zW(HRA-+2P)W})W zClV0-n<2lJ`0d0m9{SxQzD%Fu60)G~YJ%4tKfUn72mbi`Q&Frl)8kDc)WdX>_Hpb!Zlc|GE+@qhinj;e2ELvD+qd!dZQKz*{cQgwjr&%z zeG1&T7haA9cs4$9lA5!0be^1JF)R@&gqm>}?pT$55$F zL_wBAFa?+5a_;DZ4{JnTHgR&0?o1mtCk78ZE$)S{5&!C>ZSuu)!Gyz<9nwD|@*q)C zh+x2z1wYnyGQP{HlxpBu_>lau4&V({*l)6CUOB9y2OnToPYGKiV2@ zQw2?JZUbWk?ud7fiTFwIv!XU{K9UJxXzYeDd)3i$6=Hy8?=#8wkD2f=b)gT9H{e$I zz*799;GZ@VdCnkqX@xU#ZYB(fECe-*=ai!taJo1KndZKrmL(sjKm#6s6ud`FlWSmF z3>UHJOB<7=2!R#0Bmu-q6T(of=70k+OPUr}Ab?wADrAU^kcs7R4~rO%?|?t|@hU94 z$$y;x9c!&N&_#Y6KYPR70RtfF)CU4$nJN`F4 zz@6gUalOvZM+NzeY~hyqRkrWy{7SW`{p;=Zcz?XUu6fPzP<=oz(bsxG(lvpQ^l_6y zmQHucf5KO+^y;|7TeJ}Umwh9XkgmTzVib-y?IJUvzJg`-8{@0%E4GH9$02K7wL57v z<=0HWMt&P|-2jBP7eK9u-1FrQ$Cf#DTk(ig{@$5(g%I8X{*er@C``1%nhmRRgZ$> z=&hQs(>)q$FsZ(afQqi$4liGn(e()uzU|tMhsGPLV2c9%E!{pPI5=)Q)cBl7A&lYu%4&N)C_GmT2y} z+k&t$hjWb;tv+a?$Sx9ltD~fHP-)7RBiG1k2|K);Gc{oaTP`GzV}tUguR>&ZdF`tp zl?Ln2>oS`hJM2r-pdxRX(qS^Gsy}2f(sbuMxmSDnAH`8nfB_gRA zKy&=bx+U=IDU8l!Bjd9iK_Jo3j>aYLkUo^Vp;RsP)@)!`JCUZUjz8>&#Cnp`9_%O` z*k@Uo2g$P{s>vs8YDciLlTpD`^^z6lP%M5h9v)4r@*5>0;ApEh4y+1!oYY$jMSG&% zNvb$w9U}q(X8{dK89lmxu>hrOVe2V;N46?l$P?TwGak;IC#5ZX`+Tq2Ct^ry(^4Lk zr7tMjoP*oMS59FUA@JC#rPg+efzD|WX*b-5N}vdmYO5HX$x2E=IaA6Th*qcSNZ?@z z3j}tfQI@aJdNt!3JpT%nMhQqTCy)XfDa1*bK0+X+i zR3**&idu8m!A64Q@IMoW4MqXHxaP7>nX{WQ+2Faw5Rit7!2nkoUoRxyoGYFbK@d_^ z^5+$r3!S!vqA3X5xpkTNeM&u<%?PQ%>KWPy463|mAD}1sBw+|nolVj$bruBTss<#P zuF;^p{<<7&nVjfnyAB30DSrvXtbQt4^;ub+K(4BrP8z`J&z2S_n-gkfQohk$U~w^C zqq+o%@#-GHD#Tn^rqskkw)%X z$$*9BuoqSep^{qbkzsS1UOcj3Z$&|s?xzJeEl^WH;5W<=7beXTa{yI0y;o#a0IzZd zEND385}eZ{QIc(HFf@W7%Y;5~H4nPu;{gLDN0l4uf$M<>*du|!x^t+P!v@uU0bkUz z1R$7-G3om!bcfU8ONM+q!VzB(e=_2Ol_xA1;9eR;m9L7gLM>OLkikWZ%E^B<@OQ)i zX3DQG{pG_SGcj(tL&Eimc)jp^#-E?~%gcZNl>hz$o`F~3mB(_913AJ0sP8ql;fcNi z3W2RaQ25;2s4p;Y7q!tr7F0B$z0Q7)|WsGWUHGmD1GKRooi z3tt}bWyTy(!3x}t=aQf9_-gpQ6F|=drvPGQw$~b|UZOy!O&UXGiL8i^RKlg-pb?)-tCAEfIFNbGj6WrL`_JjN8zWIBx#;V~_ywXtw1k~CnUB5?~MaHjMHT9-h`O4XDMjRnnQN`Z&K1Mc#-5q})948=j> zAZyc&d7rb41II<+^Ulo!ao|($jgy)pUEwL03oM4aI(#SQ#CrVY@pyZ@&$p-D&#@lE zCgd?D5YO(G&Mdz!kXshOBH}E&=3!g6=%Y~w8nhO{>HAN2zBZWZRa}Hj@<5G$b-g*0 z^33qlh+>yC^LCy$VMZQat?2^>LfgBPuu;EW+GuP0mhUvWiu9zShBP0ZE+mQ)T3|>Xei}qkEq+XB(oS7RuTe~|YF9ZRK zMAU9Yo=~7Snp3YihWcl?gVYhny?QV}6ttfzr#Ol>dW&d#lyx=r#v>SBKH+uv3hc7c#2`}1MIZ6pEqRs1aJUogaBk`b$R)#g6TaYN1R(iUmDOrt6q6*rAFm|*O zX+8{O!h`_PDQ~Kk+%hB9g&twBxXm6zR3-kN(SntVHdBJCg+eA9WadK!Ugg%{0i2cQ zsf_?(P*s&yASH{u43he=ZN92S?h6A^(G;~b(kfF%a+19c@-w4wTWQ#+yO;+e^)7i@ zFt8J16h+r`#9oBbq!D(r$f9v`ZD=?UP#y3(%YwdPLir$J zqDVfB)$rTBKiAzpNw_*fvhY z+zXEl`cHaAr=UTfX!cs`9bI{4XVQr>p2wwci9>pMvEvS>?g)v5@7b@5hak&MP@a?B ze-5q4T#kH-L?W_PZgCM(4L9p&M zu4}Cq2yzY)y{4O$9Lg%g4cqKO#WA<=Vg&OQvO5$6L)67Qe8|W%#Vv`VQnQI*KIXbt z2ADaAsJfXq@5mX~Ig;8$07LvVjmE7%+>jI1WcYx2hTz}s(^Hfh(W(cQ0^SZ@dTPBvY%g7R5a7D zyFqcw&kCum$Ej|a+RXrQh6Zpg)=_JWUYXVO@``3XnE@1;pT!;U`r0UysF#xzFdt1ZJ8(N0w z-f}I$0XdCMKGGvDZ*eRHVmiX-i(O%rpF2v>oA>5E6_}CN$uf#V0^0JbDA4j$9d)s( z(kIQlrlLeft1hPCAVhiMc9!X^QO*QLs@s-yde_Rh?&MT)xo?0iz=Epeu>qEiG!?~c zxbhpOkNjGw!_Imxp5h6H?aVB@u^mTBFPhl7PA;$Qb}suqSJw%RFfGf7!+D@YzHs*m zQ9uy_O?HaCMpDnTU4t@o7~+OKF#0x|;0IgX6hbtr7kxxIq{Yo{B0}6b>mLQ`8yX2!53tZM!GN-|5S>3GKZuBFBz!?r(Uf-K?{qlfqE-< zzA-U5zeR02{DqZjEiLwFFM7;@s}I_^@*=ZM`b6O zphzo;z3Ok_wWzk~T>k>abQXbOFfVJ(>K;ziShz%!R`F~Ix8Q;~@pj=G`TH;U$9Kex zKi&S%KikL8fft1iIcteW$0({&TP7uy!PS#Gw`wb;fB~4}BX9>?_aiyAbQ$`Jer7|g zc8|x$o-u!~Q(l5!Te9%$HRMp**S0d`H3 zxA?jAoklWPs=;3EGTk{x{vzK!Y1CpF8^s1$A6D6e8&-~qE-vt3M8IErz?||I(qO5q z%}?_bp-IJdCUTg<{{~9TshG2V^OGTTRBgnTic;SxG%g(Qfv*Eghf2H<%kU9mk-{Jj zh$WGEa&M z6b}N49~8flSmu|iSzv{T$_URSn@ivpthB2)xg#!-G5ob;C?@pQK$_P|ss}xNIdsb9 zcr$qpTq9htARaLTOPIing(J1$UaJ9*z*DLuFS3n_0~TW>zg+I_bGU@afCT>kWc^!{ ztyyv$26{xSZ)cvWzJh2_25>}4#xYVO(~JK9f6#+uV`FnOksu%f-HmS4_2k|wBJ>d9 zvAzTk383m!=HB0BU1GV1yIYDSen$MMI!}%Q0JQ(btWB&yrb6hGCw~TTg*aR(^3Hkn z9DYo=bYKVjhZGjQFp6Q>Kkv7<$J_OIdp#cO_3+o5t*5xKy|GSvpZmQEb;X?B#}#VM zZXnh_Xc`WXRK5;|d7ga&KIa{rQO$zftTHzGe35i(xI-1H)!sX_&cx^NFj?ElIcFu? zMC*!0+93pui#5jAr+#3p6{hZ>$xH^|Dj(?RI)P|z5z#LYP5m2Le``cr)l|q^jQLG(6qrHwd#{m~z!^L2cX?*cxe} z5t^Zp)i^;xhofA}&Q|9sRQpKUz{D9^WU~s6p(sI4PmzjrI0{dS&HC_7a+l@Lh;+6= zL}w^BSCNbopIql^kVbm68OeOOGK{Kvff>r44QnLUGIuAa)Mxggtdv5ld8PxWvOS(p zp?j`t`t*x8&@~2 zpz~Z+#OX3L3V<^bnhlL1vWv;60l_o94rfCwwb6-sZ!PVrhHMkcym)*SIRml?IU@W0 zAEi?UD4QGCCJTvd7VM{)k~Pgou9|?{lfcl%Q8~*f#VfU4 zwu|q?!VS_}5m1c?cOykpMQ2a&-rgH0`~=eSwQ9%M&$ioZR%+BVY267gxUAmJqbuHe zfMWn2zO>y17{vlWmae+HCF^-Q!_g#C;?Ry_i$fk+g;1j4I(My#5vsmh=C&WdOV(AKOnNVi}jT0pZ5?` zTm2Aht=NuCyB3i9x$RoMT}_MVjqRTC{^jlYd}XFB3lBu-S~c=3&7|}IrTSR9Ui($i zK}EVJ_v->;JiHq~M3K4FO!O7r^+uLiMVM$4ht=L)X(8DF0Du5VL_t&?UbLmDl|FmN z)|;wfrzPcF?l-`yhkno{^ z$U`-oTblnvky5(E+@m6-<*e|frJll;9Sc*#b$LnfnO!nm|2x=KIoB$`u=Ig5@XwsO zPhQRH#!zX^=&bPpF+ruwm=&pBleb{-GpBCNIup?cGOS(5bv@G8#{DA6yGfSnD|>+* zT9K+O8gmbQj*3^Vt&PMTSC8;5s;AW>6QV1!AaxXB0VvCy{vlPZ;Ge9dpKd6bo@Kg! z;_Sgk|3nsQhR*qin}n0eSN+tKGlQnYZL-%}TGwpk#n}v?5B2O3D3>} zKr}M7OI-H+3aV+W9(r*$5s~mNP!d9?i*wN zbo-g zJnC@A+O?bP`Y>Oab;TqV_1$m!#!F#LsM{vnAyBohz(}_ydv-)LTT_`Hn*|0}jhx`F z9_$$Ei#yX8Kemdiv@hYc(E?)H+AmX;*cP~clB@g&4=6uSMFUT?-hx2O9qWbAR-9A- zRe`qEaNWSA!HT4LU{yrZqy5VIk9Mj-&DCyyrg@+m)OaG4CWrEVxHTw~5=L<~sJbuU zfr}m>2W}oH9Q+l7pq;o*v2t573(YL_Fb$?WW8)L?Fs6n3i7Y)6brtoc#XX4Ai;VQo zFieM#<1_E9tTGcTjhEi=qmoAqpkSoFSfsY{CXTIEmK&T9VMl|Z#eekF9V(BUSz-P8 z0)B!$u?42M6x*;aWC7NOwUDbXIOQYrPw`rj|5)sfCw$}e#K#NIfEOB03bW|O!jvJ9 z3(Jnb7I30^9KAFX&Ng6~{6ID7S5 zHb5K^_;5UH1*5K%tp?Db10-&Q4Fw9Y+n3hrr%>R5UoL#P@US$~l#Ia3^QVve;fX(Q z`F;dCEM53ys(|IfQmaH`%1)3`-&Ed#EAmgJQgNoWb;wpWxpi1=_T_4J@>%}}CA>6D zMp7?JY2&Rd=oN;+iu7wjpKJZjE-(oY%d^+Iy$6{D#tuCe1k-FX+Ddy(p8^2o5cG_oo%m znlhV83vHfw{svgg-3*0G*-5gkStJ|8>Xj-BxI#W>y2WS&N0rE_QLkR~_mCxVh4`n4 zmpu7=xLxzvEd`ZFMM-)B4)MeqCu$C#Vv=N~1guQ$u2dH^Ka*$VfU<0*5w5VGBgR4y z-Ko$5>qI)Xc1&ZZ`pv8qir7doe%|vv5t6DYfpjVBP%k7X>X4rfp)Uh=Vh0`@@xoU5 z&|*$tANA6~c9219`}}u!4;El-2;lLD%_!Nc(OJeh?}RI! zW~)AR7OUzzSy<~I*W>Ma)9c-?uhfu5N z6$Ow*w3woCOX1O}$$J>+hS0TTyd88Xz(3d>^e*NzUz&d|d*hQNSa+x|pC=^1c_g|US zw=P)iMtXHiu9O5Jl5Cs;BI#kKZJ(93rbHy=hkhi0AfF4yZS@e&Jf~q3ad|-Z(2wFpww)NPD!4NbC)rmH*TA*C4TF zF}ze8RTT!sCpG4)BFGp$2S9lf(zN}1h8qA%{^inqm=u2_Qs8X=+%Vq+w@QFmXi{Ul zd_J8$U(ZWuusLwhESB7A`#W-0O}0k_S>@9A%tR{~R{$^3|13&5?=Y+1x)yYmp~K0( z)2ND59ZKnE@BSGgQqe?7wF^u*<*3K-?r;rcGe5?FP4n>(F(Cyo-l!xv8k^@HcxnqBX4L zM44V^C8!8mK4GdJSZW@-Ym7*!R947qZ!3qoS{x*$vg8ZUlN3=~>hAT}_v37YogzAqxh|NN?WL%Ii{2Pzc@3yyM(j39z(U^EoEMmn}Gjo*v3H4_)fdZOf4G zTLj7LwQZSi50A>eLZ+HQB=$}#A3V=Y74g{hTWehkdB(Fmz$%T;R7GTGni?`ay*lGf z4ZzYh&`owmOr*3q(@tMv#YJ__kcyQ=IAf`~$1L-sA(?glw!I!JY-zhMG1H9D8+*D8 zXI(<-P)C4>6nV*1BuKkJGBF1=PmNs)vvIIg01MY+3m28@)a9^rA=)+owK$mdc{~wg zay9^Um6yllkU&RExDI@Tvzo9Fs&x{`L|+DHP5p-64(s84Oe#vry^GN`GJ)dt$Ct)^Jv_O;sza{)85VzHW3Z}KRXRqssy zP>86giZ7|2`x8|%)3F8jc|qM{A?`fGz>Q$)7R ztNG7*F8mC%RX!zXD2Yt9D$H5y?{J=glKy_v1*_9TXCxaq28CA-|HWRkaN%&k=3dA~Rqm65N<0;k+ zqELrdSQLOam@{M^JgjmFmIVo<6W*AH`UHgH5^_sH24w)slb8NNT(EVp+PkaR@svlC zAM3V8b-Pgg;_A-&Pv2Ntu5o+goa(Hvk~$Gz20QnA$8}@?>yuGlUsA=WEM{xfnS3KI zNQuu%=TT+*>T~tEBRV;f9Hwpqv=m#+KpI~@YvF=8lEDC#wA3N;iKJIldD&s&mx|RB zMjR3DFJ7wSPe$mF$Onwt9RW6}lunno$E^anlT=L=mX`rVyiyH;*57G%H-BR7C_~ z-LRJX!i)YuAvtt@3>1fvL=i4KGn|Pbwww`Yrko2l|0bJ2s<@#sg=_TFUk$#U;&N&)_-JjrUay)(O`vJ!`s&T3VG>4)uI_?qZ>O;Grb~nKzT$8}wbD z88ge!j8wbuO(pjEJBT|M=v6a;|$IrIhjr=HPc{U-xY+~!N(Xy&28#4|nR88GCPa7bMYtoxv>4qgUa zDI3f2wn!aqAQZ;63kq{STcMVSi)Zgy1VUz(1&3(;B5q_!FS*(~=FOuIBCe@{tadKT zmy2kbC8noDMkHKhONKy^8{(N?9dO7-ny4j2Lg0}mUF)dgO1NUHTpACNtrbs|m*0va zmN{Gi0i1w}pHxzE1HVWF@BltSOj0&X1s5#2@k^%S%`zi*BtpdWuM&UTRfqH=s(I~Y zOq$$Erm2Nvb4=zSw%R9bd9q`9aS#ah4uFRjSWix%WJr%ycFM4;XvShTFN76fX)FzW@Cy|Zd2Ko^NrOO*=n8yU1yk{AVT2>jO?xzYtDGQs(g(@8EJWbz&#sX9e4eC9IGzp-rgPh``vR>N<0F!C$4C(xX>K1w9R8(?K~c}8UN zO*L{Qy_>WjSFMnm>D6~WmYWT~&#F(_;^G2A9p#!TA=FzNoKP2QZCKrkF+uD=njOeCtsyhg?LU3Ef6 z24f}*%dS^tW_xd`Y$}xaMhNq&q^RnvWH*^Ax?`IZ#~B?l<-71hV~$bLRGBbad%p^@ z7m*$QuP#GgZ(NHxHkG+v_ce5pz`rbg3e`OL_ zRVuB4Wi738!(SV~od0Jqk`Ys;;iO)J6@B>&B1;Py7I4*7 zsApXDfsm}EURxp%Xq;1*Xlw1dp~*;QBzMNVRfaj$2J+4P64i-!;mM12)<=*1bN9@G?3n#~sB z^~G}$?o`WaBT#{)GiJ51)0}WY;rhubsV(NdCDoz7w4P?)QU;ac*?6kdI)-jxIWds4 zjCXTPob9`_^m;9ATf1sF$muN@PHP;Gy9bmuswyJtu~+su!pN^stBNt>>-GZfs}jgY z)#0SI2WyCiM|*INn>mA-SwxNBAejps zp9-aZaLBIu&iOGU!yX67;9e^gxazJ<5Ujb^50Dict0gj0s1soI?6T!*x`Rgbfi`e& zX{U0Ns^2=2uW6e@jCJ)CMcrpa=`<^waG$~v-9QwqhNnlYV^3>|FR75wKDethZR3pI zfKN;_qoQ=H_%U0hw^g-~_RGy10c>fkfi))zu1FXvRBl0eQdg^XMy%cn z_Xy~`NlXci-eP+I^ooY&L8R7_#GR`zieY8JH(Aa8FNC&Oq@^25Ya@cEmB|#++*R=q z13l(q5KeVc8_d{TxuC0g5u;3GTtp-R>+WE@XxA3VYnDhJR=t7C2w;IDq21Mz=`4Mz z=qR9a_c~61^;<<4?9P0aKA$uLQ6WIB&=O*%uRe!2MNyy7)AXhbzjEt8W^ui_33N%_BC{?mK0Ne3;;^pv-UCwyKg)>_{1F~*~FCB>UYBR$c zdnU)str)BlNW}~B6|XWOdNyz=512A&%AgB&!7i-Ilq%IxDQ`+&1*BoU;NF~NJv1d{ z2r^+gaCVJ(YpvB@FoVA1abfXw_o`W*d*m}@9Y%^~r+gY#J<^Rt7r%P}xuA+wetm>F z>h2VV_&H%7|Csm=ylRyd4aFVEslq#1wc2okTfB{Euw5me{YyIE9tiSqV|LRM; zTfXxWUamXve98CE{PWlN<46AgUWNcX@DjWNFTmN?TqtTX#G?6tILNiIxt7Nm4)u+_ zn+9m5AmLHho_DWtdn;BZA%chDUGa_lKfb}Q-{i64>yF<)%T5qCI$@Xq0&o%-2`b3D z=d5D@49l>9OCTlqD@Lj+TIxH6wDcz_q;!Bs-srBRg<6@&E#R_Wyy5LVA6J^C3SKE6 zJAQuVk00^+&}LcM;E}XqwW1Zy=w~y(y;j1_f`=d_cjO;;ah6H2o(pVm;ejx2GQ}p= zBCHT6YAemc<~Z`vyDzz43{6$WqyT+WhnM0%Pt{l0r6oLj%sBexu7E8pz@l|d7jOlH z$!eG4vGAt->oLUkkf3D3F#lTASJxt@HCj88QydoXs6-r|b`G{I{OZpb{W&W4uGnl` zPtSXrV4jbfgG@oU227*;-CFYo7XKjmJCT>jOYsm0nJ096w*-XddAg-ju+b&eFb&$S zhMyP(3=i?=v%H}1qQmX{bh=K_d+uFcnj2c$EX%BSvXRTPycoMa1Wa=!mcf?Tf^Dn? zGL#DSmN?f0?daA(IyN71^#?Q^NOd5c!8vPi9^iTSq^Wx0oVT0M+@X<`$b<-$4p8gk zR3swB6CV}cCHV?`RN1~0hI|QB(()}+LbSEDvJ#c|r3zD7iihUQpq?i9Ci17eP2^Mg z)NN+gzL)Z|0GSGWE8-c?kOeHHykIh&EfR1;U5QrA$qNF*zt#MQz}NcxYLjd;!}<1G zDgTjU$8zrbs&3Oh6i%K%}T74P8&cfQjrE(ANts9ZF_^Qz@?(VL+us25P@O{ zZZf`Pq24g)*nOYPq$bT7l#QhKsdCN>DWY;~NS8W@^2nBR7R%;_JM`y<6r3)4!UR#> zhhQr`&-n=+1S;X9udMIx0Ln^Neay+FyCi2-lm};TI9`n1itHX1<=ub=6QV`-#E-A*XQQh zyd*hn_}JYRb}O5AX3HFqi6!zO@MyPcQPJo$^}zuOfsG7P4eeH9dOr0TJ0gS7N!`C! z7SUZ>Q^<~<)?ETjJUj#1UF1C~hyz~cv~H$+ zJ?iiytu1J128U^#tL=9rQg~fm=xD<0nVO+AXQd1ozPA{+00S%B7?s+6Ir)fMSz+_+_(Yh*Mzg! z&LS%n5s%77Dhy7AUWJK+Yqi8pIv+@K#sj_7x(Bz2oT6RXSJMH09bVAw+;tc1ndT=9 zBh4lOtpusoG{~un1g07uoGg8VIm4#R<7iLUDKbGwwDogUROPG#!j%(X%W@AQD!eJ5 z#k9qDL*wCzGcv*8x@pWnn=2N@D}*ejU0EQ^@wzP#T!v!8FK%Od-eU^|G9h%1VPMc%%y>N(TeV(s#qZz$aP&o zf>F_iey{z4h^cyblqVXpq~X3}T`M9=r)z6@c%-XZxYvSd+MIC>swUN}J@L4;VLyF6 ztX_vBMJ2XFu-1x*b*)5tczUR+wB;VsL)QWz;FcqX>GL(UdNd=bsx9~q5m|{;G3_WSGDhf}H)SJ3a=*E4Bs{!y#$)YaLIR`H zYtL@V!%6Kiv*aF07F~wBhiG~h_VUCc98_0F6#fc7Zs zy+Lelp?;)cvh(ZCzh_m5HXZJUDILP8|0j+lh$e@#AD3O=kb-E&wY8;uZYo+zI<|o* zIls{)a1qWI?!ZdO)fpUo0rT-uTz5DRi;>FO zN2lJpqhKm3xh3&H`3bf~#|DD#NwB@xzSPre{8L>|0Yn3LfX z2fbj0q;LKtTRX=!tDj5&7fb-MUs53K6Yfau$6GfN%hf7EED|+%0=5U0Rv!6?klP66^6Z z)YnuJ(hzFUQ3|ux@V&?|H`=n38L7j3aBDxjaxK)a8zVIWsc1&3RZ6#iSeWjGz}4kex-Ux?fDua7^Syj#mRUHZJHCFqihk;L`6{Fj1<$kJ+I z!%93o+>tMIKw2}8(lZ6<47pn=ND0nxY@bDP({MdHX2fk^^kNC+d;!tn!+4Et9RS%V zg_MYdK&;C8(-oAlJ5s7T7rggzp%oseI3y*;qJG*!}oC3vWA#a{SWApaU%=d;(V7$pAAe{k?;F<6X(Q*>8N zksm@)@_m9}o-HTlJng`Dn!k!-o@A#;q7VBbUc3WOyH7zv9%u=%x=2~oMXKY<3`14U z3cpf4MFmU6RhEUANF)}P%RBO?ysdu9p*8he+%0_Z1M)qwMUZ%fJS86+ij)N`5zPyd zibo(;y67X*fG2Xh{FUZ^I{OsY0mx^n;6^Vc_xhpnW-ybM>Hj+G9(es8cxi7mm|a^D z23%oRt~Xg%=o{>-`(^ryy&w>AcCa^TaR5MJiA@D+Td^q{9{)x1Q%nFg4fOhmaEI7f zYvr``p*SRFPRxQ{-r>n$A+QNl*zCqnC;+96n+C)`o%^(G52E?Wj!8YHe>_3tjE7n9 zg(tfxt|TP4zyb0LZx?&ij)O(T z-EtsjkF2<#1B+D`5RhkJcra>SGOwt(IgB{tr zr`xf;@(d~|rxsVq8X)xNuU=**x-^b-Gk_XKbKv;oDNn6~90vWJv0MM5sxk5cksg+Z zoys#lj&a5bmL7H|rd>K_L+Q`t(Z0uK5k=j+oG|NNh5{}+`&)9HAXW0 z7*%0QYyC)vO7Q6cfNGmJ@{I$e(;^Bz61F(kwiV3^YjY&*kRC*{Alhot!MGis1dd8B zaoVi)>UZ9+6joEW8K)~3ltXnlglaUwQ~-VhvOPSM%N}df$Sg(0+-5g)D~Avll)9I> zTFm~$$TosBIh90EL28NLniKSYQ)iQ;ULs}WRIZ+ z#H(bjZxPjj*P6r5R4ejEslTjX3Rx881e0>A3%qEj`cmf&s2U|MAOx4iIh8wHVf@A( z@u*CrOk{e71eRJq3m_wxSt;R?lt2=0RZ%gxyQ*bEH22{_N`!2&_A{c;k7RJt*R*vw%2HPe+fj?0K{%rx|h37ERMiS ztK_lP1?20sYcdqES38c0q#}@rkR(NPngCh@x7O-)$vuD_TL9CQXynp%iLo9!yGKq-QL>h$=VKA1D0cU3doqWO+0WR2A+vv@+T}jiYE|-Sg)8c`P-SMCnUr@S zy2B*gYZgM-RM2HWD?ocmoQ2N#$)6!x4}IlH6l_ngrfKo_@a|7I}5Iy zD4AW8_mcNCXov{pp8A_WM;8JSUR^59C$+w-+z?c>8;8_v(jfY!HH9c#0Bbf{_1RP= zUXq}`&)Dw#${W#`=fBMc_#0ImAz(`nxWJ7;^VrQ%>6~I;R>yzFJ1mRdop?3`TaOg%R|{LLNk;5&VMf47%7P=FM*@fIvL4FW7=G z9E{xpz>ToEV<-bLBb%*J{-W4Tesp1Jhv+mq>0CCSlnq%Mo9IOu#C!$M#$ET+xm}^g zOH~*T-psg!td>>?UGfIX5rY_Q^alxI8@o=MaNXn+d&8|$)J6K;bs;#Q@GOHt0q6iN z^^PtwRVC;t;o9PF*)17sj>M~@T%?N-_!F;<7cWT`%pePxhV8K^T+s1cQf9+#5~6H} z@5@t0Oga23o*TR3sRa6HNTs5uQ1>pt8?L(S3#;UD&KN3s@ePRCZQ*F{YDxRaSZTt( z;E~OYW-(#J%}YHK03JOE8{|!(xGS*R0f3M@45}Y$Kn{khP;Ko%k$)AAIxlhO%}kPW zP;W5hCmLYG1KY6!-Fi@uUHiQlQT5_73$HF?G+TeU>~FqZ|N0yJ_7PtsRbquE?5B_) zU-^$Yo&T?o$gjXF@d~5?a>&$P_KFBJt_J9?>ISNmjnW+3Gnd-{lk_bp6X~0> z13Fg=c5$vt@K|`aw{PSx-|_y2uNQv&G5+x*@fC1w)!NvL_L|eD0zRb~;id9?!ArPv z;UTyJOAvu)G|AV(mxa|k2c>CzzWp3->{J?1oHW176qn#le)W!DzT^E74-=S1;H&44 zulVj2KfUn#;zfG#iT3e30{c&06n!zbt*w6S1wbNpcA8YoFDB({r`YAj!^L(w^GoZX zcg|@(;U+{opWGW7IidM#PJawzbh=;;T{`nc1A-Pvt;9-P8zxwWWq31uv+xdp#(g>c z__q%aC@zSH%a)IbzY9v|=i%4tq&U8yHGbxnA!jo>N|6y6@TK&G zmFCQZnRDaa-}ld$uC94<@PMy?XGSWrPq+=R52hx4#poDMGH2l1k6A$G;q!IR<&|3K zMa?C0s=u4vWghgo3i)EfNT+3{0}&aD7t_KAZe4;U`A~cqOyx~;DR!-^oqM-$j}$jn z?`Dq)P|P>)rFik^o|`dL6!I=Yb(-KTEZvWSCn8-_u>+c^r8|@Qp9+QUS@29~;%j0j z0x8ANTj&;7JTH~V)efK{z>>T~T<|gx(`y9+aCvF$Gce{LJKQ{*ROsaZNr$|Ul2GK= zOic;v#Kr3K5rslh(-KSYmh$YbO$Fq?k^C>`#Ee$V;2c9)aS@@*{Is=!OZ1%Pa9fCQ zBd$cS^ZeITte|nU{t{-mQZLAIxlFDQGb|ulwuCrVe%U-LyUXe%KRiKXkj(35uS9ox zB_aN)8i`%a-0HQDC+mvi?Q>;!xkiC15;v3$!z^VV0fNx#X$;4$#IMOE+D(fzW3N8` zAy4~g4FJsW;UVZ8iI4{h>>kYY#7>xqr)B0@xoHoMPzQe~n`_74yBBTFlKVx-t`|}G_hLxu(a-yo9)nu7G$Tq7n#kWA{2h}Hj+@JtdFDwmYaN`NiPjikH zP%xUR*?D>4HE8s(Q}>=4W{&vu5k3*2GNK(X&6GuebgGVfv>3O zy0NV*$EFb~)MXgspg!FN5jQpve?r5-_F*Or^pTu6FCoG<85^>&`S?cxY#?ElMM#lbb>Am$q%<7O7}YB%oU1jOh5XFpJ%mc- zLzh>|a3w%^F|0Aqt(Jtg^^49b)8X11BN|BH-XaAVEn1`?qnac@&T9T45Sr{r%>mkj zjR%DO@0u0fi>i0VUPh*NW_H$WQfu?H7#r2kRcnDR>?Ee{Dg^>FD}-j8=uE| z_sK1-t1aaX%mV?vUiq251xIG2k6^l9rb@dJsL0Fr!!A17c|5*xsfUMRJrHI;f`Q9zu$w-KqMRQkSW<4!6k@-Bops%55X>f6_~e7l;t@3Y0`>>`3dnQ1KrE6j4e=85x{8Nr-JUMWUt85l_kdk zB0VWZ7ZH!{^<8vLECofaZ{3SPw>px|XV>F#bu4pRL`CAZSP*a4sVLVZ@M z&CsveLK|sUk;=cMd+@!|$>T`ZtaBXYu; zsqCJZ&s1Tz2xU_g%(*%Z8|*UZE#NpI&Vm1QXQ2CR(qqm=xcAz$kh1H@n<-=)wFP-7 zxz$J9L;BMyNNKh=+}Z)$H==E+d;4am3=$b>YT@25Cgye4;c}*8mkA=}3jz@>wRJy@ z<2l>*i=SFgX1H{FR5UM@y%xB;+6c>f>lLd4ov8pd;c2^)fUXs|PpHM8XOinA^ zsGvO$W3Y*;4|;uJ5h+}LF~@e9XPu24X$>h&5lwYG(yTuy$EImD{}FLRDrsw$Y>O7_ z_!)OZG*O`fF_rAqKm!ybU2Rmj|Q+RtFu#Mj8nKVq2Y~1D^SV z-ug7BI&+CZ?)?O=*0L0o-VOdGdvf0L_y-Q+R!j0n!|pcXh8mfpU<1lU$pCN9k0uWa zC#sIr{(Ec3#opDbj&``iUwCfpCtgpy?qWM_eN{h=>A5b&Pg%)DQ!kSz)Kv&UE)d1e z7hW5i52}V}hZz|34QtVbX*GX_7b20H!`SF@MzYQLxI&P1kH+2^g97?QR3s95SoRXO zo5%zYIhv3cGH7Wfw419ZaA7UjVosRBq^ZPd8Mcjlbfg*Rqu3BX)d1Xjp~jRi4)Wr9 zcOnB`?90ZM<`m29vDmvV_4w;W?A453_TRqCU%uO4z2~nV_+oh>GQ~w+$d}V=Q|vASsjLI@)u#=H12yc5#rKc0-00 z5DI0!m4E*wfB6P8{r=ZOnlA!$>XPu?<+cY zEc;rK(;m$HS4zhO{Zx-dLlYkVrw*GH*yu46H@Nw*AvgL&hm;x+PkQmm(Ldd2r!jF# zw$D%O&`B_o8(qh{7|JRc!gMHhg9xr@9%v;V8|wwVur7Rg;M)t|C^Yd5?7*fhDdPa; zweif<)Xb@)-N9Eh(Y&0(JxH>255`j9&nHf><}DC?h{)^NQI z6%*I#TH2-{Q3wJGO-;=vsDQ|)iXt_!6PYy=+OpgL6S*Yc6kip;6a1e-#j|7=617}+ zXkQ%D$R4yesb9Iv&iF6FsVrd!|Egc9J#n@t2y+A#iH+Q;SK^hjkm@=Im!NrNsYG_> zPN|0)f^f)AGnnKOTniQeh7wH&|* zS==;OT`Cc*kO53lNf>w`ikk97Xu{yX3jb5px>eGjz3jM;@f}YyK_6LeL{y#?+h=F* z{_gfg(AB+VLw6xtM};`!hiB&V&$Wxy9rXq@Grbm1`GPUX5Irkj9j3T?s=9!K+A11$~X)x&q`Q|D}KXiygRW8l;X={D7>cL=7LM=@l z-O*4|jQWH#2qhMjmRq&nVi=N`7Y8++g-k~0B&XduXF?{@#y!=fL(5L?JHgy}e-e>) zII0623uTl8Au`)fXZ(T5PjX9Tq3?7{0dD-QoB?@uuDr8vH!deS^5Y=cIU7)acpO1#-(x?8F?Qz2 zt=y2p2_ay%;vwM*>>B0NoH)?<{(gx-rV7B*O-`8nYwCiXq#3H_0aQdTm>_UoQZ*mSn2g)1NikOjiR|;Q~=L-&@tH z`#+H$j_M4MT#w6N-2_?Wy=r|4t)Y^SO(i|li~}8Lf7(n>6@?-_vJ&G=Gg>9IUmFEy ztEyV`_=Im4L8zKqSrhDV?WLe~lRW=a`M#7TEIYG3^U(in2iaZLIoRr7I~MYTleG&eLtuj98S$$VgdZ#&tVa4HbZ*?2!!8?b= z(jR*aNI&c9*al|1C-Y;m7Be0=CL$vQ$gMur%0=_YitO~ zyuFJxrQIrtp=k@6l5=Xx%t3Q_#;6ynmf@&|DF{R&oQzSwzgTg+Ru>jit{LK8?zkoV9?g~C^~_%r}tK%l=l zI&#&c>tkfQ00r`aC9U-l9GAQnd0a51)iyv|}n=gvM%K5D@DL<$3ioiLdEAOLt))SCJR5a+I-RIu97ff0zb+9Nb zAjLz_@+hMdhIZmr_$m1QD%O_gaN}Ve+2x&PZ1>0%y#Iwx$N+Xpd_M{>rAQx;XkSF#{X4mMvwM?PDaW!pA z>Zp3#E0f9Ty0$vr$GmX`9n3|erGQ;Cv2JgQATsM(sV`+Q*w^x~j(zEDRBB3hEt zj8q~WuF!?`!n3gHXaPt4GXypw!m2yUAaOBB9Du-hr0X(>ue(yO>Y9sx3U3g&j*g}_*zy+=dX*|7mf*^^Q%`Yk$wL+7RB8opBlYbtFeN6e9*fT8Q zJN+3S0+%yXibgfyVR*ant^T`j@#{C_%Dv)M74?$O%%2ngfYM5iM7XY#vuVa*&#ZYAT$Af> zcG)E6XO4M{LvM0{c?*r{%h~Dl2K{4Rv6N+Iur;L=Gg8uq)MRIjL`LYmI@VqU3}(|2 zt_c%VhDpQ1c|kJ>r?x#@Me~>62ojnC#-+;^WWqvBQ$wJ{YJe0`#ZoNArBKPoxEIdu zuY~x7U(`Y<{?&OS)pHO4u%!!fSIWL;M`JU`tx~(`kP<0d)df4VIt0pfFhQ*^vTVc= z_*3F*0GYoJ`5Qna#nZN$$eZO>ctPjYrdQfQ~q|xLn^pt~ab@SV$9SrUFZ3L)y4W+MT~n{?VkfU?!0? zfXicD5_3Ft{v5TXnFDK#1&tba0T}_};N{4KJJLK|=j>M0;;HL&+Ly!#ia8%<`;cIO zj5+i7A+%?*u%En{wi^UD?_*{hjmCJ8XbwyxGK!QANr5AF`uezPkMj=cO!j+$;Qg#A zXHj=r_HmnQS@5lU;D%N3?Qeua?Tm*RCuh~TZDH(HKRcH?HhKf9z|pGnBd&&jg~{OzF;-^Pg`r3vgi(Ma_mAc6_U2y8X7N{{*m+ z*2Nr-lx$WE+gD(~I3uYjczCC{t)3^rP$Azl$UwQ3B%6Y3wo7ckr`CaN=m2>_{)uF7 zc1BnkgaU2gPbz4Zn|8QoKi(ev{9a# z9=av(zaf|aHb6zZpkg$ZS6?uSw`o#8IURD7+3{H?+Wakxrz>K(nZTPpV=rq$olyJ! z73p|e$3L;OL-`2GyRy8jd$6Q*ht523vXy1+0})m0%WIu{iyHeiLo z8(`#`z~x9Nm8(ZnUs?lrBB`V$#oZTDL$p?zp8kS*?4?A+&AW1`LDXYwg=ml?+_C$d zZW58JKRCbc=ffUR@~W(sn)HZe>$#t5RyLbUtarT#sHVWfmuf(ExQVLTYd2!zLr%$%xqpF;^C4AWJXpdRt2(@f}$FatW^qz zh-Irt;v^H_Hl!lLUG?R!%7Ti_CAQUfxIjJ2{zOc4ryH$>>#3P)<-(N4T}9%y0;y`s z{TNls)LvG-#zu4)jnntCiKScUX#v7bZ?{Zq{8ajCg6b3?8?i?WBYSIDN-!9y82wt~ zlD6%qsLG;xi>4`-?xY5?$QhED%S&&fKvrNUkWiCqs;JpS6J1smSvxX~SgS(OzGi`!$Z8}7VI z9J1W?)P$SfT5G$X*EV9Frd^upp=f0eb+eOnA(PEK*I42;kZTcPEUF+gR4gYRX)4D` z)8&p^8#RT!!^C7zJ;&vMJuLSwq|vO&K^rE0PClhy-u`sn)oJc1r@LDEr^&0P=Tt(y z_kh7v=+mRAlwIR=nX&z>D#GFs1ypu-H?UNWQq-Za7bzngM?Y{C?MA<ZKRIC-&eZXi^jY`!Nu?F<|c8<)gj}|&-{#&0; zGg880riZvETgb9jl?Esf&G%iQ=2)<5H>~bOInX(?+4{(8yHkq_@foihOpmR1!b*hDEbn*YrAG&(qFOEe1EbYrk9zd+2@GLgI?q*>!-!t%q2K6$bpSCXA@K?hpS% zXO>8F2>@kJjw~VCIy~8&>ibRQ!RdMul^q}Xq>elZ1e%{P0_C!>083zDl}?C>UmkFy zSSR2D*>EQmS0q7E2-XiF09;UmUeE;t6-C}zT%8wFW_ku0w?>|e%Eb<=nG8?h1+Up^ zO~&L{hb`!X&WiOaE6%6BZow8AwXPTdgVE#sf$g`x?*umXMks>G zPel|hfhkrtE7riMHJaaIDTB~EZb+I+dN|%wWBOMEd!Jyc`XV7&8>74-V;Z~7wW5@B z^|Uwx5ZUHQ!=?;4W%3fY0zD}Q?CpT5c;cmBVhC6E%h zAe6HYEm;^Uv2G^~wy?l(a)GWMqJ?3dPJC?w)i*Y7jMT!?c66*C(~I*@a7MwwTWP27qRCI?pF<*mXge$;T* zvG(!HPl}U@UYl?`Q<{7ZBb8-?`R`1A`^axsz9}xL171epwXq$4SLSJrCdKT^$e>sd zJt^qu#jtwe!<=;h&ARzW+0ZO$N-qeNijy~LYLMt0Z4EULDNW#=E@>|2Lt0dhh%k2~ z^4x+3!^5&Ogzf}ec01)@wlF{wnlOh1^bL;(-rw>5z@=D$r^6E)*uZ9Bo7GzzH4@sa zvZb^Nm3M_{c6fud9$LrRPRD|eOF#R?X91g%reHKaoIG;0z>O9~3p8i?C^l1>xtooHHddy$k= zjX2J9h??LI&#qpA|J87#C;2vKa}Ru{_$G^_3rh)zX3I%3rwEFt%h%E%1|ax5M~cYC zrD0-E6SI5@w6#*15YKTOR#wLaNfe28B&emhxc^ zAcAZ)Lmg?y6Qb}+kw}kW4q9|kQ>f;Th>Nttu0~{r#_sD5Dy49mnd%)?`%%-+T+cJk z0zT~*K?~4X1Y}6239LHXEiy?1&r;75$2lVe)oiyx$hErWg+OW{zY>id&+CPlgg25T zW0>*j55*tZZy7wYCH<@htvC?(9sfmR2W-?8Gwl{fOp|UOy_-4`(dr^+hIEd%+ifaO z=W7i@RniQD`PSv13Z~DhldLWfQMlJ%wKTl3gNGqN`BCL#{D zUZ|z27C>|^w2#$ebJs9(F>s9r@?JWutAeF*^EvEx@7@cC#F}R!5cRza-R=+kz;KbX1&x5OhOul2)nL+EXtwr3Gzt&pApUaABIEpYY-&I0kB7HZnGYD*Fc-OG(BJ zWdJ3hQ%w&MbRHWH*fm_>43*XSG~zp|E4W2kMYnlUCM7C5ws`|`bjVYMVlZE)q-ize zE4Jrw`ZYqDeoG-Z+o5YRn|N&yC8;suCC!77HyvQFk1k#9aLX9GzR!0Rj4T@?|t0Z$zU;)An>%)-=FS#L3ZpL)IEGf+bcChrqrH=Rez`=q`%IU|$= zQh(eLfqjY;X@UV^(hPUkmX+$KT6k$r=p^XN*Lxo3R546XHkhekcXdI*Xg$gFcvVNh z$T+e9>*~Z+(UgioTD3tF#3emfLrD)l=>#yNcJsqW$}2pvO^i?^n$Uu%h^cvaAWf}! z2vd9dQp^}owSl8vRiYHAq)mo@CdSYgp>D~_Dx~@d-*G8bW z;)uA`<-TkDq=#LLU%Ocz=C=ZdU$n=Ps($xiKq6+a~ zKSZZhv7rb+N$P6`36NN}dP`-SSBD6wE?Xra>#ey~zE<~2Oxs%l?5s+96KUZJGSQ~Z zI*Qd6MUB$&wla_|=_x?FY3&%Dn$%{NMdTCNJ!l{Gh)DSMvz)SisQDG{!q_=QCu)*X z=}FkwBG_4?5~bImFmy_z8{>|t4F&}d+MsmnfRFcd76Q=%m|in-=1sV_`jBOg(*2e3 zjmM9>AHXQlgDYyDt16vYFC&2PbJEuUKHry46> zt_E<|h8X=;i*^L9rkTBy)lY9mwRAV-(^63B>^L?xh-mGxb30wQRJJ5mck|^LyYvFC z8Pj8_DN4OXt92hl4pGP54x~l87U-lLnR7l{36L!UqxD4V6IF)@Q0uK%p#JZfMuEk zA9C~A=r;TUeT>?pf!-?isroAk=gH_KXd(|UCmz^pd|&8hhKWJu!S%E=-C7`X@#n#o z$pV6<@9;psm0}N#hVLbz0ZkbMEN!i+M!~6_~;d%diqKT-K_%;{#SS+Y4SalHcH}qW9jg zq_d8EV<%jIi}sJ+ZH_2fXknG^rwoW3>Da>eYy8X$RmZe;Z9%gMLcJSak)}GVR*_`8 zze2|05Crv#7`SjbqNqN(F_+S&VI%Z8rb+c$L5`}}%|~;1cH!;9J75CMzZ6YPL;Pd6 z9+SE0G^0dTbFq>g3|q`GA|2@+JrF>HrxQ>|LsENRBB5~>P;Ds77e zkWj}dDURF;gOs?`tw)&yE*WaEQmnlgS}-jfitH)}=qhOrzb*1vn`4kq4d!48 zBwH!f0f(IO=csZg>QJ76KO#QT7Q`Sc!PcxE*oo9cr2JDYBPV`L{$B9Vcz@}`^66N+ zYKnX`1{8parlzQgEa;AhNJwtsuw+SntHI(BLPdn9ksa?_h|E{wE8<=7eCetyrGa%F zk}k`DtR)MXj!gYx`8(wA53&|r7T%~Q@(omk-;TahyWajJ|LYv?(2a{_B0B8_rnO*x zFOMJW`tS7->*4yv*3)8TnADy{2c&B5uFsAZvLxCxPmUWhYj8PuBB6vHF89Adfm8K6 zs?+($c21`A_Wfie?6l-Jd(rKB2dC3I?+9U+oU>SVw#u1mHye3&95b~D4dT@ys8t=c zr)ME4F((4;Rv6JtO|gWg$cDIztN6keJWnf3ux-eW&tB3Gz3wfzJNM=@(x`i}f53pzWcKgrcsn zU+b8Lby`o?VAE+Uk%+S)`X1E$AD@~mB->E7+bXbM3FdY?&?ZMT*5 z7vVP?nMevzC1LJPX3W^Gp>=ac>8{gmvhEe6Gz2}6`twzzOY)%^IlQ?{7Q-4Dyje0) z$CB`wxzYas1Xw9FO}*CX+sv6qLk)@0SRBx#XQ)o{uZ(G1U2A%qc=UG}Z(OcHNZR zgg6p#`{C+ypS@Lp&R-GGld4BJ*cm+kz-J{YB8b57vNgJHVHP#c$HC~JaVQXv%xl-Q zfs`mqI?z$6hgOu5Qbw~l)pTZUJvShTuvCM0{p6E3TYghiEfnF@q4(YQ9CKWlMQMaS zs`uB%6_ra2JIH5wnfhyn$)}d_S-ZH812F0MliHGdr-R=5#W8%(%oI^Y!R6Iyp(;2! zJQ1s~obEyA5fzXk1&^YxNtl@Zu6aB^;AK}DD(+EpWVXeaUi*@%gDbGgtnKH5-63s6 zR+wAefQi-PHq$aktt)IywVu_OOEn?b90)-8#YVTta?$x4^Yporx#6f_2vC6!z1v#FenQ51WhnjAGEnBZ5;FogAR%D2x z+jJsCwWP_9$9lb95ha^TDJNKI$lBAzl#E?Mu*|k^5iupgrgGV1`)-^e0|J9yx?Chs zVJNY~wWcr^8BX4;!BEwZnI^L#GqX(yQ>)#C^=qHSXQwb#kBIt*$&V9Rsym`EM&#KU z>+VGR11hCyPW2*v%waLwgipHnAdk%KHD4HNRdav#D)6W{^Q#3wtWUC)j=hW=GdI*0 z+FMmblp)!DizLb2JE=e_Vrv>wNG>F+ry43z8V})thHk|`&<3+g2sUK)o<;As8E-;j zf>lTNyWH8M-fJ>zn?7_}4pos|RZdS58I4}E$^Oh|)~0{s1W`tI(QRis$k>|0$Fvj5 zT$b<&a#D@M;=Dih4v^g7LV3yya+9TPZw>$w44LFJ=a9Xh)2mPB=EK-ve6ZhVtn;d0 z?3{BD2$2y%PjS==?U;>_ELP^xwe`-}i|PDD?XNl93Av&+mt@C80?-Aqd+vf`|2w~XI4Y4-m@*V9W^l6_poJirsG<0x z51CcOL+aK-+uXLtJs`-aGh~VKvzw~=gEEi%irBP$8%%AEqjkz3bw$0d4S(ElJz>Ogwns*6 zo~TO^Maw=-O0}fvvDi03zz6mO4t=h@KqLzfSn|v)Xy5x%@JKh4mReQfG~PrXD(|YETc7gNy1swVv84wt5TlGq=x#i{WCWo0rIKd=8W2%H zrp)mINUagkmQqB?jg(rr5-C_?7Uy^cUXJb9PwW@=m=iA88!kN*V>1u|F~kclOwCD# zHr?5s=n>M;Mva-;v^IAA?ZkWV2`q-S2)pBxsWbH(I!(J}#J9TB7N0RqslfLSxObbs z2vT8%s+8)Bq*PGUB67a8E=en&DWUcB79MzC`L8tJ0X12IE$f2(UBPylE!_wpyuZEl zUPh0s+>O*1Z#3Q`12uAWCzT>+I)uYtcm-U09InX{C+4QaluOT7AyQU3O!7ay+h4xx zZy)$(r6Mx&8Txen>6PDq;JXj}>52ceffrhU;#6o$ef!4C5DOOVrBdl!1y2`AP~$Ia zB*!5Ol?hM1@{sH~PLhr+g25madtWQkg@@tY@P6SJiEs3O{sn&hmY3nNe@MzFqz-U9)km@n+9?9xL4J*VJ#&eEm`LRA5-E|u zN^?~aGl)SfiweKgerf*wSxg4Y;lIwbiW_Qwb35FbPsoo*1-?dJAsQ(zW$b9#RC}&O z;sJ5so4`s)>OWd`--Dw6A#V@1zobD_&hZ)5C!gmN9W~cKY;I_;z`fHg$c43YKR(vu zvEClnb-muwJ|tfXcQ@!xqL%GNT_}7EPoWJ}F?$1Sp+3*8J0{u4HFuRfO&om{cm}Ut zI<)Cf1zKepXH}@o=@VxmC zK1o0nx?3GvpP5_)^KMkY8gd2?2>62v^7;^A2S6!hB_U}L6;+X>;=YAa%^BA5$r>Nm z<|mk@1GXwo`(Tir3Qu4doq{k3H4kARm{$+JySR7PCe0znAx-)q8z(5Gk?1qM2c{r6 zQh(!~stqm=tX2e3rE2tEP>3+6$sh#AoE|b{{rnpa!wK0cG`TQ=phgf>W%UG4)~GZ1 znlWDj%5+W&Y>9)*VYO@1e*!XvWi6z3<`1D*m)&=c`SwGcFjVuNRmhxz^6Fh-nqD)5O(Ai z&<006w36UK8)Tp`OTU60esr{j60i-S{nSXTD=TE<^=5){gP-y0m z5X}(B3`)uh8jA0UfTqn-kRr29{#UQqy3k^OdRht|oo+Nqtp=fDG{OO!)HCvAR5kI?a5Z}oYJxByP9UHrNxG=L zdm0gUqG|ygb(3^hThPRvO1G%<=che?)fT7~pH>a`NF1aXlbj4d4J?uRZi% zL~y)|ig=cF!YU%9pzlaW8?MHV?z1QVTk^tstou9If|dimbc zss!Sz$g(T!<%{(5Qri`+Qsirdsk-}GD~Nc5(;y-r*R>m_Qzti7yE zN<~B~SPL*)>8>UwhCpVTTEt#r#ZiW)R2ohP_!;=*O*GOyi-wF+WCo%HZNq#lGeA5; z#YEtdCe;I!w06GjUq$c$p0t&PsGxS1&x+oHXT31sT87k=i@8LkNwRo2deA6KHlX`C zMw>yKwE2c)lw-ZuT1=>Q>5lw3eDGwNN)?d*V#VzOlSOi^cqN&L2$?PdZeh1Iyuh=J zj>D;HxhskidvM!TPcL?dG<-wu(wks%0Tl9Ky)M*j4cg&aZ%xz-+f6YdMIJnkQ&jL^z(;`jPt7ld!or&=>WcD6z%+rKbyHrmsGh=~e z>^W%~XIf0^|3>?>qoZ0o2CS=%q)B5Yn}WLo%Vn}2lJ_c2DCX_2OYA>kQQg6a?yC_# zkSa=Y_K~i1lRV_my;KMYEg(L}iZTGMSU-({a6ks%4Fo{!_PDY!b<9Sa#IB%*Yt z7!7its3XIaGb-C^voQSWxdU3lpo!%o$OT!aEw^+GKtsbailyRRXt>9|rvAxZ>}Y{i z`(v>qDF}f(d{{0*sCV2Pc@CmkeA0p{SuQZf!*6m20X8xq4rm{+_3Eo_sjxye(`teI zz!u;tT?>^r723oT^>8iY?*u!6Ta7u^&p6{}1Q2LRtP0ddd+2qNkRkOS#os-@<_kN% zCSuFa$nReGbC88vjtuO=9364N61h%mY8_p20%j0yzpYYXOQGCVI8_SS4JZc!!?j6W_`){~# zV^%J#)s&KNm;efWP_s2tj*f7n-}3T#Gw3@WSHIsR4Ui8Xz%qDo~JOtm^3O8Jag_cPv87UXC9mR!ATvIW8Tod{= z%`FsD=(OJA;$73K7?ih#FTgkQzkDmd{WiX=`1-Sd_hbI&pYjit{4{=Xo4F=UuUZ>b z)%6ECnTgoeWhwQ zn|L%YtUEJsWqu4vF!gXDEa3A@ZDOV{!RFX_JI%xpJ*-1iX|QU!v!X)?Jw(3HOn z7D>h^lYAj)Da8v&^eqcHrz9iQ<9VJ`oQuH#qa46O1pUqMMetBuinXu;c3~~7_a>zZ zqb>Hg*slgsi}@yWBecjn0BCRGwd=s6Z;3@AN|m%s#%=~+4_dv2Nu zaoWeBpC_dIGn|;dP+4etsGTEeBAQZ{YWJs?NPMZIP21pPHyRcgnW?EFh#t)NF7wtP zl6}o}a6Lg*=&pj>aI1I4#4qV}K+1&gqw~bFw3Q!N`r%~rnzuUkn7xRvS_JChVcaET9Ijy2xO!O z`fw;2WpmS}tNh-Y6!q2uZ<#ygDX<0EX{-6u>`r??V450uh`5MrE)}y}Rp8B4o$X+X zu46L9q{A`a%Rd0WNho9$yclu0sAuOAc5XZXU}!wf&Ut!W26Kc`Qg zFlI#>=oz=4>BAn$trFstPFk>(C8a?wtn2Ug`+s+Ry{@-weZU_}H!`eTwEMFH<>?~x zp=BpOEu4qPV`egLt#Y6fMzZmBTI0~fPal-%C{pK_khU3-_5o{?uso>Ud0w~5wyHLK zdVHWi*c=w>&~9eP*(q`kYt!*6ZDf!P@qx07t`<6jsHX-lmuuVgRNG=(?Wy{q`p;%R zS$WBhGScCtIQ%5B;6KdYlqj~wn73{_?yW+%-z_^w;Z%~b9V}fW)xF)|#cK9!ySJg< z3U3;&w!jV-QoAEFAt|i_7sde8gs)!vXlxFo^9r#FsRNg=cQX;6JBS;M>i`lEFO%~k z3A3LA_do;!YC&pmi*Ccuc1!BV)$G#YkOn|+nxr|K7&2=5oo_>}eeg>4Q0R^O8Gqf8 zp$EaNJ5e&Q$pnZ5HGG7>dj)QnEgGt~&)Y3J2n@?P9iU(YrVp+7&EA4q6f#e!c@s8^ZHvlYDE*hQ{Z!vEw1D&Je1v96p1N zaDzDI;D*Inb|d+XaHaiYCt^gyX`RFtgA+C>(#}ZqPE^`jQs+ntU(n%>!?kn!AjAP= z%UsamB|S*YOtZ(vdFi#vpSB6Umva{<>10UFIei@XoRHRZjYsre&<~EUt&qlP@*Z5I zcGwXaa2d_<%2Cx)(%>_NJd+XQp?#9#a9)78s8vbq# z*0D_@Sk~1khjF)|Fc_w(=cXnU2oHKFI|cBi~>{@vSXX5XJmM>VyvcmMu71pMMu>yX1J(U zn#^NeFMlqzG6{?f2?V0lqPi@)l0l?rq?jSwJFcFXiP-h<+IVWyt}HII%t((!UhAr? ziaL6fW1+j7tr3*_-VhNj+_E{^L_A-+2`y7q1;lFrm=?X1rD{?DVj_0gbAK#p>#PJK ze3@0Imxy?{sn)V-YJhm`wARYdQK@thy}_xJ=Cx?2vtZT;+2J)dN>GTd3s^*|)D*cn z_tb7uiUo)$tB5-HhKfdUmi_uT>rd1(?T#~rnmqI=CYlp$|8_gsV`vT-UInJz}kf*e|XPEMZ|@YzTDa8LRjz*Rt%huhlkPf=(!sY}{0sZTp?bl+qpc>GCvW^y|v# zx8Fv!oj%2s&9s)jz&%#nOp`}JR>dsWYM>vebcY)CE4Eg+V`_xq<R<+g9g>W1A`&vJdtX9vKfmft)Cw{Dk;-h9S@_g?m8b# zvpXW0pA4GX7vGaWUT21&90hMs8iq8Q8Gy!z+g8W&cU@OYCQa7mlBDQbSUX94bro;?#~P zw6cN{aBOL_6d8fqMn1Zjjs-Jfmlx?Vp;`j;geloa@iB8F8y|NhRBjYq2_`=96v%E( zl`GUB*%p6!!v_oZZ&rz+UuEag7Z7wg15B{%8lKn&Db-hYY*oEGd0{7&85{lu*8|IG z&7|~Fb4JCCyIS!d?Jf;jZOB$3QcaUM+pr29D~_ie{FLB>s|k_KZ=l3paGK)Dr%u@!XQ zQyNGVAhiUS{D(LF*I)443*TD2Azz*w_I1n8FMR*RchCI$7ykSdyc{2QUQOC^Ey^>n z9`euFzs8&4v7rfZc=3F1P=h)HQ5Y&@?k_L>=ArNJd95_9fRS+MD>9xT&n#)#4|(%j3mj~= ziSxlUZ@BQZoPbuM#-BW-LNl%uP15G^R`A}ars<9c2c#Y#ayB3gC@GZGpy_RA@EFMX zVTdj?fix1Ux-U_zg@@t&!aJ}O>%v1}f(6*ZT3BQh$^h_{4E;K`CbzbS;YUd#Cf|_& zTAGtwiLZf=z#oD#Dd%){ffp|&f3@XJdp*X!NP4&GRH@W;l=NiY z(fJ4YGx9z2$2P`7x?CO$Dp$p;rU0p85&`uE%e(|B_zT4kPB=-p2i=#=(f>`45#&rL znRS3V)I+45s;xPyTB6I$wo@S=So`OAUypAduh;Tt`i49q3fDv()ynU@;4v|)zPxk( zss)6FG_RD|Ij02WNYOdHHLiV7E%ZO5Pr0F{JfhzMF*z3+A#PZTRXEYc!3Iy$q0q$$ z^A)`;^S8An0kv#oJy>?a_2bSGPMU~hYUY(#F4t2YAG)?K*XyPBTK1#KPb#Nt1alXG zsEQ(ve64J6$CR(Lqx?u(gFC`uq@>lxhtACNzk60PdKL^}oe0C8hKFuBTU&*~b{3YR zg4tx3?3*nRWxd3%=$xac$Q+5AJ7Fns%*_t3AVvJeoY^~l=X8Vg(ZBer23L4WCHjAO zMKl=&rdU>Qi90}tPfre62V-RE4A5YOGATKOC-=c6+J%p2 z&muFb$-T_13Uwo&BmfMN8);UCTMAg!Wj;d*ELD7z*PNvuJsT~|CX=GAUuQDCb zZNZ%^;L&bEGt%w`LBIh;xV29KRg9*)3&c-t*GP9VKM91kdQvsFG~0P&Vsy&s2h%K0 zfx>;nO-P5z41$51cUi4?ynSLDX!%`IhA2H`OGA@rI;4?Awj<9Lz~ihP8WU@ni;Q&HdrA;P43zs2BGmL~G^N9>vZ&5`!&UQA zJQTFU^$8mzzgYBVXWb3&-shAWV2lZzDN}q1cl;>sN<~D>Z-E!wdK=H1#iu$*M8u#c z##NemsyLi_alxNox_0b{Shk8feZ@{->H2tmysbw_O6Cq2o_Jnv95bUHkPCm8Iqk^?5yCYU=q?vv9X%ky#;Ikrgi0>hm35oN2uoHA~-; z;vS{*QPHJV?;#>olrrc@skWXaYAWgok62QMv?PgDhXXbdXXw`q=?;kE;#ty*HmPbu zu@Z_EE=JCnJ5m#e3@wv0f%_e4N5=FzY&{IQ^>lYjXltoXmqt_IRXKNKO^ac)f}@6* z?jmLM%R|C~Ou3Bdm*6b4V3m_xmmn-5xS03y5@)903oJHKX|%2DO|?mCXMS1^DpK-BWohc31D zrVyY*)oz_dz0Cza`ASzSheWYQQ}yyqqzI(gu-| zWs+2SRpw*sdg=rEfYm|QU?14C!8&j;+$i~4wQ*rV>vGDQuAmdWu;{urlAW@}3x&Qz zcSG1CWEVoMDUBp@O*|pY(>Mulr;%AaFuY8k&Z31}5X3CY{Ra zf=Ayos8HR4Qbm+>U(t+hi93hy?)*Z9)hlAdS~Qu}R$> zSl^M#7jeNZTo*24h<(lXrV`5RgeYM#N%6vVc;Y3HH>|oy<_`gYcf?C3qI6)>lUrr0 z-$34yXF2C-QBB$O#sJl_nar_THPuPl7p!Dk3Nu(x@*=+w`GF0(2#3e_iSO%CXu^x7 z&`!x}DpdRWcl0e3_yPm?obM8Y%BV+a{dx34%{rlW~uPRLD?wpu#s^^$ALKSK(+yMl{N%& zuGR*d)$z+SOoO52*|-D``PD!qYP&M)ZT0M6_*S*{==F zADo8TQ|!RR(m7158=FWA(rmVH(HSY}nJ=^Q1ZmV;*L-cjCxGpKcqU|7D_9GdZea;> zU*vICi6&y%JqE)ixGub3cprnoi{zOVmJegYL?)hzS2Te%{M--^mDr?Qy>rMIL8=P8 zRriv>EAp%3=f3_C2@!sc#7F*0RWJPRmA@}SqD}3%H7J!mdE2Mwe*K2z3ME|Kf55kv z^MP{azf4aC0R4@2bIjFbmgKPB5bqB3SySXMk$)H#@YA2X{YBEQu>R86@j5n!3W;{N zJ06wV)EXvgD}S3(7FdXGozqN#c;<$dN8$=-O5L|ZO0!qX!xH(0<&Qo!iNb>UFnpxO zB&Q)o&Z;s?4GE{nG762U^2zzkw?skp@!e8@;krOh+PPIh=8aujEh4w@ec1MB zPAvIUcW$tWt!G#!`}CR+;>nLDT*(0Ho}f)}S2umDZB$e`8U6^NZdW(t*^R5oa|P23 zF2t1cxS)ZsL%;VqA1qN#tW@>1o!3j&OD$9!wpCsADfmu|v3y|5RQ8NeDp_+=(CMw_ z1h=ppi*Jy!s{dF$WHiXFy1mNvktAZl5^%C#U~FpM%&|{%FN?I9+)9PVOV0^1EU|X89#^l10u_ zz5=DQMXO%sD{fdxBWTMCE1vO>oJ_KFZ0sC}8;`;hckeXySLKzHi2?*?gNX=Z!!D%@ z=&X2%nG9mXuID+mNJIbVj?Z!~s%Mx{=_DN0fN4uiWEhmwD?toM=M_kST!VrY^qgdx zAQP>mH$VVr#GT@;1{RC|=xFNn1~jUw8Nx$|Zuj?gfI(&oQS6?Ok!e<`Dx%qbEjh3A zR<;kXk+=0SqA~lX9`rn39_?#83DPTa4{4u`p!SS@S<)6s6BaPzp&63%`NPmuj7jUn z>Wv6fqXvE*l&jbM%(0mw=7=%LJQsz^to#gxGHqAv{RMRF*<+WeYE}Bxp8Y^x_#WUb-;^z?pHPJ0i2dbz? ziioKQ)^opV4b3PIM*(2V>dnlYDs82$Rf;QQX5`CXY6hgKya{mQc-XaSH67FUOg3`fBr)M?guX8DHpWbc+AsL~w z)nn*MrEo%fgCZi$V>YB+uQK99VFB4B7D_(btg=`;gf&yHC)%d*%UhL_QO0R#j`nZ^ zXoz_B@@nKd3gC?By_kM?Lop4XF(eC*7pF28&R(|TwPs@q9^Z}v-AQCaT1BTOhYFH5 zHJy0Q+7PMVh(@{VIjCj$Tw6cnF1qrzyOkSZ?}budG*snTl=^`N<>U2T(9zx2z|JM$ z@hEDL#o1}PrL46+pxteaD}{I~@v^KOak^mI?u-R%52M+2y53aij9NG2Z+Qr;0Z=wXGi0b6BdNOk;mm6@jM;VLGHcenSC zbv^Zg^#zX&-`E9n*D=ybv;Yz~_f>xtl%Y)aQdzGsbGw>koA`Ml1eC3x6$(RH0kVKi zjCfFWv_8q?b~?R#eF!_C>zsUb*Zkyq(Xk*|` zfe>vEN*TbcBXK+n05$A|b>SLWr~ws!5`-e%-mA)lP=3S&;^#m7ZOF zcN;GKA__rB$E*AXNoh2OzM$o#SE%=5q8O0$waZNwB%6uUMZchmi36};#qI!VK$XAX zKxWfy(ATdw0*S;%PI6VPYA=4*s56Tv^F~wbg02$?q&Nz{mOTk{lM-84lBbNDuRj=a!$J z`13RWY3F}@2{!O@d=#UQpW2`ECjAkCDXs+-yam{M#pUj6v(82K6ncfLlY#WUAarfV z(62F(6DXIaQXnnGC3utnFh1pke_xMN8o7T>_~|Upps8KDdK{ z-m&SD@+2bT|+`H^>*UcP+fXt4U3tGYTk>ZJ0#D|lAnx7PIe`zpzDXf`G>A`&T zL&J!)yrwanL#vM{<>2?q>veim+WPk@XARcw?RppUB|5DU>v3)sfB)n2k(*Os-_UaBtoecMWN=M`2# zlTVlTC7t(apZbe{?I^h%sQxJ9*5+KCQJG&^e4=z)R51v z4~O^{h@d!P>Hc{5EmW!^db@TUKZyqE1KCUrms&M-B0FqY?7u%Jt8;`rFIq>_z{cUW z?vq)RQ1S~U@MOx6g9bQyUKI~9V<{%}IXmG=RYj2wFkq~@`{v-8*_lW!)MJ!{2uhkz zn8aGel<3&lyKb)gji0Ihgp630sRb=Eqz303E7HmqAd zatS7g54AIEoettz7N&XJed-ZJ<&JRDt4jE76x_p&irKQlxIwNIk&tRkRhcNSo()OQ zLje-*yP=Pvi`oP5Kt)*qz4j|WBvi)LP*sx<6Cad}iDJj1w9^_mAkS!L0bPHCC={Nu zu(JxC*V}9Rn7*oN%pjRTF0;&YaaD<=+a9Bp?m1^%TAE8D4xWeTB7@ZuF!KZR4N+%$#t zjowSpSnZkFOudMl9Eu{9*jr#gdeG<$K$`}kSgTBt@G2EJ&IxL&y1OL1PI-Xxa6l^C zsR6&%pWPsD2))S|;F4HswJ2v18dBD7waYiBRnnY_s(QQLWY*FmAq>b+UG6WmtDBcs z7(AdNswNuyCDsGXcc>{eGxOyh-cinCB4&}X-Br|gF=1E?tcs5}Z1-j| zjB)ez;6%gSmzl15Vtae*%+{(m*z}mYs=DuMEzhW3<1*WTdt8@ww~wc;mEkYnBG^%P zisqzMOk{gPaoO5^UsBtsN3>R~xI-aKrz3akg3WA|YN{Y#6)TRUrd5SAnJp6efM_jI zw>}l1O&SY2&ZTxc@pZixeuObiJqU_H>?K9ITN&J~YQS%I?flE0dO>{=Bly@{GDdSz zvd+n9tc|H9Xs62USuDzxpC^Y?qz|=*u3-}G1boxm^nWDW-RlUOc)GcydQB#xsXw`H zKt`5r~QMUNj=Zqq-UTb@bxUTUE5eEGW2$X0zn9f3WSLsk9a}w^mHE z7rE{&Q&la?$`)LlKA~E^)YepG`MI8{zK1oB)hG*{)&tc(iX6|giTZ5UQCbsvOo=&~ zW_GM}bTdi*DzlWu#hvh|()4&2@`W8`SJL>n#z}f)=ao)0V#kDG@F9punppDX6%Nz2 z!uV9w)>Y#$x_OEzM9d$m6Jamza=^D z6D=s?y?w>$o(!RwYXAU%07*naR47WF{SY;f>(g?!?fKnigOd*!5ChfFeHq9ZmbdB@ zLb6cH1-0Pldl@okEp}GY&$CY?G!*uJS49ioe6Nx4*eY73Re7x*=5P!EKI_oWe`srI z1G^)oGY@T)j8o+l_TNqdac;ORL^JA}RW-sF5s4u~k4mbX<iU>8kD z0C>{|rG+Z{HMyy0SdUik9$mbO)gbIE>yt96DHi&SWqpp-n{3KtQmpDPVX$)TUs$&T5GKCMpmuJg$esxa=)Vud6EA^pvRswl4q(Hrpc^cH z8cGsWH<)~Jn{uT>kpIr)_udk-v-xGe&sDAIyWpKxi6Ryd)utPArHy|s(RWETyEMwY8>GY!jb ze2+jUnP!5)3&h8_ioaO;yNCV7Lw|MYFP1Cv74X2%TfhIv@4n_AU;968#ZJ6{jn z{N{nJ@#D|&`|sob_%84fjQlAgCkUdABO*xQ%(|2aYyP-6$dy1~J6<)$hV&&~UuJYZ zjpVgHERLvI);Jj@ZX9qx@<~(>$KKO34m%R#Q6|SO@>Coowg(D7SoplPI92UXS*O^{etq>$ zy>Rp46%r_AA!GTtT=I7f*TUNy-Y-0i8Fh9H?o7`RD!#d`GD)TM*>bX^=~z{#E};%p z0N_VK0;^F@{2cz=DLXj+=g0vFz^<+TsELO(e;E@=$n9O(t(xFCWI4{AjU(_zN7b{< zHd#IeRnVn3g}tiO#UZP;MWuFPXh~~Zkgma3q>0BFGrXOd6nzO?XKJWmEHppURNx9< z$uV;%%B0^{zhOxPK`=)u!W`m=qJ= zNW?Q5(H&sy=dKO^_7FH!4tXqdcKT!HyNg>dCP|%Jm68tqry-3RK_s%D_`T%Y*0o`; zw51Qv*T$uIL?%QHA+iK3@G_)H8dTMm|1R+RdE+2CDzrO`yww;}H|rQ`S-!DNd|=-hsByh~7c- zPX>G+pkjEmLRZ7Py;0`ybvw*wo#q;qBQ24G7@wzAgw;8X16npm?A67s3vd{b`Uj85 z$O#!n`^KpT5McpxSuVE4T-Hn0Q`bw^S6zQH%U&~Y9?QaFTez3aUSDLA>|Vo@)MIu0 zU{X%wFx<(dt?J}LhTHX;x^{pkx1=I?scA;enKHs)GujR7^a_$Q5YOtC_n`6tP^nZLx}hfb;?!nWwV@o^K}LuE<{O@shpb5wUC zPo;gQq^;6W0Whk(?Ec_tm;ne|5vOdB4h=03YV3~kVXA6JOwUAx&s7h#hM#1I9Q?H_ zN+FzeH?bs)klkc2WXlHBBT$LVELS092REwmfmb(kE!x_R4TE_0FjmH^*9W%g(UCJk zMAb5KfTTX2H}}$mu>)x$gW?tGR1wX@jwr4@GQ0xD_+kWDwq}mh zN9}NhmW3T?n7PBHvm}f3!kS2tM5ZfbhnF&^oaj8fkoU*3Os(Y1?K==_t*%k6&~K}( zdWEJ46k#_Yc+f8``HgzGg_XbKBz=rxv6LkO?Y9@mH+9J?Kr%~pp8JVK9 zZP)NL(b9ymSn`UeRn=Gc64~L_8nq*&Q_<-ip#-Ip0wqXbOUNygC7Y6oDrWq)M-;nJbx7X+au&?+P^;z@J49DWi{yl%oHeN@uhjD_O7U(Y8Zgj%D91}gw1Gg@)oSaD!YL99! zw_@q0-jaJGXPv~-bBLUKE!c}Q71?uIs6SMhfPIctScZU7>r=&Q zPsQmeD7jby)ZNweSCQzW#u!j08%Y4VA>YnnpUx&T5=@QA4i9g%D(Z0*iE6!>B6|{; z1c`YdMU>rOmu{IH#T#qT8*i+DRLF31PRZJ3poUW@*URng{jh5;?ZMhbvmBDIZpdq~ z==sJ;8c3a2mF+={3I;5uC#5>4zpNDAp;TEVa^JoYa|KitEj9%xF|(YC z0cLc`YN|}9AHN2}1MBLXszyV0%dIjI4?KuGwih3lm<+br5~tP+m%&IxDV>p;6D78i zXl+U|JxezHg;(6-761rVG%6s)U>S7vjWixj-5YqPN{Ig~&;bypFQ&6;y!+JK9U)eH zDlVvE87%ryDd zto+rIUp-(CT$(?4{PD-v|Mp$r=jh9JqSB!l+c-%$r~K@Vi5|LIjFjR5m}Bqu{cDC} z;Jl6f!j4vtotY)QY_|K@jWs8!i4?3XmBe2y{Q51wedL#`bNbx#=?Qo2a)6xcufkAC@2O%XR+&}a4Dp)q&i-M~^IdZ$xKkrR zqApppRAm)ZCUIN8y(ua8gD=Rpl&ie&kcOnOXe;UaCv>a75Fwy^cQ4*CuHyr0MA=k|=t@YARB0<#;_H%2@UDR14?QXrhSmgQ+9tAVp?mI> zX4B7@quA%SHt$H7I5iCff^K%PKnAY;kN*CP$Cr<{XZma9hr}i72oqYc(jOnljtpVh zo;dyFgBc%$uFczrHLe41sOqgJ3_K>#Qi5os$ftAo?d#?^dFX+2CgZ>2vvvIabAa0* z62fZtpNzCy3tM$xn}|iKi+E~4H?GjNMMKtB{Xz6Q5ulO|Cngq6;u5_$ywNl+l*Tef zK3Hm9Jyy~=T7NnQOJ#FTcUKwov1n99mJOrvWH&xjw>jG*d^cFD^H8L_!P21eA^0Xi zl>-DSD)8p^jQmg$Bw8T{gw@MTMKTE%Lq^N0x>Jx zyf%Q*XK=%1@>7|`sm=ks{izDbHq!|HE)Fi%4?iPGZFRmf8?^h(odreL;DaCJ2cKw|KEl_x zAM&q9`ni4)=?3t~1d-5LI+zgF$vSaYV|0x+UyT{b1R`kl&nRg22cyzH)Eb;CUo5%t zI6^EiiEI+iB1JVfK=8N6b8r|mJSOfwXi1IjJNs%(Lyv5)i@f>i4ao#Sg}c}MRRvmJ zqrEU<_z@z-P#nKX$#w!Pp|xfRFssB2YWQk; za-vBR%N_Vmi|sxPD~5Rf)bW69O1OR`r{Lsnkdrz#&HK~0f#O&GMR8KxCOu9`M}k;7 zA4Q&>vqxZig+*khnKl#TFIjxXvl_&8J-Sc{K(NeSzD>ln z)6^iiu1E2r9wim-J`7u?k{%mb1fHh{WDNg`Q?b+<*rBEj+)PzXRiK zRn z_cqbMu{MJ#8PQ(Oce7e*>N>u+tmLLi3cPJ}s~5-L?er@m>=nwk%@iQDbnX%L#A|)0 zy1BP2H7g>tsTU#)JU)$IQ(c&=bv7N?EQDr3dC2E@Qoi!uzCp%;BwS_+QjWi$cq=!{ zI*)`L2y~Ezsu5h%yM#pO3+OT34C-9@9Z{3kNzk)3kSS_){AuZQ6YNrJNy}#C_z#rJ zd@}~KE27!KD$M5#i~j9%7F$ueh#^Z_<;x6O+}O1W+`3KAslfUq9O)|KJ(}IFa?*YI zEs<&_$aKF%SLsh7=#ZqOZCyo18PA#Bm02Ts(`-!`y(8U9*-h&_<#vmDv!=;&!LE4ge*kP`>TxX%@hVK*sNQA7f4?2Xq8mUtMnGUR98)bCz&zylDBg#d72 zebPrFKd=R2a8OT%Mz<#-3kwDY^K3+A_87DZg;F(5Pws*}uqs5#I|nO+s=f{12*n;U zfDBWiDsA3HL`Ds3VO@9_jAQR`dqI}8j%f_0>O7>4z1z$Ufr!$dyeLPN!cb&+J< zpWcNqU0f7)!4}pKHUy)(6dOSjTZU&9tCd17n86l|?k@#}=eY3{d=FUs&hX0%zq#}m zm;S{=ezD{sk>MWt^!)CLfBuL+{hZ&u@;`qFJn$^iMH!@2E4s81Vnt2#LTYqt^v0)p z)S=lCBSze-rP3^J$Svop!bfy^8A(MHeOQZ3pW=W2Iq~6?c4fPD zRX{}#^@QtfloE)A9m~F@ct|f76;0IY9ObMmo70V#!&mznNHm3CYlV&bkaB3vd8tCg z1Pi}<=r`~A_6V(=Gw|`k_aEsmc_P0?ZqM%>e`uK$PO=dw^D2;9$nUo2BQD~!d7cj&;B%M*DXWH+`LThW`ihT^!V>Ph z+23C8dLoC*@Li;-`s|Hh4q)ku?4Fi;V<&fw<#;JG9OvZ zkrJB6ibjBLzNiaHt#;sA8)d;ZcCmxgfD)f5gyb73pV-z1&F*k0Pi`y04v*$47BUl> z>V445j4Ue#=eVoc-yv?2D`g2x(gdpcEpB05cl0~-(*wg~}}sVR@P1?I(!g`IYO)qnK-Z=a$XkEO2pRI%Fgc`RO4 zLz#%FT9{&~e6f56=lK6c*1zpqmLy4Hps1>?ecU7Ro>kTCt|FQM0X}j5|0Cv&1V~^S zM6-LUr?RW^5}A?V?q_dPRX(VS+B!K=iKqyVbM{^rGc_|26_K2&albW=&)U9EDyQ6@ zlHp$y|CsqT`bCVT+}a z)aoCvqic9@5h`I#7jMpval29i5D_^xH~_Ly)LUv)*;@@_KA_|fLTD^_IvS-}Fp9LY zpp;w8OF_GJ;QcTd1oxL-OxP$7`1x7fQVkw!xFctFi6A|7X^~|^fVOI(;#G+Jq8ixq z{ByOxR%zi7g{fwwneDxMk&)rIL>OOjBO6O5l|Y8qF}WYq%n>3E)y;W0fT3_WlWwSq z-T03WbFi9R&Rc5)vB%Kg(!fQzaluQ5FVp}fCzl~f@LEQdbv_>Gmw2DCXf#c3@F2;v z_EX_X-(=~B%bTX;xpUB@armlft-gUVq#Q|DB;&>or#Fmg5>(-)q`K5=U?hM@Egc&d zT>2eN`M%*pv>$BtHE*OHJH780nfDpgz$N(YH*BnNm6{HkK)Q>Hnx;2=S*`R1n-Fd5 z?*(-{YT8zEyo|_%dx!4e?5&1`>$#%mu5Wz~<0sV1P$tPsolLR<>&Mxs`7O>&F586r zZt{g@ss`7pr_eDoJ+i7An0)m4lvdfd#~{Z%vP~M%62?3z5*gnE^nq+MwS7qxFG${R zAuc{biu$n%!pV#-AOsdUXrtJlyn&qUWfhf!-rCCI|GZdR?}iw#SY;WPWpfD)iO&=}8m5_g2%)3dbq(MT+kUWGE7dy#cIM zwtn*R-3wgivf}DA3P?wo>htxir{wLdnB|#9`xumF3);wP#Yd@Vk*^QCb~r?LxQZ?{ za;j;ziWp`$Npl#b$@YwkSw~Tkc)fKTiJT(6m8Okm_w0+zLZ0i%C$jF&qeI9VzI4MNNv*18oLdL&A;9(aQ+?AsVBG7ISkS>eDzg6}Fhh z@TM%dhRjx(%~Aa!m3-I0EW0;?>W-4}^rf=@Cg=fhtDnb0mVGlh>{l_<_4L%NS#}_S z-UxA0(`}dyubrC2Ee-e8)p}C0-6`FjGU%>rwR|+_mms$d9^c<-U}|Q(TW}&m%_x6w zc4K*@N_m%6vkW)#6n=AE=N6(JZTe0bDVZPkL$@7Vuff#VChes8G-?Y+Byc#X)(`ee zt#Qo58W9}!wg??0dU=>X#5pO|kUf1iEye9?oWF9G)mh{+vjkYQ8#oIyorPqb&HUmKCko0%QTL2EniflDum5gtCo74EJue+X1z&&PFDTYYpd;%$R&2AU47)k zi^75;X(|n+D5!-@MEUr6o8T1<)A2>cbCg-xMl0Ly$IJ0UBl;pjpD{<4sAX7aE(o4744`nroa{;EE15xDJhAUS6LAlBJfe_`%d zr{mm?c+KDnEd+xgU;=YgD*!aU$92&ybLcBN_7?nf>2FtjXY&2RFCO^WLw|MU&sMC= z%<#~!EClxeI*AkBgI&rfvuB6OI5gsVg=xp9JXUP1F|*n zW>s-xq&_U5X!5V?Ndd&bl|O&Py0A5Wc=!M9i@$%_c<&&*Mzs!nR$o1&ue2^_u67AD zVS-PJMC0BlxUL7zKguf%`QZLLVnLW&{c%1%&wo{uwoSRUq>K6OD~9t6944|?Pm0)t?_XdEtuR%|tQGf0^C@I0L_}0_ z0PT=7EOmsx(d1LlT>0=oN9>i;H8Qsx%tjSL+kr47G@&UgW4T%#`WzXkpkjpVvSUjQ zJb@w?Y6te>kgXxV1YKL|4LQdqf4-gY%~@Mw@dZ{sZh z$dfP!qN1cKvq?d0%1H9TQ_^pw;)$fHM;+a+3K+y80==-V-|bJI*Zbpo&;6eM z)b)<&2GW*DNZch0AQG8#^k9V`bUVLtY{IMin~@v+3&B-M1Og|#Skl9Ddg6(fM>zpH z?EtsG2+i?Pb=Ru&q2VYh&Zus@|5g@|dA5?#>NHK53p_1ET_n=NEJR)9Df**O2!59G zNiwdUB;4Ln(aGL>?_+lfHtyt!!J9vq`vjzEZS5hId*GqLbSSG}ekx$>U6|dnXfK^- zUf;JFiO3ac?fnwb4)}A8g-vw$+xUQKyUJJ7VP+g;zq@YBS*Q+83S|1=R8sWSo!Eqk zn)>!3RqAmHu?Rx#X$7@ss-SXi_=@|iePSbepVW0i?(9tp>@)ik*q=mN*Az*aS5il)xc;A#Ivgy+J@do*)Cy-<|A+8d)^Tmk?bN?==S`_u2gb9yg~RT ztor}!4bVU6Pi>@1wNTV#Q!;4cGf9>lxBuQd)U~!%W3f;=Mjm`q1zBpxct@M#wHk?n zKV_Zimr*G=lvWhBGbW8?MwCg^9F>fPmbEqYl_0y+I8p(uw-jk*q&+F5X|6}#ngM_&h{I?}84 zp&jZviE$#D>L;1slbPubNOdjrc%@6EY5P~#FV?yu7?^ysA~$E1fn0o=QqN@;$ef;OxS=V~qlyS;aaa%ND@g1bA)g?U{`$||DZ ztjb9#F@H|@TmODox~ga752J)#6yRV0yAqmpgI2ga9tV};q6 z0a(w_0MW7hyu)#rX%Ti#-Z zR%WhM5SEoCTIJl-!z}5lM+!QJOc)KbbgAfbKUGbw+yzM?B+S;{FB8iiunEMqt{r|| zk4oIhOn8dP4$oeIU?QUCdy6VkRyXw22_`#`Q#2wfP%6ULS~cWM3=_fVc)^#gNWa#j zoEy_Kw1~Rev=@1g%PwR(4}nEpFKG?aWmgehHNs7`49NttL>->3+Mx_pL{(qDFIynv zYNDBLYTo{SEPA&_kchM;26#sOWJ))gME7BYpnTC{o5HT9}J0W7V8P zc&o2e(59DO#ixZaO+40E?JY<56WH+*lqQ*25Ni26QE(s=3xSGJH-aHHricz&(94r( zZz}psa`@~Udousn{8_z)nxS32dsZ^?vos>D^OhUCmKkX`f5;kjpK~0Dk+X1r_)l5H38OciU6?cKclM^IkYBMH8VYP zO#_Uar7&~$Iyt06|7UhH2>lR;<0YnvYalMR*7_ zB}Z4=OPWegEcPrwW(hfVz?mVYlrc}EPPS!UyLTd+DDrtFqiV_srjm8no3u>wEH35Z zof)|A9=fTmmK2-)f1OUl-ThS|d!!zR6jZS10ym~jXvr8EBC0$sB+qVq+t!QrZS+r< zQd5%==W%6D!(e@BcKB+67@8j3$u-}^wLe7+k`Y@v0GKqbAU%Lws^7(Ws4ey%O+_)- z152TW{%8AdRA^fhDhAU7J!=V83SN&!-;}?2ADk`w@IJMUC1ZMZ5c`_J8}up;;Q3GIR1olRr8-f#5S6gsvD1ljrg~m;;OGR zz@-JbU{1c20aX3-B%JCASkNX8(5s0tx@4ZV^qg3lY+t@$ghrethP+*6YhAb~ZWst2 zfEd<>Ge|5SuzHEBBZ$xmWl#Vw>=F!(K+Q-zDBtyf1*3Y2{^#~yOFTZ|S}=ZQkO`~r zx;M}EKh)4d6KREILK<9di^7|8U@>IoK(sz76Zyb(VLfmuY~fN|B^|@BJbp;W>*m4- zG(R^_HHpO4zeXSy>}R+HR`R5IXg2=75`v#;{>t$Eh3_x@#T$Nh<@cH^A(;>iB8n8xBEopTz(Ls!ESFoCfevEQJC~@g}$uDC0gV z1Bwqzw1wl|*c+S2w%QRYb)tIX+=*fFLF zpiB<4rARUJic4`VTuOx;QPBw@D_%-@kvX{| z($rDiLG+%lZ}updx$6`_a3SlyH}DEPH~z)pH4Ns5g^@WrZ+7rjIS(yw4=<@S>QPNv zt15Yn?WUN&_bjS-s8g!wjMGWR<9qhW=8C)Px4yLxgmDRk-!O< zovs6AQ%J8G`4`EZx{wwUsgZdlR$xV@R*qLH_u9zJfQdW=pCrC>{M_U(i9h&^nR@Jh zBTSU0&@`Du$XPIC%Gt=ttPJp=hiU!ff;nJV7p}K2`}4!!AFr>rFX#n*N$fr*wuzP; z>meXLZmt*=0>n{0osaj4nnW(UudZ1(;ZG><_v3@^4!Rlk$#*W;z6a|Px%rJ}^7ABt zGReVR=`gWU1l?{6Sr2ZRODWPhBm;cTJ0%&Kg3FK4cdr2+! zLh5wP6Lf?1^`eWxD8BL5^a?2o{C3h88_TAgx5qcqaBtzlg=G_u06B&fWpd1Tl|>dw zsio?x?R7~RR5};^*zo~p^*RU-0fGKwH62>321=o~S`=MDqL9fOKUf5bw}kN&dPZ21 z9|ha}ZhRG{skz;;s;RCR5dx_knFs8>wHvEXTzRma*@JipH8F=wd@U(+#o%cpu#fs6)Tm(3+dS55>B=VuzWEiO=C{9IA zkKg_-W?b~B^X_b>AWsunPo~DqT<73vHTRNMcs($}hZN}rsP;~0b`n47!Z#$`xya0j z*!`kQG}+QYO1C-NN;;})5t}q{Ey$&l2(E5TGYo9VHtU8}lM{J%(jw&~aSA|9sWuQ3 z`jxevVLPCbAJyhiV+s>^7vL6GrbGzOjxF2?SGu>EjF*>tc6!z;UMeOelLc5}&v-?| z%U>d5Y6CZ-(n&HrqrCWfdztAOWzDz4A9fAjX=*C^@@KtV5z)q7s|smmnwe&Lc(w_l zsp$3%lz2<4_=iN{f%PslQ;Sw91lUy22}ygu0GM@ul%X-%~9)RHPMHO`?b;z4MV zWI#qb+{{WdWNPkPw3`A&kUQ!Jw!5hU<>UlYtLv&IvM8T^duE9056)NYOe|gW=8Zl} z3fx~ySEQ?$idBb^iM?{0SW!nJ^78#y7fM!$ly=@ai9BV*mznNfsVu>Dq&O#S23_bA zrgfp?^q7n(^5A*7c`4YjsXVL9=_8B0uVY=3r-S5BAjP&G=-Azb^W5rQiUiuOjnCJ- zn@wdRMad3eM2di_?!D#4NTHbM((qLgs1>QK&V~%`7Dye%ON0EaL%~^*aq2l6kB(b! zEw#1AsYr~roBNXq;?aBg>Xp&!BC6208yvFC-Vp37V^QV{Z?>-xc@zaWW$J?{r(eXd zjD6_q&&_pAV8M2?+Blk(@N?-g3BA)%a~eTG#$20o95QRz)!SmFWPz3Qfa?Cvo5rkj z)9yCw=}=SL!|^MoCugX#+KKXkPLrLoEnmhin3(PFswK{(3-9e*X;T_b%i3Tp2 zm?yjprh{rqd0txDzzZ+zi!J3qCc^~~_9_*^acM(Qz?CNV8>LBGf2VRoe$ zqzrt9JE6TEdrSZvV3FbA&ENDre!`>lG<*m%$VHMJfDVrC(P4?nMeJ zTgp=(-o4O(6bZx|N7B}8+U*&zklA!g@RH6rE_cCkTgv|RLMqQjQ|MMlcBf6}$XC^s zPY3r8T;+Cd;m1$(<)@TD_I*kMa`NCdMgFLVt&P0LY z2$RW44d6Oa#qvX&e%s_LLvtbHlZ_bq*1URV&ioSh?-Q47BLKK zI5stvb+m3cR5qK4dp?=9$}saR=0akrbwb*rmXYPv6dmD3|7B3?5vct8K>4|mFV3oD zz6*2RQn2#p7T+)Z= zk>B3Q6UT=gO$HO5^rUYb*u)gg?;fF=2E0Okdc~jL^N;WH=baZM6_)w6(u_Rd z$>1~+5@!!oPm)nmhd T%r2?f-RWF_klm;sMA;#dmMs_H>&iw+2ISy8xM5zos&Dh zg4PDre`?6`M$Lk(3~tTu$Knr7ust65?rKHjA}i28f1?AN=nAHPk)NIr!4Gp#p~)PC z8%1P*%FDV@;S2DW#J@RuE(AH$w)nd>T?6MUW+b0S5$CH1F9&ZtZ>Vlj3o?7&JK#(9 zrLfbc#0DUi7cvqXaFKCwgGhENy0oyZPKzocxrQ?7zf)7+hwBkIa^?B$?L)7tP-PRTv|95}?yx*?t?RveXy@|c6uawB#-8d%G5+~%n8pJ=H z*flSv5GgS={zf0hirSC}m^H^ht36T-Jgn9C^vAa=h&OUi{B{c69LgF<=))L0*5^teF#-EWQvMM+!$aPUagMLr@502qhR_={oN+EWFQ*py6V~V9uM&SSd_*= zI`IUBlUn7hNbuBH$Nys!N64Y{C>TF9og;lgb=vL4Tu1U&jPoWNsu$fpinPzj_KIw9 zO4yOwPyIF>S{T%4d`@)CFrb99j*X&~MQ`-@k^Ymd)pyGpGD{iJB*v&vx@6Y02ba?> zi^Bj7U&!%3WC2Q+#|H20c}kFN210de>#fkGNM`Q6&GfdgrPl3_Cid2;R~9~V*;61@ zk1<1yx5Ags>;jt65pvND;Iz9;A-zLaWdz-PFwdW0)bP8^t@sc+&mL$7me-3dfPz!! zy!d*X1Vax+o&Efq{H#}_Vihb`kY;(=-MKnW1mbBoA;Yk`)>;bcw6B__ zY^X2OVYr*ztvL3KnCXVjp*hbX!X^1yvlcvlH8e7$+%kdf+r&f_9@-7)ni;06Nj?$k zfnaK-sVda;6`LvGP?4RkstS3<25{N+itXVKT^@Vsdih4D*5I=1<*zDIaTGiNQf&d) zv1=@@>x%G*DE13TTUK(MynbshUjQYkxI~n`Z!Vk0z zh!8}gcQ~1524Ja0U`u3%duFLaGHqEcAnEDpB2cwzU@ezF;c`(8790bJjHTwWtL`qf z%qVxOVxv7AkX2@eYCHEOSG{WwH&GLPMQ^7?WQQ+lCdou0Z2(v{lH_W;(k@dk`jgE< z9-Rm%U_epb!ro{iO6rmzFjY(kh_oc9?!BH{^;a>;44Em}&%7o-#c);6)68O$rRqA*V&hN-wkF9;i48z zcC_R?misM8C*yz=`WynK#91-iPn`;CVw{T>$?&2{m(@&0GSV~Yt_C?W)!_!li~PP? zlNoAIXkPF+wBf9y-TR>2Xts#>G;?L0*l+aQ{3j8K7LOB+;FCIIS|kBmb|l!N&0)(# z*J#ohTs)wnfvD47-~0Hqq6C|FO0Ro5W}B`eD18sOFVE;?xZCYjzk?jMsv8;3TNat! z!%QQRGR5kN067)Kb*i-bgg_+!>)1sLFUyR?+O`zg*?GkKp~! zX|2Wq-4eJHlb%acNBZ8o9Cx#xPCE%$Wy0BWI-DX|!K#wJxd4-^zgad5#Gr4vuGGgi zA(X6hE(aFu8@99m-6)sYx1a?*eYjZA3rk@et}PWhKYCsx5w~ppL3@7@$1L$pcc2>- z2!Sa*@hUOpg}_O5YOlH450ya2`oj94G&l%o6~ZYzU{l}Y!06rm*Q{6)x6J`SrSIh~ z1uOJ9L8M-8?telAM6qsp(SyZfIF%$K7w~EZA-b&$mr`yI>*>wKG$6H(R%6hCO2P4g zbOF481^J8%kPDWTvwqYbC1%o14>vUZ5XGigGZq?%7j~EMhVOX_BOtY4!Cu!VUJ4OB zFDz@6K}M)_%MxpLXTd670xf>(9vMn8263(jvIMJm6a%t39=%1^sKbTsOfS4%*qi1~ ze1>(Y;T;@xjL4X7rK|nB`g($Mgv3cRZTIN4X%;4JV;l*hw_m&oDS*}?)msY!@zZ6T zx2EI!&{c&cB%tf>O@HygFCO{J3*Rk&02&z);qnUk>&`!a!ymuNe|Z8=VQP^e7fhh5 z^SXkzkmaITG)zqd9fchAY8aZfg1DqvlMm@H+WN3Y;;3HA(M3rQ@`FzWATYxx!Dsnj zzms2m2V3@sFYo{9%f^?)p4>Ye>Uv%-)iiZ|pBk%PZAC)#G4#$iBSR&UCT9JE59SW_CM4e|6TLI69AwxW6XA7c3nshV~ zU2CrmyBCD=3IVeCTH#!nyGSyB0F(~BGHBzKPSZ>vWdCb=Y4ePJ0AQvE6dJS0OURGunJ@oSAQA~0Zc zXobx%@`WCh5w6Pd32GT9>iC$!}1H$_@rR0Zv?r6g1e1r|X?fDA~{ zD4s~QS`Q9iVDUJ;43~gg5`&tWRSRYf+UyjiOfzlxHGA+LEfH^n;0|yDdNPY@#R+M+ zH$mfgf7Sbzd13TAo4**S#^b-1x7#f1Ljv%;Bmxn$mZ8jN`QXSH(TrpVnfHx%)u;du z_J&K@UJz6&2#Kc(a?PUQ9hDJY_;M#gMZ}StL}SXm0MgS>ZeY#y{t~ut31-2p$&T21 zQ0|FrL#?SB_3lI}AS9bt>*?4d>u{gVf=FR7oRYYW$WK6PTj8O@JmAV4crQTo>h;%aSgv|i6H!X4q1vxhYHl9rldeutZ z7C5}FMhSIFu`pB3tY2kKlfGbf?oL|C?9|Qx%_dkQLsM@AR>$oWkH~lhbngNnYNf$p z1?$RmgD8uv(!2JKS~NMtYV9z!)`Efigh-kWblHE%njnBj;Go9Mc9R+Hw^V)Yz=~u zx|g6iSyUnH#`HEiV^e+eL`i@u>+Z#}QN`L!(SHo1I=#`AK1P zldH;MVpho@C1c8bnCXbRnz_28Ayq1UGL9#A!oL(C5e~&!&YSO@QhuZbZmG)g%Z{+ zJnX8+?Y<$EUzMh<2sgVTwm^2c$!R{=zEhy0CR*<$rMCgCDdbYP+VD+8pf~zt{-(t` z)1XginpgYAGCyl%H;Za2LX<6x!tM|vv=yXOJog!{46RI(#v)pJrLHPHRT~AGOYN7^ zI_a__o`O&@E|L*qM(s(duWk`>$Wsc~TaY+Gh8YPhiFO--cF{J=%;K}Dtmws}yFbX> z+|Hd`$8TzB5eW~-DJM-MjXEW~ewJt(3bD zgww~u*nxlDI?x-Ro)9atk)Wh&=kx-AhZ9ms6z~c>LVGn4mDX(bz$&}O=^W?+Pb8u@ zyE(9f(p!*^d-z5+-`(ztia4oI=U#@8eT1S9&^5G9t?ckWsod2Ir|^ zq&V`y*15SvGIm^PmUsjE4nqw-}V+7O{q`LsN8}!vkx7%bOGu%tqSQK=R5x$^W zBr9(XgW27B2a8k`;30O2sZ@}X-+&kq$mVHtU?5PMQ@6@ZQ|~?u%dix(0PjO@61o+( z^OuE4>0OFf=*7%v04U^j_;)rbHC5I$%z%v9=|}DRQNQJM4HDvywE&D?Uy%@H3^+VN zw6T~n*tvh#_!xypbTszeh3X~*8QdwPd5hUO4N-x`vRyKiBz{LCBpmTV2=HoQQ*XQ` zkcnhrE-X8m4|uZ~GT;;rrhkmrh6xzDZj$XCneatXf*=+J4HE!Bm5&noi18=n{xiSf zMU6u1sTn<$f!x>^B;ob6%lByFz9!PCZq4XZ^lBwsU;l;=MOvJb!75n=SQj+G4j}dj zBz)tQ$OR3^hClHdMa#f3(#kE*W*l?GL)JIagE^>#Fd7nSXA5<9-Ha$9+sd&md^Hlc zH$@SZ4OnAL?x|`%$`1}I4S4d0xfp6f6Sn^DlHXkVn+rdG;AhJ}!80TSp7MOjPaA)E zs0IyjAal~#VK@y5erI+T$X6_Bw!P>3T2y1$!Ino5TAsS8gl*eWp zLe8pg#KgUGq)hRqc*y_q8UOIMe}3s-U-74}i7$z#o@bc2k+K|2|l7h3t+81sC zSN_w|e~!S{jjz%Jc*y=jxQb%I5RJu4Rs7!6MoztYTva8C!&`~R`VXJvx8KXpzKh2r z9|leN7WnBIKYWd^&-}wvka!9t@k%t}*`9=VQuz+&furtr4M01on!p50zF%<30KO%u z->Q3HX4rkCRiS8Kl?l2RD#M1W%N7!i7Nnl`U1iv~B*iyP-WY82c1UB?N2tLb_;lfI z;bC|f9s&ckj2}nQDZGLd2FOus7$8?VlWSBjH(p9hi3L-niYqK|z3_Sk_HawLj*8xu z-S%=ax7M}y9}=GgUy#4A7X7oGBF7nWeCJxx0up{xD+Ds;f%KP?q+*y@ZD8NLd~dCI z<$YAEznY{WHPb?_$j^|kH2`LHzU#VhSI%V{0v0PaEa{9u5xP4P*Y2K=Lb~ySsHQVVmO#=JQw^rYbVzEAHFEptARzgUO;6RWVba&eI>U&HraC+(s z=&$}VQ{C=KV7kfRA%6tnea>Rr5RxV3*45|e z)6vgn^Q`X7`cHjHm5R|P`kR!o*qx&K27R6gH+&Q<$}L##wv0_aPy$dJXP%zm^D1r7 zn@$+ga+E5XIfs?uc&=NTV4l-4eUxewL`~NjwC<{yQA$9R@9vIPodstQx{MS>MHw`_ z#g`uCh)5u4?tNN#AtYDk(cGR}wrZIq9E)8H5mi6}K{^qbiF^43^|n))%`?D3bp{WV z%r0;=DcI>d+!#fwY0s(Kh{gTM_l>iv1k@}ul3+*;Ej25?xmKYA-F4tYp>u_|RPLCP zPOD;cxSaZ&a96XMr^2k7>b@_4_+YiOQuLeWn>_B9wYP{d>}>{m92*mCAl7VG&UsVX z-8j%Q;0u0- zJzjfvOq$f16~1@f+3WRsJRW5^Yi43A+*Q>eJBOk!E>|SeGsE3h$86VT&deG<&p}p2 zQi@GcQxP>=wM0W#P6%graS9Tii4si~2rH{*t^!ZrBC=$et=HZnQWBQ3s8iu*RI9|e zjMaIP)L>**Y#4hsyta0g1lFfAJx;ktPjW1w*%84P?a?1ncz9cIkx4q#I2E5l0i2MI z8UluEZ%D7675eA1eoYHS6Ghx_5O6-0Y{I{~1X08p9yK+%vgh#kat;>b7El{n?&rA^ z5CLQwXnHLUn$6-^?eVR9U>x;j&ZM@(tZMm=v7!-$xIQfCNn7wNye!WgcxSN`VOp? zdDD&{3$n`D?z<@>zBf+egR}><$gN7}6yNZ6dy`U;pJtD)#->(_ab~ah=zSWQB6?Yt z{=Ik1B-Q(8y4|2}O++&svv@0$^n@f3vL#-%Jd*7^>qBp(YIly_``#ru+8uH?Sq%T8>|>2#=CmO$2#597;Bl9OaxKPocY z_w_KtJ8`b&i*l3A9U-63`0zF>)Q2kq3#|h?oe@2F(V3QQXp8Qt$v&g@_UHIS*0?Xb zhCVm-^c3!T`;qjyJFAnYKe;ZdlI!jPcela>MUR;}PA^kIs#T(8bzXCu0A$l1y1Ks@ z<;#cO4UJIHy5`fiEesMw-6r_CX?f)2#?UT4m+*ux0IJ69*V-bdrtukd>0QfDKaFS4@y2)B6rYA$fXfZ^CVp~gdDr+ zMJUXA;Hr`t&@B#>5n zSu#>1gpxMplOg;DuTR>zWk2(z)DO~a>=1;}8%+)ZJHO?`bjr)CPJ=?=rkK1{aas27 zF8#YV`T1M^-9vt^@g|{}JAl+z$d{M=<%K`J`|sZK{T+A*p1?*^xraaF1%Q{NL(nFI z`U5s>bExaB$9Xn2!`q?h%r-SR>cf-p0s>iqPlh-7$4~k{e3!p?AR>PJY5%96 z0$-JORUAJ>Al1t~(C484LGg$|uEuRm{y7rgHohiaez3W!CrPu=`ikO8wca~!uX{jn zMb;Qjq`?9vxE9{@fA}81{4T!#oSzjC?96XZ{N)?|^2A@Bz!O;V3&F}JM3C8{BrY$I!v<Y9WgbOHlN25-QId4SyVKy+3?r&AA2cAV(Y5mwiLA zz~NfIV$E0-ncEP04m90RUx;{!M@9&7hTgL+E!S05jir2{dHcr1S%`DE@OdTNv`j02 z3PQN3ks2??$Z!6rVAo4qSq@Vn@0vR@0FRP#xj@SME(Lq^A2HM^FdH)R+<(mchnH_t zyHZ}rCD2?VmD*r|OuPXu^3;4Oz7zaL^N+p$)s2KfKQVg22`A5WG1AnZ5tNch9v@lDs(45CvYKKn7;WE1j zjKOlAwFV`Xe&G7^J%Y;>d7LTA8e(^$;y6LADW6gh`HD1$rhLnwC|}zTph8 zEsouNGS1#~EQst;DT~>z-f>pJlu)q3C^K1MtkT?ACSTtSkJioOF`O}-TV&V~Vlj}^ z6t?XJBQEDLZ0sot54FS%zsHu+%J}aN%e`jV2-=-OmzcvUiD!s2n=q-Uu~2 zL}y(%i-E|!aZ^s-6<|%__chnI^I{4@?ACG~?ir4C&$0pvUMOh}!^chx29Y9?%UuU# zq)ichv<8KV=fUETXYRclarg2d!i*(Bp^==GO-0<&UYY_$~ z3=7p#Bss%^mq&_@HZ}w1`mRKo7B)p^(lmh3-ggmAQtH^*@zt?X)SuLPFas_*a)!qe zk-D9`Hj6V$8>p-7_F9k6SlFB!2fkR24nPo_^-vmL`T--ymq z@&#dBp4UMJR~6YvwWBgjL_3W#_p14txQ3RUz z?rw<69nqO;Vo2=1o}6`o#^ZgOShlwB%a*$*QpG%7p(4t3uE=cRxSht1ylfTG_`aXD z1#fv8XPDfWY19fsu(^ZK&8! zuBfJfUu#9?PR8D=RY=n7wM$Ytg+0Pmbu=6EIoiD%sNeQo^lem>S9Gxn)Ak4j5Sfqb z@(h7IU+>e$VF zs{*!Ejd=CUoIHKByvN>{MkZh)7{9dSNGx6=VfXFCh{gS^bhVN2IO#{yDTX>Kg5F-L zWMmmIW`vn~cA>xdkO~r=E7_E04jPzrvK?w?TExz_h|^6e>=+FDZzfdpNw@x$ZfQ6j zBw8eEX*#2Gxn^;=!TPh_j`C8crK3yJ{6ue=(2uN(px{#Dy2?)RoKWmaaLZN!}w zs-~{0DUTKWTIrY*aLG&a^2UEvSNUgfh?hulxH=P{xMch%TJT9wpSZ651n zSZ%fa0y?O6%#^#6Wil#5SkRS8wmqpbbzR}R_XEPSEXBFK7fv8t)YMi)kUF{%ge|mh znqSqpc>;UY`{(sIA|A@52)69{l!NMKCi7gSfOSr^0;Z!alt<0txl5f*!Q0Ip|8AECYB8a8m;7-q}g=jps;3|rL!_7gzfy&EB&xU zGPp*zK~uShiFdfsA?T9GsRKrTxx3BR;FSVwN{fcpU9hn$lpA@5#%=53>)SkN`Zv!_ zpr^o4X%r36bnsHr@GTJ5&~rm$i#)+>J%QXEdp@9l<5P=nS9S5;W{Iu5!8!1^&M9y* zW;lqyEi9oiWATpli$IBI2EY$|AJW$^st z(!YD?uioUB7k+O32_YK+LFg;w>z1E3{`!o6d&M8$6Yq&{f|oY^Aa=&569dkffS@jl zMB^PXF9=v@_gAKfMPO^T*fA5Jz%HM74XE^JhpyRjBnPT3yeS@rPx9}+%g+xyANcxp z|L%+9OCYpfBOx}&_U01WNv0?eYnfun6Kz2+$OXmE1wU!V*B8D3uWTc@QG3**c6tyM zwN@M?g#)Az*mXXYS+fIsOz|fFuTT1$&-trQ^4TgJQ@(lr@a}*3>i_whJfBgI=xyGA zLHr|@YU}zMX+_xvFxr~9@jwY^Gn5OtEG-st?_r5(u+|#2p_a*P-K?%oc`OG!4eVrF zz*&R(lj~g+krr}3_C9ovSzO$b+gPI4!nLr9U=~2>R!7YG;MilXNJ_FvFMe!rrkhY{ zN-Cmu@5($alXaoDOP<~wEg3kO0-h~TJX>P^C*wUD1aXPP^h>0mLX#Y{DMJerH5$h^ z^!qY0jzUbfWJG5?-}M~$br!!PBH|EQZQWo4!D6ySk>is2E03?`y+&1i6EnhPv1hbW z+$=4;OTOp<(EGqa6R;kH#YiQ9e?$HwWJ3%oDFUp7M@C8rT;NxxO9V2+G7%z%NHM{s zSSk<21M)rOr)v0^3phZJoB&JAUXf$0#%FPv)9x-fAL8YAI*Yz>UvW;Xg~ESJ+fsds zV%0AubwGv+iiS7^x#8fOj7bui-=IGt-yHfvUbVf1M6TMO3YMgbynu(|U2tiB61fbo z>_+s%>r9RMI5F5rK^Y1-+6iH7NCaFWcD0X(i-y5MwNr}HM0NE9*=&q?Zhe!?S0cqC zGUZP(bBZ(c=90sQjN1&I$v)je#3Lt@wAAY5u(}+$uonE&PmfQZ-oLs%TrZO+P!7~0 zz5R-C5tT6=8)9qdcBtlh0lT{rB)o0Z@ zEp{huSN)1JiE)7vP*uY$pLw3=9+7*;0P#4ruZLOjC?9{NDC`5U4|LAC(2pxx#8B;; zg)TKm)24G3l3mJ%?&m&v>4ya4&J*+lJ>AUbZxyUuZP7iB3S(52@Mu1=Mps4>?E)1Q z^9u#}F?^^$J*QC>rvwa=Bh;C7MN(2s1jS}YRltC@8W;ly-3WpTH0qhqFRtGWaRs7n zQ+b$zAi>P+reyl7bDo&cz0Ikr;S&&%9-L*&COmzTwh5udVgW_T0igsdXa4rPtO3+0 zqz>xW_;_m@_oqFzc>vj=YBg!F_p2^~J>#O#ee5Xr-ZtMVP5GT7rZw%2FZ;!n3yp zK9M191E{9+bpn{$EB20E27NY6`OxIGP8~iQ06gQel}L}s$kb9AF^Nj8IF*8u*Sb6! zhT5?k5zkarvn9ap_WHWP6WU{9)+#ZWt<3cBvP}a}vead^-7C7OJk}a674gW&SJEdHjqlwHyra?Xk6o0X}nE=ihhAW-xWHPE+E zMBJlPvs13CR)~;s>diwr7PPGxn|RtCF*En};}vg$9}8y!X}op(xtkvqMi?$EoXpqx#H*R4`pEgPlBkII0CG{tgL zz}?RGb*D7o<}QYNo!+4In%#XmT{Fu$q^dBD;H95Hh#J^O7_ntD8!3j=c z1`V7krBSIZD=GMntL?0Q>{yg#;eByuG1%lsb{N8}wOh z87(Vs2na|I+R9iSHL%3f!IigJa8#=Bm6vpliSB8-yvFxN9n<^paNT1 zH}&6YqZ8L5M- zdcVzJ(pzsR1PfU9A0PUg&-le#UMoL|Y{+}$U*F>o-~9i0cRUqQG>m;&0P-(;2#jRg zc(I;FSFU-;#F~>Ln4%Tw-G}8*01?06Gd^$Z&Oztr6%*6FWlJ?NQv#r4gh+Jgj196u zc^mV|Qar4Hl5Q!sqyDoRu7&l$C9n&ZoUzwGQ~mX~R{b_Ey)#?EavHaR#IP}Nx+SNQ z0xSP+;mt%epOQQBr&r*WsH31V0!>F+bX(Nw9BcCg1eTEqm6zVQpr9FGxdKkj@5>Vy zU>sUDUvXH3#$&nbCO4dyh|sK*Eh)^FZ%j|Tsb1O0of)3rLiv0~g;1Ut_l1Bsa&k>^ zy;Zh(n^V~1Oh63k9&e#hSY&m*A~RCAzEaXeLSkdDT1Z>0f`lS2(WUaxd9xB|D4;6&6an1Q2aeQUVfqp=(sBd}i4?)f)(TB6rF&<-t89_lOCR zH;p&Q`!SZ3b7DQTkpS^$$9DqVd2P*|xu7B>$pCA#kL&$nOYPzXDI><+3GxBSh1>`$|$Qq1xP-XhLBsVzMJIlQ)8Qp87rTr_yoy zNYaHE{BTj+7ae&zU)FIwB_8X{Cll>vnxt52*&pojtM!KUChL-YL7srCM5Z=7rQOEP zlQ~0fc+H|f-lDmq0I&M*^gmb4qK#qlf_wavDk7vrHl zmIIhpiR7gIn@tE7AeF?GkTg}etEhMCM8ORVlrjPez#IdM#JIf6p$7xN=c<#(MeYvD7FI0*-_9-qtLU9<}s$ip5a4ebfJzh z=RfOwp7T&^iEi=La$ZSYcD-X_CTO3bzZJw(q{Ix~K5_Qp3=_ssQAxEGJX}JZqYr|w z4fwDDV9stCXLf;TxDa&;dY~<>tR^06+f_FB}!gKKkrRB0b`n zV?9g&nh3~w)*e%gYw~s2PG?jI1RCLO_*2+lw&13(SU&h4b|Q=F@T6ph$ZK|axg7$l zhSFTF5xqGO%J4DO!OU0RPZr^rF8;rL=oOBR(2`@&i~C3fclksAZ?RKOpM?h zn_X_|z2Gy9yN+<~q5iEz3}!vLM<3Q$Ty^m#NKVYWP!exwy{QbTk&Sxwxx~!c?J5JI zO(AuseH#*Xjc*FM;}{45<0v6$fjL1Bl{PY$Eqpyh+u9oV5Z$>Sr3M@#xTyXbgHH^Yv@08sS!9 zRP?FJOcUJ^T5eq#%hY3R{Q%(7%O_f?0-O>Xdx>o?i#bt|wU&pQsz8c^S!z#zExUZb zv_n`VksjN0y*a22t;ozsM_E~__y)9T!g}CKH8Y2%dbtC*%nE3chDRaM`Bk|m^D)SgX zrk2nuV%dregY4KScRm0a&J3_gEh;{5XFRlJVZsiQB2ZQ9`KPJ%Dv**mmUKy0EIBZU z!YGu=|-G!Mb)6v?nj%2BmAe(?;J|(%^dVMYDO&p;Jk8n~dy^=cd*QK_KcX>2@ zU-@rr5|X2MsW3e19E2`wEn|pGan_OErILoAH}46g>B5+US0gGdF9GUCle;r6rqxWt zzIEZAwrgW1h`bFA5*bg`-$RiYP6nF#~f#x_CE4MQDa+E z(~n0Fp11#sF5JkBa+!||C)-57V=%I)F@tGrtbE=N1)Y3Joq3do$jGFvK(VH6qb-N1 zESUIKCEzQjig@g7B=l(EHK4B3yI*T{@B{d{elccur zY+6k6G33-RlL0eJDc-nmnb8vd&TPerq?4LFr&**iNa5jGn($ZU zdy*OC!eqSa1ryOe$56;-sS*;qtXB$A%>`TSs3vSh6mlU5#H9EDsePUdBF@FZSahmK zIb5|yA(1aye+)mQZQ=yBxAzU$fyVq6VkDzS0o-X3cDG8K@ezm%YxE^WFV$iAM#e~S zV*%%dvD>OIPvo#}x@b8?w60Fn!9~s?=}A?KwhW0|ZbQCuO(gZJH++mQff) z_EAQ9B^|`2Bq4Grx1`Hfp~lbi^khLVTn{V(9#{f-!2y>eDYNo&T395pjK+5w9lj!h z-0&BkFFXr_Qy{Pzme7(HY1F=fR2riJw?GkxD@A6~7ikOjz=PI#O+oe4g-hGxH11Xd zWAt!I7_OiXX;wMAhvfr`H#$6!Y#4B-NYMKH!VV2k!<+su5BvPU&z5|D~9bY#7vg1$R;)f^x%QNsEcmg{S?}&u$egd_vh`Y>dG=g)+C*+#L#cIFw!$;&{PI74_5b5b;#=`giM`>RM57jx zn~(_7`B(|)1D6r~_ou|a`o{BxXTTHN+DWn*<$XhPwdICt&sLwHoGnF-dYEeEu_-xTSexV@Bw75Y3< zE~ErB)5>tC&Q2*?B&0HzbBFvGF-j(IbRWHJKb{+{F07?-U+fN4c(}me`q+a?-=xZO z?CCP(@JP32rDoK%VClxlA5<$5GsA=kq}oKOV7jcD<1gxzTm*WB>o9lflFd-bxSu$ zU?nc(f~dwrEJWPeg>E>o=@Vem!3H_IbU9z1ud^mGC~k(yAWQp4_s+#{NX7O|TE^t!6;dOI$=m4e20% zk(5}?KD+x=oYBAZdg>FzX<4hwkUklm6HOyrG6sgXr^S;$kQsjb{Ric%K}#gBi7rQ~ zX*k5-5G^L!Q&RE@Nzo9oNMuU14Bs*`Wg#+BGBXq+4!Iz&$jtmc)PYy*Kc@AG%1iyc z+x8&2OmtJrqfPl*5rvlFVhTtNNbj=QoaGE_u9!VvIGpZ$GdMjZ0X(wf;Yh8k>gL(D zn@Txf1{s;NJ$TwYE$1Sg^3BsF5pGgGS!c>7*VLF{JV8o~bOHxq5{@_wR{LRSp)!>b z6Fq==kntJt2HKsI#jlH+8PI7WZ5fvgMq}EJwfl-v+tak+_6KY3z zs7Z@I3Y293H~EZyp3QHaT&R|LWel8zy07r0F$S$&lC%qioLZ4=3uy}z-j1m`yzaDkM>3M6 z-U`ZOwUKgA?AMgt9IR7yLe;9xx}L4OSJD`>N>F}Bs#0nY)$ZcQL<0&+1Yy^jhnxO|< zs#=h&>s=wS$;=9@%2qKB^)#XIs+qZLCExG8`#d6q1HO4dR`UGNf6#65APZH0(N%10 zhx+0Npc8GYu_-~(?+BE;&8{3Y!As(HgpaCiTa=6?UqJ(dBE1$KIYneq_w6Ty_DXAP zd>G|UZ@*b9HDKT6h=bsGL-L)Ykm5Y$xjfw1IE7|*Nv@|%`#W8KYFPD@ohJ!NR^9W$ z8>VBg#^Cj(TO~mSy3*bI?Jy4?T|b*Yo=95v=qQ@<0;&5&dZdS6>&i$^SBSU&AxyOX zzGE+4^+-(3BbJ#&hD2ptRb|`Z0z|m2)lV{FXDDRv*D?#lwD*>OV8MVTxKx(n%rw=# zH(HWA0+5UhNikIq&xpt4ZToAP?eL`*5fyr+*R_3DhNW<`WM&&);huIn%>J=>_Vw&k+K!7l2}tdaVSdG`D_n`k!A^- z*t!|QcdV*Noe`ATx1l9}VUtUqhtBeh9kHyP2B+DeY8jh)&fbMv<6gzhY5>UrV5>~* zUMYJBsd7Ax7vpSpd#8I}SBlv0mI$lQ(HKKdaQPsac@Lf95jD?0WBe=79mnI;z) zFw&X_HLpY@!z0qx3iqxWB*W@=OZvmTm5`wg_MSkP2`A*|Fr;+%1C}M-NPH@Qs;En$ zQee7(&!B5*ks5WfXI<~j4M^2;Yt##L_XM%Ui%;yN%*_YZ>L{XR>mk|O-(c%XEd!?Q z!ot|hYzy(}^&TgO+L8EN6%O%Ujm}cJL7SmAm{nd@kgY|2|OX=dL ztG)N91%HNIopc1w&J1Ii6s7yK+D7XMOVb!@ee(JJAKxcD-*v*2$Dit7eg8 zjx1|6Z>FvPUUp|!yU2j8Bju9_^PVMxQ!e_C#}bt%o1roK;@G+X@jg@ zXU2hWmL2{>bZSh4*Gpmcxbz?2>{oC4vrE3ao!1$Di zxTrz`lCXt`;L`6t>A!o!uP*&-=3l@d~&gi74;v>*+>E^1I;(fi7GN z55-bgK=$4nPsc8sN3>y-7Qyx+zcsh8pjQ)GjnpTlfy%krC|$obQ@q)K_$0r6%g>Jp z6UTDNFR%FHSO5K&*B`zL-uZ?h0RCK1mD9U#2G57gp&M5*Svw$IVZ`o5Ao5Frm&fW9 z7H;&r9_qZ=fnq@rd1zE_hRfa%%zcTGN04ire#V*zF`p@{$wck*=1nJtb>Tr$YP`N7 z=X!egdl%?{u5moQ2Z?aZ+n*1q9IWVGibBE-SZWu1;i>Z4MIFiEM_U*N-q-CJLT>lP z4I=VKksmGJCsxX-;VUZ0X>BJ>iNQdYbncE^-97iK38Bih>MFgZcSH?wX;KZSf-~A>^3v)7#VH*F zhJa&L6VR^^g*=d}v)~J%msAPld((q?-rY?*K!@Z6@`sem2B#neD!6j)Ts%g?b zD4F@#W$O;lj7SgUf+YTZ;9qXXV$MxD11#?i3I}N8lVKx{@LbUioY{3J7Epe<*SGcQ z`|Brve=~iAE|ri54jP`1M*17sVedA%je-6@_}2rdblXIt9bn>|;J{BOA5d6QmMR4k z1A~U5Z@_marrZ&|G3&o^44`;o$d4?oNw2u4EA7DI)8i!qlMdra2;_xO6)U@_1xaYk zn$U;b&tE=w1hp3Ue?}H zs^tD{HjOehbZS1xPEDmc?J~bi`CPSOHebE;*IuecB@oYM1X&CP?b+Cd6O2ybD<11P z&NpeEwIR>b`Frc(6*;C-(kRP7w>XyX{j1f#tZ2@L$CxR;dsk>Z;}+XIp)|tKWGZ{8d*M%h zX|)2wJD%fLgs$uJoYf~A-=}30#{JME3Mxsu zt{tk9?3*5hj=qrx*z6bx3@^*-+y}3rGi%N%|0_>A7 z`Jd3vLn(CgJNRq+3feN?E|T5&f7-9bsW}^Lq{LaH@RMmRC!6`nuJ?$f+BpuEa_`pR z(uxlysig}v(dlNCY#SSbc|{|&Vjb%qnY8SCqR5 z^>i^1>{3zpjZ&!0(7mBy{q?L*^E2eLx;0JP7<1HYb&;eAg}&nr>y!%`1ezJkRuUcK zd;*)N@7ptihG$*nEtNi{L2?{Kz4&+vpo1AG5pH&QY#lzbD?H74iXqOn)=*KXy!^#V z8+-z1&Qh>b$yH%$<+Nr8xjk2XdN(sotv+nKLvYy&4J2OsMaQ$!QE4JyzC~q~A6@@l z>KQGwSMFsiJWS2g6B*fJqlk!p?4hb{eF;cP(Y*jN!_3^bh&rKH5jB&DceZZF&td_iV6&2(=(;;QhM+7r+_Nuqc(cFV*!8k{160Tp zMiCh9p<0z#>1CNJ$|l%Za@rBN$?c-k=W_tC$o!KQ;P(!uto-AkLTv=zXQIA4Zt}QKBC!XqA8nG$ zKCJp_&yWm73b54drH>ZwlxBO(^qNLzKYn8k+J<|17itLfh5ZQ+gGsXvpo|6+624$B z?4-#|DRKgNK@_&>zEy9i@{JZufHI~7&%jSO55vWH4U|6}Bp_`w5F|c!f3ptI#!5T! z2sr-r&Ck&!s`AImS4~DzAV*V@WydE?v1B%H;f7_TxDt|xXY&f7)PyOp2|HGAsW6u) zXNHmS3gE&jE@UGBarkB~O9BC3ZLu}`yCA<<|4YI^m6HylW%)hyvuy>R~E9?yOmdqVY8m<)pXa^~epfaG?{nt5x3axI2&m-wkS~0FF&J zFc6Zi?M@V#YSill@*AF&l2r_uk|wO@P_*j>yI|H7u8+4Qb!S2a;1II%UQv+1779R4C%YO68e)CDbd+7HMd{R72kYR14 zn(6ZN{PNO2@BGut|K*u~dJmL4SpZ?hmY!KtW1|zH22#Aig?((X+p1Wj%ZRIJLs6w~ zlX+qr?qoFQ1T~5r>6c{}Rsa=|rMMIi{U6`(c!XGf`5OP{Pl>OA_rOj(v+bV5NN{q_ zf>84KbC&Dlqms8uNa5JUxq8#CE#q4^`%sSOigweYdwUrqcwTtXNFn(=FJRgKbjhzS zeS6!v62MOR=J@ls{Fg8L|MLa-hGy{zF~0%dq8zb=^{tdcpf~ia;PofP?vNnqRGWlm zm4dfId&XC-qxEjGlDX#kSLOfO9&LUqn=0ElwBi8nd03ggj9{zIj31e@S-D?T^k$?P ztvKfbghS;H`;-nQ_-V|qYWPwH-@31oXA|Ir^`%rwX$D*g&3BdG-S6@2purCxIT-;g zOqT%HDcJ9eK0)7Tvrtto+FumS1!|i)HQ`;Vso9JrYvwco&9}EABsTOV2t_Ei-JVgM zLzCQ7qDsP+e4m%qztxD3|OeXXTa786ju{kQJQ$Vc0m6>uO5|S#Z2$fv24ZDXWb<&l{RnKs0De_zo?gNI>jh;1n zi2w571rQrAgD`2VNw^u-z1}@DKSjs23eS>muq`Je5Xl5VzU3j(`m8H|1Rf!_(_DPZ zzF-L}6Pvcx7E!3BC`@w+Pnu()%w1v+%3d14I7uEFD~bA|9_j3b1S-E5DDZ~#Y8Op0 zq$W&cNiHCzl1YI;ETCB3B^eqK5{Zz{iC=I0=@1gr5XU!lmXCYE8D`?%hY<(-bgn*8 z`?L$!<9Dyeck9!2y~}=k_#@+mm(j<7$(ZPIC{m^c0zh|g{9 zHfY<82w-gcjZQ1LOcM>;xSih_)`c zV-$t#56jx*888|PV!$oL;W(L1ByF0Ag5Q>~K_+TK=)s|L(1{_mprm#{vZQ{c2Aa1M zPH{yyY`8u&c*<_1gZLFEgLWH5Q-X5)-9qq+sS;`zIvRu+Xm>hS{IiVCj5tIDM)xDB zcg04V`3fEN`l=6(+*eR1uJU5aV4vuOUiWWhL?lFectzpM&zj?7FMuz0>WNh4D?Gs`9J zoxMq-MxjjFbvfmrsTjWQt7ITl27LPM@r9hkK zHxP+XX}w3f9ZrH;7vktBDePUKpNDtLbk!Uon8>Ud>_iST6Xgr%e2h&W9WFU}rs$=k9#Y`L!>nh8jL_k;Sr(bs zo~;7K6s^e25;kq$6D_9H2u_964&^PlviZpBWl5xVpi(XL-Aa|5DauFH&EpKP_i)dw zM028(Kqb#L<0TMm_qA4`zI*TMx&UnVs!@flt7=CNsv@EclW3Y1s#dKA6|)ivH7WI% zikMhYyb%!@zFn^iIpP~!wqE{+B2`U)O2sp@ZzD38#ZmJUzTI@W!Emg;V|oQvO*5V2yO;#@|O-pkNy>M(R=ha)kOPvN< zy|cHIEGYXyi|q8D+T0>P>__g4%K?E{&ue$lAASPi8q&Y{2lVI~x*`)53d^CGYO8I! z-H~){<<_izu4_I7bf*(&2RvmrJ60uWf-W=j*110X$8l-S1-Ts{Yw}EA22=H@;%VM2 zZ@qGSUH$iaFA0i=ygx^oo}mB+Sh732_npOIg0`e}v0b@NFU{dg&b}&KX-(U^ zN!6x9`iaf&X=c!ejvI~x77JMxaEEum8VudkLOEuHWN*A=d}fmeAx?)(l%r@jb5c?;e1YIg#I`{bGx90DRO24~w3dm+hwlSs<&Qd-HJ;7-RJFS)z(vI*D>fvB#C zT}FQb&2$k}+SwzSGMb#deMH8zO}p>nlcvk2K#xb72(X)k2&8@RNt3uyN`NZUo?gVr z3Z}2O_k!Qh_MV*^Zm@AhlU~B5x+wu{gfd_1f(1iah_nKLLN2Uq*d^Ynnwk@DcfK^P zbPU0y!8O?gd_iRlA+)x|&f(wWKBd?ecJar2$RTX1I6W#47AL3{yF?vaLNSP28f zuxLi41SSUKE+S#i;}Qg^1r$dGYP^D9`oJoG!lFnotQsvF?kqSLr(|aU0Du5VL_t&v zy`VroupU_J++%SSCdYZQe!l73c}GL_5sMrRnUa6%;>;wilNzNA4JynUjoFDF!C5EJ z*l(}(n@{%3Px8w*`PD(xm4Eo=|NBplFNt@j_aH6Qin}eLMh5}ZzecMk^}<@XYJlxVjmu6gXn56A?1ezn zs5D1o~l0MFas4{A%IT70UocKz8EC_x$76{l}kxcWF7)TsHDs zjpQK7T2IC}3u>FuFNi5V9})P&3P7Mu^`5k?T7nr$1e9|ie|ZFa9~cYb;2GHUIqZU$ zAtU*L=P1k16oU0bMJM^BR$*GJ^lB{GPy%zrIh(%5||WPQN|;_RUKKVD)#NSVjJ%_>E>jkBsWZ52heeHlJFihWfWzO7cbwH27N!)(}qj zB^BUlkeajkL%x^#P`Q~90-5HIV5E`(3Dhxq5DVE zKY*@Ddj3aA^%84xrZJU~3TI-K=ouDo_ru&xs8?d$dSc0I1wo9#=~*cJ<^ z0nr=j9Oi&IJarByaB)rwz_{)AuT<)_H1yVi9Aul4t>t}UB))t$_6cH?eQ;rmVRxrC zWSPfHDlr}=oY{ol_1An7wmi3k0DXjm4kT@w-7#NLML=w@iXj>6I0JtI|Bw7ops+r@-} z51z=V*zd#RU;|_pi1HF}l*4iXP-B~IW_R*7BvQ8bV0*(r)D0kj(0aQCj*NkeV)uQ5 zwHzNNm9|sO^i5|6Q{@Nup4l@X^Q^<(R@y#LR2Iot+4(ICebs|h;OjaRnM_g1Ors~#DG>Zf6@o^ z)~S?#K1B8|grSu*TIN?^gFE#=-EWQw`kM6+gZ&MAlI&iu0BIVsb#-JcbOKVstZ;TW zbN5julU@A#CeUnGZ`ATc1cl!;d1ohohKgA*saZJue7ED-Im4SyS9>1j3<;E}<5ax) z^BF5ans6*hK`u7(oJ`SCMu{U1BT&>V^R45kg*#cz)tKoqG%M!Ej@pkg6I_lz2p<7~ zQ-{}JAp4FEfLjHbJpu)ik}JtE%fXZmPL|9NqF>DG8Uf;Lo(?~%DiODqaK7OhM`s$w zRE&P)H_#U2yAvaE$e23OhIn1zrjcTRG7#dc0>UHs0sng#5|t|cgUYZPEXw7zxQ3Rq z1ed}y$LsAhb}VeiAPQ1%f4k2Y=UjFZc(q=43|J=Cx@L#bteB4 ztizrmS%HX6vq2klJ%D^`Br+>~PP7%hAR|fVKqLaxgOc)}lWq}m)*`A~X}n?2Jv~W} z%+l5r>5&3wA>GxTb9iw&f%VK)<&IU8b+$SKrjLQaY$MqJc*x4UMX!}~)%hCu)pY)B z0_EHk{o&BBNxCkHv?y`fBe=u_D?ldwtY^0$R%kAi*tpjU)T}&iJ4@0-#iG?yq+u`R z=~dntSIOptuBFQDTS>7eED6Ax)rd<7KxU|(rjklYwCbDLU*+{Dz0g+gLoZr|6v~*@ zHt823#wQ|YqOFvAh}}IATDZ0hHB18_DFQsPxlrT zC{QA>;O9=OMQ_|hb@fQ?z2~feq+zLq8O?^BH2D zII{Dm?3t%QtfUnr-d;wp5~Bd}fH~rcJya82cR=hCal>9Gvd9kdf^r$s#?i{ZE|M&Q zQfP(d7~uSnGUf`l7<-$56}u*eu+BT_oaa0p!QC|K4WlT-s2Y)v4r+?c8yR-zh3$|R za6uMwERyP847L#hhH>yf{%Vt=oT#l5hiy1uV?0)VPCYf8$PTqY&;%`9iMi$VeN!$+vsH7}7RvBuWKxnX^r_-d&Qihjtidbi zh}#p#&VWpTF*u4V@+?ue4tnD=xl!^${81L9$pskxo!W2TSxG4#!zm` zXS?AqPAK%Kljt}KN#sT-%wR^oN3jf_fH%P#K0k7;+}r=RFHiiKcm|%pGdt6$ILoG> zj1!QKj{3$!9cXN(Dp0O$}hN8eI~Vw2jD965&6V zLa2e`pr@R)SS?4+h$e)_Dh<3NVciZsc^NV5WH|A+ift`O(ZQEKLnxc@E-j{O(+LGR z=;*za<%NauPPr|>lAIe3b5l=`Yh&lG_2c3TT8brDf(s9Y7+xZm=WC~W64{!GJyYWr zsx)o(|NmR`rt4|}Nx0*g*x8b)@=+8!&$*s(MHQ_(XMpw_Y{x&6J@Y6OKt8pAWk|DJ zsk37sJJm3X#1;wTTHpKu3)6T9u^=Jxym-2_CBWV3t zj&PRDTp6QgV56g|stlyGrLA^o)VD;WA|)~{3k^p`dcpgxu_gi5`rGJL;E-E=#(H}0 zEx9CjrfEoqc3!HA8nYBl@uv7*@fET$vtG`Y6C4jjVv$o-y05egR6U+q^i!UQNDY-O z`GUMc9#vAL+vi2F5a?1QX^6mci3hfeM?Nw?C4QUu!$GTYhOx{B<~FvcIU_#)7-l1? z{I6NEAE2y~uYb87pX~8wddX#Si9ORzY|Qj)Z!bXhuw^IZn3^{UR7Vfc_NRcGI$PAP zyWA!Xs3WMfy8-=32nc$nXp6 zllc!Rkr(;+fG*(KT|44Hbhc={^QYiiy#xfH4|Xz5zk+oEPY@J~8#>k}v%iro{)qTR zigwiu38+Z)K}XR47Kmk9?bkKvs>QR26Z*qQG6@MlM#XJwpFkS`N)aVr)nFWXwT>2M z$8-zts=1haf@Fj(BNEUxR7N&KPRi7(V_)VpTV(l>%Lt@_rzs^wcX5mVDNnYe`I~2{ z%2oczjKH7IJiHNM0Rj8LZ!zz;zUJw_o>|V(UAVMlq^fmZ7e9I}tMfR}CVNvhnx;9T zF&qNPdy$!cs4+3raFH~OST7K~l~r}Egj{PXegN_Nf^TZJdg2*vgwtC=)hMLTPjBJ@ zX&}fTKPikiZYj=yR)Wsq;f!;9$0EIBu!%@;^C%dYGqZo)QVg1@MfJw5w4;hzQh1rc13!o zM7+#QyMbQ9-E1XilBoq^>3aFABt%04NOnVpbRup1txqU~tsqNS6aXkY2<=*Pa$ZK8C*ApVktXoJUU%6}4SLWR7^~}u2 z;{ia$UcPG-?FdsVnG~AlKqfo_B1E7AQEJv+v^^h`9Ik_V)s{Rss!uRv}zCrLJSkKtw*0f-jamm(0{ zF42^g+EU@JdZ?zBZFDV)`=#%1sLT}^rVCt~39gURKwWaz7j9cC;Y#O8&Bkq>kLVn7Kq4etJx%Hl6yesAkYF!FWa~W-yTIq)#TUXDODV7&wUpX84xMnerR3pS8}=$rHf(5H3^4lpBTDJ?T|mW zop+Lsm1N4CSo^UCJ2+G{!$oc$`dJt5y>+9mtTClL8>VD%YepG36^&ODHEx6B%-x)4 z&0P_Cc@|O2aHxC;f5JI}mP@LsM(aAIY2>mNt$b#-^T;GUwjUKUv@^QFgcl&HW&S%f z0QX5LiFk#&R1_ecReALuM>k{mg|m8`1hQ zM+A0}E6(qd#v^Fc%ty`8spgztG%|7uO$f;pAUiW2Y~-|6=uyYUtU@|y0GQFZ)hsYg z+!s${(2KBX4dQhix4_+a z=a>KI=f<;K`xflc<9{?yGO-PEv#ey>J3vNo-X5O`5^(GaiWq9ppnwJ7h*$ri@Ga|* zdAG4K^IZg{xPVLXkni8{ddnZu|L-4P_-o=l@T@ef8`)W6tRzvCKzJ+UnjbPO#VQlL zgd}#7kqUU;Lc%}`&}@vG*%aag#a_lbIW@B& z^>c$<^+xE^oEb0gIEdc1+`>(qjTkF$&kctgs{v*+Lij(K>N=tU^tz*hl`zspL(!dm zEeVyIsokNUwMZaQlWPeyFG}^s87!kxTTm#MjO_yA>|*A@}&YaX;+bIpku_KMU*FAgS3TqGWT}r5OzpkQoR= z$_}Z}w-pdgNinDhHb`J6M`PV%f1R`hc8FEMGmtxT2VNUd|}Y;F0z|OM^hu zQ(miLhjP2PO9VbgJ_CQyTj1QVo*mnnX99B2k!b)oZ`|(RxzdKYq6LbT#`4Z-n9Wc?gMNLpVBE%ok}=IN&lTNz0KjwVgESKWg7wATg><$G=9Ekhg*ZT> z`insrv+Yt6bz00Dp~#lX9F%r&&u*b-12Tx6722-sTZO1D{~;RkfqkIX7dP+|dcx z(%eJ?3wa*w?DIcJMp|TWJ8#!rM9#kD40KdkLA?QKgC+VC_SPu!z^T&!(`7r!3V^)G zjb^y*lZF5SV9SWa6;ofW=$vxZu};?9cd$V9CyNLI6XNAW8>;B@k zEgIz&16sc>9ofJ+lEt%z^ra%hMU9VLl2K~+DOoR7R9RT=nOVaZWCEs>8^^(ui53I* z;czl9%4IYoDnX}fNYNhMCfBPANDp%We`uv5{+8?ntC>(QRebLnW^CXPPTuBT3%ixh zWp~UmK)~2C43*W(Rs)H0e?(Q_JZ>k={vsE(EctR>9IHkD#l^CQeROmS3w3ANQ@PSK zQVylZ4s|hd=k@fQY=lR30*R^ZaBE3J$mkO0`V)!ty1P=BW~%7Uts+GcnRF7BaQ{FF zCU_0Ixr7cY!cJedD(?z_6~sz%B>{Jbpe3Y$itX?Wj4moXp?a25v%Ot_wXVz*rlk}k;yc#51lZw`sdXs(Ua+mgN+}j-@ zW>!b6WhxO!=!6nM0I{`R+po1mprU#$AWd|;L&8+bL2AcNI)_mh?f`aEvq1(+BC*V< zR!9JLZzGiEcf8-LTJsU_TGRZ@UEFM!pcMSwfv~Q zRTaf+W+XBQVk2`xjlgdidcM}$Lu3>Mb*$gG|CQDa&w|_ZeAk!c-fOB_c3KeeZ&59n z)2U|?YEF`=az$9q(lDEx>M_d9qWz>bzL7Mgtii^{rUY_{3eA*zr%6|0Osj|-`WK+5 zUqrP;_1#QoN>uC05Om#uFjRlrj!UnvslC1Jb+f92x1XuwCh$B%HR%IOQ^@IH<@a*9 z+;`?DL8&eDbNZ*rMdf{}j8rRK)yinDKTVWjuf5Kl?xflK@{lyPx%Du?^HnXj=NOh) z^zRNg0kKo(v)m6?b3ml{$BdTt zUC8VcyTp7&^?~4w^_WabHvsz1Rb)WK#i>wzWN!1lA)?NXpTJEJz%^p@){3`dJbnA793xFI{SH_6OUPA#%t_Ns-x3Mko7a zRf0;VZE;zw7hcC8*Yw=wcbo7KPiR)uPil4m=>@!rJT;fWmRz3h5|I{oME*SFM*`m@ zvv0WQNhlnyZq^i>zjDN*0%Tv1DN>=OC%*ps_4ey^rM;!SM_lPH0a7?B%}GS{ks+(~ zzBB=K(>#`74s@rV8kh#Mq@6f>^Rk`5bUX}bfzaSlRP*1B1?|Iab3bfnC%mXfNy*3%1&TqDI{G}GVQ|UP!}<% zN_c2OG-HJ=;F0nK{t9_7SV>4jrnW)1Nb3$yCaW?wtlCFPOdpiQ&CI1TJ7*oIqJ8q- zR8nN6XtbrMNP$NTEf>kCaQT??S+gu?wu7xjbFi&Jt7h83sXch zniG(s5*q|QCM!K_8a}QQec>$~(@<2o^ni~}{-s2$7t!9)N5%-J$cyxoET$ozolFXTksdnN%swPT|gou#F}2ppr@;c&cJI# zweO`~YCTNlB`F~SxYHlE@sxyb%O* z1kR#HkfX1OfHEfm_b%NHpS6o^xQPiD10FUb<-Qw=>mK@~gAv=lM=H?EGct%D22#?@ zw(o-(5{nPUG7)j)=Nt~`+~l$4luCk0rN3Euw6*-JD@2-FA|rg++P@q0S$R<< z0U1JKu+PVJJzuZKmHofE^(n={MC3!#hUj#iWAb0s2@L$t}&z zd*jCCkw|uRx+}QDtX*1~v~5FVRO!Ak@v~m`=!!hFWf3Pe#irw-k#p)(TL2wLeSCOv zCeoc1g!5fQQzG2as|h31YGbUW*VoLf3RWV>m}b--5uN*p>@}qp-D6}OY|P>!?%~44 zwxjEozGE+&CHTJWtg`pmQgl>*Bws2Zu^FmO2iGnR(A#>~gbHgZi&95w zodtLHeTTy?cCC=i%vJlM`|gTmw#UScni6Q6{|--1ny&cOoYjuqNv$*K+%0zZ05jUA z`t>e~pChvvoT#o22N9A%EX2J@Nv<(W=*?Rr63e_Cf!BQlacVlo{Gs|`M0oz`w}>&O z+kVh9LaIT(S54giqOV}w8+I@z8#lRFe{wYUAtF)=zKu(2 zi|i@9FNtd;fuYu23FN}sm@SK(EH|naCK6ei1Zoui9!6z22Gs@x@`8aGNXcv`ZMrKH zxmk#WI4l5xF3Noz>BxHQo8)f!eU@jMH!W)QukhM0=U8dy|ABn0U5Bwx+thZ^GPDd5W4aI zrr|hMtSGC63s`~W(EO#o|1$IQBi=>7cl^=IL1+{to5p>Mao@w64i`0kTzcMD;pVN8 z8Wo5fH^m}3&VZ+!tIih{?19Rvk?&O3n|^we&zhf*TDqE)06c0xB~O=cJO8-j4?F*_ zm*d^>3V6xv$TOiaUre*Z2tG!DDAXHePGu%-?#-u04z|^xp)edG`DFTLhl3Hw#s;vk zMzzf*YhJjNpQ*Hn; z8LujL9V!;73}0zlq+TT$x@yfmQ+rNbQfD(YDM=PO{v zO@cBY8YF6Imzjyx%D)dg8;;T{jFUbU?8Qa*3U~?f5{W0oMHWPnKS4XuASe@f;V+uM zeBt%P)9|R~2W^?A0SlGC?4R=XD*Gwb(Yp$iQ$)ppH<4Zo%2~GoXP9o&^4}1#=&)5Q!)J$MQx5Q7vt()F>>ynTn@zP#og(Eig&xT(^-ZJ3`7kH|} z8Y~tCOmHb;X?nWqW96?RemDXw_T|i6I~>(mO;XX@TmS>8k~JpK?_jqR>} zoeO$}Uf=A~=k;x^H;r#%@8QPr#2Q3%f{Kx7greZWNoH+;`)srO+*ovv4DikQc225N z$8y2gRA5{#I<`6?ok(9@&y{O^cLwY8%k;$;vcSi8S^&$WIten)pOf+kM1=qieVU2V zjnU6Elx7mBBgyeG%NKd9LLsYFe_fi6t*M&oZS;`=Rlw3M)`qEwz~6fdoygt6X1HLE zT#$`jZoo`#3$zz;FPefS-5N3n9ClptjL-`O+$qHGViY2U`6Dy3;({frhq5pl;=AOC znrf2j9PQ9@&?znlY166N!(al$f;nJ5AfpW`3&^SxMptdnKf8yL8&DP>Jch26|Wy`#>gU=Bltinp)CZ&iSxeAi9qZ zvKqlq0@6-~M>EuCUJ_};ljQ`r%+K`gdkpuJ1h~tY@P6e&bs6OvP2?(p#i2kz489@e zAmx_{koUU1@zYgJ?yTB*wcUFd{lLc{or*NeZc>&}!*IMJv(<97rSy}4-o>FBx~rFz z8}Hsl^rMVI>u|#p>fVWR$Y2Q59bebRT=%1%84BT6fb;yp5J+e*$96=l)?`)Cwugeb zUY!pK$~QjOf0$Q3`I1<%otpH{%|~bW+ddmj=`L$c4=J#t10!zy3T>_W0N&O$1FvUk zN=`E|Up+u@4?THH_}H?NVeC%peNcK+kBc*@9D&ywTCSd@iueROb*bz5b1394={f!T zosHB@oEPIm#~^FLQ|dIzKk6|MZ0o;7#GR;1BZ zPKE}x08#fn#EJaP*QtbiLE9?g?wrL<<4uXEBY+aYA2Lj+u*`&smPu0kAwjemS(q&k zS5=Q#wm{lOL`qqNo_z(B2UH?_>8dADmZXE;py{p}9#+nEs_HFY7S9gQmC{9JdiPN$ zkmL1L1HrRTOSQS5tx*y7s=C9kYXRM7-ok*Ks-6(EO=5(IT_`4WS7_oS2J<|z2z>WyTPpLn9vo#1H-iS8gWD^d2@he zlMkl(W9H6n5s)_YHj}b68=xVFHmdLPS>G1hDuw+=!rp-M^v}_PnOxkP^%ILD#&2dt zs?ny879q4q;s$|j2$5#m2O$!)bH?apU>JVzSR%8!*K4qk1Ow*hwHb&iyDb~~uzl#Y z5NbBJlk8*pn>?!edG9SzN18Bt$M;%INX z@c}HjCEq#wyxYEZybib*itO$K1{;zjJd0mxq3XRi?Hqjg-LAup+Wnkx+QHeL$SKF2 z{eAx6yvx%GRx8(O>gie~-P|}UDBH3`wMc|Q4^?fB!S&VEzlOH4E=ISmoO$Qn*h5+o z9Xuvv2Qx$KYa`U8tvVQ!R*>sU;(`irK?P=51`D8aeT;c9gI^@!Cxx!I!fPQ>aFtq! z@|0h&g|*-t?$F&>ZB`HUMhbE@aFf(36-#ulU#fJ~%a%HgumIUfSIP}rq320i$?ToP zkwP;)(CTU!HK9S-jiXX_7K7YCkC42*V;~J$K~3{%8xg<@ctKv>a$fu3v|B^E@_&V~ z8Vv9xns2ZR>KzzR#zc*y1rPkw`(LANwI<2Pk<5{xzmD4`89{@=O0YGwU%k6h4qaHg zHW)V)60igmcwm)v*zQ=SCgds?Y7|j8yo>QPRj$Y7@)SXYv9f$NUd1tNLcno2s$v0g z@r4ezWDxD3AW%g&R7&4$0sW6oo0mE9EG)(Ph7D{$_vekDOukCK9RF(kHei3l@x82fo!)+lP5cCX?WWl| zPap(>AlSvaAuS5oqmUc-!lQJcMCQ)aPEZiA`j$P!Y!d!qf3Ae?M{<^~I$otc)w?&O7+wrHyXGjAiX zUHKF8rCss_QEEAmlv1Wi)t3s#9x-br!4L_F_Wc_d!Me8DV$D8{CzfVqxEmqun&$x) zH;}aeNoUk4=Txyii0N`3)Tcjio?_oJL6g}48|87BCB7Ibevyf5ipn>{A06__d@&&* zC$20S*?0DE)N@lgLm_xVIdBGT_nd;B=8Te@M9n-QBL!G?lt%Iz+A=g*=X-y2YI`Pt zXCM=s1bL(c%EU%HB|F;0uV<=i`;_*@lS=tOW+N;Gh9mv|eb+xnlv)2f(o+tFvxrXP!J~@7NLRf#nW&|G_l^;#iTPwMb`;v}F>OsbYq;@{2OZd|M4fQ!DHz<59iUS&EfQTYGwG=8F&W!__AK_^;)(reW|_F0_h#Q z!rODeaZHmL^L|cGSOs-7p59sL-R|9)QkYe(j%+a?wSrhMk%c--Q>@d7o}Aupg6tq7 z(8nkxQPSb(P^p2^*{H7oqZ?bu}TaFEalo# z$M9i~nx!6(nQ`c$qZr5)5zVg|GMa;=rf2fxzvVevc5nt%-PPI2>eZ@=*%=9@roP#_ zA&RmyqMzY;(x*cl^TawtO{Yj0(32-?E^dUFTPn($x}DP=>_wrpt)ZcA1E?$vohIoT z^V0K$h;~!$N+n7$G18bxwk6~2J3w=dNyesNyfHmGdnO@BIrtJY<^gsy2zg@iUjkl9 z|F7G!Px|1w?Oj=A33dTIc&}N4z)?7#{WLK56>yvpw372&4s2NK8`>7k*IHw=nA{Vk z>p;F#-6BCm6I2=b$l9oB>vL6ncGT*mGJnhE}F$2@mfZ>w%WJRC!-3Orftw zFNlN}<<2D(-C)su+?q;>1tiaUlt(u*CbU6pgcFH|5RB_4`eI_S^Vg%jm>CgPrZ zZ&O7=RXio4oexxX`))J&@>`nc>pwN{d2_w;2A@_0BBp*_3-ctmEedvz;mCkPQ)w(4oBsF?wB z-){(Ba*1N{nU>lP_w>^Gsfvnj_l&GCtu|QCjEqVosv+96s7QI`G3GF567*Z3k2VFN zdt>s)DX#9gstK?&khtt}@wx}w_hqYIbD6n^hbQt9H4%<@&^Z>7rx&mo&6;l}l98tB zc{rhd*0uKPVcu(X>2{OM%-U6n3(M3Gy@`ZpRyI`+^7fPl)uol0RDzR=$78cqokgjD ztR5-u6s0!dp3=5k0qT~XLXECj1=CWHXQPnroAK^|$!`R{pfOwdWHy3L0w-p4KI;|$ zC*-Ne8wU!8$OOn|lV7TJN!5A-YB0KE0=u!V4ujm>8&2=uy{G#p%Pms=Q?S=k*DF;} zD$3&iLElRg5M2AJ`#{mtUdyHTSiwR>rL$qjigAQ>j}Uwm#PAwAY) zkE6xh#I`C%jF;}Hsd!$htM%T!MsSyyhNj2tbP?rQzB<;X5d4Nt>jzEB^bumoan_XE z1gVL%>`rHgo+g^!^ZM2Y*uo=^!W!LAMc$9~-E65-VP}Ip{f^)6VadIJ$VBOen993s zo^?*FHUBj`gX-{>h0YMbq1$e8RJMf5%GpGRW2>U09#b!YWs;i^(`whLVLBA_F)6O= zp_5Mb)6eH;%;#}MrcmHS6^V2et9g?pU#mL?QL&H>%tStS$*h{QPFhuiMWG2ybZwm} zwuRpl2r>3$g}t`pS+;eU6Kv`u;=(f8?twWy^|#9DDaz-<`oQZ#?sj|28(Sa(V=l!3 z^@6CYc6avN%mZ{*m$9_r*rOCV5UP~h?u~fCZfr0e2Q0yu;WV~yPQgu+QVUCo-VyVv z7ETCqHzs2Bjt|vNpTG-td?UU>z92WC$QSf#U<>i5@KBkg*`P|+7o$MD5eX-Nm1N@z ztC9tK8vQ9P`1KGoIS&rVFW3(`h?4MVgQKL2S`mub=19onl9UR0_0#Y6hYOcJUT`&| zY1tpDw;UdwG~sk?LnT5@_Eo)0MFH{a=ZqoES9H0*;WY?@S}wYE6^W9DRriesbRba# zhX59EKozgV{pv}SufMmANPHHgz^xJC#xH6tU0By4Cz>JS?$IGkac+AtD0O*ws`SuD z!m~~e{o@$zxdudGRvAnG{$+pp(m#LUN5#Un&|e_;##iWDb9pOnzIy!h&fo9&>2};3 z_Zzn(u6BT=(+?p(w%BJfS_u?TW<#8D^KmR@O$|y!mPiOE%_EznqT;u#NMFV{d9l~h zuTW8Krg$Zm{LR9T3vbE)?`Own$6GrQt(JmvlZy%u)1Ebz_~nmCv$e2FoX5zJz40{; zo?fY1?Oj=P-EKmw31dKudsgyBNsLU+e;s6n{s@=iL;lwv@Z)!>7eey0>+e73KYsE5 z{7c|-$zN*4D<7F8yRUzd@z2FbO-j=_g5oTa4cd~i?gyGA{%d{1UuM2CKQezw>O4IA z1GcJZ7^wkq$zU-Y6#z1Dquu{9zgeGWIZckG=DLO$mcw_B1g+41rAQcXNlLjt)}5XejkMdp`G`;?d14#@8_(-Ubu>B2fc zvihr3V1Za&z?bEvPAg2Zz>A~Yo!^D02H`lDdOCngE)`d4#8aX}yXO_Ezk3Le@JOlP z3U!`9A7D+NCH2?)VLFTWdsBuycvD@6v z@ZcmMn=V~BH-}LMZu$U2o{;FwB951K)*?r`o2d@t^t; zW=GAsEGlip)DCIH#cJLQ`*H*o<-o^r?!StnX5S3*eHQJNpk82;RTZ4wAq_X58r4r~XuPmtZDb&uMt0I*W#0!-pDJI28mQv`yS0mlb zg1Ibc*fZFBL0YI6x(bBTO4I0YBa#Z?9ubvxR`Xg^D)^DW`spJt2yF7*lU?Z`TB#}s z^(_A}4dw~7DPM_fx_dxWtj3H$UhDD>HU5)C@WR8q_Opj zuEpI1R>Xgelh?YoyNZUyGV`+dGueCh$1!Vl*Y=RpPGn~H-pI6NH9l1=rCH!+u-Y0i z1+v|Z$xPQ;f?iKdO(5mz7ZK}P?ZcU&(4piDZmmC?YQ37rM)qD-qy+%eHM6dUyNQa( zj-9laDr-cLimvX2Zq!f(g*C&M)`>0s7{g|MF zy#m=CA^jeiSsGk@u8^z|vN|@BS$EvQPaH8(POxLIj6H~8gWTEg zo3NQ8pj#VeFYjo+oQSNt*U2|7jaaMpl8Hw$uY+C-%36>KVAavko_jKF)`W{^{&h7| z=DAtk^=IoP8#f`1&(Sl`oT!rR>wogTfn0IQXt7%em0cK<`(2)UqxXW85=)isS~Wwn zOo6jZQOTno!64K54z!YdEWP;;0Q6vk&l=HwaV!NC$n=0}DV9zFEjkmeR-eFFx=&py z6Xi9dNce1;wwux(h{HZcS5y=f(|8Zjy$NF^n*9x2l*KHGAbKQYu8M0mL`*EA$OiHp#?3Qwr7^y=uCjsHYFCL@ZbXbqGL;wgEwBqejgU2HDUB z`-GQ~wByjqVt^8;tf>m1+8vzJ(k_KK*`RVwN=SvhKYLS??80h7PO0)SgRSnSFLFZ9 z+3GPu0!VCTbTz^o59Iy9lq8S~1Jh)7&Q{w4zA4fwS|SkE*!Rdj20)vYGoP$j;UfV4N`v5s`?H@@C@16WY+H6-Us7L?XN$8_;I@Z>B$ zy+9fDjza4pg$b7aFW>2JzK4Fwy+i_ke8)e1$^ZO0@J_rF`;nnl3uf(AneUWO>0dlZ z-_HC?1WDOhW@lLt(ikxtoj~$RXyy-*8u%xW^66!d!kM}T=Zk3eO$eMiGk|bxe}spD zvMoGD!$EQQpywT@>;ONtbdy0Xwlz+a<6)ZRpPN8EuLf5O_w4$%U7?;&!DgX;EJr|3 zVU?DFNJija5_kvh?7b;x0Fbe34uISnH`!ik!4FvbV@>~%4lT=WNrM$EOWY?<=v=Sf zX8|N3rqP>P;LjNJazI`CS0I^>pwcab9@Tifv+i=_+SUNB5VGA zpZVv&KOCLkq{n25i8P3fI=+s$}_Agwrl9*K#TPPm&h$x zny*BNn5H2=f?*cmb7@=Vq_g64Ta+_YcxE1Zr6J7vK{v3XNChfm^5gUM>GSpJd;e-78AiKN@@aqK2o!%wCOFgYlMN6A~zI-VyB z5xTU~TI8D*K99Fmj<-ApJ3S&5c4#)NJyA!KsV3~*WBdQgInsyGc#r6 zP&K%lL5c9=^Ajl|dv{1i&5hs)9Ef{nN}PNbu6UHf#6jq!)6_=Z1Uxj0r%QT>Kmtrs zzIIQbjr>QSMNaxP2$}KR%?x=#yZtamUQ)X|yT`vC%BR4LG*kDWt4^WzS|nTpZO#;{ zF2QQ9H>6N&L^~6*xfmQRlP&k`?C$=7p|v>EYYY@{U#4UQbkMf3u6UM_R1P*AZm-(5 zdtfNF$|+f%tid-WYLS3AQ=p`#VhGn_JPu^rx7XiQp0UbR3dC))77g&s1kRbtje6pHG_#}Cm$7_i^0{fcTj zG$hqpEfGz&5ipY?lgcn*L>6aWuT)IFDMs;#Vk1L~^%-gtcy=>N)7|xTa<27kx2zGBpqx71v~HNU0F1%-rEMx>U84Izlj7L`*C) zj8c_~6oQ~4rk0q;HnkmI)2Qa7wsBw6X$4*{Tafhh?VggJJt6MWb>#uqvs4Om3ho|9Qgi)iBxv zfc4h{O{n6nE?Ap~;qKY%^H>x20(clUJS&;QQAKyK3~tE>$I~~rn9_A|KN<}p0@H(a z#LcX%Vxj~|VW2X+5}{TQV}|dx(sfrgoa&-!$SG}3mr}CSY0ss;!l0W{S;4LWik=+O zln94coL0dqK*0uzV&Nib7)@k=JHnJoe8x%8ajcD7Fw~4Z0+M$4`i5|;$8M+G)5aY89^|C4uFtK?Gcxv$dEr$wKtZc<=qoO-)B`D<$^hKhK(1B>445klL%SF1 zMQgbF)bGZxk0YWZHZp)4uFM7-2uV9d%!V6ac-WeJjwFqd^t?)Pa*waakRQ_|i)LaP zumIteL>lwD78A}2?<>Jg zkMj2Ru*Bn=8DdluZT!cMzuzow(NAh#t>Bp49m6JyRk*pq@;{pX_>v!%e7AHh*DWde zHROxuXV16i4TajLPk@Uh-kv}0_127(!&+a1$^X3K2ep5Gzwxu<3$UvKq623##VVuI zp3>V-Y~p+%O`sN4sgC`|mmBYC-I)JYXO%V6GtO7NM4uI)o?Y8Dpke#aM_L$QiI@Jz z5BZz#@`snKOYW4TuUaw52jn+* zzGxndWuG-ok_4kmEsiKMTH# z5D`~w#a1!Ht&^sE5&x-FH z;o)2MlG|}R%;D9>$Yn<)C1DasU4jePi3MD#j`$Ao7a{*VcOH-FGxM|BgP!`l9KM`m zsYld&w0?8sf&?Uh1$+Jd_1)|BS@z5PL*uLYDlRZ+Gf+5So~j;=7Ge^13#{xfONUql zd*%kZMV?uo#nSX^Cr(23(+@-rAWWw(;(S?s<%I$OCn0zi6d6E-LWAjHpZ4uJMv0XA zGYjIWQ*gC9GN=+0_gzydiOixr1KTCvev3D0URkNz%rWB^MGS`)aaDJ1oiY)uo$ z690(!68Z+dM)WQm+D!a6ze^kUIbf`l9aJUG6s~bAv`E$FToqr7vX!z(fk~a2DOAtk z?L!+hNs=HysZ~hnG#w?HlEp{?q9!NwC?Ahlj@S;QaLzNwfgMz*A$P#t-r{PAO0T|y z`AmanA!7VOHq)a#a!wo=$*SPU$%k)8Zvd(fwcjz>;}mGmu|eP2S}hnFKm^2kMvGJK za}6RBV3ye*=hpOgAMpF;v*{~zQ zlSk9NhgnCbB%$;VM*Vl*#AHLem?m>pY9XYFi|l z5q(lr5;I+}^aLJ$%*-MVQ$Bu}KFKITj-;`(VqB!+MTVCxgiV^tYNEcb&CH@DQz4Z` z(E%iGuLHeq|oMba^_5cMt4VQDlcCMYI>6 zsyh5Z6<5bhn&{Y?ZFlYV6j71w4)|KjBQ*04uh*}3BUN3pwr@1{Tg-=-U)k-c#EPq6 z=}+x$YLJe-94I|AV~41kDg^G1^0?dEmYJDnG!dRX-s<;Km-t!O11Kv(u~}d<61Es$o5?;-D;i5lRgBF9xEypEHc^@r?gfbA_Wtzb=@OOOFQMg_()y3 zmu#!XhKYeYu83MIRk{-M^g;~1G_NI*e=Kp?nNKRMSLrQpEsH4KN>T|ELiOsrz*HI~ z4yjGEMmY0LYx4~?We}A-3jmyXfu`p&(R)49e;#{lbmZRjOP#GJk*Zb@MK-SADv_FH zs&rVDQSY2xW&rSEYI~urP{RTFb6o4V>hf2~ghUivRC9{$nxK8ngMt^(58kLQ(x=OT z^Q_n@E^h8(Mt_1?dS#Hwd5bB)Fr>qS73wQ&^-(T@7c;bS)@=Gf3aP`4@*gl`jAC+7 z;}$=1vfw}<*#ufiZx1=ea0BRKS?FpK-if~o>8$H$rR%rY$r)nW1E zAdAZ{rlel&Q5Aup^Ym%3(Wg9i`+Q%qz)5AI5I3*gw>Y-021*C|@DmfJhp{PQ>X|?3 ztE%&l)EubJf_k3mlfEN$mpB#@-FPCx1UHbp-I=m;_3+v9m_g|h$Ob=UKyW&pGC6}|3wv4d${jbeiT`vz_c7>z!?=IdQOQW z?n!G)!1hv;lR;80d=1u^acD_PBOp^ z7s$pPW4eM|jFl=b%9wO@&RLKP8qMT^R83Dh{#4K3H?|`UUblV0p5S}IRS7X5`uQI9 zRVm4mB*ED6DQ~qv__Av4w^ps?^&=%9>-iH+JKY?&<>fLyR|D?`p~-WdoA~}HoQjP6 zz!FFeNNG8dJAw|4&2X@VyDnzb3GjZ|9CN%2Ye6oo(lLAh@Ahre0sXZJS5$yE;Z9{+ zeUl2$ik2gsXTrvFrDRXcFe{!z4J%sj;uHvqw11`nD*wUk&oB9L>Gzh)g#Xu1nov!E|X1=)Oo$&Ts(|PL5ih9r-Gc^T+aw# zMm!Xc!m>}YU^7AOAVaj&QZvtM$Sv;Ohm7H}P%F&_Ot35Iin2KYR8DNq;kSk>k(pl* z{G$0Q;Mb0y)4$x=FSP-mrO)APq#(RI536zi`K*%X4%FBV3J1ZrR$vu{7Tv&+h>!WZBefi{Ia3)g7c9)^B(0ad~Z&eRY< z@|VP~QG+f$(wQu^A+ERs#!b0~)0G*C3)mtqoy2`h{#oLm^PFu-II;Jo=c%(R<=flE zqt;Q{@v0%{eWbWIT+j<^{gZwASfBR&W#LMFDUyPt1O#SKr3h40a!2=2EYSk)Z%ch4 z1lXx}E>-_7@CJS#2vVRI6!x6ZTTv=RbM6%q9zE1 z0d1lf#D&|`g$!R)l_uSx&Zi;Tw+iSCt>odqltAT*6Au>+3^>3+#glKcMVBO23-5|q zLkSXOp2^Ti$wPWep(eeW6c<5j(4L`A!IgvcNT%X(n`})T_9s%JUbeodQh-mlT1G@h zXn8jkmgfUy6y#J0ofb05i{$yXPy9RpKRTB&mL&c4HkBO9ov4^BGy=lpo@1MnJ7y>8 z_YejU94<_h_A|wi%r*G-v4k0}_v z;EyF)xxzd5-rsuWPGE=9x_E_gNE*^(;@PVg^1k`}|=4 z*@B|xS+ac~xm@s$hmrmej=x4GbG9=9*J zKM`8Xk;uV&>pob@5QwF6nn55V7sf1B=%aH|S~rMf8Dl=?<}^LjW!Q}HUc^R-+@40l zMd;X90uxfgW#ojiq5Mw0;$*nmbW=VNHaRE~1nzJEm)bjSh04eUr;b(>~IMM`l(`l$^%Yo;DF#*YEBLSck=6sTIqDqoc^O7Oam@ z+w#5prxc>9JNDAm4sVbV^_F-e3MOquA@^jisFAT_D?5|2`-EEf;f)WcF=D2uWv_av zs4NewZI^2yx|6B*)h<_(*ATTGFz_i;q+1PYLiPS~x-k-f6*YyTrkcA{Dfg^z`cY)o z^4SMf<20j$n9UkC-;{HHPhm-5zBik7#xnHWr|_cmi04&Z)8^H9UOel15$cu(GcP~P zQo@yWFbND9R*bb@%Z{@>`sxWX7IbnPs~N(?(@kw+vYw3{a+dqvwfpv}qA`CR5pNUr zA_B=SW9eqX29~KIrjKM6;mBO95=2mKRowNZj^l^Q6Db{DbyAI=LxTGBp$e)jZryuhbzSwT3=o^hhu(p>!}W!-Z;%w=6fe@Q%F`e-n|DV_ zvijlaj?EEO3y>PkK6|ui@x1&AOPmqS|C`FMhl_Htw(w2eWeMlWLeIEkDNyqpl*F;X zAa`e$dn;Y_dKC6sD>B`;Jf_F<{MlTzShQ#7V2f!d*MJj0+$P`5TXgD z@Fq>mXrvpTAmxMVsQ7c%Pz z?<5UVkr*;ZU+v_W6vVD>>1L#wj&k_j$802G1G|Ca5*XRS`oQ{tI#YPE^<7 zFIauXM^cU&Wb^2rfU&u|ZlEKCvX$viSIAe~UvbyC{e;UO!1{&{el1vlTv+v(=+ty> z(U4MT#&HsZ`W?wF3Ght)1I@&W0^H>VSH^k?&31mP3w9%J=x{*%2u--T0xv9nQhBgz zfd|9XTye*lB=tR!5Ni{yVw;FQXabQgu>Z|AAFOu)fFE?-lUjHRM_o^zQU*%aX zXN6>mO5&t>kyAy0cZ}SW(XejT(g({kLjy5lDJQ|NTzMW=mSs#{ClyTqCbbVidBts6 zFKwE$60agE4p4G1R{kEnrs?CO&E3OvdvG<=Joh1)R;5(X3XKW4j&S_sSv^FM`PK3E ztYj~bT$tE<_2n@6VsPcWo6d$q7P9jQ*-?daBCbdm9Hy2_+OF~k2NUm)tbM6P z@OXO2P9XBu;^Os6+~5SUVyuOpV@~JEJl!_Mb7vs*Z#7qh-YOS#BUdih&OQ?n>wt;=(o@Z&~VyxKX241(Xwfj9%b2@8$tVV(H=wjPhbmX9aK72 zil6oaN@`HM>C4ZNTp(2#sd{Gk5Fv@TrBy1=il&9ZtU z%D2{=k;*_`{jYU*zLAh}ml1`jhgJ(yh(Il^XyHO262(Xz+@2scK;2%0%Xplfi9YjPCX{Hd z=VVdOYTej-H`-@IZ`w#!kVViOBOGAt)%cef5kyv8{M@35Ff);goycy9n5>#S=w1P) z{oEhR!lZqFJJ)L;qBclo7(5gnZ4p^3MtfHPuxCJhP2*;svIcKqltT+O-pY81!CrvQ zSOTCmZtI3p4atLEsZ5%;mSMemmgS)T&20~L@EAyaI}YFoK2RzGRR$%TN0U#3N;V}E zEIvm*A^OpkTxL@)nI)w7_SbWu_MBH@zWwzIHSv%R9BOQ&v9bsy;0uAOcyv&as$~>R zS`GfLbn2VV^#edDol+09h4$+!dVqjz{j2-t#Uk9x-w#t~&_)Z5rF9a-Y+&^#&J|#i zx@7j*Co}ZWcNX*0K3fTG#I}L)IhA$PFM>#8geKG#*|A6@R?; z3lCQ@QI!0s?{leCMMSg==sZgOIO>^behw+Bwa|F*L?;7~R@=HRUGBST(#(n$ONf{# zimmT}kdmcZWhve6B6zLqy>F#}Mu7KzKS~P(!qnckFIzjdK$cx1vi(Mr=o-vtsvYXs z(w3l*Gsq)~`f_)7H?w8Sqm{_xFWQer$||7MY@%w?l$fZz^zv%!_m*ocv+b@TYpu~I z6vL?^3f_^&S3PSd&lWRx zrU2kQn{L|KDY(e9dn}uwEE*|h%^aj1BBGL#Yp^7FxPRA!tCcGvz^0yB)}yt$!lA-Z zG{sp0=-RY0uO3I6;Vrs)5E@N3BOo=f9BjdpZVRg!?Pz!D)?M{Cbi$sV%jY?{*6e2+ z0!Rz@$qHwPPt#W2I8jLtEgMR1%%?4=sst^KXSMUyOdC>1B3yv#IuRuO^rnZ4enB#0 z<5@fmY$v^~p4n1uGqwEH#n(*GTKa21$XF#5Q$jSOJVs5&NVLv%R-J;6r$5eG+@6I{ z@X$_~dF}_RF#f%1u->172Jsa%Chr|w9~Oy5pFxzQ@f3r}7-aw; zlyhN~`c?Q{Y^9!>FW zKDObTcv&3ark6_Z82Abb5(-~(z{FC444PTig&QGFuywrzzGGdmBLWC)KoqXHFI+EN z1%w#^uN%4h`r9e1Y`YY)`}o}fHc2R4nOjpw_hE@ZWNO-{y(LtMQT%Y0+ZtNjfoCi-1<|VikFZ~}c`IY78JO1kz$6NZ^>K#jlRYOQ< zeid|U9A`@*$S0KXdA8MCj0YwKUZoT2;o)sZmPrG{8429B3Jb{D#rQN;@LKqB{ZHT9 zuYSbqLsZiozklaXU*o@i*?0%OqOB%Nb2fo$EfpH+81HZ@?;h!49m7Ks17P!G4>>fL zUU;;oLB0xQH^GV7?&x%LI|RyuS{U!O-gY-O$OJ2?>n9!t`U}3TXZe{B`C}k+=hI zYNX;3c9Qd;d3k+XzD3oAO3kKWxO(UFBS<3;IyA|nNUAk$XwKrWJaDtg+LA00>G@{J zawAhDM4-`Z*TB@03Aj{mMMz4PoH=G%J#}euMid*9ngT3pcKFetU}&zsIG$(;8WhlI z0joXxsOrCXIBoI{JK)$m)8!qqa#@B-WNJvJVrz;ZM4{fKl=H{?GV0-u4*2;?yFH>2 z^~IL^=zki{<7xTWn+x!7-rPwM<<&)u$k&p>)hzJVVD z;(|;M@xaZolp+^k(zD%=7Xl*N@>bZwTDTmaLjD~1=`7vzj?b|^2N`QSMB(@6EFGjG znub4Zw@8B+uplqk_1RuO+DF(6_93wm213swMFj*8+11+GN58~4+J$$%0v>t}bkId_ z`t0W9xNWVKR3Bk|SlYjLf<(cR!7v6*zvIApC-ZwP)H2A?_a;;10}8mk?uO{ zQ$hCWkPVkSgJiXz*UFsq4Jh}B)n5p4ht?ETxS&$1?o?1Vr8$ixVP8X~Y36VB55>qYkj_FGcwHKz+cd%tT zNyZKllwf44nTRC2Vu{uA1a=zF%a)3FngjbR^%bI{phZ=0Yn8Pw!*&d1TEqrMFHuHP zgwEk5oI$`e+efVBBqB9dNnr&n6auTO|cKNz)U8Jj?OD$n#d;(@fHN;h#UJHrck3R`?+|er2ZC?0MCWH{mQ!Ld z4MR{ka=lf3ay2dcDh!G@)Xt!!W!}TPmux^{@9NxvMmB0GIGQ1l#mQa*MWvORNXB@# zxng}HViav}mH1|cv@GD;w)!S7YkW4gD1bc3%_g5jD(n8(%+w}U1hcI*-MzJLmylF# z4Xt=4PDJI73)J091|$p-&rH+)`rN9L3aBZK1kb8|*bx$*A}G5vVOS2wI%K{%GM^pf z5EaIvbBXVkf<|mlgMN~_DkfFv)g@(hjON&eKA~h*$n4IO9j5Ss3IfHgia=Y;F_2pO z_0&_Qy0!vk+qc`*Iv#ZV6=Ej<4I%bBGxt`IP zQtr_yQ%KW73hTy%sh04CtGZ@f*W;2T)NIG#Eto5y|(WuQFiQU03W_> zTJAiy=~9ylwJL>H4SP9ktsR6m&FI)AMo1~qGU$y{Lvr=Z5>JGouW;qL_JB!Vf5@2}dW$Vtk_ills zw>Aa9b{8NrYl;;c1i;-xU~1*6TsTb8W6KP{4zH`_%GS`aM~F&E;_9WOyK~A5k?koD z>vOXuPPa*pq3j+y;e71%QP~&LhI`U`Ye$6+T0G(twsF*w>=_fcUel*ulmRyw^#w-v zBi$9+z<8NV)$?sdN6&rj?|rtpSTU?q_K;cVs^+l}7LnsX8}(rtP=xsy`uvHWxA zF2?R2z}i9{9*!uIbYAEPZb$*17l}&!B7x8$Z$;l1cRDTqL+m{o88DKg*=c4n&T&-O z9@BN#+>zq*Mv9xr?2eU#iFWB$8;m?zqPbO5D~MWlzaf1#s!TFkBZ^i$Kuw;p_3X)C z1EM@NpZ@%<+N&`TG;JSG-q!lgqY2+?CR7NLHp*#qc@&7Kud@}h(uFs3D>sIO+M?6$bFxZ9l!nLr7*aCpF zUA?p~e*$3}6WVv9U{)>Ht#i znnHDmt=7OTN_-^YibVVE)aWX{M;@>y{79F+L15=LZp6k;xWgCp!WwwJ@aT%FGT7CC z9dJA(og%FsZwizfUhuhGxaysXH{}oBY{>02qmg-)72(CvQmZmo!z8zHjJYPni;#*K z^r|J~5yK|4mLls$*~ywp4h(=%Or=?gzOW^~x#Y)7el+UJk{f?d!D0% zX@dpq4R>l^iv3NF&qUr7oIS(aC<+vgkQ`xCw(nNSHz6>=OYpt^58vspe!%zNjQ| zL<4Zl8uIwBd@K*{%s?5NN9)$WcRZzd|^=h==kG| ztJ+6uQ4-FqkUOwF{1y**Ncq%;B2{x`T6}N$yTT0P$j|;2A^m*xkFWIb7WS|{?gO6K z_c^Kbx7DR88MROZev14tWM%ve{WpQkykQW3Jnn*Rg~bd;R8Hy8hbZyREih zE>}Q2%_2gyv;!)p_)xq>SNTA8sATrYsaar;w5_s~RH5Ef&Laydclk+o7TFOIl6ACC zeG`~gq$RAIqolUn5G<2*;T5)Dp#e*Lx;am?W@|75i9N2vP#*VlPTSZv^NP^`xNkE_HIu%jzN`fUeQrZlthE~FWsEbLm-@F&gn%&LJ^?e((v8*i^ zNOczVexNZ19#h%2c@9I18)2A-#bX^X={qWQM0*4evu!%Ed=zCo{6(@cQ?lkdJ0XK5 zPJivNZhd&tdB;Z4D>Z=AU+D^ZNQpj+VrWz*e9U^$6E7w>3P}iczC-DvR)G_<#1_IU zC*NoQlW9gKM%Nq|vU9Ma%ppsHGk5GU?0en}JFBM5foCH!WMJ+hRZv{VRHjV5s@qr? z!_cJ3UeJRqO*d+Q9)p4V}y zLK?{QHf7@!c)*>!|767;#AL~~vicm*Egm3nH2ANeH5Ty}0(UJUzfdek%#P zoUAE+`Yeagtf5lq-dtDr7S|J0(`q9i@kaWO-`0;OkrZAGgN1Dcg*3Hr0^`bRUCuZ-;Z3 z9ie1%V2#C>>u;(Gf22~o*tJQDB?aj@XknKR2SPn`Z%5X(_b60VQwokRHnjXLtB3Un z(IJ}>C=hHdd$DN7NIckyo4JO+%-p^UlvC8Ql1puet4N8i?tOb~Q)@d}5sy9VSa+X8 zfZX8`EyVHg%)IQ{;RUyD-5_DAZ@Dh>OS`V+kMXO02rC0U+ zD@YYr0im+pms%i~Ef4S5W7DPf<#wgmx_;bL+w!h|X)>9~y_(L!v%HNk&G?F74YSOK3Bu-SNOYyxH48wX{pIARZt5eJ)^4%kP^+)*3xhC%;8>+ zx+-MLc>TR$qYnn{do0@RF>Ezb5v1&hcZP4c;x^Auq93Y1==$wan&vThpo616S|hcJ zx=1n;4zJ#VKqb8f;fBWdE47IFo`M@nD&*O*F3VsWJ5X78@mP3Hjzztcw*}U?Dv1CW zu7uRJUBK#(P(90OeX0^ld~y(8+Kc&OqjZuv`xcmPz?w&yQtB*6DA$W8!n+yOQ#`~A zFMf|XvJ#%(aDf{?pZK7;uvgXU^`sf!bl27nEuvy_&CXfat_A))a|-)Kl#X_54ElB z%wVNAnsBem+9|5CbS(?3p6|rQSd(p;gzPJR?Avhyo{z!AuXVwcH*lzkXz;xe8%BJU zQ11doS`Tw1A;UCikxWB<_Am&d*txqQvVS~VE~5bS&o2G*OTS z14OYBqBr7aFACFpb>-#SwN(7`+;a)ignDjWmC+M`2RXyznSeEGzOqUiC|R?$bf!HdECIYB-$6ce zT%Q|Qe+@GYp5fF%)>D81slIsB8kt#&S}>j%zcJz6EtOL zmZ{w;j?TytAf;eHMnq&`k%<%U(H}`91zy;AktimLlt`EYfdz316_~^iDSxOLP)_Eu zPd!kHi5l(AAm@sEfl5#TpX~kW(V|WA_F#^bh2avVip$>~g~*`rf+Ct0VERI+%JPz;c#zfnkRv->ZpDQF>8G(l?k?X{kyblLHbN97Y8)kMPQ}C{Y2UKeC z^LaH?SW$EJd_j_nQ@)>(bX1S9@*|8pvIt^d!be+7NeNB5bU7yGn4bo3aN7l2_*1zQ(Yt9cV!as&Q(CYFe$3w_dv*SdD?MPsah8z^H?o2J7({V5hYI_DXZ4U?o?Y_ z8@Zh>oJ?T!VP4#8X7IeHHIgaQ#FI&>-V|Y}$dPVCkcI>A<(X8NfI z?yNu7q}{9HscKyaA*I|jSvPZMQFd{$C{_)ca5e{^7?pXjE#p)z(-<*j&RkYKugoeGq$5JaDiDmtVwY*k6I%}hk+HcGQI9aXA{=Q2=}JTj>Xlj7(an6gG#b&-3B6g+ z9vmH9hHPW?=Bs)jb3DXjBxHoETJ}ePpmbEVS{V}a{Zie2LY{e2siu-+rwRv1c74+a ztLn9BJ)=H;cFww0t4H5_a1f-3S_>_QFgqmntezzNfXJdUL^RT;5U-K&Rs3__-`}tI zYm2FhR0pnXB|z5JY76Uy>(!0$NZ^J`!_7HyQ{-bJn;_Bs0O(5!^XTCX0N&v8ePMkx z^j|*lUFJA>yy&j#qKmr1nBM<-mklQe(+66D&Sh0^k0+Pb|gCK4y1zRG=kh^}*krMIoRCP+`ho8V3m~ zqp&0REMg$88}SR?-vKXNbr}m%0Y4?9QLNQ8VG3070>L1GI(Df{5}R~mBcUXm+9|6< zRHbt8GZ;V`BPRl6J@DG~xI;^2?2V%iHA-A^%+aKq1sSaWkH+VAhgkC&(Ol_mZ~< z2p>l{!DS|{)eur+3=i6mu~X$(VaLRG38_R{BPNGs;VH^6Ayw#w#o5=ciOi1Dljs9e zTnW?vc_BsqZ(sd{KNV%@rIt%zG5YQaX_Tc82Y?Q9sZhsu+>T^Co{agAlArKXQ}l3M z?c#{(Ob?F0TH+?J2yIRFk$B0UeW&050R29f;vM+B2BA`O^%wSGZU{mon!}vU0QV8{YPg?+2_0p_V?^|#!nx387hLl~% zLoE!=#utVPG!R%Dd8fb8q@>|v*#H2507*naR1_G-Gr3{Jf5s|ou zSUSjC^!@`u6tDSc$Oxg-vi{@x)7-|0+vwfb_i6@)K;%cj1sCv{6&=o=kkBkX4Ibi} zk=qd=tC`$Pfof`T(kuFRpLr^WOgb8rq$br5xhT_WBZopeD!}0S%;7zqRhbC3cZAmU ze?nS_Wk|vkpOdsua)KnAIf{g?8dwcAG3D$fb#LFEKYOm7uO%KCV=kzHz)NJSUY2je zmGPnaRV$?o{Y2Q~b#8u(U(4^80`yr=ikfn%F`tP*ehFB@LM-8t=1@gw2BajHtz{7L z8gG>^<#>7e*7pmq#IHU7uBtM~VXylsOig&Wvd5JFhC@0->mbC^TV5E_AQ$xY%lde& z*R3zL>*WS7x;he~=Itg#KyUOOowR~FFlOe2T?=`Oq$T?t6$w5T&OE#tI_XFH-VzXu zezq!rFyNXe+K#h5?VrtU@OAo2JZ?X5+e4h-5gdZX-pb#5%mb4ra1i^;25|eNh(1Jg zodORp96_Yl!U7=siL|V$^4@DQXi8d%Lq$kZ3~SIE!6or0T0FE()Dm8~ha zDjtlUtIcEs*@2zTZM_eYYHHBYm)fOBFH^!I9(tiyy6a2HrKbDwaJM|(DiYmaI8r1C z8)=`;`8^IOp~&t8rHrFiBIa0sL&8X(jT<5_hM=axWFhoOquPUFk&{d_Pdtyed(np} z+KKl^#>9a-TRj~G3+#q1a}Q7_r%l}UXs^!hY^I?P_Jf3wmgtYD51+tpC+Gz4WX(xg zQkwzxrR#LE7S0e({+w8i9L{&p?Ssiw@PW37D@8dao12so)s?Qs^5MJ9tLcwJkJ zcOUOL^%(n`c?aNtR6AnEOdt zPh0S%_n$dX;SnaCOGmMC0Sf%YT#Ns-lI+_G*R;l<1p0CUy2%3MJ?4&p3?{U?z>Q+MU&W z*6IV1g->I_u}*sY8c9T}XH>#{P{?L3J5PlxgFA_sdJAe1hCl0&6cr#PwYo3rFB#}? zsZxqCgXh6OmWZt|{nD=tMc+fUOr+?^2?0{mJt$EnPnsUSY)NM*A~VcZgh$5G)yn|c z#Nt|Z>}E%@0Rds%P)+uQ8Qy-su9t^DbfmR=iyZ8ID<>8fn6d9Fj zw$w6G6dn#i2FxrYRj|X6siK}<+5k~8RTZtgsEAxesmtCWCxNP0NZ@*DVhZ&|8DJ3vEOuSTkLDa;q$9XO5MYY`nQq=&j zj{tIY?fQ_sfq;tabhH5#NnAu`q*(2ns@75)Xd+c?N0iH7W_YZgtm0k4v@7L_xW`kz{^#zhX^_&ij5I98%Y<%xK_@|K_@hC zj;nn?TO(FGdhc`N4$0%4h^EA&fiHVk6Yhd}ssThoO}cDyR&S4UVza=!+A5Wb)X~X< zO&qB;Q%r~B%7k;vKvTi)&WU|yf&l3bw8CsWtjUKyjsvI4s6vUlaMm3)>#9wLpQy&L zAw7qWY|=Y;B$!ewSQ1p|&yz7KhD>|Al6F1?o!?oP2~UV+QY>Y{Ac-iXx9gAS*2zpU z>x%*or9m^0L{@XhGzXq^&@BsKdMbyWdmMtV?+MaE3zkN)LT==c7z_qA4Rq_t=) z8-y;HwRBXAZ0$$pj?G~Z4@Rmj+Va5=Ye%w8lYY5@1&jK&;2X(H@|JFR z0%e8?hD+hZ2=(tp0N>v*QuPXneoW;K*F3I5M^HGVl)!+Me{K5fm%!qG|FZGL@wLJy zDx}Kco0Mlw2j@_y)my<^QIBt73AKZcy*YZbi7Qu%heeR9<#3^MjK1CHU_8MtD z*=w;ayTSvbu(dY{_l$jCn@2nDz`b#2QyoBCp^i8PWG)XvK6nC&heL-CTca>8`!xt{ z{N)4|tyY;AFAC6;zIO6(O5`}4jH~BjcqS1qtZyULkV%g1n}&uO21022K}XGD&7umR zeaEkN{xs6V!ywN2M;I*k7`+G6B+t>wn%Ttm*{9dkf|xw{&<;zrph88|4Ea1LIcJ;XPe1q=}5(R4rAW! zr{04@;|=&9n!OUXEwbvbcY*SwcxH8mkwiAwn)@-Dl}<+zjlH0^V9wzC5@ zpHiVF$RU~V_~Lxn=dI=<&_pT6rPxRSe+*ff5;e;82gDoVT91+pP$;)2Hq%64Ux$$H z>Cn6IA3eW!?8qJPkO;h_v97JSG`HX~v5@a1zt()$pcoNw?U8TVUV@nU3tA zLnbWWDsPdO;?8_|ZWr-f8>*QQ7lddCn0*5PDyhH%K9;B}uJp)UaL@lK;ds z^YLt>TvP|X;9;rnc|L9wIBSZO5{NcVua(;hw}^H_+_yWnwy zdbl=HK;JV6kadph17@F!;?ZZxTUrJ9bk_<2PZSW=KQiz%WIjv(L{(5!!1Q}+!jYPi zDdA~LLNXM&LR>vV-nW10tja{YlPmmDUY07)wf(f~uLtDrTWJW5E&?=wpkLa2#%4q@ zo%>~ZmKm8#J0%FAG3xC`Xp2w zOgy;(KI_K68*P1XRLIA-@GQ$v93n29NRzSS`negn+Md9Ywqe&zI6?OLnWbA~G6iFV06M5EeZjppf z+v4<_N^o5vfpNZ}DJ~jqQMU3Z9MISgyHfibC}N{w<2m}5(&$V;8F@#3vj!%zJQwSG zBC6T8=PWC(p`E^RyrBe~Wy0yBvtu0EsjV_4+c}AA zY6qt&Zb*$V2~~;9b|Kmjn1_hVAg?K2x2$rglg1on>Bk!o+Wpm94g2oh9ZqoBcw3E zrnC{#sw0V`tsM3jJ=g%yBDLAv5^qjJ>2z;%GponfL>O;%GvXm=7)7E)Hr7J2O2>G**J1uw@Z0S|L)6;i;I?xlSSP`)iGk;_wVh zDUAp|R<@FqT+acqCb!hHvz98KY3bUrO?7YQ3Xt?Dg>$_H#XU72FIji&8nruIA(43% z6ECV_+qWal%v9YYGxy%FwTj8BwnMT^S;P}YcMR(1>xUo^EK(ycnpaOW)Yz0;xbVa}tj&^ncKijT~)jNmrW z9Vb(}2~4z{QAeUMv$VH>Y6podU*94{5ZU&Oy@G)YLY4)k5I9B!7-UvY03Lxowg2f9 z-_7E8#q%sthaPVq`P!deM%0Fj~qns;xNXXJH@h z7@ux3tuO*KDlYK}YZBgRYyWfYPt`Arj;lPa1`amjfK1gq{D{o1(7b^DS(K){sXqkC z`$Vp$jz#Js0%>M@yLHnI3(V?zV{Q;p4k&l=a+^JM$fHHBZ4<3CY5T|&W&YdkMXV`{ zLy4A~J?d#p5NIB;PTkU}R6=6bS(HA%Fb8VsO!U+--_hO_6=Q`gLsjgYlGvxSu$6C-DXB&petf}C3IH+p~()^Ead zK(du=3iLDIPtm|^w+9zEJnw-|bx{x#ak%1;U8(tGa!+Z8qi&Mux z?&`4B^I!d3Gki&`?@=rF;nsabkzgU-XT1)=N*lb(#KGc8CF+eerbq>hf9vhdW}y7W87&N)6wE zybT7VVde4eYes4=9z-OwAEJ{ws;<8r=N50pPZI;tR##n`7MobFOFuw0vZ{D+C&}A; zBi`7b@G3}s?B?nY?{;{@gnQ-r9e_GQVb3uDKwy)(RX%2v7)q+jpDTVe{aE__vQH~b zQ!?J3a_83@KW+SO>)!!z$3_p=y8q5(uff5~ElucO*Wc-MsN)=&2pAqYDqY%x=y&6j+^9S17qIs9_l_p#i^fSJ2%=#CbR1=g=zT0|cFiq>~{OWNr$T z9@?X3Js<&9d?;Rmm;9&i<tin+sq zEf|atI6|~ykmOMNzZNy%d!#{bq^FXf0Qucnj*7a=a3G)o1gz~TyJOW{8gko!o^$W+L){+L~@BNi6s=%G$9(y^$cw3Yo4cC zqZh_O9z8@`W>Slj{PB|Cq1FnLJxTFod&pq9X>6pVKbn99U{_ zik2z2JP#{f{6FY}IY|MN9>cC(zS!n6)Vl1uzcCCHZY&IMZMg|UWStPiJJ%ib}osB=Yy zQI;?W1eM2LHJK|?cuI_nso*vf0_!R$Gred}kzxJ{+iAaw5^Z!Q2XLANF@uUn^|PSO z5?4RM$A3_&J0K}-gqe8?=^^osA%9(B#qE?1 ztP{lu>+5*Y_eqwnXhkX<1R%zALz{wf0wW|@t44aTX|(HaPja6Yb*N5l!sXel1z zp?00;-U~Gw^n0???DrTaHgv|u0jd7%9)HcpjCnOAeL`LIH789X8G8^}2`Lq(!yy|;}d$Kqpz_o~Vjm22!{ zQ1vuFT@r_!KI`)7W$7*iRJsg-b*0XK=}M&MLt00t3Poqe=r_b(dPZrgYJjV%N}GkW z$d3vz2$n5V3uI>8v9Go6ee3lK5<;q~6x|kW2Ear`5t+-@z4ug4PKxX6{ngB>-q`yV zk?nVVgwoAqwz);y@nxD?eM5utBH4ZO^-#%NNSYeO*=xt1R^6H_MM9LQddFUEFv8-83_-M63!x#T`YO&V@N%jYLJ)EPE-m3S{b>dJt0(_DQ@Qj!*aO zs>L_CbJiL?5;Lw_QGsXNTaP-s1_4=R(k$PE$41|a;b2bJ26xBt4-}E?rxk5;tWtrB zdAdlb1gslDTJqGnMY?bRVOHMm_!Blbpr=4rqTEMg3M`ysM@Vn`N_JG~ank!-4ZDYf z0lOsXc=QP3s=aNvMBYh$^SQ7tsNx%zP8ekt-^i`_k!wc1Y`uMRj(6)y&!w`Y%ja;a z1t{2N5;Pa3P;)v z_HP`-!B58Bh-b>~tT_dADbwt!B~%7To|i(K^Qv^wprR3;S!eQg@Mwbt!58yC>euhy zuXKS!R4uHmpI%Uf2T<6`8@rHwhN5F)Y1&`rnC!`gYc+__B#JP*WkD6#a6p|XQt<)O zn05UXpsFbw&B;zzXq-ZsLDkx(q8|jf5k~K?Y0D)ylO(9Bu1qI1Tz&mYS(Q~YF<-}l z1v6x{IH`m+tvJcn3P=L4+33jFq22cM1^ra&5ZHn(h#)rJ)=*-O7{yS{jXLDkxF>06 z-pCZ2IJjpY&9x8tG5~9!Ox(>Mg=Iwjxb>cJ z0nd#fz%E%nUB^$Jg+CmFiKg$17cXhm>KzZiVuYm_(>~si(G~91l)h!*nlU}VA+$a?}N7Z zL*{us{p-qQvB+bkCaly{HGbr9)lFzgtFJm;=7|xVg^UXFagOk5@T{KsR#siV&Av}yQm<(W+$HwODB(^`&_<+?7r|F^Q3_E z_(}TymDGa{X05`lNX1^&`#%HJ|@BA=`{8idGnNvyjJ?ZsIAwi?p>}B&wz!baFeuKlxjGY5YW6*bI z?3e3edRh%^FFhYcULD^iq5@4dm_+5-Dx{9sk8vdLOzi9!tsU5O3uJONM;E7UN#X+h zFG;qMlFY{w>3f?)nEeH^#XOlGDPLx(33+T-v!a&kW2B30)l4m*R&=3dQmTH8B1wIb zx+ZBcFZ+D)S+SpNQ(N_--7ta=WaR7IAMeD^6LKRjY6vP^A8+l9zUb$X+3d%kkmxjZ{g&cKr7q?j_!x zxnnJVpQ>!eHop1)$LXvM#go7Ma zqPw9ZyYZ7nJ|RNDw-@R>z%KjKr!lV!@E+&MEHpyataOt`hh!?di*Bqe#GeS19NGW@rh0T9 z@F1<({|=pdIHNs9P@y+SY1PgOiEcHEcs$aW#thz#m6ce|YV@A%56Z+-UJV#{P`zI04&W33ALmS!DLsDvis?BVTn4rp$nLnrFsWY)kThmjF zGo+wKvWdhuYs4NxuDIsLUP#mSsx==YnUIt?An)Trrij&@_vpC8FZmIeLggd(Wel zb`%@*xaapWyTc0wEoI*8dcE)Wcdt*5%!nnn!>!s`Dyt=1Wu>BY%(E8c3CdEnzF3!_ zT2{negzRu68Cj6(5UJ1dc)=*93P`U;fPhFYT{UkmUCvY_Nf`hWW%OFq7qG)GTZp)9 zO&?Eh%MvzNB_N5tw@N=35d^~2-uo75F;mpy*yU*oWQWU{S9$ww6@S=ldP>5b z%Seru5j%Pb9-0USknkzhn!8RjE7CE#3_}DeLQJ?mA0%PCUfFNKazKVBBkZpvp{7A9 zdMXi`@3cQ2ql(=;#YFP7!bKPlw#*(}33mz#f*C!-AFaLwx&dpW>&3%mW*hVoYv(r7 z)IVJWF>|zs-=k${pmG*qJ>Ru=tHuDzWK_vrv4cmM8H5sk%!e&gwAHV~bpiBWCRbKV z{NPdo-%*;q2w@X3{;{Q;Wc;dn>uy09p!D4J&o!pViM=RyIE zc#DdV#G; z?Ssid@w(poz^aTQ^aXR}qGl-D&#l)D@}`5?q)kZVIT6SOHO8V4#}|@$!>cK2!XkC* zDyffb5VA?fis@b>)U&VRV^M-Oa0bX0lY zNleEAL40W9`DIfhHSuXlx3qfH3haiDWi=bue2KwcxY!G@ z-}thzwOzJQpl~@!CWVp@3QdiEm_?R56)n+IslI1%(iJtn6d#5U{l9#t|K>;e^>?DL zlmNcQ{W*U3i~sj8z{W|FYUjPjloW; zE2~I@LI~|W7?FM`e7cFB13!j%!cz)t6$z;rQ%hC36n7)V%HFHFpYhEAz&K1kcN1bX zLqSDQQw0jAxP$Zu)l;$5awLG@Cx|MRAP%-y*Notr^ zfP~TRj2F;LMMT;%k0}o8hu9KcIGP^*2~Wrj8JQZrEml3j4)_!9!gF$IBwD$}+P;qc zk5N1&zF;IQOfoO``%pEI;+W48F%FmLm275TA2_70VEpLvkq=aZu@iZNY@9SvB*z)A zNSY`%r?OZjW$!Q*3EVrD>aD(9T!4uD5cyK1Y3ZeOc1E-kIu`(Tw#{eYeis0wsM(dF zS<}o3E4S`M`rOqpjyw*<(!fpmZq;)t|BPUJbvE^R%0>}U)9`q-@b{q)6pe{U^DzRT zLMBR~(%?&cE6`z}RDr78*!l_wXL+odL{7UJG>Ztc>J2Y5tHOOA-PvFx$0u71%rFDm7U^v(C} zXnj&Ev=~0m=GrrSOL`S}dKI1;7(FRx0^w#H=WSOb;Z1tYqwHXtByz~> z5Y^x_a%{~>L*`38k z#ACipkip{BxLz^XY)i3SM9KkjS0Y$VJY6;ridno^dnlo_99D!WYWM0aeJ z>WXTU(wa=QlX%4TqH3UGo|Rlg28 zzBW^P`>iQzDuCIl9T0%^`UZ#ua+#G1&^<+^EF7y6*53I^HfW&=k5SF8A^{$wWj2zT z2pwZdod6-ob5zIhJu2dR&(6#K67G@B^7hzoO&jZ)9z3Fu_P|so)TnAn6$?!$Hn3QN zQCcTy2&FN~ll}dsTZJFp%(im@(H2ED+}`Pl&dpbcMG=u?WjJCEK=|$Iie-*XtU6Hblly@o+xjv8(4E)tD$P_`${C;OHH%dtq+`XTuedtyM7U*dv7j) zB+g2*)^aXfbusW+sjwR7f0{KnbtI}7J#>z9F_4+2BR*|Q@d^a){Wm)BHV^Yu+d!w8 z&CZt$ml9Pwfmfw>>4TM7=e!B6`M5!xewikdaQd0Jt=hAr?i9+8w7vKS4aB6OFtH$uZ=k|}i(vCfC)eVc$dbHB37f27&8y!|6=FmHRwKw&ffn**3r4>t^@b@k zu+fSN55ya9hgbe{V6D#R%0S*c5>BHxm1;>Mih4Uzi3VAGj@~+$nN8&j=Onj45W(w% z;2Tgc>9siT($rCT19C{5v>wVDJRVUu@6Ic#+ioAYE?fb;;rl_Z)N4L`kU#}SQ-h_! zQm8a5wxX3=;BsqMn^WcJfv7DAkw{?eb38yEdp-H3-W>2 z&v^5yTIn(-kTatn6f9dRNJ^2B3i3ch!3L?>Gwc=Ot+L`tujkQ~FvS91_CJ27fBuml zhMzyj|Lcq6Ymrj&m~_N*TOd`-QF%owX%asDmVe!Nk5gPNCJw(^d8s^})v%OxJP85h zrYUGV7L!`vijusWrdn3UegEM@{_2zd<~#iODHGy5-e2P9U;Mv(No;M4ydHZv6R=Jp zDlqE=tRx3EmFVLT_)P>3$~r75{{kgn=tp3{;&+xHrlbO$+y9KEA-?21={+43PYi@+C>p^}i?UFK_TxXcpJ>B&T+8rsS-X*&>@d7qA#lU&O zg>ogq`gA|XdUh#n_NB0pzHaPpeKkb(tyK@3HXXS*ASo?(_GLD3P0{}}ko zz#ZZ_UR9(TDZS9_mjzwYCH2U}L+|@i6RdWfB?r zohbl*m+~R`=a4c|4UsMQsYF*Z{t5B)?5>R_1774|e)CU>|DO01_#CnV=4nF&1Yl-Tn}a$b<}7pNme1x81Q)C`a99oSR0qg+Nv6QH-NY#y|Y23LdyU# zsdp386cb%(OV&zX`N`ofx5pp)OhGm0{CZ+f-z<70qH==fhLM_KrbtCDhyxewdfV&P zkC%N-f2H23H++fQ>0={??%4uFyh)}^A#&ZvpHAQ%j)yfl8L|fuhUOLeu+xBLHVD`~ zm0Yp<`UCCkPJ!+Bx?(5>i*^g0hKanFIv2Uf1*%=E22n$)_Tv!PLKOYsAI$KVh+Ll%}VWDaj?JoAl>7~LDq58*}^eb$;fqyB;MPtV*_Q^Yh0o!xv>Aq1*!-^IKtXc zy4Kz*YhmqseBQf%e<7{fyQBmlsUayrAN$OXD?!)fS0{YWcMR=ulm@yjRp?=yb9bX<1a#5VYT zPzyV9h@NwohG14TrDCd0R_ZOgD_T2@paFd}Ccxf&6d;I}^|VW21-;0bYxOCI3y;ns zf@lH~?y3j&H7jQ`&}prJtK!CF_&ljKFdOEg^Im`2v4U*m+87AgcP3{RFw3f1$~i(v zLNXe>IACh3P6R_6(}{e;F~p=p0YGUoNNIqn4Kmz6`_>=1Yc&V0ZOW#cpMial)>(M# ztxLmqjzoeHPbjaGAQ_!%`XN5KlTOyCrl5o9&6?2On{8SJVuPph<_b`k+jeuE_Q#?} z8vQ9qZMxdef>k?pJIzYAI0I#ThsSj`@codI2$`PloG9>@rQX9jmM$G;8D3s$n)B=$ zYCmFv31VS&f}DFvYp89Eq*Qvh#uayOwX*^5I0lFgHl)sAL~>VG>!A*~JKB9Ehx z4pmh#5BC~9;U>Ljm|8JIwZ2ttQ2V$ww@+p(p{jS@3znGyU$Rsop`H=Znyp@iCinyJ z^dg?uTD2)eX7N~~z#bZG+48;XManm?%>7if&_9pjgnA@+q=;7{ z6k1KC@XS?#Jy~!T3DL;ZR23CNX1ixpEX&j_l2Jbl@^eJ7g1eXUn**+st< z&Kd2Ln1EI&k%|rx+uTb>cY@C%6S;H=sh7*v4v&nbRSHI7pq3aW(SCgM%gn-^?GIBPobzvO?%`e7=sAGrS}RJ* zscfpKZC#o{SGLjA`e~48H*6r7mc_X^Q-@`0GCdARR_!^T5R)xeKCVgQQj|8PyWmpI z+}&DDD=JddW53&D#*SBZk{JQp)~diPJYP$8Qb1p1p5(7_wXFf`b#H}IFm_&5`P7R* zxT`68g-4<2J_$|X5K%p7Kf5c=Qa_I}J3tZXK;|kLW_{9TNxF??jLq1P}qry2e^dnYLC}%>?jbSO8k!x~`)l89Z zw1a4m!8x=&*{rIW-1j$L0Zb;GE3dvV3W?ZeHX3CC#F!E5ndt45+XZK`jEioQf6eE0 zwqA|Aj^XX1iU%Z9S}zW^JMG)bWx^(f3(be;Fl6P)P-E^CeDmDsWp}^+@))adg!O|B!+t{%^B)DB zv3EXQcJbHjx>uwm3Y#f=)UKy;Fp|)&eeJd6uSNbvY%dkr=>r6;MouA+g*9u}fGGr9 zLm?XhI>MbQ{(+{EA+WIy8%Lu6-nc7HrodKXbtc4;ia?sR)LK+j15DTm-<$+-SOwA= zhh_dJ`6`?|m0Waa$B}mw@ICsE)6vNeeGf?4$c*V9psPK>M6s;(XVo37phSQ)kB||O zAS0NF5)v`LXJ>JIeQj~$E^91<0AaYK)gdj1DwYwzCi?sUzJbPt4af`Y!g^sP@{N1q zOp1_jGH}v2+?y(4J#?6FXk8Y1p6KBLND7E>5B(3+(Tf-Og9M zV%~~UG65n>@o&{WnOzGn4TWxvEA^dnLpJb-$lrSYQz7-Gi>mmaH?Ti|53G%RAHkpC zaKlZBjS(eB5x_Z>*dMTj-#rBy^)?bnHI@wWAn;DnF{m`P9lfJlhi@%3bNpnf8^!Yn zOZPAztIqw8>wow`{`wO>R_0g#hhH3@9XF+r&!Vm68rj+=y(nyYl405mdffQBaqDs2 zj2pWGs2&3~7cBdObT$n2piM_LVA6M~cnAwXTE}?_7XIRufBC`>ANk`-f$WseZ~Wn% z|ME3)SCmxJjIPU(KV_uKCb1sY$wEs~;I+t*)-5iuoD54mSNw63tEfUv1){$}{{Bw9 zX-wCyk97xz#HG1hy`VkHX?b45gEvZ!k6FemY0t8$*&Wz7?u5r#e!)9jHj-PKl&Wz| zP?q0T^tl2~(e1#~XD1<&{@{3`+&Dq|Q%XO_l3wUtse*-;AJughX*6_8&Yh%ONwgo< zn0)3?xnN%f7oK5#p4zhJnV#eankvAPT*285L8X^Xx80c-Eh1F845qpaOXbp3V4*`5 zhDK~-$P`EXRP>(MDDg>3E!=A`*l z;Y>WyjCp26v%ku##d+D3uSF+OIG#OV{D>Oo-sHFX!c##_x!1q{5_x&Pw_L00i$t`g zMt1>fs$)&5T$W%KXJl6NS6K)5EROS7JCrfMarR3f@CVNyUEYf2xjfCWH`GK0H(iR` ztrM*SD&W{4W}zzAl6|S}e0koUugpJl`CT&KD<5qi-`t1C?V5X)oGfQ8d43RUGsL{0 zA3xcr@7C83f0@4|-Wf~P(-qCc1S-y>DWEcQFH4p(#E*Okr=~UR>7FOw2p0J#!mRV< zCInl%p=zGddWL@ULe%;lkQw~zL2Mp%3#S(^Ke@P~1U1ur}6zg~lZg7;+==0xv}Lf=*hFU*UujB9dc=Hj8>OSxgq$0vm;N5KHpT}@h^)byiQ_}#@y{2o@V+@%6AUCpKNCHPW^STF4;eMSvSkz$U`BD9I z3fNwISQ_!34clE;SfpYNlIMdjb%ndnha#xT(@gFB{i$1V71_2%X3Kq_5%`3&vd3hh z>lx8-*{oN@AUm>-3!-=IobK{9?cp~}vBdb+R_{2yw*Z)G5j-uwfB^9Hx#8`a*s4Tz z_UpH89a2PIc5SxT4>;b2fg-oxlcwQ4AEOATuluWZ6;_LC0BAs$zk0ZuN@2^?SM_m2 z5D`nQ;J&-aX4IN#5wUE!yP2tJS>6RQJgO02Ra-0h(Bqm;<}@VYTG#e1CY29b8tYob zF1x<$uNmpHaw02v%fpK#-g~#dR~SE>e@IC`BilmQh~m zKxXARX=DRdW!8riub3Ww*@}!ok{3bj0@W)ZDv8`CvMp^}64u-{Rp#v~S<~4&t1MJ3 zGM6cDLQF~@8G+M*M7kZMf_oE9HaZ?^gu^y7r|>u=?%54Tvo(J5{dOj!%I$R}j_l+y zl?HxzGyyac!iQsPwFw?jb_`Qp^s7@JTR)!M=K`tSH$Wrk*((!XBrei-i#@azKA8~H zrW+$-E;JaZR3b~@h)_LfJ|N9Q7om7sT3NA>wt*SNphLe`S9fLwdOZPr_LopISH(MP zJOC|bnFrU^O0Trhy&b)u)+b$Df%}J&Wzgv`Ra)D%Su?mD=G2eG0%`#HksOLJkE=PV z`tg1w|Ed6*U8)6i%xP>wpnI%(!x%E7EwAPDn4pISm*A>L$rSNTvpXvCcsL6GP_-QQ zIi-IE#!S3gY${+$j@ey53bhW9M14JiG@A~;1!m>Bdo@)&c%b%|=$h%Vc~4~~q0tsva~>dth>TwiRRMh+dt#f5iru4zVC)mhQhZXd$s zG$qknbJ(8h8{%}TPf$#2u~5Bu#4~`Z=lnMvF0*>|My}5m8x}Y*GfKFwbvfAio$lo~O5#gC9*t9+1l0QZMSC4nep>k4=9I+8Xkx)s~yg#^&yosNv#$$i8K{gV|7uH4u@JGA_ zxNt?Ild?{D5{mBxeegI1Cg7*OVbk8N?nb3ISweQFj@2QxJQ58wA7gty1ajdjVGv;Y z<0tvI-{DtR>^uMK=l!?81ilI_=E${9e5WhWh7dftkzAzz~&SI-3@9mP$x3Z!po1{6?$ z&8=z{#-enBms&gE5-jZ|kD_yqas>|rcAhoMBGO}jsiLKPQ@Jcus z&+P)Avcn~{W{Bil`9LkcWqIXT0@m$PT8tK{R4`}&f!`*+4{=9s+zT2rDbIT=0%zJ7hNne_&fxv)Ie4dZx@=Gfr*^MkSoP99jTlpH)b%SciN;BVYYMIg?2O175L$wUYSBx6_j=!bIX z??0~QeG2Lk$lkuPCFu^_Ith3bR1|I3acrP?RAv(}U-)$L^*BpC)ar46r-4F`ER@D8 zyWN@%i#46d&BS?pkL^g9sUX6B`DJPbhlB%)TQeo$A%CX$ZB;w1l+t4~U0iyX-$@_8 ztP&+nQ{{{3cdDy{nDP+~qN-8#{y9YVITWOP>c* zU4LBvuzXU4bL5MuN4>5uA zp7LqcEe?-e=Xn;05tckLCc5ou90xb3ifig*vdaeiZ$z~e*7AVrS>Gu+9nfl0b( ztyL%mpJ#Gq>yGba?$=akjnd|mfHX4}i5tZ2v@`5LGRFA94sR?VC!C zsTsO1lKIgCI*r*+<53uk%eDdM%9f|^oLiopyb(ZB&7A4FG*3I1RpXn~U{8_yjJ_uW zr7x|pL8_e!TBDW}6_r#EW;1B4doLcgW5=&AGfOh{T%aOSrKI}ju#!MTm@1Il9l%m^ z=D24M)OlGT;Tg-UD68VDN?bcwc^zIv#4{RNp|4{Vc}9Or53d)m&s%AVSpu@sajF!a z7b4!wJ<=U5rLdUZ2VtIEf@wsWs>ePLxo(=- zXm{$SO~I#a11ey{0uOYZvB?dL)AjK>Nv8$$w`X&A$?zm8ldv`n7SR&cjm2W&^^;$f zM-45BjxugqsP|N|HOXwZa)31r->h(Z6K+p)`HWvNy>VX~Jk+EE(q65vQI5z!Fb zw|`uzsd+>GJn=6H;KCZrg5b3hh=)S=Icay&%p(uuG{#akB-96B6Ai3!>Nr~<`1<6o zqR{qwJL(2>xTSHYd9Yu4bxcu42SjDkK*o+jtHC&Fa;TcApLz9N0oEe*f>l_K9Ef>* z4%J(SF)1CTA4)quLd-;L_Z}hX6+=r5)<0qLAPg1VCS2D2#Tqs@;MhU*a7t5e2RuG< zBvb1FhV)#p!H8!-)d#c|0rakFuqRO`kGGg2Ioh$XXaE7Q+AOgRAz5D)2U%Bs655GA z*Fcg7hm--VM%9&6fk?5cM7z+~+;e8693CP!Vj;AZe}nw?NS?0`r&t7xVF9w4VV3O= z34_TemEWj-n0}aiNUUrPMydfJfjjUuWhZXrow}hAgycV#SfMo~V|Qjyfd%4jYO~$L z&>y@;i;t>|4M*#@8-+6fwqs}K z6%Bz=!W4H(qx2!5agH~dRhBTZ{Tuah!v}vZb3BFF|M>%d@kxGsB^E-|GrxSrAHL@Q z=PMA(>a(}84r5DBe*u!lmBwRXyiJ)h5b&I}p?PEzAWB&=<%=_RR?JmfR#HVmb zKYuFD48?Pvp5A;WMG<^w)%nnGJ+1*{9fW$vg(d@nj*-DmjUIjTQv~#tq#Ya0@K`aY zUhYRz=6Hj)_fPpTLG4U9UXKk-pIkqks~J~3;jV4G^NJrNU#ey>izQf=DzHj!t|~uM z6PBq3%BK91k55`7M|obFp)UV=I~XSKd|-lsQGV<+$=X zTFq=ArTU;|le7)UVn(x#(z!SS;GXTe12{SPdXOBj=TPIdV5T&&mq5t7S|(htz}*JY zbum%LMA3xc2guJ&Cr7VZHVk57>j@<5m1GDo76}<}g#{dCq4twX?pP*EacQcG-H?ns z#Z-1CQbM32*1Z{tl=6y&A{1fbD@thf5%SLbjpO%6_bHE${}@jB*tH4Z{i{cs-)NfM zfCX8Q3-bDB`}kO&KCLfn-=gnu6PT2nV%Ie}?SJwh#d^^dPal-?SotRn*1GU6>%U`k zR_qagJSKRQKh}9u1&K~1dK|~uxW)*Osi#iG4Q5UE#e*7Rk1+WNV_xd8FNm#&hAkSn&vyclZkWMGvq)ZXf zkZ&Lx893WVoA!5OD|1{PjM2Cq^0A^Zf%Xp}vo-vK_F)jF)QAq$&VL25k%v~JTV1P1 zIr5|iG#i?Ec*XTTK_;$i-S;MEhb(U!q@cVp z!B8}41?e%;jW1T%dP8=tUaE@O3`N6BPp;>RIuDx5>d^TdpYDvWBd3ar32b_-Un`jF z7BLXw2s(qADD`{4dE>LG53CHeJ)OpPDpeYAuV=^1JYt~Ps|u_YJiK|p0Zk)CWvuUN z=SpGB9|8A<2PdzQ5fvkAV7yHcA>!`HqVB5CV7A>fRcE|kJuhy`2l_)_S@oKDX4&EP z4J^O75-MGK1;W#tq!d+E0ZMRF7I~HQIIh3568`_$`qL)MmLy3GQ#1E-Bl2xk-PH>) z!~jDfNZ|kf5>N<%kN_#fU@$$s)%M=YCE^}8!v|9}_ozWib#}hYh#P0|#az`?wfLAt zNg`IP3NFfwYg~Zcd7Ig=QJ)M20s*+je7m0&MTSIJK76bUGkf0m7^8$cgTK%B`-z0l zQ6vdLakKIjkG&z+DOoSSE&z zJJt;h^Wo;K9S{P*ixeb)ojFP_pIEtujRj;RJSyv?0Q{<0xE+?SZ!RNG_QC%{>!3VW z(VY{t_oy-?nDR)pyfy@qh+Z2O2K_TjLKJF^DI&C*7|iU$&Qc3{S6mgr81%B_Fsp^X zTR6;QwA|I>(Rq+by+^2Pp_FimEa$^H2CG5U%_$mKYZte?njm9%F;Cc{$;!?os~st} zqSJU9x33bd7F#|G5<1#!+1t^Ehg1DR-BF9!m^~z}gZxdcz^)`9XmKwp&@{Z+ zyUTI5{q-zu<#z)0z{Ko8ZipjAs`mLF@DfKQr z&)$$?$3C9rO+$9ED(yvIXJZl`u?XMms>bFa6|q$(Qk9IdO>F#28@$E~&pAd?msQ}P z!qII9BzE1vw|ex}ogFSLJr8q6Qb)Bx^#>XNH4y2$q;bMQsmMlvz2G`7vN97rR(n;; ze(7?NZUe_HRFxA&l^#97n{rD<_N2KF)(*vR4&RYz8Mv3~QF)A{2q7uIOMsS9 z64s-IG|z-=S;+u7twh9K>o|d2RTF(I@09|`Sa-6B*vAMcB6+H+gg|=#@f6TR4|e#8W<;y0fHh&&yI9U z){7`$u#4CVq#uffa4ZO?s~|eS5WA4veI6Uu$<}uho#!%LF_v0$W)>&R~nl zNj}7^EDXAx>_{ONT|<7ab^BFOKCu=xKn8gPrC_5W<%+~-aqm1HZb7YF83Z@8Xod$o zXy2Fp$%S7xlvy3lYxrY~Ps4r240xs|BJjM|8}jYOm$YBges6hDDK+qJ zWp~8^cNT`27)#K6yS>$Q76${;1JvRuNXZ|XHI8usz;vNGwzW+OVa$1qv6|p&-p^pH z4pAZK$pbfV#m;#$F^ImXJfmBqVtB4(?9;UW@;m$SHQfErU+(|;>%zChvxyVEH3HlV z@s4N3GBqRBR|{ze0ui!OT!dmbs$N9eLR?f0=D~nMjAoG&J#+g-HyOjXkTAo`3$3nmSGNDh8Y+k4ll`_r^~7_kGCe45qU`TAP)r1 zDR&I~WR}2pfj@5rUg6>co_teV7j=;C#ZGx{2)n^+96z)~pTjT*GihovuE1JYjT%b8 zQ8!YQ)w=QYLqneLZK|N63@W1 zsfEVB)!(ok{mu!pfaXwJL!*1Ct6d;JM{e<;BI=RS0#rx!#g|4+B41Nj80zd#C1Cez=ld2nBJZ ziv^=C>wY7(h zxZQYm#scpAWyBB5-dE0LUd}Ni%MTF)SQxNj@q+x^RATK@YjQx9epvZ>R4NNGbn!8642yaz6~myvH1FThWLCH};T zBXY>{!#F?Z`v)jF_rh|>Ghtq`o41&g$0-1(sP_!O!_bd_Z{D-rGG*5pd}{?{4W%m@pvCGyN= zbye&K_e{4oYD4$GRv?fpZnckPz+B=mg;6ECu@#LxsrNa~(>->bQJbk7)qzGm1m_fw z{6-hDbGC{&ApTR&swtM-;1^>{(3p-ojwyLK4MM*K_s{L=hI}L z)m_N;;rg=&eg`b6v;1J~jrC{des?u!4)M^D*TQ8|*h?)n+cpZekyBkk!nuZ|+WG~` zkwD_0a>m|}Dz3dr5oNs)q$D_~9V9x7glnFj9@haXoXQx@AjKGnu$dvGY0GeB7~CMh1otWUC(QVUi#W zC22**y`E&#>a3B8%mv}+#PZY4=NKufKM}*GncZu_Fx>-LCVs<5IW%HeW>{E*sJ*Gx z=S&-c$iy@64myI`1u7%zD9<7ljzyjWdwH+E5b_yyIE{p1WvCqFT|+@CbX7{Mqr0L! zPOEvP)oDm9$x%44QQ%bZYK-rx+Ol4KSzIp8l@w1@mbq<-R+Uc;(T^NoUA75$*UHAD3s(vKdu>eAWOIY& zC)V7z?heoG$@Wz8s{RDFa(-PugW>7br74ayp6v1*AZ8Bqc`Q~D`&w0c-n zHQXk)Dl$np)WNVE>iWEjUDoV~jot2{S;W1~%xq+Btx`g+4|DY#=&05=qhb1Fiw_-PO?=}@D=l*804KubL$D$m6*I0Bvcm(?3~{EkmyN8 z#M~>8ZDdrfV5dp9p8P>&@JtAoINB+^G}@<@^_cr|yybn{vWE@#%cqCmw#FLx1Psg1 ziFYeswMEmg&OPeZ*Agf|!R++~)l+W+qZA2?jzCsa)_^TaZFYB=oOb-i{X|z+{(|vN zdnl4FDI>Kab-H=B;mZn1URaAbCSjo3ARE?Pz|DT`4+5_S;DR^E*HgD}Q4^N^uO`_O z#EOY!Q+v@LEcflYoW|nF^Q>V4x zRNr`Ce|f>R>l!vBn!?U-qS(?*Y}mAZN*Q^zg6)ar%n{+MY2!XQ(fOngmDj^DAGn4+ z-O}=}fZt-iTF&%gHq7VD;W;hfcii@j@fPv*#+S^0PnSiP=BzMiPz9y#JPuBh-1VT! zV^9nY^&e%p7i1HQB3)113-=5BL7zhtspjhLw}sW?d~%L(ppI%+IG87OKyd0AsnAWT zwLjYw`tPt)ZerU+i-4eG8eaXcU-I!O^ZEY2|8nD(h1;-9##w92@-mS(p1EnN2HG1y zhw*_tdoJII$=6B|WW~=+JdA8#iSs_F5^HNuQRBWH-kRsOLT@J?_TNA7yGQ;ob4(9d zyy452_?OT5AK!qx?D7OU*DDSUPOInO7|CJKv<(l&i=j$fu@6aN^B*a<%uc^EjS$c8 z4L9=7MT)G{^VFA(c5X~9wt}-utmA^V6MGDpIEis+q&8ibxI=`>!3tz@p<5jg$lVhT zC3SVc)yY^lJF*+N8=O%Sfb@m?B72bNYIjuO(UG7POQCDYS5_>0t;PrDB;Liy1awnY z9?MZ{e>uQJK4nJ$ir@)(u9m@ItIh2Ag35fNq-;amXd>Sg;A9wP!)=Tlp62P6ZV0#8 z;YLU#{m3HHBCjpnF|uO7v4;vy+)o>Og21-j9GW#WPBauRRhaU44usnhAiiJ=_!a&6 zMpfc6oVS=j;9j0#ZwoJhJ8X_98KJ{wU|#lmIi88>m_eMH13@cBa~>azAN7@{A69E% zZRI@1%Q}^#$fTxfI;zNBkv$P(S$f=IufRLgnY4&MoT#;~cjKpd?sl)=Oc4`3kD}XkdYzVr(D{$lKemm|v z-;7O7o;ExzO}u#qY|g^+1Hed&Ob0HvHEmrHF9m}7%4MN zu{`P@z1$wDMb^dy9vCl(=a>2Rc)ncY;r@8|m3~)<9kmp!@BKVm)_)!jNaAJ!B>+-r zjT);se2nMnW2FGvhXd{GB>1dA60;7_1q=AB?XN}#MF7+pK*#&+VB>ntj>szOtF5-z zE7!q>v`Vs5Q`8GLG{XN@EFw>=v=r+pTE6IlG9jzL$VxRJ$k85gMD&7@Hp6{oHAn{A zuad@?Hq93Ng8dlx9rot$STC@Jc*p$$ziG2s4u09&V%1LZ8aB;_MJ9Wcf-d4gcNY;t zkq%F_aq2K0=Zh}jWLdMM{Kd!+YEa};di2P%Q;-eC8@z7DQx8qc=|KXClG*3nep3ix zpo!|f-^M2zh`l^l_br2_K762zMz^*JB4#w|TE&&wzVzpbvM;rsSK*A+w$DLsC`Mt2 z110gb!ix*b21kLm&%>F;b_*udKuUc4*o4ua3|argYC44beWOrPvypXf)5Q+Lt|$wU z8o8FN*I7Dfq!kmOXE=tJ_enoVJ#EzS+PJx@ZM(^an)!@GGFQ*Bf$`oZ863ClI$9P& zMOT}ok7A->QwACt`G9TDqvv0XSS}Uwy$)21x~l8cldp?xDwNM0h441ruqTnzLT00e z>NzaqmZMRy{#|qMfVfAD@L%18G z(^$`ba{wCX6eKD~#|B{Q9TQOY1~;uUGH`r6|A+=uQ#a^d)x%NU^XI(S|7ksc zn?K>@=kAY&cB85{oii@>KnFxwXkBpr9eSpHOGY)6PUGVI1b~Dr(91&}+uWQXY zgOx_!>#ouGTsKMpR|6iFCHFLoh--}Jy3Onwvn>8n)vVclk%@RPnF1{qW<)m zcf@e3E3YxLVgc5U&+ayTX67}fcsscF?6X8xlgAyaHtON?dEZV56xW#d276Kb-!M-m z*zK~}%^gfCqUg5`lV!R2Tu-s~w*7AnC4?GAtEyt2}1!-7^RF-RZ@W$%RS> z9tA5}$g{F*n&X+NIXHF?9+#30vFVUZ5;E=;!~4UXC%dMjie73TfI55Bf@j;$p;$-u zIH;Iwey&~yh{@5<)VkCwn?*%!AxbZ$Gry|d@|y>nSh9i{VWzGUzNvaXC~HxjOMAd| zzeaufTFVT@2xil%rtLNs5;k1Vf@wFgqY-uH)W~&n(7iolMIwfc+TBDGruiW5rq`8A z(Z#$nGp6(dlkcAtyrm?ffUt-_j}l8X>rUSgeWHNzBYud|wowk<#`8~^Q} z)z74#kC>;E=TIXLX@k4J>xuQi9PlZazQtrHV>ha`sD@GK5n5jbkz@-P)WiTJ3CN!} z`4`0EA=Q`Z@2lqs3wXzEunW@xLk@&v9H}REW7Q{wge(Ko6lWFEj-+CZz#|qZBBQng z#rC;qJVC=z28K^&*l?uPJ_UrK5ga>L;DIp$B{xRfF#n)FnWgwca6Oz~jMoR`wzcUm>(=D2(1PNElQ*Oi?mWgq!F9-^r?k8i( zO72!nWN$p}3K$}5xrV#m%Z^F9Or%R^EE!J>7akA1I40nw{i-S1 z`pcSw3YH`C4w^vLm?^u+%W(OB^T6M{;QI@&16Snx(|&%--+zn$mk%2kssP`03e=`xC8HjXK^h~$(li*d5k^R z{W>%x#Fj;~ya5Az;u?4W4;9kSR3_8HEda3xHuaz&|GJKNCkt2*TpyK*BsP8TPFmaJ zT%!Ar-3@@f)Rcs)S+`B&2?TGl!8m(x8;4M9qF6EL!;2Me+SQtBIsh|MPE|qwbSW$o zKMhU})OKx^&EV*xgw%3HV=pOC(R52sy5L$G{xD3-%WRUPG&tO72emB2oorXY60vgj zc;iD(IuWWpjeCk8tUq^8?Vj*Z_~A;v?rY!zWzAI-)fI;yHGu6aPqk}Uv#kyX#yV_=59SOa zxB3`)_6U43%fvhI3HEo1f4*%~)S+pfy*2mAI;5P|me*1$ z90f+TKoO6du$O2@u1lhDUOz5)tz|fa92$t7d|wg#%^?| zZ@4EnCuXXpy->L()%pT@-L2z*8P0fhh@L8Am=bu^5C6bSiy_Rl35gKKpw-JhjM>mzl8UvMmOGK1OU!TMaH7rDE26#-HhZgcAW zoJmjX8^`dFUJ!c#rWh$cm@=v?zAX?OE*HKJO zi`5WrV~>T^Gc`@?RRLc%Mn;Swp0%zsbynf>&Be|~?clLi%rQAJwHs#A(ruI;qP*su z^S!2FF(oT=+UV)9VrAwvuUb0Ou*fk3N?<7?qC0M6#R?udG8Onlf@kx0!v>+-px~xl z=G?~+s#o7Xss{oY5sz^N^40=Xk?@rYGTc^%yHoSDi%&HiDP2VQN!30w0D5{kyyzAI zpIoNyGW5!*Ov>8Xi@+1a#H>r}Y))X6_nne>fV{>803UYfx(WsbLnza|%pI&nNY+OY za(3CM;N zU}P4V(-OII%wcJIXKivVhTnwu$)AQ(x`3H>@2duuR4sL^r=aH4*wevsm2Z@fN5?0f zV^Ha%K88H*Heg$OaGeB7Nh2mkhc>93)nltwA|l3cX>lSEW^T@cs6Mk7;$1v-shF9- z7_*!M&#lWujqy1w>U&ZieI|o!|0$RN%YND}vhoH0$HKkS36;pjIveY4xK0J;bwF=~M_Eycy^mv$A z7QLMAW8G^G&u4ynSvjjYVtK6T)7gA($nsq&buER59gDCE_a8W}8Y@@5ZeH z8K+J=siXyRY)h7O7iL?}by5xpvoe+$^o1L0sBe&koWYVNqki-Oja9#Vq5s|o^2TD8 za`^yUJCi95crv>sryyyUAKg2aVL8bI`hpH>p=fzGF)qx*6ooKWH>FnCMxmk$Yhpey z09&#K8kjX+Q#3}l-6gWx4%?Y9fhYUQJEw;z5|9R81f*7D%Jq&6D1k!1sl_OxEHY?q z(xUgGl-#O*gr{I-(O^5zrfHQ*j;V9Rz_d>`ew_J}`RmMK_Guu^;W;fB=@MaUy{82- zgYJIc5?>;}-uWNP#jct$TR-H2fYDBgI<$mI0Rs{e6B4r2%?IMfedA8GRKMdrMbcEh z+8bvC-}NG4P;x*{)z%x;?umo%X~s17kSMyeR2}AGMw3dS8pAcb@4(WS86tHm&+GzT z?Em(#-#)^Cub-d)AdI#a5VYnMF8aM|C!;x{k(=}G|48{gj6FJI#S`5AaO6xh0? zPB;;VCU=WHxPW1}RSz~GTW|9?q4P%QPdGNcRPxjM`Wh{k-y3ejzutl8jXQC-YR3Uk z!@36m_@9-tRpj*UUHHgdnK85K1-Jl9yd}`Gp4Per)qX4ng7+1H6}UU%&M^(s`9Ctj z{UX5juFXK7pIOUJ9FjiSqnT2X_VJ`4+}Z*(@oQ^f>=wuab?0a=C2HQ#6Z3ryb$Pvv zcZt%P^3`(mWe6QK3Fo zVzrG}<_IjoPkc}enQ-Y*c3^vxZaNAAz`+r!Q18Xrjt6cAvp>4efpDuO@Usq;+QoY5 zv!mb*nBzv=z{+(a(pF-iB=oop565)-yNQ4HlZmt~IbAMRRX?SNbd)ft^|Fx1->@h+5CepBY}36 z;0dnJR*dkQk%SWvZ&)?}d~Re=VX2^r(2u&atqn`suN zbUX~ZUgq1M?tDiw`*12&DtmEWiu25c;&{^2+{Z95ETL|^b#Lm4(!QXBWV&0r&aG^7 zwUw*ZM7;~e>=}myZ_f-(M(R|K#%SuF1J(0tPCM&HfRE7!;W&Z|j3>Sh>ZnfHYL_JX z8D-5PtN-EUFq6E%Eg-1D5@dtU$k}S03XAISB)wz6XlI*+w>Hny6{czaE{Yli*eI-zFZZij2Fg8*+4k53TYdiM{I))B%X1}}vrt^3 z_q8<$+ei)pSs@TkFKhztJ11kCP?dWZg9)12XVjJ_}u26FsXGUB;Cy+WN2KmxLHbVkpNR0(18o ztaZvV(JkZMgbnTHei}U`5yH+I)U-9Bp}hJ%%em~NrSQiFZAj9(If|KKtki}v>%^6! z2%G<6mu+s-SCx8P=m4a7=dzF&Oj7N25yilnk63qq!KpQ=4Kbyu5=IMIH>H{+DqJLy z7;`4QF&AG0>KSLRxzOjb?n<@80mvhPOwUt`g#fRHr&+~l6%Rq<$Q8>}kQ%G2h0?e< zpJjH|nqei$U=4sGNs1=fFS`8MHvi3uoqG)LfnGnf>zB>(pw$k-X^q_`m9^uS!lmlW zQ|c>&O&^i5Vol#!;9d$+)G|Y536XH%zMn-?J@032A`(TV8AGJBlEiQ;*_KQmbNXDl zMrBHI8{-BeXpZBsvJKsanPuLh0CI@5<1HeywmJ30R53J)^{g-v?dHR*MMckiv@llI%$vcdPNF z-M6)pk%9!@`dd)^R%DriBBD~MGBZjm7GzkFnZt)=u3RQBL#t4$K`z7HtTLHY4%2K_ zKTFkE?)XfxDmr1+7R&;p+#m~-C?#3f+7a1wwGVIGPj1~AG+xdwulA$v%Y&A(TDKum zk*C$8y0@;e>tMiwzuTHrq`)TiLn}qFf2a}lK%M0Y`x@w%Dw3ct+1iB}5;B)O|1bKj z{$eMn6_u2=)@XH{F~m|ep=Crg$;=;H&o0zb37@llT5-yrns#O#RE`Qny^lOe>q0Y| z8WpAN*8Y_wT2(^r)ze_rcVKcnsQJIQD?NmWMS5vWAj?UwTL_UbFEa$o8dYV-89f{Y zBRSh=s&PLo&4>7%$}a1)&+N|DZZqjxjfI-&aThDrMX)T_8<^-xo|$uAV40|4ppr^W zBWz602p^*gJATMo+nc_$knVm2uhlWZ%*}nR+qsFFK#R0Kj&j9eIab{z1@f4e&PhVGpLmF3bWtU9Nxy30$&EO7&R}wdzX6LYuX^Xz@=7$}-B83kQ8t zS_eNI_K4m(?12%4P0<$*F)K^7SsBw!*oCo>1912)N4TUvUXp7%de|>a+0zyL)UHrI zfq(~AALt!6$;^=#Tnw7Bj+HvND4Lkt=Dk?fbWoN9j+_|vRBy^wim)_8IK~54QqN?C zP`4ngf1JTeB-A8dFW*HF=8@N>r`MyX;ECqsHk=x;{2|AW1FwjYZi&kk1idC+pXJUIz&g|rG3vaQXbsp6uOmwfO@tz#P8aTz9l z^D_SJcletZdzty=ef`I;fv-slr~JOkrjnh%PdsnwC;F{v!71xbglI4mm!`^?zKARf zFfUva7ZilHuvBK4q$))uSce9gxWM8Jl+ZTqzk9)NUh>l;eIf#1@A&y!{NYRBo8w)A zl%>GDM)Qj&gJIS{2syTabD1ki?ocs4clf{m3LpN%_8*1n*@TiCQWhWaf5Z~ z-WHJo7dZzL71yLA4)ql{5*p+oBL(TJL=})494t8c>mvbwOgs`JsW+vBv+^`V#*kna>9;xjmcs ze4CN#Z+D`{`ypt)huH#d$FS*m?=jO4w|hMI>Uz*HVHh%QO(_>ijpeUlzsbB8a=A0( zfjBl>Dcs!fI^xrfuf1+phf1I7>s+!V0ld4@rGc&L{^>Watv~Z$)7}Dqv+~{2!^~kT z+{~gHOta3KQCR3`ANoQkvXT%d@JM@sugsM}t=>7(PNPL0^17qQK2FL0mg5g;D)0vE z!niPBzRkyrzg+%4?KQ{KW0(b@F^*5rE`r0EM?BC9AB{tAGplw7FC4j#j14&@UGV2Qh|oXI1U>(zw8eli`kvQJgAiK|o8Aq%T>A82I5 z3rv2bnhW!MM@`)8#9UWMU-1KCro+BlMmkJ7q+Euy#PnEU4)aVb8@R{9fK6N9!oCE) zmCOZInrBZ6D?z{@G*KxBMjp1 z?y*AWTR}NYt1_Vwysh>|69SxLj83x-7eW}?90OVg(9E#Ez__!|ruhkdmg&_2#9 zLq68sD-`K=m>>(Mawt=xERNWSu=72MTC|cM72S2X+wGqHa-bT1zGuy^50oLtm}uR~ z;W@i+D}oE6=aKzdaEgR#cy|xJ9ghagCo9&W)2f=>I^JCqG+OyL{s{Q64>RkC+kjOc zvl*SlIPSS^9T;7;)z{oNNmaD0DRn8+%+}hWEP_E2vKKl)QZ`-ddAdk%7x80F%ya$U zBTlqUA>T#Qa(_uwPL(uC%_6vBiYSLA92dJix~Ecy(5um$K`cb-`D1EWr2OIv8|+;V znQ~9ftbI+UrkR0g-NT-+W=if+-iJ%IS$OY?I}>xvd)>uq)B{|(h7Btjli7+W*FIyQ zim3>RV6q0CpX6n_+-!`wQFR~jADi=*UA{h#O@s3HJvYtQTJB!8FB|Bk|Ea~t-H?(fSubT(wRTsO+FOJbb1_(~ zEyen<3U&ALsN@}5*4bln$Yu+PRvd*SoiZToY&UH@eBav9IRIZkpuc~7 zWUPBMUJvDLrdav9Ti z-L$vbxMA}a5`u^>ChWz{X%MRJLoHfrzi2d;VxPu*RuI1)XW%N(!s zu_orkXp^_Po(ilEBo2mT?)xa1w+oz+eVOfDxD%)Uy?u@ixt@*BTY`3iK|SzN)2QG_Qn7H`|+DkYrN#w zxBK^Bpa1;p!dscksavPJs4Zw6$>#TCMyGy?wXhP%LG@lg6Wnx)^NI0-YhVU(-e^j% z;m#!A* zME)V{>()^PnaeqFIbQO+k)JYtAAx7$zUe&}gSWGdAA1cekLVi6daf?&yE4p@>zq4Yn%hu z!qZr3h1LO8ms~mPqe>!n87&jbudi~X%6HCX?B=>J>j$g$H^9?M`Z6rb5)x_h%`g$I z){O_t@J2Se*(TeEO7wQ4h0CNuJ1q{j4ddAs+Fzy#3&8Nz?2l%jkUwNT0>4@LFH1pd z^>CJyWaPJMWa4w=-z{4!@ibg%0rugV4a;eGn6`lO7j+3M((66{r zR#l*V0ziARci+(Ar;Td0%J;}mk^dT)u)D#Ce#pgurZ~~i=&F^6`io#(U$^5>V28Jb z*WtM$Z^z`xsKm^e<_p)tfPLrqYWZRA>~jn;Ngy%QUa)CM7mH?!|5O=}K;|zCuW63- zVV;W>VdTm&Eu6H7r=iS9DEzZ58r^1937Ehm>>2hl?U8ujt0i_$-bV_ALMX;%Z2=JM zyv@)p4A?~8$P0G;<9K-;ua9|KzCYq&953$y&CjA<@p@*u`!A3)OK|(~NJrUKMN|ThxI$d|qW%G*dPF5I!$q|7a zcy1X18KLmmT8li8rHkURPN6R<{CRhEREaR*r#ctLa4)B%K0LZ~r2jAjhCMTf z*?UQ&&DNdpHQd%*&tdrj{0jT!&b{bCq!degxvcZY!bfH28H4&TL4$Ql((#b{$|2}umv0&+^msfWk7NyRPh4J?~bS3z0foi zQS_QFSOayV!)?hZ{iDG6xCCN}oX?qyE{zyB2lK%J12MbOx~1uQrK z0Du5VL_t)ipHKD|a80xdS*edcnrrLJKx#xksiL-E8=0?9W9C+opb=zA&Q*GIC^dy; ztV(Y;vJu%Nv39CTus7P;nlbf_4^+-beJOR{8>RqI*(!WpnTbe6(36E?LKS*ooRku+ zDK!N)6lw%m`Co!lt-zZu=FVTXAJ@rEW=iv-rb=KEPhmXfIS+#~L&RsrRPSUz-3S z(JEwJoz3DY^-R>_-OjH)W=u;~L~+1S^ak+`85-$z_p(sxJ?c)1l+}182Fy4+@nr z*-8;EGGmNkE%dmKLQSnX>E_%m+PqRn++&!&vX6}P)zGFPl$b4)(i zGuDf)V5366?pRH+tYi4`mgVL*%vZT7nq}tnnHkf^d&FSxQIazQBI26!j&QSkEr@pj zY^~efi~d=wc=rgc4@lJYll6&AJo^^}N^4Yr{B%E^SY`WgbQLR7(m_-lSuU z*ebx5V}f)}g_xpOl0JxNpE{gvhuNw}QA?vU3Kso58wBCf9+^4ExYsh~4i^7IsC+W- zFpHN`mi*o;#R!}_7E)r4Lqr4yKspM|ZZ{va%nmx;=VE&n&wA?Mk?Je7`I_0@#%hfs zxnYS&lrg4k18bp+wR}*}oMOvRfjjMgJ3JFUJSs4V=G%Ogk;L_VQ*KUg91yp#W| zKcbg>>6x^i@!#oh*0Df)FWeOgSj~4omKgP}&WX zTbw7GNLdWohgVY z#Rcy)ZId=-yyE2rkLE=Zc_ZF&cleD-`lA+g6k~#T`Kx$uq)r^Zp_b{T9%TV)T-CmWpQ=h zcj2)k@KN}&-SXME6smqU>s7X zQ83rQn3%4n@sh>3;<7rYO5|b#&~5LQ5@}Tzxf?uREPgV4@3=hScr`rSUfiZ0cR3P5*Y;lt58OA0&>Maal3|u9w znSb-$_&1;YuU_(#<6Go^{Br+)eqHz)c!ySlq1RMQL-H2P6@fNM`tqs(EUYJPC^n{T z+>YhKONuw?4o8E*C+;AJ;o_>dQf)FjswSyx!KwY}VgL76{MC2syH{L>74i8C{`e*S z;p_UtyWZXScm;(>PW8lSXBr$CXhVR$%R3@^X{-V^V@^WM`I;$Sww0aW|Fcd%K{LE|1##L|o5C2;{O@GbF{cn`dn z0BCKSvSLjx$IHOO@#=UO$@`Uf54}762+FV&`n4HThv)Z~{rJGA%f5Sw*9)%~9*(kf zc0-y)+MV_m{(j@_8DHP>>s$W&CnU*gs_763@$QqPtlJiKRaRL&tI%um^QCLt&SEhK z9>BnJz|CgbTf%RAxq%fT@3pKY$m5bb#QU({O}tE81LJ~?gxfQ*(pFe3+)sSHa<{rbtEs9MnL(3&@ zvUktzjwouMt4(+0=ygBF`L*E(% zn#rZE*FDYUFIMWlC;^H^8jV3LD{RNhPq$Q4)n#CS-N1q^v&x~UY_&V_Ec;K0dm~?% z$EEfvqU-5u7fs!1ZiGZJVzWEW#q4~1srX(trRk`MZYOW_5U7Ug>;q{p1oCx4`eQ!8G-HdKPS^tKyhB z%GfNAoO-g{j`=p+0QUh{2hSg*1@>Aa33O4I9ecJxJn5yZg4>Jq(y;H{qM2_ET0X{nK+R7+llkJKM(^_yd^Gt36PhH&P&xj})2M%7D&6=`T!{GOHP z>Tq`a4EDie&Y79RsH!o=TNc^F&IxL6qA>NDNUb2J$y^vVIV!w%n%HB+`XBp8tF*CFy-d- z^sMP$^~v;+N_^~nGcq*U6K2EwUUyH(o=P=M<`|?hTkw8FPS*H2v$(YOAds1R1U3>j z{ETQ#F0ksGO06nd(=v+zsfpIx3K9}mnSGf4^;4aNa^a&{Vx;<-*sF?}cvG~$Ty1|f z>x&`weUjw^SMd*3N2Us`H>Y02YPL2hVff1#M1G4?`V9Bt%3U*PNAdNH(ucFuBw?1j zMEAXos%X);J$`R{Llt1$#PwA7#HhE~Z4+9(<&8JE4zNnsYOW=gikjYi_D8cv%_m?? zOOv%rgUJ}^YG$FrHQ z-jGtKkEADPrva(5}uf8`HxI3V*p zlH7bm{EiXV!xpC<+ctm zI@d#82P)jRH-e6oI^Y2*VYn(nMu{@34;Xb{zH_6^juMS$GxnXNCKc@fTxp+03|`~2 zz5Ovh|A6mb?B%kg<6~NWm+M}B*%&?s@aGC+=WG)F1L*?_BJa~}0WNUi*HwNmgpuH( z-4+0cJ?eCiFxqXgq?8bcKjn$#WF5wUxu7M8!P2H!6vzTqS>yvQj6jsR+#syRf~kZ% zE_F!LU3xD>>s&q5DLXsKZlGP+E9R%I`9LBn2em-}?1|gTjulAU!i-h|6FxE3xvc(B zWFjQu+oQ#I3QhrXp-d?ZSrs)wQk>1&5;U9DV=vJgw+F84AnV}4;W*#q$>5~ZQA4RWd;V){huI8BekqmbcClwX2&S1YQYWD{&wJdw{ScSS7j_UX-6AL z!1Iy$YnIELy!iIb3M1)LD5qN}SFdq)SA0|)tBUT+4)V!V90@AoFSV6PeR6(%|NXHe z)KXSDvk8!LJ0xWV7FYG3{q~Ma;wSgIA%t;2ADi`vGz^MME>WiQ8>2s}DB-5!B>d`a zs5tm)(b$x{-0I}?WEETDFmo^Ws+x$2G^JS?Ec4oDrrhS1{pvOYr%a~irJ#Q`Ws%oV ztnIHCcA2zrF@d^Zm?V)i^G@?XMn4L%UwI6ba2mj6V&QpPl!IUGX_dECPPK`V@teSB z{aC&fKZ58wveCq%@iA!Kt0l-n1MislA4aptNREfY(GdXR{T}Z_1B0SQZqXdW-7kh> znoYDyV08jwpX7Cs0VLisbH#J{TFBcl5QFT<7&(W%k94=k$j8FF=Lb(+^`r4EXJCzO ziPj?^HR|L+PX%f}fym#R4YN-pBM`96e0>J8k|i^2EHfVkEu{wraHlP|Fst3z!mfdP z*o=5x_UDn5ENk(QWpU5qJ{vWBb{s#B^XdLqyWokKum{G=KaH0;F4)8DW%xI@VOU^N zf(pcvD-DgpW^9N~svDhR2qMK1_<*>fzl9Ms=mAa^pd&ewUOBvcuoHvecDD2Hlvytf?Y7n)9?Z^#7d*wZ_rEh84 z_IWMPx5qN5&NXI}GI>}aVJkW|O%0_*jrcEh5OJ6gkOhPzz0tg-&&o6U`1*9`KWR@qR1pd8Oss@b8# z=;Je#w9&8;`mnnL-4>p$(kl9dKC8yZSbO53N{Ak2LnH06{UjW zCSg?El}Y)Q0}g0XK<^PcWdcvN*plz0O_r$~>oLGyVZo+{9qPUY_lS}t z?yJ?IuSrZ<^%SC-5pi7S_D1B7=@wxtsS3cZowu)5|_s+h!WDvIGdc@r?=9 zI-h2pV5CvBw?&Bs3+c=ijTdk9cVm59<(W0Rtt@s>J4p7_3k1T=+>KF8brPiqX=Wlx z8dVRYwGa)YVAm!?%EZzf8D>~(v3e4j%4O3nR<2e7k!-&1)#Tjt5mcgV z`g8ca*Ij$_@Nvgo3aSz|^(r)dtXP4VHg4&J+`WvK7$s=F%FPv={s6@~6hZ3mpZD!% zbIf8&rq4U>l1WYTJIhqihK-;cHHVFU&MQ~lu>o(-w=ssn+BLrP~{H7XshN;w0G~}N3Dn%%G}Q^GDgv;CV0OK1+TKOI5WE_wRWoflTpIW^rBg=_8R zcgU7`atd)wTY;?ppvsd}8xDs)FYgq+lJk`7V9HXX;jk_Y%`l{9g;^CeA=_&X+1#(F zdrZ0^2uKRLS>JmTeliNzUG%r`fWeZ8Ikfd4)Je1lBq*9rMETyD3NNTD2WnWeGUw&{ zH@^nV9(H{*8LRfJ=jg0P(W1wuiN|!3NJEa zm)(Tnh&4N5jQE;=o%D%n%XM*zDpOGnab zo4�{O&x%DYUnx;_ML?$q^YN}kmZDi6I*9coPOd13-kD7?CZ%_st+9He#~r@@-F ze(N9=TF!-*6B%`2{MVM3)pd@;t_?cl=u-3W;DW9S{VIDH=o<@cbs=;gPgn|A0^i+` z9;;q=qxm!|^UB7?Y|UA-CbmDkO~7gvMVZSj#y5h^B->fDHhEvLl`vC#wHkqv#aohLE@4i5)H8k4N8_5-@0+% zGRv&XUfUJ0XiF}s3-QD)A_|8lpfAQ8eqjuFfm=0n_c#T_#qtT4x>2EM%Yh4X9>#-p zsd~Bc>%#;d1E0qK^*jIDSG!(3GXM44{r~zk@Hz1gS__uhV~rUr@l!K)i)jANUaGmd z5DRPJHek|;j?F$PcR@{rF(e19F>qtq)>Dq&ai`Q{##O7vc1dJrV$T2i)&BmI{pS08 zd>^*Q5T(~g?a^^3wJ2rZonUSOgttY1CNP0NEA`{e8^?W zZ7*}jfYsV!Yp+E(G=F4gPeMy;j{BBLU5NmmiFYs->sH7OdsllAGitD0SF)!P%K=Pa z8eV}%B7i&bHSsO+47|yku{;c5K+z0iWxq)kgZmz>k-I z_iC>X`*h`lBDxBQaC1Dzuu4mm2Pvqs(DeG;emm^DiR-d?8D_?yci~!i zUzu9dYcF3}6u1^}>rRyOnd<|G_OEsUb)RFz%$*q- zM)SrnRUpUXt%ZUylSV-*>2Qc=1;fuAKMwokc({*>Osc}Re0Q?;CPZLld|onF><*su z#IRY$rp)!~Q#a1P3tB1eP2N8n{tUaw{HKSEFm2pMrPLCk5n+)Q^{UIYCJbp2c86u= zmokY7`5Er52*)3q4{q;S{Y~QF?`{9i{UVS7JZ;@`-B&(Gsiwk|-_8sGjum4&S1lC+ zRJ#RUm^y#V(P6loM!TOE!d5cn5H7kRV%OVsboSu|{h%cmha|!}XP&z(b2aFnrcTQ~ zEgwAr1{nSH`yqCjT`U)q67%vD;%0D;Ts8y4;knFZCGA8NnT2YaU|b!U3& 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

H)Mg*wl){_@Sd$nYZm_- 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 z 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

f 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 z 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