Bug 1863255 - Update to libjxl d3a69dbeef78f036969a2500f949f931df857e17 r=tnikkel

Somehow the tests are down for mingw builds with the latest commit 9487d3ab76, so this only updates to a slightly newer commit.

Differential Revision: https://phabricator.services.mozilla.com/D192872
This commit is contained in:
Kagami Sascha Rosylight 2023-11-07 07:56:53 +00:00
parent 3c2f4aaaa5
commit d8f8727b8d
31 changed files with 1083 additions and 1141 deletions

View File

@ -10,9 +10,9 @@ origin:
url: https://github.com/libjxl/libjxl
release: 4c23a53dde8840884f424bcca7c60947235fb421 (2023-10-16T11:08:25Z).
release: d3a69dbeef78f036969a2500f949f931df857e17 (2023-10-18T13:56:09Z).
revision: 4c23a53dde8840884f424bcca7c60947235fb421
revision: d3a69dbeef78f036969a2500f949f931df857e17
license: Apache-2.0

View File

@ -5,8 +5,8 @@
# 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.
# workflows during the continuous integration build, as well as from the
# command line for developers.
set -eu
@ -84,8 +84,8 @@ if [[ ! -z "${HWY_BASELINE_TARGETS}" ]]; then
fi
# Version inferred from the CI variables.
CI_COMMIT_SHA=${CI_COMMIT_SHA:-${GITHUB_SHA:-}}
JPEGXL_VERSION=${JPEGXL_VERSION:-${CI_COMMIT_SHA:0:8}}
CI_COMMIT_SHA=${GITHUB_SHA:-}
JPEGXL_VERSION=${JPEGXL_VERSION:-}
# Benchmark parameters
STORE_IMAGES=${STORE_IMAGES:-1}
@ -182,27 +182,6 @@ 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
@ -227,29 +206,23 @@ merge_request_commits() {
# 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 -C "${MYDIR}" rev-parse HEAD)
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
MR_HEAD_SHA="$(git rev-parse "FETCH_HEAD^2" 2>/dev/null ||
echo "${GITHUB_SHA}")"
else
MR_HEAD_SHA="${GITHUB_SHA}"
fi
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}")
MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "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
if [[ -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.
# We are in a local branch, not a pull request workflow.
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true)
fi
@ -268,40 +241,6 @@ merge_request_commits() {
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: <https://gitlab.com/wg1/jpeg-xlm/merge_requests/123456>
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() {
@ -947,15 +886,7 @@ run_benchmark() {
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.
@ -1183,18 +1114,6 @@ cmd_arm_benchmark() {
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.

View File

@ -48,12 +48,12 @@ Status VerifyInput(const PackedPixelFile& ppf) {
return true;
}
Status GetColorEncoding(const PackedPixelFile& ppf, const JxlCmsInterface* cms,
Status GetColorEncoding(const PackedPixelFile& ppf,
ColorEncoding* color_encoding) {
if (!ppf.icc.empty()) {
IccBytes icc;
icc.assign(ppf.icc.data(), ppf.icc.data() + ppf.icc.size());
JXL_RETURN_IF_ERROR(color_encoding->SetICC(std::move(icc), cms));
IccBytes icc = ppf.icc;
JXL_RETURN_IF_ERROR(
color_encoding->SetICC(std::move(icc), JxlGetDefaultCms()));
} else {
JXL_RETURN_IF_ERROR(color_encoding->FromExternal(ppf.color_encoding));
}
@ -325,12 +325,10 @@ Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
}
JXL_RETURN_IF_ERROR(VerifyInput(ppf));
const JxlCmsInterface& cms = *JxlGetDefaultCms();
ColorEncoding color_encoding;
JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &cms, &color_encoding));
JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding));
ColorSpaceTransform c_transform(cms);
ColorSpaceTransform c_transform(*JxlGetDefaultCms());
ColorEncoding xyb_encoding;
if (jpeg_settings.xyb) {
if (ppf.info.num_color_channels != 3) {
@ -343,8 +341,7 @@ Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
JXL_RETURN_IF_ERROR(
c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1));
xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB);
JXL_RETURN_IF_ERROR(
xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual));
xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual);
JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC());
}
const ColorEncoding& output_encoding =

View File

@ -21,7 +21,7 @@ Status HlgOOTF(ImageBundle* ib, const float gamma, ThreadPool* pool) {
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
JXL_RETURN_IF_ERROR(
ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));

View File

