Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ set(ODR_SOURCE_FILES
"src/odr/internal/text/text_file.cpp"

"src/odr/internal/util/byte_util.cpp"
"src/odr/internal/util/color_util.cpp"
"src/odr/internal/util/byte_stream_util.cpp"
"src/odr/internal/util/byte_string.cpp"
"src/odr/internal/util/file_util.cpp"
Expand Down
67 changes: 14 additions & 53 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <odr/internal/abstract/file.hpp>
#include <odr/internal/crypto/crypto_util.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/util/color_util.hpp>
#include <odr/internal/util/stream_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/xml/xml_util.hpp>
Expand Down Expand Up @@ -360,49 +361,6 @@ std::string html::color(const Color &color) {

namespace {

struct Oklab {
double l{0};
double a{0};
double b{0};
};

double to_linear(const std::uint8_t channel) {
const double c = channel / 255.0;
return c <= 0.04045 ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4);
}

double from_linear(const double c) {
return c <= 0.0031308 ? c * 12.92 : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
}

Oklab to_oklab(const Color &color) {
const double r = to_linear(color.red);
const double g = to_linear(color.green);
const double b = to_linear(color.blue);
const double l =
std::cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const double m =
std::cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const double s =
std::cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
return {0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s};
}

/// Linear srgb, possibly outside the gamut.
std::array<double, 3> to_linear_rgb(const Oklab &lab) {
const double l =
std::pow(lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b, 3);
const double m =
std::pow(lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b, 3);
const double s =
std::pow(lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b, 3);
return {4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s};
}

bool in_gamut(const std::array<double, 3> &rgb) {
return std::ranges::all_of(
rgb, [](const double c) { return c >= -1e-9 && c <= 1 + 1e-9; });
Expand All @@ -413,31 +371,34 @@ bool in_gamut(const std::array<double, 3> &rgb) {
Color html::dark_fill(const Color &color) {
// White lands on the page of `document-dark.css`, black on the lightest
// ground its `#e6edf3` text reads on at 4.5:1.
static const Oklab page = to_oklab(Color(0x16, 0x1b, 0x22));
static const util::color::Oklab page =
util::color::Oklab::from_color(Color(0x16, 0x1b, 0x22));
static constexpr double lightest = 0.5;

const Oklab source = to_oklab(color);
const util::color::Oklab source = util::color::Oklab::from_color(color);
const double t = 1 - source.l;
Oklab lab{page.l + t * (lightest - page.l), page.a * (1 - t) + source.a,
page.b * (1 - t) + source.b};
util::color::Oklab lab{page.l + t * (lightest - page.l),
page.a * (1 - t) + source.a,
page.b * (1 - t) + source.b};

// Less chroma until the color fits, so the hue stays.
double low = 0;
double high = 1;
if (!in_gamut(to_linear_rgb(lab))) {
if (!in_gamut(lab.to_linear_rgb())) {
for (int i = 0; i < 20; ++i) {
const double mid = (low + high) / 2;
(in_gamut(to_linear_rgb({lab.l, lab.a * mid, lab.b * mid})) ? low
: high) = mid;
(in_gamut(
util::color::Oklab{lab.l, lab.a * mid, lab.b * mid}.to_linear_rgb())
? low
: high) = mid;
}
lab.a *= low;
lab.b *= low;
}

const std::array<double, 3> rgb = to_linear_rgb(lab);
const std::array<double, 3> rgb = lab.to_linear_rgb();
const auto channel = [](const double c) {
return static_cast<std::uint8_t>(
std::lround(std::clamp(from_linear(c), 0.0, 1.0) * 255));
return util::color::to_byte(util::color::linear_to_srgb(c));
};
return {channel(rgb[0]), channel(rgb[1]), channel(rgb[2]), color.alpha};
}
Expand Down
24 changes: 10 additions & 14 deletions src/odr/internal/html/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <odr/internal/pdf/pdf_document_parser.hpp>
#include <odr/internal/pdf/pdf_file.hpp>
#include <odr/internal/pdf/pdf_page_extractor.hpp>
#include <odr/internal/util/color_util.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/xml/xml_util.hpp>
Expand Down Expand Up @@ -291,12 +292,6 @@ void write_page_links(HtmlWriter &out, const std::vector<LinkOut> &links) {
}
}

/// Clamp a colour component in [0, 1] to an 8-bit channel value.
std::int32_t to255(const double v) {
return static_cast<std::int32_t>(
std::lround(std::clamp(v, 0.0, 1.0) * 255.0));
}

/// A PDF device color as CSS `rgb(...)`. Other spaces are already converted at
/// extract time; only `unknown` reaches here, falling back to black.
std::string device_color_to_css(const pdf::GraphicsState::Color &color) {
Expand All @@ -305,19 +300,19 @@ std::string device_color_to_css(const pdf::GraphicsState::Color &color) {
std::int32_t b = 0;
switch (color.space) {
case pdf::ColorSpace::device_grey:
r = g = b = to255(color.grey);
r = g = b = util::color::to_byte(color.grey);
break;
case pdf::ColorSpace::device_rgb:
r = to255(color.rgb[0]);
g = to255(color.rgb[1]);
b = to255(color.rgb[2]);
r = util::color::to_byte(color.rgb[0]);
g = util::color::to_byte(color.rgb[1]);
b = util::color::to_byte(color.rgb[2]);
break;
case pdf::ColorSpace::device_cmyk: {
const std::array<double, 3> rgb = pdf::cmyk_to_rgb(
color.cmyk[0], color.cmyk[1], color.cmyk[2], color.cmyk[3]);
r = to255(rgb[0]);
g = to255(rgb[1]);
b = to255(rgb[2]);
r = util::color::to_byte(rgb[0]);
g = util::color::to_byte(rgb[1]);
b = util::color::to_byte(rgb[2]);
break;
}
case pdf::ColorSpace::unknown:
Expand All @@ -332,7 +327,8 @@ std::string device_color_to_css(const pdf::GraphicsState::Color &color) {
/// `rgb(...)`.
std::string rgb_to_css(const std::array<double, 3> &rgb) {
std::ostringstream s;
s << "rgb(" << to255(rgb[0]) << ',' << to255(rgb[1]) << ',' << to255(rgb[2])
s << "rgb(" << +util::color::to_byte(rgb[0]) << ','
<< +util::color::to_byte(rgb[1]) << ',' << +util::color::to_byte(rgb[2])
<< ')';
return std::move(s).str();
}
Expand Down
48 changes: 5 additions & 43 deletions src/odr/internal/ooxml/ooxml_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
#include <odr/internal/abstract/filesystem.hpp>
#include <odr/internal/common/path.hpp>
#include <odr/internal/html/common.hpp>
#include <odr/internal/util/color_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/xml/xml_util.hpp>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
Expand Down Expand Up @@ -220,48 +220,10 @@ ooxml::read_drawing_rgb_color(const pugi::xml_node parent) {
}

Color ooxml::apply_tint(const Color &color, const double tint) {
const double r = color.red / 255.0;
const double g = color.green / 255.0;
const double b = color.blue / 255.0;
const double max = std::max({r, g, b});
const double min = std::min({r, g, b});
double hue = 0;
double saturation = 0;
double lightness = (max + min) / 2;
if (max != min) {
const double d = max - min;
saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max == r) {
hue = (g - b) / d + (g < b ? 6 : 0);
} else if (max == g) {
hue = (b - r) / d + 2;
} else {
hue = (r - g) / d + 4;
}
hue /= 6;
}

lightness = tint < 0 ? lightness * (1 + tint) : lightness * (1 - tint) + tint;

const double q = lightness < 0.5
? lightness * (1 + saturation)
: lightness + saturation - lightness * saturation;
const double p = 2 * lightness - q;
const auto channel = [&](double t) {
t -= std::floor(t);
double c = p;
if (t < 1.0 / 6) {
c = p + (q - p) * 6 * t;
} else if (t < 1.0 / 2) {
c = q;
} else if (t < 2.0 / 3) {
c = p + (q - p) * (2.0 / 3 - t) * 6;
}
return static_cast<std::uint8_t>(
std::lround(std::clamp(c, 0.0, 1.0) * 255));
};
return {channel(hue + 1.0 / 3), channel(hue), channel(hue - 1.0 / 3),
color.alpha};
util::color::Hsl hsl = util::color::Hsl::from_color(color);
hsl.lightness =
tint < 0 ? hsl.lightness * (1 + tint) : hsl.lightness * (1 - tint) + tint;
return hsl.to_color(color.alpha);
}

std::optional<Measure>
Expand Down
1 change: 1 addition & 0 deletions src/odr/internal/pdf/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ above. So each run is raised by one font ascent:
| `pdf_writer.*` | `IncrementalWriter`: copies the source through and appends the changed objects under their own xref in the file's flavour. Refuses a recovered or encrypted file |
| `pdf_annotation.*` | The markup and ink annotations and their appearance streams. Only the highlight blends Multiply (11.6.4.1) |
| `util/math_util.hpp` | `util::math::Transform2D`, PDF row-vector convention |
| `util/color_util.*` | `util::color`: the sRGB transfer, a channel to a byte, HSL and Oklab |

Consumers outside the module: `open_strategy.cpp` and `html/pdf_file.cpp`
(`create_pdf_service`; the per-font PUA re-encode, OTF wrap and `@font-face`;
Expand Down
5 changes: 2 additions & 3 deletions src/odr/internal/pdf/pdf_color.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <odr/internal/pdf/pdf_color.hpp>

#include <odr/internal/pdf/pdf_object.hpp>
#include <odr/internal/util/color_util.hpp>

#include <algorithm>
#include <cmath>
Expand All @@ -11,10 +12,8 @@ namespace {

double clamp01(const double v) { return std::clamp(v, 0.0, 1.0); }

/// sRGB gamma encode of a linear component (IEC 61966-2-1).
double linear_to_srgb(const double c) {
const double v = clamp01(c);
return v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1 / 2.4) - 0.055;
return util::color::linear_to_srgb(clamp01(c));
}

/// CIE L*a*b* -> sRGB through XYZ (ISO 32000-1 8.6.5.4), under the space's
Expand Down
28 changes: 13 additions & 15 deletions src/odr/internal/pdf/pdf_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <odr/internal/pdf/pdf_object.hpp>
#include <odr/internal/png/png_util.hpp>
#include <odr/internal/util/byte_string.hpp>
#include <odr/internal/util/color_util.hpp>

#include <algorithm>
#include <array>
Expand Down Expand Up @@ -54,11 +55,6 @@ class BitReader {
std::int32_t m_bit{0};
};

std::uint8_t to_byte(const double v) {
const double scaled = std::lround(std::clamp(v, 0.0, 1.0) * 255.0);
return static_cast<std::uint8_t>(scaled);
}

/// Undo the premultiplication `/SMaskInData 2` declares, leaving the straight
/// colour a PNG carries.
void unpremultiply(std::string &samples, const std::int32_t components,
Expand All @@ -72,8 +68,10 @@ void unpremultiply(std::string &samples, const std::int32_t components,
return;
}
const auto value = static_cast<std::uint8_t>(samples[i]);
samples[i] = static_cast<char>(
a == 0 ? 0 : to_byte(std::min(1.0, value / static_cast<double>(a))));
samples[i] =
static_cast<char>(a == 0 ? 0
: util::color::to_byte(std::min(
1.0, value / static_cast<double>(a))));
}
}
}
Expand Down Expand Up @@ -188,7 +186,7 @@ std::string pdf::encode_image_png(const std::string &samples,
const std::array<double, 1> value{component_value(sample, 0)};
const std::array<double, 3> rgb = color_space.to_rgb(value);
for (const double c : rgb) {
palette.push_back(static_cast<char>(to_byte(c)));
palette.push_back(static_cast<char>(util::color::to_byte(c)));
}
}
const std::size_t size = row_bytes * static_cast<std::size_t>(height);
Expand Down Expand Up @@ -217,9 +215,9 @@ std::string pdf::encode_image_png(const std::string &samples,
component_values[k] = component_value(sample, k);
}
const std::array<double, 3> pixel = color_space.to_rgb(component_values);
out[out_index++] = static_cast<char>(to_byte(pixel[0]));
out[out_index++] = static_cast<char>(to_byte(pixel[1]));
out[out_index++] = static_cast<char>(to_byte(pixel[2]));
out[out_index++] = static_cast<char>(util::color::to_byte(pixel[0]));
out[out_index++] = static_cast<char>(util::color::to_byte(pixel[1]));
out[out_index++] = static_cast<char>(util::color::to_byte(pixel[2]));
if (has_alpha) {
std::uint8_t a =
alpha.size() == pixel_count ? alpha[pixel_index] : 0xFF;
Expand Down Expand Up @@ -281,7 +279,7 @@ std::vector<std::uint8_t> pdf::decode_mask_alpha(
native[i++] = value >= 0.5 ? 0x00 : 0xFF;
} else {
// A soft mask: the grey level is the coverage directly.
native[i++] = to_byte(value);
native[i++] = util::color::to_byte(value);
}
}
}
Expand Down Expand Up @@ -316,9 +314,9 @@ std::string pdf::encode_stencil_png(const std::string &samples,
// of [1 0] swaps that — paint when the decoded value rounds to 0.
const bool invert = decode.size() >= 2 && decode[0] > decode[1];

const std::uint8_t r = to_byte(color[0]);
const std::uint8_t g = to_byte(color[1]);
const std::uint8_t b = to_byte(color[2]);
const std::uint8_t r = util::color::to_byte(color[0]);
const std::uint8_t g = util::color::to_byte(color[1]);
const std::uint8_t b = util::color::to_byte(color[2]);

std::string rgba;
rgba.resize(static_cast<std::size_t>(width) *
Expand Down
Loading
Loading