@ -82,7 +82,7 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
ppf.info.exponent_bits_per_sample);
}
const bool is_gray = ppf.info.num_color_channels == 1;
const bool is_gray = (ppf.info.num_color_channels == 1);
JXL_ASSERT(ppf.info.num_color_channels == 1 ||
ppf.info.num_color_channels == 3);
@ -113,11 +113,15 @@ Status ConvertPackedPixelFileToCodecInOut(const PackedPixelFile& ppf,
// Convert the color encoding.
if (!ppf.icc.empty()) {
IccBytes icc = ppf.icc;
const JxlCmsInterface& cms = *JxlGetDefaultCms();
if (!io->metadata.m.color_encoding.SetICC(std::move(icc), &cms)) {
if (!io->metadata.m.color_encoding.SetICC(std::move(icc),
JxlGetDefaultCms())) {
fprintf(stderr, "Warning: error setting ICC profile, assuming SRGB\n");
io->metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
} else {
if (io->metadata.m.color_encoding.IsCMYK()) {
// We expect gray or tri-color.
return JXL_FAILURE("Embedded ICC is CMYK");
}
if (io->metadata.m.color_encoding.IsGray() != is_gray) {
// E.g. JPG image has 3 channels, but gray ICC.
return JXL_FAILURE("Embedded ICC does not match image color type");

View File

@ -33,7 +33,7 @@ Status ToneMapFrame(const std::pair<float, float> display_nits,
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
JXL_RETURN_IF_ERROR(
ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));
@ -71,7 +71,7 @@ Status GamutMapFrame(ImageBundle* const ib, float preserve_saturation,
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
JXL_RETURN_IF_ERROR(linear_rec2020.SetPrimariesType(Primaries::k2100));
JXL_RETURN_IF_ERROR(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_RETURN_IF_ERROR(linear_rec2020.CreateICC());
JXL_RETURN_IF_ERROR(
ib->TransformTo(linear_rec2020, *JxlGetDefaultCms(), pool));

View File

@ -19,7 +19,7 @@ static void BM_ToneMapping(benchmark::State& state) {
linear_rec2020.SetColorSpace(ColorSpace::kRGB);
JXL_CHECK(linear_rec2020.SetPrimariesType(Primaries::k2100));
JXL_CHECK(linear_rec2020.SetWhitePointType(WhitePoint::kD65));
linear_rec2020.tf.SetTransferFunction(TransferFunction::kLinear);
linear_rec2020.Tf().SetTransferFunction(TransferFunction::kLinear);
JXL_CHECK(linear_rec2020.CreateICC());
for (auto _ : state) {

View File

@ -6,14 +6,34 @@
#ifndef LIB_JXL_CMS_COLOR_ENCODING_CMS_H_
#define LIB_JXL_CMS_COLOR_ENCODING_CMS_H_
#include <jxl/cms_interface.h>
#include <jxl/color_encoding.h>
#include <jxl/types.h>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include "lib/jxl/base/common.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/cms/color_management.h"
namespace jxl {
namespace cms {
using IccBytes = std::vector<uint8_t>;
// Returns whether the two inputs are approximately equal.
static inline bool ApproxEq(const double a, const double b,
double max_l1 = 1E-3) {
// 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;
}
// (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
@ -91,35 +111,220 @@ struct PrimariesCIExy {
// Serializable form of CIExy.
struct Customxy {
int32_t x;
int32_t y;
static constexpr uint32_t kMul = 1000000;
static constexpr double kRoughLimit = 4.0;
static constexpr int32_t kMin = -0x200000;
static constexpr int32_t kMax = 0x1FFFFF;
int32_t x = 0;
int32_t y = 0;
CIExy GetValue() const {
CIExy xy;
xy.x = x * (1.0 / kMul);
xy.y = y * (1.0 / kMul);
return xy;
}
Status SetValue(const CIExy& xy) {
bool ok = (std::abs(xy.x) < kRoughLimit) && (std::abs(xy.y) < kRoughLimit);
if (!ok) return JXL_FAILURE("X or Y is out of bounds");
x = static_cast<int32_t>(roundf(xy.x * kMul));
if (x < kMin || x > kMax) return JXL_FAILURE("X is out of bounds");
y = static_cast<int32_t>(roundf(xy.y * kMul));
if (y < kMin || y > kMax) return JXL_FAILURE("Y is out of bounds");
return true;
}
bool IsSame(const Customxy& other) const {
return (x == other.x) && (y == other.y);
}
};
static inline Status WhitePointFromExternal(const JxlWhitePoint external,
WhitePoint* out) {
switch (external) {
case JXL_WHITE_POINT_D65:
*out = WhitePoint::kD65;
return true;
case JXL_WHITE_POINT_CUSTOM:
*out = WhitePoint::kCustom;
return true;
case JXL_WHITE_POINT_E:
*out = WhitePoint::kE;
return true;
case JXL_WHITE_POINT_DCI:
*out = WhitePoint::kDCI;
return true;
}
return JXL_FAILURE("Invalid WhitePoint enum value %d",
static_cast<int>(external));
}
static inline Status PrimariesFromExternal(const JxlPrimaries external,
Primaries* out) {
switch (external) {
case JXL_PRIMARIES_SRGB:
*out = Primaries::kSRGB;
return true;
case JXL_PRIMARIES_CUSTOM:
*out = Primaries::kCustom;
return true;
case JXL_PRIMARIES_2100:
*out = Primaries::k2100;
return true;
case JXL_PRIMARIES_P3:
*out = Primaries::kP3;
return true;
}
return JXL_FAILURE("Invalid Primaries enum value");
}
static inline Status RenderingIntentFromExternal(
const JxlRenderingIntent external, RenderingIntent* out) {
switch (external) {
case JXL_RENDERING_INTENT_PERCEPTUAL:
*out = RenderingIntent::kPerceptual;
return true;
case JXL_RENDERING_INTENT_RELATIVE:
*out = RenderingIntent::kRelative;
return true;
case JXL_RENDERING_INTENT_SATURATION:
*out = RenderingIntent::kSaturation;
return true;
case JXL_RENDERING_INTENT_ABSOLUTE:
*out = RenderingIntent::kAbsolute;
return true;
}
return JXL_FAILURE("Invalid RenderingIntent enum value");
}
struct CustomTransferFunction {
// Highest reasonable value for the gamma of a transfer curve.
static constexpr uint32_t kMaxGamma = 8192;
static constexpr uint32_t kGammaMul = 10000000;
bool have_gamma;
bool have_gamma = false;
// OETF exponent to go from linear to gamma-compressed.
uint32_t gamma; // Only used if have_gamma_.
uint32_t gamma = 0; // Only used if have_gamma_.
// Can be kUnknown.
TransferFunction transfer_function; // Only used if !have_gamma_.
TransferFunction transfer_function =
TransferFunction::kSRGB; // Only used if !have_gamma_.
TransferFunction GetTransferFunction() const {
JXL_ASSERT(!have_gamma);
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);
}
double GetGamma() const {
JXL_ASSERT(have_gamma);
return gamma * (1.0 / kGammaMul); // (0, 1)
}
Status SetGamma(double new_gamma) {
if (new_gamma < (1.0 / kMaxGamma) || new_gamma > 1.0) {
return JXL_FAILURE("Invalid gamma %f", new_gamma);
}
have_gamma = false;
if (ApproxEq(new_gamma, 1.0)) {
transfer_function = TransferFunction::kLinear;
return true;
}
if (ApproxEq(new_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(new_gamma * kGammaMul);
transfer_function = TransferFunction::kUnknown;
return true;
}
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;
}
};
static inline 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");
}
// Compact encoding of data required to interpret and translate pixels to a
// known color space. Stored in Metadata. Thread-compatible.
struct ColorEncoding {
// Only valid if HaveFields()
WhitePoint white_point;
Primaries primaries; // Only valid if HasPrimaries()
RenderingIntent rendering_intent;
// 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;
WhitePoint white_point = WhitePoint::kD65;
Primaries primaries = Primaries::kSRGB; // Only valid if HasPrimaries()
RenderingIntent rendering_intent = RenderingIntent::kRelative;
// 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
@ -128,7 +333,7 @@ struct ColorEncoding {
IccBytes icc; // Valid ICC profile
ColorSpace color_space; // Can be kUnknown
ColorSpace color_space = ColorSpace::kRGB; // Can be kUnknown
bool cmyk = false;
// "late sync" fields
@ -137,8 +342,430 @@ struct ColorEncoding {
Customxy red; // Only used if primaries == kCustom
Customxy green; // Only used if primaries == kCustom
Customxy blue; // Only used if primaries == kCustom
// Returns false if the field is invalid and unusable.
bool HasPrimaries() const {
return (color_space != ColorSpace::kGray) &&
(color_space != ColorSpace::kXYB);
}
size_t Channels() const { return (color_space == ColorSpace::kGray) ? 1 : 3; }
PrimariesCIExy GetPrimaries() const {
JXL_DASSERT(have_fields);
JXL_ASSERT(HasPrimaries());
PrimariesCIExy xy;
switch (primaries) {
case Primaries::kCustom:
xy.r = red.GetValue();
xy.g = green.GetValue();
xy.b = blue.GetValue();
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_UNREACHABLE("Invalid Primaries %u", static_cast<uint32_t>(primaries));
}
Status 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.SetValue(xy.r));
JXL_RETURN_IF_ERROR(green.SetValue(xy.g));
JXL_RETURN_IF_ERROR(blue.SetValue(xy.b));
return true;
}
CIExy GetWhitePoint() const {
JXL_DASSERT(have_fields);
CIExy xy;
switch (white_point) {
case WhitePoint::kCustom:
return white.GetValue();
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_UNREACHABLE("Invalid WhitePoint %u",
static_cast<uint32_t>(white_point));
}
Status 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.SetValue(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.IsSame(other.white)) {
return false;
}
}
if (HasPrimaries() != other.HasPrimaries()) return false;
if (HasPrimaries()) {
if (primaries != other.primaries) return false;
if (primaries == Primaries::kCustom) {
if (!red.IsSame(other.red)) return false;
if (!green.IsSame(other.green)) return false;
if (!blue.IsSame(other.blue)) 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);
}
Status CreateICC() {
icc.clear();
return MaybeCreateProfile(*this, &icc);
}
// Returns true if all fields have been initialized (possibly to kUnknown).
// Returns false if the ICC profile is invalid or decoding it fails.
Status SetFieldsFromICC(IccBytes&& new_icc, const JxlCmsInterface& cms) {
// TODO(eustas): take icc
// TODO(eustas): clean icc on error
// In case parsing fails, mark the ColorEncoding as invalid.
JXL_ASSERT(!new_icc.empty());
color_space = ColorSpace::kUnknown;
tf.transfer_function = TransferFunction::kUnknown;
icc.clear();
JxlColorEncoding external;
JXL_BOOL new_cmyk;
JXL_RETURN_IF_ERROR(cms.set_fields_from_icc(cms.set_fields_data,
new_icc.data(), new_icc.size(),
&external, &new_cmyk));
cmyk = new_cmyk;
if (cmyk) return true;
JXL_RETURN_IF_ERROR(FromExternal(external));
icc = std::move(new_icc);
return true;
}
void ToExternal(JxlColorEncoding* external) const {
// TODO(eustas): update copy/update storage and call .ToExternal on it
if (!have_fields) {
external->color_space = JXL_COLOR_SPACE_UNKNOWN;
external->primaries = JXL_PRIMARIES_CUSTOM;
external->rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; //?
external->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
external->white_point = JXL_WHITE_POINT_CUSTOM;
return;
}
external->color_space = static_cast<JxlColorSpace>(color_space);
external->white_point = static_cast<JxlWhitePoint>(white_point);
CIExy wp = GetWhitePoint();
external->white_point_xy[0] = wp.x;
external->white_point_xy[1] = wp.y;
if (external->color_space == JXL_COLOR_SPACE_RGB ||
external->color_space == JXL_COLOR_SPACE_UNKNOWN) {
external->primaries = static_cast<JxlPrimaries>(primaries);
PrimariesCIExy p = GetPrimaries();
external->primaries_red_xy[0] = p.r.x;
external->primaries_red_xy[1] = p.r.y;
external->primaries_green_xy[0] = p.g.x;
external->primaries_green_xy[1] = p.g.y;
external->primaries_blue_xy[0] = p.b.x;
external->primaries_blue_xy[1] = p.b.y;
}
if (tf.have_gamma) {
external->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
external->gamma = tf.GetGamma();
} else {
external->transfer_function =
static_cast<JxlTransferFunction>(tf.GetTransferFunction());
external->gamma = 0;
}
external->rendering_intent =
static_cast<JxlRenderingIntent>(rendering_intent);
}
Status FromExternal(const JxlColorEncoding& external) {
// TODO(eustas): update non-serializable on call-site
color_space = static_cast<ColorSpace>(external.color_space);
JXL_RETURN_IF_ERROR(
WhitePointFromExternal(external.white_point, &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(SetWhitePoint(wp));
}
if (external.color_space == JXL_COLOR_SPACE_RGB ||
external.color_space == JXL_COLOR_SPACE_UNKNOWN) {
JXL_RETURN_IF_ERROR(
PrimariesFromExternal(external.primaries, &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(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);
}
this->tf = tf;
JXL_RETURN_IF_ERROR(RenderingIntentFromExternal(external.rendering_intent,
&rendering_intent));
// The ColorEncoding caches an ICC profile it created earlier that may no
// longer match the profile with the changed fields, so re-create it.
if (!(CreateICC())) {
// This is not an error: for example, it doesn't have ICC profile creation
// implemented for XYB. This should not be returned as error, since
// FromExternal still worked correctly, and what
// matters is that internal->ICC() will not return the wrong profile.
}
return true;
}
};
// These strings are baked into Description - do not change.
static inline 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_UNREACHABLE("Invalid ColorSpace %u", static_cast<uint32_t>(color_space));
}
static inline 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_UNREACHABLE("Invalid WhitePoint %u", static_cast<uint32_t>(white_point));
}
static inline 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_UNREACHABLE("Invalid Primaries %u", static_cast<uint32_t>(primaries));
}
static inline 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_UNREACHABLE("Invalid TransferFunction %u",
static_cast<uint32_t>(transfer_function));
}
static inline 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_UNREACHABLE("Invalid RenderingIntent %u",
static_cast<uint32_t>(rendering_intent));
}
// Returns a representation of the ColorEncoding fields (not icc).
// Example description: "RGB_D65_SRG_Rel_Lin"
static inline std::string Description(const ColorEncoding& c) {
std::string d = ToString(c.color_space);
bool explicit_wp_tf = (c.color_space != ColorSpace::kXYB);
if (explicit_wp_tf) {
d += '_';
if (c.white_point == WhitePoint::kCustom) {
const CIExy wp = c.GetWhitePoint();
d += jxl::ToString(wp.x) + ';';
d += jxl::ToString(wp.y);
} else {
d += ToString(c.white_point);
}
}
if (c.HasPrimaries()) {
d += '_';
if (c.primaries == Primaries::kCustom) {
const PrimariesCIExy pr = c.GetPrimaries();
d += jxl::ToString(pr.r.x) + ';';
d += jxl::ToString(pr.r.y) + ';';
d += jxl::ToString(pr.g.x) + ';';
d += jxl::ToString(pr.g.y) + ';';
d += jxl::ToString(pr.b.x) + ';';
d += jxl::ToString(pr.b.y);
} else {
d += ToString(c.primaries);
}
}
d += '_';
d += ToString(c.rendering_intent);
if (explicit_wp_tf) {
d += '_';
if (c.tf.have_gamma) {
d += 'g';
d += jxl::ToString(c.tf.GetGamma());
} else {
d += ToString(c.tf.transfer_function);
}
}
return d;
}
} // namespace cms
} // namespace jxl

View File

@ -12,6 +12,18 @@
#include <string>
#include <vector>
#include "lib/jxl/cms/color_encoding_cms.h"
using jxl::cms::CIExy;
using jxl::cms::ColorEncoding;
using jxl::cms::ColorSpace;
using jxl::cms::IccBytes;
using jxl::cms::Primaries;
using jxl::cms::PrimariesCIExy;
using jxl::cms::RenderingIntent;
using jxl::cms::TransferFunction;
using jxl::cms::WhitePoint;
#undef HWY_TARGET_INCLUDE
#define HWY_TARGET_INCLUDE "lib/jxl/cms/color_management.cc"
#include <hwy/foreach_target.h>
@ -35,12 +47,11 @@ namespace HWY_NAMESPACE {
Status ToneMapPixel(const ColorEncoding& c, const float in[3],
uint8_t pcslab_out[3]) {
const PrimariesCIExy primaries = c.GetPrimaries();
const CIExy white_point = c.GetWhitePoint();
const PrimariesCIExy p = c.GetPrimaries();
const CIExy wp = c.GetWhitePoint();
float primaries_XYZ[9];
JXL_RETURN_IF_ERROR(PrimariesToXYZ(
primaries.r.x, primaries.r.y, primaries.g.x, primaries.g.y, primaries.b.x,
primaries.b.y, white_point.x, white_point.y, primaries_XYZ));
JXL_RETURN_IF_ERROR(PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y,
wp.x, wp.y, primaries_XYZ));
const float luminances[3] = {primaries_XYZ[3], primaries_XYZ[4],
primaries_XYZ[5]};
float linear[3];
@ -71,7 +82,7 @@ Status ToneMapPixel(const ColorEncoding& c, const float in[3],
StoreU(b, d, &linear[2]);
float chad[9];
JXL_RETURN_IF_ERROR(AdaptToXYZD50(white_point.x, white_point.y, chad));
JXL_RETURN_IF_ERROR(AdaptToXYZD50(wp.x, wp.y, chad));
float to_xyzd50[9];
Mul3x3Matrix(chad, primaries_XYZ, to_xyzd50);
@ -176,14 +187,13 @@ bool CanToneMap(const ColorEncoding& encoding) {
// If the color space cannot be represented by a CICP tag in the ICC profile
// then the rest of the profile must unambiguously identify it; we have less
// freedom to do use it for tone mapping.
return encoding.GetColorSpace() == ColorSpace::kRGB &&
encoding.HasPrimaries() &&
return encoding.color_space == ColorSpace::kRGB && encoding.HasPrimaries() &&
(encoding.tf.IsPQ() || encoding.tf.IsHLG()) &&
((encoding.GetPrimariesType() == Primaries::kP3 &&
(encoding.GetWhitePointType() == WhitePoint::kD65 ||
encoding.GetWhitePointType() == WhitePoint::kDCI)) ||
(encoding.GetPrimariesType() != Primaries::kCustom &&
encoding.GetWhitePointType() == WhitePoint::kD65));
((encoding.primaries == Primaries::kP3 &&
(encoding.white_point == WhitePoint::kD65 ||
encoding.white_point == WhitePoint::kDCI)) ||
(encoding.primaries != Primaries::kCustom &&
encoding.white_point == WhitePoint::kD65));
}
void ICCComputeMD5(const IccBytes& data, uint8_t sum[16])
@ -335,9 +345,9 @@ Status CreateICCHeader(const ColorEncoding& c, IccBytes* JXL_RESTRICT header) {
WriteICCTag(kCmm, 4, header);
WriteICCUint32(0x04400000u, 8, header);
const char* profile_type =
c.GetColorSpace() == ColorSpace::kXYB ? "scnr" : "mntr";
c.color_space == ColorSpace::kXYB ? "scnr" : "mntr";
WriteICCTag(profile_type, 12, header);
WriteICCTag(c.IsGray() ? "GRAY" : "RGB ", 16, header);
WriteICCTag(c.color_space == ColorSpace::kGray ? "GRAY" : "RGB ", 16, header);
if (kEnable3DToneMapping && CanToneMap(c)) {
// We are going to use a 3D LUT for tone mapping, which will be more compact
// with an 8-bit LUT to CIELAB than with a 16-bit LUT to XYZ. 8-bit XYZ
@ -366,7 +376,7 @@ Status CreateICCHeader(const ColorEncoding& c, IccBytes* JXL_RESTRICT header) {
WriteICCUint32(0, 52, header); // device model
WriteICCUint32(0, 56, header); // device attributes
WriteICCUint32(0, 60, header); // device attributes
WriteICCUint32(static_cast<uint32_t>(c.GetRenderingIntent()), 64, header);
WriteICCUint32(static_cast<uint32_t>(c.rendering_intent), 64, header);
// Mandatory D50 white point of profile connection space
WriteICCUint32(0x0000f6d6, 68, header);
@ -434,25 +444,25 @@ void MaybeCreateICCCICPTag(const ColorEncoding& c, IccBytes* JXL_RESTRICT tags,
size_t* offset, size_t* size,
IccBytes* JXL_RESTRICT tagtable,
std::vector<size_t>* offsets) {
if (c.GetColorSpace() != ColorSpace::kRGB) {
if (c.color_space != ColorSpace::kRGB) {
return;
}
uint8_t primaries = 0;
if (c.GetPrimariesType() == Primaries::kP3) {
if (c.GetWhitePointType() == WhitePoint::kD65) {
if (c.primaries == Primaries::kP3) {
if (c.white_point == WhitePoint::kD65) {
primaries = 12;
} else if (c.GetWhitePointType() == WhitePoint::kDCI) {
} else if (c.white_point == WhitePoint::kDCI) {
primaries = 11;
} else {
return;
}
} else if (c.GetPrimariesType() != Primaries::kCustom &&
c.GetWhitePointType() == WhitePoint::kD65) {
primaries = static_cast<uint8_t>(c.GetPrimariesType());
} else if (c.primaries != Primaries::kCustom &&
c.white_point == WhitePoint::kD65) {
primaries = static_cast<uint8_t>(c.primaries);
} else {
return;
}
if (c.tf.IsUnknown() || c.tf.IsGamma()) {
if (c.tf.IsUnknown() || c.tf.have_gamma) {
return;
}
WriteICCTag("cicp", tags->size(), tags);
@ -662,25 +672,112 @@ Status CreateICCNoOpBToATag(IccBytes* JXL_RESTRICT tags) {
} // namespace
Status PrimariesToXYZ(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]) {
bool ok = (wx >= 0) && (wx <= 1) && (wy > 0) && (wy <= 1);
if (!ok) {
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];
Mul3x3Vector(primaries_inv, w, xyz);
float a[9] = {
xyz[0], 0, 0, 0, xyz[1], 0, 0, 0, xyz[2],
};
Mul3x3Matrix(primaries, a, matrix);
return true;
}
/* Chromatic adaptation matrices*/
constexpr float kBradford[9] = {
0.8951f, 0.2664f, -0.1614f, -0.7502f, 1.7135f,
0.0367f, 0.0389f, -0.0685f, 1.0296f,
};
constexpr 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]) {
bool ok = (wx >= 0) || (wx <= 1) || (wy > 0) || (wy <= 1);
if (!ok) {
// 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];
Mul3x3Vector(kBradford, w, lms);
Mul3x3Vector(kBradford, w50, lms50);
if (lms[0] == 0 || lms[1] == 0 || lms[2] == 0) {
return JXL_FAILURE("Invalid white point");
}
float a[9] = {
// /----> 0, 1, 2, 3, /----> 4, 5, 6, 7, /----> 8,
lms50[0] / lms[0], 0, 0, 0, lms50[1] / lms[1], 0, 0, 0, lms50[2] / lms[2],
};
if (!std::isfinite(a[0]) || !std::isfinite(a[4]) || !std::isfinite(a[8])) {
return JXL_FAILURE("Invalid white point");
}
float b[9];
Mul3x3Matrix(a, kBradford, b);
Mul3x3Matrix(kBradfordInv, b, matrix);
return true;
}
Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]) {
float toXYZ[9];
JXL_RETURN_IF_ERROR(PrimariesToXYZ(rx, ry, gx, gy, bx, by, wx, wy, toXYZ));
float d50[9];
JXL_RETURN_IF_ERROR(AdaptToXYZD50(wx, wy, d50));
Mul3x3Matrix(d50, toXYZ, matrix);
return true;
}
Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
IccBytes header, tagtable, tags;
if (c.GetColorSpace() == ColorSpace::kUnknown || c.tf.IsUnknown()) {
if (c.color_space == ColorSpace::kUnknown || c.tf.IsUnknown()) {
return false; // Not an error
}
switch (c.GetColorSpace()) {
switch (c.color_space) {
case ColorSpace::kRGB:
case ColorSpace::kGray:
case ColorSpace::kXYB:
break; // OK
default:
return JXL_FAILURE("Invalid CS %u",
static_cast<unsigned int>(c.GetColorSpace()));
static_cast<unsigned int>(c.color_space));
}
if (c.GetColorSpace() == ColorSpace::kXYB &&
c.GetRenderingIntent() != RenderingIntent::kPerceptual) {
if (c.color_space == ColorSpace::kXYB &&
c.rendering_intent != RenderingIntent::kPerceptual) {
return JXL_FAILURE(
"Only perceptual rendering intent implemented for XYB "
"ICC profile.");
@ -704,7 +801,7 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
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()) {
if (c.color_space == ColorSpace::kGray) {
float wtpt[3];
JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(c.GetWhitePoint(), wtpt));
JXL_RETURN_IF_ERROR(CreateICCXYZTag(wtpt, &tags));
@ -715,7 +812,7 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
FinalizeICCTag(&tags, &tag_offset, &tag_size);
AddToICCTagTable("wtpt", tag_offset, tag_size, &tagtable, &offsets);
if (!c.IsGray()) {
if (c.color_space != ColorSpace::kGray) {
// Chromatic adaptation matrix
float chad[9];
JXL_RETURN_IF_ERROR(CreateICCChadMatrix(c.GetWhitePoint(), chad));
@ -725,7 +822,7 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
AddToICCTagTable("chad", tag_offset, tag_size, &tagtable, &offsets);
}
if (c.GetColorSpace() == ColorSpace::kRGB) {
if (c.color_space == ColorSpace::kRGB) {
MaybeCreateICCCICPTag(c, &tags, &tag_offset, &tag_size, &tagtable,
&offsets);
@ -750,7 +847,7 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
AddToICCTagTable("bXYZ", tag_offset, tag_size, &tagtable, &offsets);
}
if (c.GetColorSpace() == ColorSpace::kXYB) {
if (c.color_space == ColorSpace::kXYB) {
JXL_RETURN_IF_ERROR(CreateICCLutAtoBTagForXYB(&tags));
FinalizeICCTag(&tags, &tag_offset, &tag_size);
AddToICCTagTable("A2B0", tag_offset, tag_size, &tagtable, &offsets);
@ -765,11 +862,11 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
FinalizeICCTag(&tags, &tag_offset, &tag_size);
AddToICCTagTable("B2A0", tag_offset, tag_size, &tagtable, &offsets);
} else {
if (c.tf.IsGamma()) {
if (c.tf.have_gamma) {
float gamma = 1.0 / c.tf.GetGamma();
JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({gamma}, 0, &tags));
} else if (c.GetColorSpace() != ColorSpace::kXYB) {
switch (c.tf.GetTransferFunction()) {
} else if (c.color_space != ColorSpace::kXYB) {
switch (c.tf.transfer_function) {
case TransferFunction::kHLG:
CreateICCCurvCurvTag(HWY_DYNAMIC_DISPATCH(CreateTableCurve)(
64, ExtraTF::kHLG, CanToneMap(c)),
@ -799,12 +896,12 @@ Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc) {
CreateICCCurvParaTag({2.6, 1.0, 0.0, 1.0, 0.0}, 3, &tags));
break;
default:
JXL_UNREACHABLE("Unknown TF %u", static_cast<unsigned int>(
c.tf.GetTransferFunction()));
JXL_UNREACHABLE("Unknown TF %u",
static_cast<unsigned int>(c.tf.transfer_function));
}
}
FinalizeICCTag(&tags, &tag_offset, &tag_size);
if (c.IsGray()) {
if (c.color_space == ColorSpace::kGray) {
AddToICCTagTable("kTRC", tag_offset, tag_size, &tagtable, &offsets);
} else {
AddToICCTagTable("rTRC", tag_offset, tag_size, &tagtable, &offsets);

View File

@ -8,12 +8,12 @@
// ICC profiles and color space conversions.
#include <cstdint>
#include <vector>
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/status.h"
// TODO(eustas): migrate to lib/jxl/cms/color_encoding_cms.h
#include "lib/jxl/color_encoding_internal.h"
namespace jxl {
enum class ExtraTF {
@ -23,11 +23,24 @@ enum class ExtraTF {
kSRGB,
};
namespace cms {
struct ColorEncoding;
struct CIExy;
} // namespace cms
// NOTE: for XYB colorspace, the created profile can be used to transform a
// *scaled* XYB image (created by ScaleXYB()) to another colorspace.
Status MaybeCreateProfile(const ColorEncoding& c, IccBytes* JXL_RESTRICT icc);
Status MaybeCreateProfile(const jxl::cms::ColorEncoding& c,
std::vector<uint8_t>* JXL_RESTRICT icc);
Status CIEXYZFromWhiteCIExy(const CIExy& xy, float XYZ[3]);
Status CIEXYZFromWhiteCIExy(const jxl::cms::CIExy& xy, float XYZ[3]);
Status PrimariesToXYZ(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]);
// Adapts whitepoint x, y to D50
Status AdaptToXYZD50(float wx, float wy, float matrix[9]);
Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]);
} // namespace jxl

View File

@ -49,6 +49,8 @@
namespace jxl {
namespace {
using ::jxl::cms::ColorEncoding;
struct JxlCms {
#if JPEGXL_ENABLE_SKCMS
IccBytes icc_src, icc_dst;
@ -486,7 +488,7 @@ Status IdentifyPrimaries(const skcms_ICCProfile& profile,
void DetectTransferFunction(const skcms_ICCProfile& profile,
ColorEncoding* JXL_RESTRICT c) {
if (c->tf.SetImplicit()) return;
JXL_CHECK(c->color_space != ColorSpace::kXYB);
float gamma[3] = {};
if (profile.has_trc) {
@ -546,12 +548,12 @@ void DetectTransferFunction(const skcms_ICCProfile& profile,
uint32_t Type32(const ColorEncoding& c, bool cmyk) {
if (cmyk) return TYPE_CMYK_FLT;
if (c.IsGray()) return TYPE_GRAY_FLT;
if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT;
return TYPE_RGB_FLT;
}
uint32_t Type64(const ColorEncoding& c) {
if (c.IsGray()) return TYPE_GRAY_DBL;
if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL;
return TYPE_RGB_DBL;
}
@ -600,12 +602,12 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
const double init = 1E-3;
const double step = 0.2;
if (c.IsGray()) {
if (c.color_space == ColorSpace::kGray) {
// 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)) {
if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) {
return false;
}
}
@ -616,7 +618,7 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
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)) {
if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) {
return false;
}
}
@ -723,7 +725,7 @@ Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
void DetectTransferFunction(const cmsContext context, const Profile& profile,
ColorEncoding* JXL_RESTRICT c) {
if (c->tf.SetImplicit()) return;
JXL_CHECK(c->color_space != ColorSpace::kXYB);
float gamma = 0;
if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
@ -924,17 +926,17 @@ bool ApplyCICP(const uint8_t color_primaries,
const auto tf = static_cast<TransferFunction>(transfer_characteristics);
if (!IsKnownTransferFunction(tf)) return false;
if (!IsKnownColorPrimaries(color_primaries)) return false;
c->SetColorSpace(ColorSpace::kRGB);
c->color_space = ColorSpace::kRGB;
c->tf.SetTransferFunction(tf);
if (primaries == Primaries::kP3) {
if (!c->SetWhitePointType(WhitePoint::kDCI)) return false;
if (!c->SetPrimariesType(Primaries::kP3)) return false;
c->white_point = WhitePoint::kDCI;
c->primaries = Primaries::kP3;
} else if (color_primaries == kColorPrimariesP3_D65) {
if (!c->SetWhitePointType(WhitePoint::kD65)) return false;
if (!c->SetPrimariesType(Primaries::kP3)) return false;
c->white_point = WhitePoint::kD65;
c->primaries = Primaries::kP3;
} else {
if (!c->SetWhitePointType(WhitePoint::kD65)) return false;
if (!c->SetPrimariesType(primaries)) return false;
c->white_point = WhitePoint::kD65;
c->primaries = primaries;
}
return true;
}
@ -971,8 +973,7 @@ JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32);
}
// ICC and RenderingIntent have the same values (0..3).
JXL_RETURN_IF_ERROR(c_enc.SetRenderingIntent(
static_cast<RenderingIntent>(rendering_intent32)));
c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32);
if (profile.has_CICP &&
ApplyCICP(profile.CICP.color_primaries,
@ -983,7 +984,7 @@ JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
return true;
}
c_enc.SetColorSpace(ColorSpaceFromProfile(profile));
c_enc.color_space = ColorSpaceFromProfile(profile);
*cmyk = (profile.data_color_space == skcms_Signature_CMYK);
CIExy wp_unadapted;
@ -1009,8 +1010,7 @@ JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32);
}
// ICC and RenderingIntent have the same values (0..3).
JXL_RETURN_IF_ERROR(c_enc.SetRenderingIntent(
static_cast<RenderingIntent>(rendering_intent32)));
c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32);
static constexpr size_t kCICPSize = 12;
static constexpr auto kCICPSignature =
@ -1024,7 +1024,7 @@ JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
return true;
}
c_enc.SetColorSpace(ColorSpaceFromProfile(profile));
c_enc.color_space = ColorSpaceFromProfile(profile);
if (cmsGetColorSpace(profile.get()) == cmsSigCmykData) {
*cmyk = JXL_TRUE;
c_enc.ToExternal(c);
@ -1078,18 +1078,27 @@ void AllocateBuffer(size_t length, size_t num_threads,
void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
const JxlColorProfile* input, const JxlColorProfile* output,
float intensity_target) {
JXL_ASSERT(init_data != nullptr);
auto cms = static_cast<const JxlCmsInterface*>(init_data);
auto t = jxl::make_unique<JxlCms>();
IccBytes icc_src, icc_dst;
if (input->icc.size == 0) {
JXL_NOTIFY_ERROR("JxlCmsInit: empty input ICC");
return nullptr;
}
if (output->icc.size == 0) {
JXL_NOTIFY_ERROR("JxlCmsInit: empty OUTPUT ICC");
return nullptr;
}
icc_src.assign(input->icc.data, input->icc.data + input->icc.size);
ColorEncoding c_src;
if (!c_src.SetICC(std::move(icc_src), cms)) {
if (!c_src.SetFieldsFromICC(std::move(icc_src), *cms)) {
JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse input ICC");
return nullptr;
}
icc_dst.assign(output->icc.data, output->icc.data + output->icc.size);
ColorEncoding c_dst;
if (!c_dst.SetICC(std::move(icc_dst), cms)) {
if (!c_dst.SetFieldsFromICC(std::move(icc_dst), *cms)) {
JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse output ICC");
return nullptr;
}
@ -1109,11 +1118,11 @@ void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
#else // JPEGXL_ENABLE_SKCMS
const cmsContext context = GetContext();
Profile profile_src, profile_dst;
if (!DecodeProfile(context, Span<const uint8_t>(c_src.ICC()), &profile_src)) {
if (!DecodeProfile(context, Span<const uint8_t>(c_src.icc), &profile_src)) {
JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse input ICC");
return nullptr;
}
if (!DecodeProfile(context, Span<const uint8_t>(c_dst.ICC()), &profile_dst)) {
if (!DecodeProfile(context, Span<const uint8_t>(c_dst.icc), &profile_dst)) {
JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse output ICC");
return nullptr;
}
@ -1243,7 +1252,7 @@ void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
#endif // JPEGXL_ENABLE_SKCMS
// Not including alpha channel (copied separately).
const size_t channels_src = (c_src.IsCMYK() ? 4 : c_src.Channels());
const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels());
const size_t channels_dst = c_dst.Channels();
JXL_CHECK(channels_src == channels_dst ||
(channels_src == 4 && channels_dst == 3));
@ -1256,7 +1265,7 @@ void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
// Type includes color space (XYZ vs RGB), so can be different.
const uint32_t type_src = Type32(c_src, channels_src == 4);
const uint32_t type_dst = Type32(c_dst, false);
const uint32_t intent = static_cast<uint32_t>(c_dst.GetRenderingIntent());
const uint32_t intent = static_cast<uint32_t>(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 |

View File

@ -5,253 +5,24 @@
#include "lib/jxl/color_encoding_internal.h"
#include <errno.h>
#include <array>
#include <cmath>
#include "lib/jxl/base/common.h"
#include "lib/jxl/base/matrix_ops.h"
#include "lib/jxl/cms/color_encoding_cms.h"
#include "lib/jxl/cms/color_management.h" // MaybeCreateProfile
#include "lib/jxl/fields.h"
#include "lib/jxl/pack_signed.h"
namespace jxl {
namespace {
// 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_UNREACHABLE("Invalid ColorSpace %u", static_cast<uint32_t>(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_UNREACHABLE("Invalid WhitePoint %u", static_cast<uint32_t>(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_UNREACHABLE("Invalid Primaries %u", static_cast<uint32_t>(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_UNREACHABLE("Invalid TransferFunction %u",
static_cast<uint32_t>(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_UNREACHABLE("Invalid RenderingIntent %u",
static_cast<uint32_t>(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<int32_t>(roundf(f * 1E6));
return true;
}
Status WhitePointFromExternal(const JxlWhitePoint external, WhitePoint* out) {
switch (external) {
case JXL_WHITE_POINT_D65:
*out = WhitePoint::kD65;
return true;
case JXL_WHITE_POINT_CUSTOM:
*out = WhitePoint::kCustom;
return true;
case JXL_WHITE_POINT_E:
*out = WhitePoint::kE;
return true;
case JXL_WHITE_POINT_DCI:
*out = WhitePoint::kDCI;
return true;
}
return JXL_FAILURE("Invalid WhitePoint enum value %d",
static_cast<int>(external));
}
Status PrimariesFromExternal(const JxlPrimaries external, Primaries* out) {
switch (external) {
case JXL_PRIMARIES_SRGB:
*out = Primaries::kSRGB;
return true;
case JXL_PRIMARIES_CUSTOM:
*out = Primaries::kCustom;
return true;
case JXL_PRIMARIES_2100:
*out = Primaries::k2100;
return true;
case JXL_PRIMARIES_P3:
*out = 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 RenderingIntentFromExternal(const JxlRenderingIntent external,
RenderingIntent* out) {
switch (external) {
case JXL_RENDERING_INTENT_PERCEPTUAL:
*out = RenderingIntent::kPerceptual;
return true;
case JXL_RENDERING_INTENT_RELATIVE:
*out = RenderingIntent::kRelative;
return true;
case JXL_RENDERING_INTENT_SATURATION:
*out = RenderingIntent::kSaturation;
return true;
case JXL_RENDERING_INTENT_ABSOLUTE:
*out = RenderingIntent::kAbsolute;
return true;
}
return JXL_FAILURE("Invalid RenderingIntent enum value");
}
} // namespace
CIExy Customxy::Get() const {
CIExy xy;
xy.x = F64FromCustomxyI32(storage_.x);
xy.y = F64FromCustomxyI32(storage_.y);
return xy;
}
Status Customxy::Set(const CIExy& xy) {
JXL_RETURN_IF_ERROR(F64ToCustomxyI32(xy.x, &storage_.x));
JXL_RETURN_IF_ERROR(F64ToCustomxyI32(xy.y, &storage_.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);
if (!storage_.SetGamma(1.0 / 3)) JXL_ASSERT(false);
return true;
}
return false;
}
Status CustomTransferFunction::SetGamma(double gamma) {
if (gamma < (1.0f / ::jxl::cms::CustomTransferFunction::kMaxGamma) ||
gamma > 1.0) {
return JXL_FAILURE("Invalid gamma %f", gamma);
}
storage_.have_gamma = false;
if (ApproxEq(gamma, 1.0)) {
storage_.transfer_function = TransferFunction::kLinear;
return true;
}
if (ApproxEq(gamma, 1.0 / 2.6)) {
storage_.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.
storage_.have_gamma = true;
storage_.gamma =
roundf(gamma * ::jxl::cms::CustomTransferFunction::kGammaMul);
storage_.transfer_function = TransferFunction::kUnknown;
return true;
}
std::array<ColorEncoding, 2> ColorEncoding::CreateC2(Primaries pr,
TransferFunction tf) {
std::array<ColorEncoding, 2> c2;
@ -260,14 +31,14 @@ std::array<ColorEncoding, 2> ColorEncoding::CreateC2(Primaries pr,
c_rgb->SetColorSpace(ColorSpace::kRGB);
c_rgb->storage_.white_point = WhitePoint::kD65;
c_rgb->storage_.primaries = pr;
c_rgb->tf.SetTransferFunction(tf);
c_rgb->storage_.tf.SetTransferFunction(tf);
JXL_CHECK(c_rgb->CreateICC());
ColorEncoding* c_gray = c2.data() + 1;
c_gray->SetColorSpace(ColorSpace::kGray);
c_gray->storage_.white_point = WhitePoint::kD65;
c_gray->storage_.primaries = pr;
c_gray->tf.SetTransferFunction(tf);
c_gray->storage_.tf.SetTransferFunction(tf);
JXL_CHECK(c_gray->CreateICC());
return c2;
@ -284,106 +55,12 @@ const ColorEncoding& ColorEncoding::LinearSRGB(bool is_gray) {
return c2[is_gray];
}
CIExy ColorEncoding::GetWhitePoint() const {
JXL_DASSERT(storage_.have_fields);
CIExy xy;
switch (storage_.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_UNREACHABLE("Invalid WhitePoint %u",
static_cast<uint32_t>(storage_.white_point));
}
Status ColorEncoding::SetWhitePointType(const WhitePoint& wp) {
JXL_DASSERT(storage_.have_fields);
storage_.white_point = wp;
return true;
}
Status ColorEncoding::SetWhitePoint(const CIExy& xy) {
JXL_DASSERT(storage_.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)) {
storage_.white_point = WhitePoint::kD65;
return true;
}
if (ApproxEq(xy.x, 1.0 / 3) && ApproxEq(xy.y, 1.0 / 3)) {
storage_.white_point = WhitePoint::kE;
return true;
}
if (ApproxEq(xy.x, 0.314) && ApproxEq(xy.y, 0.351)) {
storage_.white_point = WhitePoint::kDCI;
return true;
}
storage_.white_point = WhitePoint::kCustom;
return white_.Set(xy);
}
Status ColorEncoding::SetRenderingIntent(const RenderingIntent& ri) {
storage_.rendering_intent = ri;
return true;
}
PrimariesCIExy ColorEncoding::GetPrimaries() const {
JXL_DASSERT(storage_.have_fields);
JXL_ASSERT(HasPrimaries());
PrimariesCIExy xy;
switch (storage_.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_UNREACHABLE("Invalid Primaries %u",
static_cast<uint32_t>(storage_.primaries));
}
Status ColorEncoding::SetPrimariesType(const Primaries& p) {
JXL_DASSERT(storage_.have_fields);
JXL_ASSERT(HasPrimaries());
@ -391,69 +68,6 @@ Status ColorEncoding::SetPrimariesType(const Primaries& p) {
return true;
}
Status ColorEncoding::SetPrimaries(const PrimariesCIExy& xy) {
JXL_DASSERT(storage_.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)) {
storage_.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)) {
storage_.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)) {
storage_.primaries = Primaries::kP3;
return true;
}
storage_.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() {
storage_.icc.clear();
return MaybeCreateProfile(*this, &storage_.icc);
}
Status ColorEncoding::SetFieldsFromICC(const JxlCmsInterface& cms) {
// In case parsing fails, mark the ColorEncoding as invalid.
SetColorSpace(ColorSpace::kUnknown);
tf.SetTransferFunction(TransferFunction::kUnknown);
if (storage_.icc.empty()) return JXL_FAILURE("Empty ICC profile");
JxlColorEncoding external;
JXL_BOOL cmyk;
JXL_RETURN_IF_ERROR(
cms.set_fields_from_icc(cms.set_fields_data, storage_.icc.data(),
storage_.icc.size(), &external, &cmyk));
if (cmyk) {
storage_.cmyk = true;
return true;
}
IccBytes icc = std::move(storage_.icc);
JXL_RETURN_IF_ERROR(FromExternal(external));
storage_.icc = std::move(icc);
return true;
}
void ColorEncoding::DecideIfWantICC(const JxlCmsInterface& cms) {
if (storage_.icc.empty()) return;
@ -466,56 +80,9 @@ void ColorEncoding::DecideIfWantICC(const JxlCmsInterface& cms) {
if (cmyk) return;
IccBytes new_icc;
if (!MaybeCreateProfile(*this, &new_icc)) return;
if (!MaybeCreateProfile(storage_, &new_icc)) return;
storage_.want_icc = false;
}
std::string Description(const ColorEncoding& c) { return c.Description(); }
std::string ColorEncoding::Description() const {
std::string d = ToString(GetColorSpace());
bool explicit_wp_tf = (storage_.color_space != ColorSpace::kXYB);
if (explicit_wp_tf) {
d += '_';
if (storage_.white_point == WhitePoint::kCustom) {
const CIExy wp = GetWhitePoint();
d += ToString(wp.x) + ';';
d += ToString(wp.y);
} else {
d += ToString(storage_.white_point);
}
}
if (HasPrimaries()) {
d += '_';
if (storage_.primaries == Primaries::kCustom) {
const PrimariesCIExy pr = 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(storage_.primaries);
}
}
d += '_';
d += ToString(storage_.rendering_intent);
if (explicit_wp_tf) {
d += '_';
if (tf.IsGamma()) {
d += 'g';
d += ToString(tf.GetGamma());
} else {
d += ToString(tf.GetTransferFunction());
}
}
return d;
want_icc_ = false;
}
Customxy::Customxy() { Bundle::Init(this); }
@ -569,7 +136,7 @@ Status ColorEncoding::VisitFields(Visitor* JXL_RESTRICT visitor) {
return true;
}
JXL_QUIET_RETURN_IF_ERROR(visitor->Bool(false, &storage_.want_icc));
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.
@ -584,7 +151,9 @@ Status ColorEncoding::VisitFields(Visitor* JXL_RESTRICT visitor) {
JXL_QUIET_RETURN_IF_ERROR(
visitor->Enum(WhitePoint::kD65, &storage_.white_point));
if (visitor->Conditional(storage_.white_point == WhitePoint::kCustom)) {
white_.storage_ = storage_.white;
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&white_));
storage_.white = white_.storage_;
}
}
@ -592,25 +161,36 @@ Status ColorEncoding::VisitFields(Visitor* JXL_RESTRICT visitor) {
JXL_QUIET_RETURN_IF_ERROR(
visitor->Enum(Primaries::kSRGB, &storage_.primaries));
if (visitor->Conditional(storage_.primaries == Primaries::kCustom)) {
red_.storage_ = storage_.red;
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&red_));
storage_.red = red_.storage_;
green_.storage_ = storage_.green;
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&green_));
storage_.green = green_.storage_;
blue_.storage_ = storage_.blue;
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&blue_));
storage_.blue = blue_.storage_;
}
}
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&tf));
tf_.nonserialized_color_space = storage_.color_space;
tf_.storage_ = storage_.tf;
JXL_QUIET_RETURN_IF_ERROR(visitor->VisitNested(&tf_));
storage_.tf = tf_.storage_;
JXL_QUIET_RETURN_IF_ERROR(
visitor->Enum(RenderingIntent::kRelative, &storage_.rendering_intent));
// We didn't have ICC, so all fields should be known.
if (storage_.color_space == ColorSpace::kUnknown || tf.IsUnknown()) {
if (storage_.color_space == ColorSpace::kUnknown ||
storage_.tf.IsUnknown()) {
return JXL_FAILURE(
"No ICC but cs %u and tf %u%s",
static_cast<unsigned int>(storage_.color_space),
tf.IsGamma() ? 0
: static_cast<unsigned int>(tf.GetTransferFunction()),
tf.IsGamma() ? "(gamma)" : "");
storage_.tf.have_gamma
? 0
: static_cast<unsigned int>(storage_.tf.transfer_function),
storage_.tf.have_gamma ? "(gamma)" : "");
}
JXL_RETURN_IF_ERROR(CreateICC());
@ -625,189 +205,4 @@ Status ColorEncoding::VisitFields(Visitor* JXL_RESTRICT visitor) {
return true;
}
void ColorEncoding::ToExternal(JxlColorEncoding* external) const {
// TODO(eustas): update copy/update storage and call .ToExternal on it
if (!HaveFields()) {
external->color_space = JXL_COLOR_SPACE_UNKNOWN;
external->primaries = JXL_PRIMARIES_CUSTOM;
external->rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; //?
external->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
external->white_point = JXL_WHITE_POINT_CUSTOM;
return;
}
external->color_space = static_cast<JxlColorSpace>(GetColorSpace());
external->white_point = static_cast<JxlWhitePoint>(storage_.white_point);
jxl::CIExy whitepoint = 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<JxlPrimaries>(storage_.primaries);
jxl::PrimariesCIExy primaries = 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 (tf.IsGamma()) {
external->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
external->gamma = tf.GetGamma();
} else {
external->transfer_function =
static_cast<JxlTransferFunction>(tf.GetTransferFunction());
external->gamma = 0;
}
external->rendering_intent =
static_cast<JxlRenderingIntent>(storage_.rendering_intent);
}
Status ColorEncoding::FromExternal(const JxlColorEncoding& external) {
SetColorSpace(static_cast<ColorSpace>(external.color_space));
JXL_RETURN_IF_ERROR(
WhitePointFromExternal(external.white_point, &storage_.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(SetWhitePoint(wp));
}
if (external.color_space == JXL_COLOR_SPACE_RGB ||
external.color_space == JXL_COLOR_SPACE_UNKNOWN) {
JXL_RETURN_IF_ERROR(
PrimariesFromExternal(external.primaries, &storage_.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(SetPrimaries(primaries));
}
}
CustomTransferFunction tf;
tf.nonserialized_color_space = GetColorSpace();
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);
}
this->tf = tf;
JXL_RETURN_IF_ERROR(RenderingIntentFromExternal(external.rendering_intent,
&storage_.rendering_intent));
// The ColorEncoding caches an ICC profile it created earlier that may no
// longer match the profile with the changed fields, so re-create it.
if (!(CreateICC())) {
// This is not an error: for example, it doesn't have ICC profile creation
// implemented for XYB. This should not be returned as error, since
// FromExternal still worked correctly, and what
// matters is that internal->ICC() will not return the wrong profile.
}
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];
Mul3x3Vector(kBradford, w, lms);
Mul3x3Vector(kBradford, w50, lms50);
if (lms[0] == 0 || lms[1] == 0 || lms[2] == 0) {
return JXL_FAILURE("Invalid white point");
}
float a[9] = {
// /----> 0, 1, 2, 3, /----> 4, 5, 6, 7, /----> 8,
lms50[0] / lms[0], 0, 0, 0, lms50[1] / lms[1], 0, 0, 0, lms50[2] / lms[2],
};
if (!std::isfinite(a[0]) || !std::isfinite(a[4]) || !std::isfinite(a[8])) {
return JXL_FAILURE("Invalid white point");
}
float b[9];
Mul3x3Matrix(a, kBradford, b);
Mul3x3Matrix(kBradfordInv, b, matrix);
return true;
}
Status PrimariesToXYZ(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];
Mul3x3Vector(primaries_inv, w, xyz);
float a[9] = {
xyz[0], 0, 0, 0, xyz[1], 0, 0, 0, xyz[2],
};
Mul3x3Matrix(primaries, a, matrix);
return true;
}
Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]) {
float toXYZ[9];
JXL_RETURN_IF_ERROR(PrimariesToXYZ(rx, ry, gx, gy, bx, by, wx, wy, toXYZ));
float d50[9];
JXL_RETURN_IF_ERROR(AdaptToXYZD50(wx, wy, d50));
Mul3x3Matrix(d50, toXYZ, matrix);
return true;
}
} // namespace jxl

View File

@ -12,14 +12,11 @@
#include <jxl/color_encoding.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <array>
#include <cmath> // std::abs
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/status.h"
@ -85,6 +82,8 @@ static inline constexpr uint64_t EnumBits(RenderingIntent /*unused*/) {
} // namespace cms
struct ColorEncoding;
// Serializable form of CIExy.
struct Customxy : public Fields {
Customxy();
@ -92,15 +91,8 @@ struct Customxy : public Fields {
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);
bool IsSame(const Customxy& other) const {
return (storage_.x == other.storage_.x) && (storage_.y == other.storage_.y);
}
private:
friend struct ColorEncoding;
::jxl::cms::Customxy storage_;
};
@ -112,73 +104,13 @@ struct CustomTransferFunction : public Fields {
// transfer function, otherwise leaves fields unchanged and returns false.
bool SetImplicit();
// Gamma: only used for PNG inputs
bool IsGamma() const { return storage_.have_gamma; }
double GetGamma() const {
JXL_ASSERT(IsGamma());
return storage_.gamma * 1E-7; // (0, 1)
}
Status SetGamma(double gamma);
TransferFunction GetTransferFunction() const {
JXL_ASSERT(!IsGamma());
return storage_.transfer_function;
}
void SetTransferFunction(const TransferFunction tf) {
storage_.have_gamma = false;
storage_.transfer_function = tf;
}
bool IsUnknown() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kUnknown);
}
bool IsSRGB() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kSRGB);
}
bool IsLinear() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kLinear);
}
bool IsPQ() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kPQ);
}
bool IsHLG() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kHLG);
}
bool Is709() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::k709);
}
bool IsDCI() const {
return !storage_.have_gamma &&
(storage_.transfer_function == TransferFunction::kDCI);
}
bool IsSame(const CustomTransferFunction& other) const {
if (storage_.have_gamma != other.storage_.have_gamma) {
return false;
}
if (storage_.have_gamma) {
if (storage_.gamma != other.storage_.gamma) {
return false;
}
} else {
if (storage_.transfer_function != other.storage_.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:
friend struct ColorEncoding;
::jxl::cms::CustomTransferFunction storage_;
};
@ -194,7 +126,7 @@ struct ColorEncoding : public Fields {
// Returns true if an ICC profile was successfully created from fields.
// Must be called after modifying fields. Defined in color_management.cc.
Status CreateICC();
Status CreateICC() { return storage_.CreateICC(); }
// Returns non-empty and valid ICC profile, unless:
// - WantICC() == true and SetICC() was not yet called;
@ -205,22 +137,10 @@ struct ColorEncoding : public Fields {
// subsequent WantICC() will return true until DecideIfWantICC() changes it.
// Returning false indicates data has been lost.
Status SetICC(IccBytes&& icc, const JxlCmsInterface* cms) {
if (icc.empty()) return false;
storage_.icc = std::move(icc);
if (cms == nullptr) {
storage_.want_icc = true;
storage_.have_fields = false;
return true;
}
if (!SetFieldsFromICC(*cms)) {
storage_.icc.clear();
return false;
}
storage_.want_icc = true;
return true;
JXL_ASSERT(cms != nullptr);
JXL_ASSERT(!icc.empty());
want_icc_ = storage_.SetFieldsFromICC(std::move(icc), *cms);
return want_icc_;
}
// Sets the raw ICC profile bytes, without parsing the ICC, and without
@ -228,17 +148,15 @@ struct ColorEncoding : public Fields {
// 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(IccBytes&& icc) {
if (icc.empty()) return false;
void SetICCRaw(IccBytes&& icc) {
JXL_ASSERT(!icc.empty());
storage_.icc = std::move(icc);
storage_.want_icc = true;
storage_.have_fields = false;
return true;
want_icc_ = true;
}
// Returns whether to send the ICC profile in the codestream.
bool WantICC() const { return storage_.want_icc; }
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.
@ -249,16 +167,15 @@ struct ColorEncoding : public Fields {
bool IsGray() const { return storage_.color_space == ColorSpace::kGray; }
bool IsCMYK() const { return storage_.cmyk; }
size_t Channels() const { return IsGray() ? 1 : 3; }
size_t Channels() const { return storage_.Channels(); }
// Returns false if the field is invalid and unusable.
bool HasPrimaries() const {
return !IsGray() && storage_.color_space != ColorSpace::kXYB;
}
bool HasPrimaries() const { return storage_.HasPrimaries(); }
// Returns true after setting the field to a value defined by color_space,
// otherwise false and leaves the field unchanged.
bool ImplicitWhitePoint() {
// TODO(eustas): inline
if (storage_.color_space == ColorSpace::kXYB) {
storage_.white_point = WhitePoint::kD65;
return true;
@ -274,7 +191,7 @@ struct ColorEncoding : public Fields {
if (!IsGray() && storage_.color_space != ColorSpace::kRGB) return false;
if (storage_.white_point != WhitePoint::kD65) return false;
if (storage_.primaries != Primaries::kSRGB) return false;
if (!tf.IsSRGB()) return false;
if (!storage_.tf.IsSRGB()) return false;
return true;
}
@ -286,7 +203,7 @@ struct ColorEncoding : public Fields {
if (!IsGray() && storage_.color_space != ColorSpace::kRGB) return false;
if (storage_.white_point != WhitePoint::kD65) return false;
if (storage_.primaries != Primaries::kSRGB) return false;
if (!tf.IsLinear()) return false;
if (!storage_.tf.IsLinear()) return false;
return true;
}
@ -297,7 +214,7 @@ struct ColorEncoding : public Fields {
storage_.color_space = cs;
storage_.white_point = WhitePoint::kD65;
storage_.primaries = Primaries::kSRGB;
tf.SetTransferFunction(TransferFunction::kSRGB);
storage_.tf.transfer_function = TransferFunction::kSRGB;
storage_.rendering_intent = ri;
return CreateICC();
}
@ -306,107 +223,70 @@ struct ColorEncoding : public Fields {
// Accessors ensure tf.nonserialized_color_space is updated at the same time.
ColorSpace GetColorSpace() const { return storage_.color_space; }
void SetColorSpace(const ColorSpace cs) {
storage_.color_space = cs;
tf.nonserialized_color_space = cs;
}
CIExy GetWhitePoint() const;
Status SetWhitePoint(const CIExy& xy);
void SetColorSpace(const ColorSpace cs) { storage_.color_space = cs; }
CIExy GetWhitePoint() const { return storage_.GetWhitePoint(); }
WhitePoint GetWhitePointType() const { return storage_.white_point; }
Status SetWhitePointType(const WhitePoint& wp);
PrimariesCIExy GetPrimaries() const;
Status SetPrimaries(const PrimariesCIExy& xy);
PrimariesCIExy GetPrimaries() const { return storage_.GetPrimaries(); }
Primaries GetPrimariesType() const { return storage_.primaries; }
Status SetPrimariesType(const Primaries& p);
jxl::cms::CustomTransferFunction& Tf() { return storage_.tf; }
const jxl::cms::CustomTransferFunction& Tf() const { return storage_.tf; }
RenderingIntent GetRenderingIntent() const {
return storage_.rendering_intent;
}
Status SetRenderingIntent(const RenderingIntent& ri);
// 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 (storage_.color_space != other.storage_.color_space) return false;
if (storage_.white_point != other.storage_.white_point) return false;
if (storage_.white_point == WhitePoint::kCustom) {
if (!white_.IsSame(other.white_)) {
return false;
}
}
if (HasPrimaries() != other.HasPrimaries()) return false;
if (HasPrimaries()) {
if (storage_.primaries != other.storage_.primaries) return false;
if (storage_.primaries == Primaries::kCustom) {
if (!red_.IsSame(other.red_)) return false;
if (!green_.IsSame(other.green_)) return false;
if (!blue_.IsSame(other.blue_)) return false;
}
}
return true;
void SetRenderingIntent(const RenderingIntent& ri) {
storage_.rendering_intent = ri;
}
// 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);
return storage_.SameColorEncoding(other.storage_);
}
mutable bool all_default;
// Only valid if HaveFields()
CustomTransferFunction tf;
void ToExternal(JxlColorEncoding* external) const;
Status FromExternal(const JxlColorEncoding& external);
void ToExternal(JxlColorEncoding* external) const {
storage_.ToExternal(external);
}
Status FromExternal(const JxlColorEncoding& external) {
return storage_.FromExternal(external);
}
const jxl::cms::ColorEncoding& View() const { return storage_; }
std::string Description() const;
private:
// Returns true if all fields have been initialized (possibly to kUnknown).
// Returns false if the ICC profile is invalid or decoding it fails.
Status SetFieldsFromICC(const JxlCmsInterface& cms);
static std::array<ColorEncoding, 2> CreateC2(Primaries pr,
TransferFunction tf);
// 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_;
::jxl::cms::ColorEncoding storage_;
// Only used if white_point == kCustom.
Customxy white_;
// Only valid if HaveFields()
CustomTransferFunction tf_;
// 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,
double max_l1 = 1E-3) {
// 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;
static inline std::string Description(const ColorEncoding& c) {
return Description(c.View());
}
// 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);
}
Status PrimariesToXYZ(float rx, float ry, float gx, float gy, float bx,
float by, float wx, float wy, float matrix[9]);
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]);
class ColorSpaceTransform {
public:
explicit ColorSpaceTransform(const JxlCmsInterface& cms) : cms_(cms) {}

View File

@ -5,6 +5,11 @@
#include "lib/jxl/color_encoding_internal.h"
#include <jxl/color_encoding.h>
#include <cstdlib> // rand
#include "lib/jxl/cms/color_encoding_cms.h"
#include "lib/jxl/encode_internal.h"
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"
@ -12,21 +17,23 @@
namespace jxl {
namespace {
using jxl::cms::ColorEncoding;
TEST(ColorEncodingTest, RoundTripAll) {
for (const test::ColorEncodingDescriptor& cdesc : test::AllEncodings()) {
const ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc);
ColorEncoding c_original = test::ColorEncodingFromDescriptor(cdesc).View();
// Verify Set(Get) yields the same white point/primaries/gamma.
{
ColorEncoding c;
EXPECT_TRUE(c.SetWhitePoint(c_original.GetWhitePoint()));
EXPECT_EQ(c_original.GetWhitePointType(), c.GetWhitePointType());
EXPECT_EQ(c_original.white_point, c.white_point);
}
{
ColorEncoding c;
EXPECT_TRUE(c.SetPrimaries(c_original.GetPrimaries()));
EXPECT_EQ(c_original.GetPrimariesType(), c.GetPrimariesType());
EXPECT_EQ(c_original.primaries, c.primaries);
}
if (c_original.tf.IsGamma()) {
if (c_original.tf.have_gamma) {
ColorEncoding c;
EXPECT_TRUE(c.tf.SetGamma(c_original.tf.GetGamma()));
EXPECT_TRUE(c_original.tf.IsSame(c.tf));
@ -74,26 +81,26 @@ TEST(ColorEncodingTest, CustomGamma) {
EXPECT_FALSE(c.tf.SetGamma(1.001));
#endif
EXPECT_TRUE(c.tf.SetGamma(1.0));
EXPECT_FALSE(c.tf.IsGamma());
EXPECT_FALSE(c.tf.have_gamma);
EXPECT_TRUE(c.tf.IsLinear());
EXPECT_TRUE(c.tf.SetGamma(0.123));
EXPECT_TRUE(c.tf.IsGamma());
EXPECT_TRUE(c.tf.have_gamma);
const double gamma = c.tf.GetGamma();
ColorEncoding c2;
EXPECT_TRUE(c2.tf.SetGamma(gamma));
EXPECT_TRUE(c.SameColorEncoding(c2));
EXPECT_TRUE(c2.tf.IsGamma());
EXPECT_TRUE(c2.tf.have_gamma);
}
TEST(ColorEncodingTest, InternalExternalConversion) {
ColorEncoding source_internal;
JxlColorEncoding external;
JxlColorEncoding external = {};
ColorEncoding destination_internal;
for (int i = 0; i < 100; i++) {
source_internal.SetColorSpace(static_cast<ColorSpace>(rand() % 4));
source_internal.color_space = static_cast<ColorSpace>(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;
@ -108,47 +115,41 @@ TEST(ColorEncodingTest, InternalExternalConversion) {
primaries.b.y = (float(rand()) / float((RAND_MAX)) * 0.5) + 0.25;
EXPECT_TRUE(source_internal.SetPrimaries(primaries));
}
CustomTransferFunction tf;
jxl::cms::CustomTransferFunction tf;
EXPECT_TRUE(tf.SetGamma((float(rand()) / float((RAND_MAX)) * 0.5) + 0.25));
source_internal.tf = tf;
ASSERT_TRUE(source_internal.SetRenderingIntent(
static_cast<RenderingIntent>(rand() % 4)));
source_internal.rendering_intent = static_cast<RenderingIntent>(rand() % 4);
source_internal.ToExternal(&external);
EXPECT_TRUE(destination_internal.FromExternal(external));
EXPECT_EQ(source_internal.GetColorSpace(),
destination_internal.GetColorSpace());
EXPECT_EQ(source_internal.GetWhitePointType(),
destination_internal.GetWhitePointType());
EXPECT_EQ(source_internal.GetWhitePoint().x,
destination_internal.GetWhitePoint().x);
EXPECT_EQ(source_internal.GetWhitePoint().y,
destination_internal.GetWhitePoint().y);
EXPECT_EQ(source_internal.color_space, destination_internal.color_space);
EXPECT_EQ(source_internal.white_point, destination_internal.white_point);
CIExy src_wp = source_internal.GetWhitePoint();
CIExy dst_wp = destination_internal.GetWhitePoint();
EXPECT_EQ(src_wp.x, dst_wp.x);
EXPECT_EQ(src_wp.y, dst_wp.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);
PrimariesCIExy src_p = source_internal.GetPrimaries();
PrimariesCIExy dst_p = destination_internal.GetPrimaries();
EXPECT_EQ(src_p.r.x, dst_p.r.x);
EXPECT_EQ(src_p.r.y, dst_p.r.y);
EXPECT_EQ(src_p.g.x, dst_p.g.x);
EXPECT_EQ(src_p.g.y, dst_p.g.y);
EXPECT_EQ(src_p.b.x, dst_p.b.x);
EXPECT_EQ(src_p.b.y, dst_p.b.y);
}
EXPECT_EQ(source_internal.tf.IsGamma(), destination_internal.tf.IsGamma());
if (source_internal.tf.IsGamma()) {
EXPECT_EQ(source_internal.tf.have_gamma,
destination_internal.tf.have_gamma);
if (source_internal.tf.have_gamma) {
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.GetRenderingIntent(),
destination_internal.GetRenderingIntent());
EXPECT_EQ(source_internal.rendering_intent,
destination_internal.rendering_intent);
}
}

View File

@ -11,11 +11,13 @@
#include <string>
#include <utility>
#include "lib/jxl/base/common.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/random.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/cms/color_encoding_cms.h"
#include "lib/jxl/cms/jxl_cms.h"
#include "lib/jxl/cms/opsin_params.h"
#include "lib/jxl/color_encoding_internal.h"
@ -71,16 +73,17 @@ MATCHER_P(HasSameFieldsAs, expected, "") {
<< ToString(expected.GetPrimariesType());
return false;
}
if (!arg.tf.IsSame(expected.tf)) {
static const auto tf_to_string = [](const CustomTransferFunction& tf) {
if (tf.IsGamma()) {
return "g" + ToString(tf.GetGamma());
}
return ToString(tf.GetTransferFunction());
};
if (!arg.Tf().IsSame(expected.Tf())) {
static const auto tf_to_string =
[](const jxl::cms::CustomTransferFunction& tf) {
if (tf.have_gamma) {
return "g" + ToString(tf.GetGamma());
}
return ToString(tf.transfer_function);
};
*result_listener << "which has a different transfer function: "
<< tf_to_string(arg.tf) << " instead of "
<< tf_to_string(expected.tf);
<< tf_to_string(arg.Tf()) << " instead of "
<< tf_to_string(expected.Tf());
return false;
}
return true;
@ -164,7 +167,7 @@ class ColorManagementTest
ColorSpaceTransform xform_fwd(cms);
ColorSpaceTransform xform_rev(cms);
const float intensity_target =
c.tf.IsHLG() ? 1000 : kDefaultIntensityTarget;
c.Tf().IsHLG() ? 1000 : kDefaultIntensityTarget;
ASSERT_TRUE(
xform_fwd.Init(c_native, c, intensity_target, kWidth, kNumThreads));
ASSERT_TRUE(
@ -246,15 +249,14 @@ TEST_F(ColorManagementTest, D2700Chromaticity) {
}
TEST_F(ColorManagementTest, D2700ToSRGB) {
const JxlCmsInterface& cms = *JxlGetDefaultCms();
PaddedBytes icc_data =
jxl::test::ReadTestData("jxl/color_management/sRGB-D2700.icc");
IccBytes icc;
Span<const uint8_t>(icc_data).AppendTo(&icc);
ColorEncoding sRGB_D2700;
ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc), &cms));
ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc), JxlGetDefaultCms()));
ColorSpaceTransform transform(cms);
ColorSpaceTransform transform(*JxlGetDefaultCms());
ASSERT_TRUE(transform.Init(sRGB_D2700, ColorEncoding::SRGB(),
kDefaultIntensityTarget, 1, 1));
const float sRGB_D2700_values[3] = {0.863, 0.737, 0.490};
@ -270,7 +272,7 @@ TEST_F(ColorManagementTest, P3HlgTo2020Hlg) {
p3_hlg.SetColorSpace(ColorSpace::kRGB);
ASSERT_TRUE(p3_hlg.SetWhitePointType(WhitePoint::kD65));
ASSERT_TRUE(p3_hlg.SetPrimariesType(Primaries::kP3));
p3_hlg.tf.SetTransferFunction(TransferFunction::kHLG);
p3_hlg.Tf().SetTransferFunction(TransferFunction::kHLG);
ASSERT_TRUE(p3_hlg.CreateICC());
ColorEncoding rec2020_hlg = p3_hlg;
@ -292,7 +294,7 @@ TEST_F(ColorManagementTest, HlgOotf) {
p3_hlg.SetColorSpace(ColorSpace::kRGB);
ASSERT_TRUE(p3_hlg.SetWhitePointType(WhitePoint::kD65));
ASSERT_TRUE(p3_hlg.SetPrimariesType(Primaries::kP3));
p3_hlg.tf.SetTransferFunction(TransferFunction::kHLG);
p3_hlg.Tf().SetTransferFunction(TransferFunction::kHLG);
ASSERT_TRUE(p3_hlg.CreateICC());
ColorSpaceTransform transform_to_1000(*JxlGetDefaultCms());
@ -335,7 +337,7 @@ TEST_F(ColorManagementTest, HlgOotf) {
ColorEncoding grayscale_hlg;
grayscale_hlg.SetColorSpace(ColorSpace::kGray);
ASSERT_TRUE(grayscale_hlg.SetWhitePointType(WhitePoint::kD65));
grayscale_hlg.tf.SetTransferFunction(TransferFunction::kHLG);
grayscale_hlg.Tf().SetTransferFunction(TransferFunction::kHLG);
ASSERT_TRUE(grayscale_hlg.CreateICC());
ColorSpaceTransform grayscale_transform(*JxlGetDefaultCms());
@ -351,7 +353,7 @@ TEST_F(ColorManagementTest, HlgOotf) {
TEST_F(ColorManagementTest, XYBProfile) {
ColorEncoding c_xyb;
c_xyb.SetColorSpace(ColorSpace::kXYB);
ASSERT_TRUE(c_xyb.SetRenderingIntent(RenderingIntent::kPerceptual));
c_xyb.SetRenderingIntent(RenderingIntent::kPerceptual);
ASSERT_TRUE(c_xyb.CreateICC());
ColorEncoding c_native = ColorEncoding::LinearSRGB(false);

View File

@ -13,6 +13,7 @@
#include <stddef.h>
#include <stdint.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_cache.h"

View File

@ -199,9 +199,9 @@ bool CanOutputToColorEncoding(const ColorEncoding& c_desired) {
return false;
}
// TODO(veluca): keep in sync with dec_reconstruct.cc
if (!c_desired.tf.IsPQ() && !c_desired.tf.IsSRGB() &&
!c_desired.tf.IsGamma() && !c_desired.tf.IsLinear() &&
!c_desired.tf.IsHLG() && !c_desired.tf.IsDCI() && !c_desired.tf.Is709()) {
const auto& tf = c_desired.Tf();
if (!tf.IsPQ() && !tf.IsSRGB() && !tf.have_gamma && !tf.IsLinear() &&
!tf.IsHLG() && !tf.IsDCI() && !tf.Is709()) {
return false;
}
if (c_desired.IsGray() && c_desired.GetWhitePointType() != WhitePoint::kD65) {
@ -239,7 +239,7 @@ Status OutputEncodingInfo::MaybeSetColorEncoding(
if (c_desired.GetColorSpace() == ColorSpace::kXYB &&
((color_encoding.GetColorSpace() == ColorSpace::kRGB &&
color_encoding.GetPrimariesType() != Primaries::kSRGB) ||
color_encoding.tf.IsPQ())) {
color_encoding.Tf().IsPQ())) {
return false;
}
if (!xyb_encoded && !CanOutputToColorEncoding(c_desired)) {
@ -264,18 +264,15 @@ Status OutputEncodingInfo::SetColorEncoding(const ColorEncoding& c_desired) {
!c_desired.IsGray()) {
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));
PrimariesCIExy p = srgb.GetPrimaries();
CIExy w = srgb.GetWhitePoint();
JXL_CHECK(PrimariesToXYZD50(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y, w.x,
w.y, srgb_to_xyzd50));
float original_to_xyz[3][3];
JXL_RETURN_IF_ERROR(PrimariesToXYZ(
c_desired.GetPrimaries().r.x, c_desired.GetPrimaries().r.y,
c_desired.GetPrimaries().g.x, c_desired.GetPrimaries().g.y,
c_desired.GetPrimaries().b.x, c_desired.GetPrimaries().b.y,
c_desired.GetWhitePoint().x, c_desired.GetWhitePoint().y,
&original_to_xyz[0][0]));
p = c_desired.GetPrimaries();
w = c_desired.GetWhitePoint();
JXL_RETURN_IF_ERROR(PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y,
w.x, w.y, &original_to_xyz[0][0]));
memcpy(luminances, original_to_xyz[1], sizeof luminances);
if (xyb_encoded) {
float adapt_to_d50[9];
@ -313,9 +310,10 @@ Status OutputEncodingInfo::SetColorEncoding(const ColorEncoding& c_desired) {
}
// Set the inverse gamma based on color space transfer function.
inverse_gamma = (c_desired.tf.IsGamma() ? c_desired.tf.GetGamma()
: c_desired.tf.IsDCI() ? 1.0f / 2.6f
: 1.0);
const auto& tf = c_desired.Tf();
inverse_gamma = (tf.have_gamma ? tf.GetGamma()
: tf.IsDCI() ? 1.0f / 2.6f
: 1.0);
return true;
}

View File

@ -1071,11 +1071,12 @@ JxlDecoderStatus JxlDecoderReadAllHeaders(JxlDecoder* dec) {
// Other non-successful status is an error
return JXL_DEC_ERROR;
}
IccBytes icc;
Span<const uint8_t>(decoded_icc).AppendTo(&icc);
if (!dec->metadata.m.color_encoding.SetICCRaw(std::move(icc))) {
if (decoded_icc.empty()) {
return JXL_DEC_ERROR;
}
IccBytes icc;
Span<const uint8_t>(decoded_icc).AppendTo(&icc);
dec->metadata.m.color_encoding.SetICCRaw(std::move(icc));
}
dec->got_all_headers = true;

View File

@ -1740,7 +1740,7 @@ TEST_P(DecodeAllEncodingsTest, PreserveOriginalProfileTest) {
jxl::ColorEncoding c_in = jxl::test::ColorEncodingFromDescriptor(cdesc);
if (c_in.GetRenderingIntent() != jxl::RenderingIntent::kRelative) return;
std::string color_space_in = Description(c_in);
float intensity_in = c_in.tf.IsPQ() ? 10000 : 255;
float intensity_in = c_in.Tf().IsPQ() ? 10000 : 255;
printf("Testing input color space %s\n", color_space_in.c_str());
jxl::TestCodestreamParams params;
params.color_space = color_space_in;
@ -1785,7 +1785,7 @@ void SetPreferredColorProfileTest(
jxl::test::GetSomeTestImage(xsize, ysize, num_channels, 0);
JxlPixelFormat format = {num_channels, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0};
std::string color_space_in = Description(c_in);
float intensity_in = c_in.tf.IsPQ() ? 10000 : 255;
float intensity_in = c_in.Tf().IsPQ() ? 10000 : 255;
jxl::TestCodestreamParams params;
params.color_space = color_space_in;
params.intensity_target = intensity_in;
@ -1811,7 +1811,7 @@ void SetPreferredColorProfileTest(
continue;
}
}
if (c_out.tf.IsHLG() && intensity_out > 300) {
if (c_out.Tf().IsHLG() && intensity_out > 300) {
// The Linear->HLG OOTF function at this intensity level can push
// saturated colors out of gamut, so we would need gamut mapping in
// this case too.
@ -1837,7 +1837,8 @@ void SetPreferredColorProfileTest(
JxlColorEncoding encoding_out;
EXPECT_TRUE(jxl::ParseDescription(color_space_out, &encoding_out));
if (c_out.GetColorSpace() == jxl::ColorSpace::kXYB &&
(c_in.GetPrimariesType() != jxl::Primaries::kSRGB || c_in.tf.IsPQ())) {
(c_in.GetPrimariesType() != jxl::Primaries::kSRGB ||
c_in.Tf().IsPQ())) {
EXPECT_EQ(JXL_DEC_ERROR,
JxlDecoderSetPreferredColorProfile(dec, &encoding_out));
JxlDecoderDestroy(dec);
@ -3741,7 +3742,8 @@ void AnalyzeCodestream(const jxl::PaddedBytes& data,
ASSERT_TRUE(jxl::ReadICC(&br, &icc_data));
jxl::IccBytes icc;
jxl::Span<const uint8_t>(icc_data).AppendTo(&icc);
ASSERT_TRUE(metadata.m.color_encoding.SetICCRaw(std::move(icc)));
ASSERT_TRUE(!icc.empty());
metadata.m.color_encoding.SetICCRaw(std::move(icc));
}
ASSERT_TRUE(br.JumpToByteBoundary());
bool has_preview = metadata.m.have_preview;

View File

@ -2497,10 +2497,10 @@ struct UpTo8Bits {
GenericEncodeChunk(residuals, n, skip, code, output);
}
size_t NumSymbols(bool doing_ycocg) const {
size_t NumSymbols(bool doing_ycocg_or_large_palette) const {
// values gain 1 bit for YCoCg, 1 bit for prediction.
// Maximum symbol is 1 + effective bit depth of residuals.
if (doing_ycocg) {
if (doing_ycocg_or_large_palette) {
return bitdepth + 3;
} else {
return bitdepth + 2;
@ -2560,10 +2560,10 @@ struct From9To13Bits {
GenericEncodeChunk(residuals, n, skip, code, output);
}
size_t NumSymbols(bool doing_ycocg) const {
size_t NumSymbols(bool doing_ycocg_or_large_palette) const {
// values gain 1 bit for YCoCg, 1 bit for prediction.
// Maximum symbol is 1 + effective bit depth of residuals.
if (doing_ycocg) {
if (doing_ycocg_or_large_palette) {
return bitdepth + 3;
} else {
return bitdepth + 2;
@ -3623,7 +3623,9 @@ JxlFastLosslessFrameState* LLEnc(const unsigned char* rgba, size_t width,
5, 1, 1, 1, 1, 1, 1, 1, 1};
bool doing_ycocg = nb_chans > 2 && collided;
for (size_t i = bitdepth.NumSymbols(doing_ycocg); i < kNumRawSymbols; i++) {
bool large_palette = !collided || pcolors >= 256;
for (size_t i = bitdepth.NumSymbols(doing_ycocg || large_palette);
i < kNumRawSymbols; i++) {
base_raw_counts[i] = 0;
}

View File

@ -1091,12 +1091,18 @@ JxlEncoderStatus JxlEncoderSetICCProfile(JxlEncoder* enc,
return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE,
"ICC profile is already set");
}
if (size == 0) {
return JXL_API_ERROR(enc, JXL_ENC_ERR_BAD_INPUT, "Empty ICC profile");
}
jxl::IccBytes icc;
icc.assign(icc_profile, icc_profile + size);
if (!enc->metadata.m.color_encoding.SetICC(
std::move(icc), enc->cms_set ? &enc->cms : nullptr)) {
return JXL_API_ERROR(enc, JXL_ENC_ERR_BAD_INPUT,
"ICC profile could not be set");
if (enc->cms_set) {
if (!enc->metadata.m.color_encoding.SetICC(std::move(icc), &enc->cms)) {
return JXL_API_ERROR(enc, JXL_ENC_ERR_BAD_INPUT,
"ICC profile could not be set");
}
} else {
enc->metadata.m.color_encoding.SetICCRaw(std::move(icc));
}
if (enc->metadata.m.color_encoding.GetColorSpace() ==
jxl::ColorSpace::kGray) {
@ -2005,12 +2011,8 @@ JxlEncoderStatus JxlEncoderAddJPEGFrame(
}
if (!frame_settings->enc->color_encoding_set) {
if (!SetColorEncodingFromJpegData(
*io.Main().jpeg_data,
&frame_settings->enc->metadata.m.color_encoding)) {
return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_BAD_INPUT,
"Error in input JPEG color space");
}
SetColorEncodingFromJpegData(
*io.Main().jpeg_data, &frame_settings->enc->metadata.m.color_encoding);
}
if (!frame_settings->enc->basic_info_set) {

View File

@ -325,7 +325,7 @@ struct ImageMetadata : public Fields {
// 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.
// to them. To use ColorSpace::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

View File

@ -215,8 +215,8 @@ static inline bool IsJPG(const Span<const uint8_t> bytes) {
} // namespace
Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
ColorEncoding* color_encoding) {
void SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
ColorEncoding* color_encoding) {
IccBytes icc_profile;
if (!ParseChunkedMarker(jpg, kApp2, ByteSpan(kIccProfileTag), &icc_profile)) {
JXL_WARNING("ReJPEG: corrupted ICC profile\n");
@ -226,10 +226,9 @@ Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
if (icc_profile.empty()) {
bool is_gray = (jpg.components.size() == 1);
*color_encoding = ColorEncoding::SRGB(is_gray);
return true;
} else {
color_encoding->SetICCRaw(std::move(icc_profile));
}
return color_encoding->SetICC(std::move(icc_profile), /*cms=*/nullptr);
}
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes,
@ -309,8 +308,7 @@ Status DecodeImageJPG(const Span<const uint8_t> bytes, CodecInOut* io) {
jpeg_data)) {
return JXL_FAILURE("Error reading JPEG");
}
JXL_RETURN_IF_ERROR(
SetColorEncodingFromJpegData(*jpeg_data, &io->metadata.m.color_encoding));
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) {

View File

@ -8,6 +8,7 @@
#include "lib/jxl/base/padded_bytes.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/enc_params.h"
#include "lib/jxl/jpeg/jpeg_data.h"
@ -16,8 +17,8 @@ namespace jpeg {
Status EncodeJPEGData(JPEGData& jpeg_data, PaddedBytes* bytes,
const CompressParams& cparams);
Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
ColorEncoding* color_encoding);
void SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
ColorEncoding* color_encoding);
/**
* Decodes bytes containing JPEG codestream into a CodecInOut as coefficients

View File

@ -627,7 +627,7 @@ TEST(JxlTest, RoundtripGrayscale) {
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());
EXPECT_TRUE(io.metadata.m.color_encoding.Tf().IsSRGB());
PassesEncoderState enc_state;
AuxOut* aux_out = nullptr;
@ -710,7 +710,7 @@ TEST(JxlTest, RoundtripAlpha) {
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());
EXPECT_TRUE(io.metadata.m.color_encoding.Tf().IsSRGB());
PassesEncoderState enc_state;
AuxOut* aux_out = nullptr;
PaddedBytes compressed;
@ -1574,10 +1574,11 @@ TEST(JxlTest, LosslessPNMRoundtrip) {
}
}
TEST(JxlTest, LosslessSmallFewColors) {
class JxlTest : public ::testing::TestWithParam<const char*> {};
TEST_P(JxlTest, LosslessSmallFewColors) {
ThreadPoolForTests pool(8);
const PaddedBytes orig =
jxl::test::ReadTestData("jxl/blending/cropped_traffic_light_frame-0.png");
const PaddedBytes orig = jxl::test::ReadTestData(GetParam());
TestImage t;
t.DecodeFromBytes(orig).ClearMetadata();
@ -1586,9 +1587,14 @@ TEST(JxlTest, LosslessSmallFewColors) {
cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 1);
PackedPixelFile ppf_out;
EXPECT_NEAR(Roundtrip(t.ppf(), cparams, {}, &pool, &ppf_out), 563, 30);
Roundtrip(t.ppf(), cparams, {}, &pool, &ppf_out);
EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}
JXL_GTEST_INSTANTIATE_TEST_SUITE_P(
ImageTests, JxlTest,
::testing::Values("jxl/blending/cropped_traffic_light_frame-0.png",
"palette/358colors.png"));
} // namespace
} // namespace jxl

View File

@ -10,10 +10,10 @@
namespace jxl {
void SetIntensityTarget(ImageMetadata* m) {
if (m->color_encoding.tf.IsPQ()) {
if (m->color_encoding.Tf().IsPQ()) {
// Peak luminance of PQ as defined by SMPTE ST 2084:2014.
m->SetIntensityTarget(10000);
} else if (m->color_encoding.tf.IsHLG()) {
} else if (m->color_encoding.Tf().IsHLG()) {
// Nominal display peak luminance used as a reference by
// Rec. ITU-R BT.2100-2.
m->SetIntensityTarget(1000);

View File

@ -369,7 +369,7 @@ TEST(ModularTest, RoundtripLosslessCustomFloat) {
io.metadata.m.bit_depth.floating_point_sample = true;
io.metadata.m.modular_16_bit_buffer_sufficient = false;
ColorEncoding color_encoding;
color_encoding.tf.SetTransferFunction(TransferFunction::kLinear);
color_encoding.Tf().SetTransferFunction(TransferFunction::kLinear);
color_encoding.SetColorSpace(ColorSpace::kRGB);
Image3F testimage(xsize, ysize);
float factor = 1.f / (1 << 14);

View File

@ -151,21 +151,21 @@ std::unique_ptr<FromLinearStage<Op>> MakeFromLinearStage(Op&& op) {
std::unique_ptr<RenderPipelineStage> GetFromLinearStage(
const OutputEncodingInfo& output_encoding_info) {
if (output_encoding_info.color_encoding.tf.IsLinear()) {
const auto& tf = output_encoding_info.color_encoding.Tf();
if (tf.IsLinear()) {
return MakeFromLinearStage(MakePerChannelOp(OpLinear()));
} else if (output_encoding_info.color_encoding.tf.IsSRGB()) {
} else if (tf.IsSRGB()) {
return MakeFromLinearStage(MakePerChannelOp(OpRgb()));
} else if (output_encoding_info.color_encoding.tf.IsPQ()) {
} else if (tf.IsPQ()) {
return MakeFromLinearStage(
MakePerChannelOp(OpPq(output_encoding_info.orig_intensity_target)));
} else if (output_encoding_info.color_encoding.tf.IsHLG()) {
} else if (tf.IsHLG()) {
return MakeFromLinearStage(
OpHlg(output_encoding_info.luminances,
output_encoding_info.desired_intensity_target));
} else if (output_encoding_info.color_encoding.tf.Is709()) {
} else if (tf.Is709()) {
return MakeFromLinearStage(MakePerChannelOp(Op709()));
} else if (output_encoding_info.color_encoding.tf.IsGamma() ||
output_encoding_info.color_encoding.tf.IsDCI()) {
} else if (tf.have_gamma || tf.IsDCI()) {
return MakeFromLinearStage(
MakePerChannelOp(OpGamma{output_encoding_info.inverse_gamma}));
} else {

View File

@ -162,20 +162,20 @@ std::unique_ptr<ToLinearStage<Op>> MakeToLinearStage(Op&& op) {
std::unique_ptr<RenderPipelineStage> GetToLinearStage(
const OutputEncodingInfo& output_encoding_info) {
if (output_encoding_info.color_encoding.tf.IsLinear()) {
const auto& tf = output_encoding_info.color_encoding.Tf();
if (tf.IsLinear()) {
return MakeToLinearStage(MakePerChannelOp(OpLinear()));
} else if (output_encoding_info.color_encoding.tf.IsSRGB()) {
} else if (tf.IsSRGB()) {
return MakeToLinearStage(MakePerChannelOp(OpRgb()));
} else if (output_encoding_info.color_encoding.tf.IsPQ()) {
} else if (tf.IsPQ()) {
return MakeToLinearStage(
MakePerChannelOp(OpPq(output_encoding_info.orig_intensity_target)));
} else if (output_encoding_info.color_encoding.tf.IsHLG()) {
} else if (tf.IsHLG()) {
return MakeToLinearStage(OpHlg(output_encoding_info.luminances,
output_encoding_info.orig_intensity_target));
} else if (output_encoding_info.color_encoding.tf.Is709()) {
} else if (tf.Is709()) {
return MakeToLinearStage(MakePerChannelOp(Op709()));
} else if (output_encoding_info.color_encoding.tf.IsGamma() ||
output_encoding_info.color_encoding.tf.IsDCI()) {
} else if (tf.have_gamma || tf.IsDCI()) {
return MakeToLinearStage(
MakePerChannelOp(OpGamma{1.f / output_encoding_info.inverse_gamma}));
} else {

View File

@ -28,9 +28,9 @@ class ToneMappingStage : public RenderPipelineStage {
// No tone mapping requested.
return;
}
if (output_encoding_info_.orig_color_encoding.tf.IsPQ() &&
output_encoding_info_.desired_intensity_target <
output_encoding_info_.orig_intensity_target) {
const auto& tf = output_encoding_info_.orig_color_encoding.Tf();
if (tf.IsPQ() && output_encoding_info_.desired_intensity_target <
output_encoding_info_.orig_intensity_target) {
tone_mapper_ = jxl::make_unique<ToneMapper>(
/*source_range=*/std::pair<float, float>(
0, output_encoding_info_.orig_intensity_target),
@ -38,16 +38,14 @@ class ToneMappingStage : public RenderPipelineStage {
std::pair<float, float>(
0, output_encoding_info_.desired_intensity_target),
output_encoding_info_.luminances);
} else if (output_encoding_info_.orig_color_encoding.tf.IsHLG() &&
!output_encoding_info_.color_encoding.tf.IsHLG()) {
} else if (tf.IsHLG() && !tf.IsHLG()) {
hlg_ootf_ = jxl::make_unique<HlgOOTF>(
/*source_luminance=*/output_encoding_info_.orig_intensity_target,
/*target_luminance=*/output_encoding_info_.desired_intensity_target,
output_encoding_info_.luminances);
}
if (output_encoding_info_.color_encoding.tf.IsPQ() &&
(tone_mapper_ || hlg_ootf_)) {
if (tf.IsPQ() && (tone_mapper_ || hlg_ootf_)) {
to_intensity_target_ =
10000.f / output_encoding_info_.orig_intensity_target;
from_desired_intensity_target_ =

View File

@ -123,9 +123,9 @@ ColorEncoding ColorEncodingFromDescriptor(const ColorEncodingDescriptor& desc) {
if (desc.color_space != ColorSpace::kGray) {
JXL_CHECK(c.SetPrimariesType(desc.primaries));
}
c.tf.SetTransferFunction(desc.tf);
c.Tf().SetTransferFunction(desc.tf);
}
JXL_CHECK(c.SetRenderingIntent(desc.rendering_intent));
c.SetRenderingIntent(desc.rendering_intent);
JXL_CHECK(c.CreateICC());
return c;
}
@ -139,7 +139,7 @@ void CheckSameEncodings(const std::vector<ColorEncoding>& a,
for (size_t i = 0; i < a.size(); ++i) {
if ((a[i].ICC() == b[i].ICC()) ||
((a[i].GetPrimariesType() == b[i].GetPrimariesType()) &&
a[i].tf.IsSame(b[i].tf))) {
a[i].Tf().IsSame(b[i].Tf()))) {
continue;
}
failures << "CheckSameEncodings " << check_name << ": " << i
@ -233,30 +233,19 @@ size_t Roundtrip(const extras::PackedPixelFile& ppf_in,
std::vector<ColorEncodingDescriptor> AllEncodings() {
std::vector<ColorEncodingDescriptor> all_encodings;
all_encodings.reserve(300);
ColorEncoding c;
for (ColorSpace cs : Values<ColorSpace>()) {
if (cs == ColorSpace::kUnknown || cs == ColorSpace::kXYB) continue;
c.SetColorSpace(cs);
if (cs == ColorSpace::kUnknown || cs == ColorSpace::kXYB ||
cs == ColorSpace::kGray) {
continue;
}
for (WhitePoint wp : Values<WhitePoint>()) {
if (wp == WhitePoint::kCustom) continue;
if (c.ImplicitWhitePoint() && c.GetWhitePointType() != wp) continue;
JXL_CHECK(c.SetWhitePointType(wp));
for (Primaries primaries : Values<Primaries>()) {
if (primaries == Primaries::kCustom) continue;
if (!c.HasPrimaries()) continue;
JXL_CHECK(c.SetPrimariesType(primaries));
for (TransferFunction tf : Values<TransferFunction>()) {
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<RenderingIntent>()) {
ColorEncodingDescriptor cdesc;
cdesc.color_space = cs;