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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- PDF functions reject non-finite values and invalid exponential domains,
and interpolate large finite intervals without intermediate overflow.

- PDF AES decryption validates every padding byte before removing a trailer,
preserving data with malformed padding.

Expand Down
18 changes: 14 additions & 4 deletions src/odr/internal/pdf/pdf_color.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <algorithm>
#include <cmath>
#include <stdexcept>

namespace odr::internal::pdf {

Expand Down Expand Up @@ -134,13 +135,22 @@ ColorSpaceDef::to_rgb(const std::span<const double> c) const {
}
case ColorSpaceKind::separation:
case ColorSpaceKind::device_n: {
if (tint == nullptr || alternate == nullptr) {
// Without the tint transform, approximate a Separation as additive ink
// over white (1 = full colorant -> black).
// Without a usable tint transform, approximate a Separation as additive
// ink over white (1 = full colorant -> black).
const auto ink = [&]() -> std::array<double, 3> {
const double v = clamp01(1 - at(0));
return {v, v, v};
};
if (tint == nullptr || alternate == nullptr) {
return ink();
}
try {
return alternate->to_rgb(tint->eval({c.begin(), c.end()}));
} catch (const std::invalid_argument &) {
return ink();
} catch (const std::runtime_error &) {
return ink();
}
return alternate->to_rgb(tint->eval({c.begin(), c.end()}));
}
case ColorSpaceKind::pattern:
// An uncoloured pattern (`/PaintType 2`) carries its colour in the Pattern
Expand Down
45 changes: 33 additions & 12 deletions src/odr/internal/pdf/pdf_function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ namespace odr::internal::pdf {

namespace {

bool finite_values(const std::span<const double> values) {
return std::ranges::all_of(
values, [](const double value) { return std::isfinite(value); });
}

double clamp(const double v, const double lo, const double hi) {
return std::clamp(v, std::min(lo, hi), std::max(lo, hi));
}
Expand All @@ -29,7 +34,10 @@ double interpolate(const double x, const double a0, const double a1,
if (a1 == a0) {
return b0;
}
return b0 + (x - a0) * (b1 - b0) / (a1 - a0);
const double width = a1 - a0;
const double t = std::isfinite(width) ? (x - a0) / width
: (x / 2 - a0 / 2) / (a1 / 2 - a0 / 2);
return std::lerp(b0, b1, t);
}

// --- type 2: exponential interpolation (ISO 32000-1 7.10.3) ----------------
Expand All @@ -49,7 +57,13 @@ class ExponentialFunction final : public Function {
const std::size_t count = m_c0.size();
std::vector<double> out(count);
for (std::size_t j = 0; j < count; ++j) {
out[j] = m_c0[j] + xn * (m_c1[j] - m_c0[j]);
if (m_c0[j] == m_c1[j]) {
out[j] = m_c0[j];
} else if (std::isinf(xn)) {
out[j] = m_c1[j] > m_c0[j] ? xn : -xn;
} else {
out[j] = std::lerp(m_c0[j], m_c1[j], xn);
}
}
return out;
}
Expand Down Expand Up @@ -161,8 +175,8 @@ class SampledFunction final : public Function {
const double max_value = std::ldexp(1.0, m_bits) - 1.0;
for (std::size_t j = 0; j < n; ++j) {
const std::size_t d = 2 * j;
out[j] =
interpolate(out[j], 0.0, max_value, m_decode[d], m_decode[d + 1]);
out[j] = interpolate(clamp(out[j], 0.0, max_value), 0.0, max_value,
m_decode[d], m_decode[d + 1]);
}
return out;
}
Expand Down Expand Up @@ -582,6 +596,9 @@ std::vector<double> Function::eval(std::vector<double> in) const {
const std::size_t m = input_arity();
in.resize(m, 0.0);
for (std::size_t i = 0; i < m; ++i) {
if (!std::isfinite(in[i])) {
throw std::invalid_argument("non-finite PDF function input");
}
const std::size_t d = 2 * i;
in[i] = clamp(in[i], m_domain[d], m_domain[d + 1]);
}
Expand All @@ -594,6 +611,9 @@ std::vector<double> Function::eval(std::vector<double> in) const {
out[j] = clamp(out[j], m_range[d], m_range[d + 1]);
}
}
if (!finite_values(out)) {
throw std::runtime_error("non-finite PDF function result");
}
return out;
}

Expand Down Expand Up @@ -654,7 +674,12 @@ std::shared_ptr<Function> parse_function_impl(const Object &object,
c1.resize(outputs);
if (domain.size() != 2 ||
(!range.empty() && range.size() / 2 != c0.size()) ||
!std::isfinite(n)) {
!finite_values(c0) || !finite_values(c1) || !std::isfinite(n)) {
return nullptr;
}
// ISO 32000-1 7.10.3 constrains the domain for fractional/negative N.
if ((std::trunc(n) != n && domain[0] < 0) ||
(n < 0 && domain[0] <= 0 && domain[1] >= 0)) {
return nullptr;
}
return std::make_shared<ExponentialFunction>(
Expand Down Expand Up @@ -686,8 +711,7 @@ std::shared_ptr<Function> parse_function_impl(const Object &object,
}
previous = bound;
}
if (!std::ranges::all_of(encode,
[](const double v) { return std::isfinite(v); })) {
if (!finite_values(encode)) {
return nullptr;
}
return std::make_shared<StitchingFunction>(
Expand Down Expand Up @@ -733,11 +757,8 @@ std::shared_ptr<Function> parse_function_impl(const Object &object,
constexpr std::size_t max_inputs = 8;
if (size.empty() || size.size() > max_inputs || range.empty() ||
domain.size() != 2 * size.size() || encode.size() != domain.size() ||
decode.size() != range.size() ||
!std::ranges::all_of(encode,
[](const double v) { return std::isfinite(v); }) ||
!std::ranges::all_of(decode,
[](const double v) { return std::isfinite(v); })) {
decode.size() != range.size() || !finite_values(encode) ||
!finite_values(decode)) {
return nullptr;
}
std::size_t sample_bits = range.size() / 2;
Expand Down
2 changes: 1 addition & 1 deletion src/odr/internal/pdf/pdf_function.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Function {
public:
virtual ~Function() = default;

/// Evaluate after padding or truncating `in` to `input_arity` values.
/// Pad or truncate to `input_arity`; reject non-finite inputs and results.
[[nodiscard]] std::vector<double> eval(std::vector<double> in) const;

[[nodiscard]] std::size_t input_arity() const { return m_domain.size() / 2; }
Expand Down
19 changes: 14 additions & 5 deletions src/odr/internal/pdf/pdf_shading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <algorithm>
#include <cstddef>
#include <stdexcept>

namespace odr::internal::pdf {

Expand Down Expand Up @@ -121,11 +122,19 @@ pdf::parse_shading(const Object &object, const ShadingContext &context,
const double t0 = shading->domain[0];
const double t1 = shading->domain[1];
shading->stops.reserve(sample_count);
for (std::size_t i = 0; i < sample_count; ++i) {
const double f = static_cast<double>(i) / (sample_count - 1);
const double t = t0 + (t1 - t0) * f;
shading->stops.push_back(
GradientStop{f, color_space->to_rgb(eval_components(functions, t))});
// a function without a finite answer leaves the shading unusable, like an
// unsupported one
try {
for (std::size_t i = 0; i < sample_count; ++i) {
const double f = static_cast<double>(i) / (sample_count - 1);
const double t = t0 + (t1 - t0) * f;
shading->stops.push_back(
GradientStop{f, color_space->to_rgb(eval_components(functions, t))});
}
} catch (const std::invalid_argument &) {
return nullptr;
} catch (const std::runtime_error &) {
return nullptr;
}

const std::vector<double> background =
Expand Down
18 changes: 18 additions & 0 deletions test/src/internal/pdf/pdf_color.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ TEST(PdfColor, separation_tint_transform) {
EXPECT_NEAR(half[2], 0.5, 1e-9);
}

// A tint transform without a finite answer paints as the ink approximation
// rather than aborting the page.
TEST(PdfColor, separation_non_finite_tint_approximates_ink) {
Dictionary tint;
tint["FunctionType"] = Object(Integer{2});
tint["Domain"] = reals({0, 2});
tint["C0"] = reals({0});
tint["C1"] = reals({1});
tint["N"] = Object(Real{2048}); // 2^2048 overflows

std::vector<Object> array{Object(Name{"Separation"}), Object(Name{"Spot"}),
Object(Name{"DeviceGray"}), Object(tint)};
const auto def =
parse_color_space(Object(Array(std::move(array))), context());
ASSERT_NE(def, nullptr);
EXPECT_EQ(to_rgb(*def, {2.0}), (std::array<double, 3>{0, 0, 0}));
}

// A colour space's initial component values (ISO 32000-1 8.6.3): zero for the
// device families, full tint for Separation/DeviceN.
TEST(PdfColor, initial_components) {
Expand Down
64 changes: 64 additions & 0 deletions test/src/internal/pdf/pdf_function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

#include <array>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -204,6 +206,8 @@ TEST(PdfFunction, malformed_layout_is_null) {
{"/FunctionType 3 /Domain [0] /Functions []",
"/FunctionType 2 /Domain [0 1 2] /N 1",
"/FunctionType 2 /Domain [1 0] /N 1",
"/FunctionType 2 /Domain [-1 1] /N 0.5",
"/FunctionType 2 /Domain [0 1] /N -1",
"/FunctionType 2 /Domain [0 1] /Range [0] /N 1",
"/FunctionType 4294967298 /Domain [0 1] /N 1",
"/FunctionType 3 /Domain [0 1] /Functions ["
Expand Down Expand Up @@ -331,3 +335,63 @@ TEST(PdfFunction, calculator_instruction_limit) {
ASSERT_NE(fn, nullptr);
EXPECT_EQ(fn->eval({0.5}), std::vector<double>{0});
}

TEST(PdfFunction, interpolation_avoids_intermediate_overflow) {
const double limit = std::numeric_limits<double>::max();
Dictionary sampled;
sampled["FunctionType"] = Object(Integer{0});
sampled["Domain"] = reals({-limit, limit});
sampled["Range"] = reals({-limit, limit});
sampled["Size"] = integers({2});
sampled["BitsPerSample"] = Object(Integer{8});
const auto grid =
parse_function(Object(sampled), context(std::string("\0\xff", 2)));
ASSERT_NE(grid, nullptr);
EXPECT_DOUBLE_EQ(grid->eval({0})[0], 0);
EXPECT_DOUBLE_EQ(grid->eval({-limit})[0], -limit);
EXPECT_DOUBLE_EQ(grid->eval({limit})[0], limit);
EXPECT_THROW(std::ignore =
grid->eval({std::numeric_limits<double>::quiet_NaN()}),
std::invalid_argument);

Dictionary exponential;
exponential["FunctionType"] = Object(Integer{2});
exponential["Domain"] = reals({0, 1});
exponential["C0"] = reals({-limit});
exponential["C1"] = reals({limit});
exponential["N"] = Object(Real{1});
const auto linear = parse_function(Object(exponential), context());
ASSERT_NE(linear, nullptr);
EXPECT_DOUBLE_EQ(linear->eval({0.5})[0], 0);
}

TEST(PdfFunction, rejects_non_finite_values) {
Dictionary dict;
dict["FunctionType"] = Object(Integer{2});
dict["Domain"] = reals({0, 2});
dict["N"] = Object(Real{2048});
const auto fn = parse_function(Object(dict), context());
ASSERT_NE(fn, nullptr);
for (const double value : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity()}) {
EXPECT_THROW(std::ignore = fn->eval({value}), std::invalid_argument);
for (const char *key : {"C0", "C1"}) {
Dictionary invalid = dict;
invalid[key] = reals({value});
EXPECT_EQ(parse_function(Object(invalid), context()), nullptr);
}
}
EXPECT_THROW(std::ignore = fn->eval({2}), std::runtime_error);
dict["C0"] = reals({1});
const auto constant = parse_function(Object(dict), context());
ASSERT_NE(constant, nullptr);
EXPECT_DOUBLE_EQ(constant->eval({2})[0], 1);
dict["Range"] = reals({0, 1});
for (const double start : {0.0, 1.0}) {
dict["C0"] = reals({start});
dict["C1"] = reals({1 - start});
const auto clipped = parse_function(Object(dict), context());
ASSERT_NE(clipped, nullptr);
EXPECT_DOUBLE_EQ(clipped->eval({2})[0], 1 - start);
}
}
19 changes: 19 additions & 0 deletions test/src/internal/pdf/pdf_shading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,22 @@ TEST(PdfShading, bad_function_is_null) {
dict["Function"] = Object(fn);
EXPECT_EQ(parse_shading(Object(dict), context(), color_context()), nullptr);
}

// A tint function without a finite answer at a sample makes the shading null
// rather than aborting the page.
TEST(PdfShading, non_finite_function_is_null) {
Dictionary fn;
fn["FunctionType"] = Object(Integer{2});
fn["Domain"] = reals({0, 2});
fn["C0"] = reals({0, 0, 0});
fn["C1"] = reals({1, 1, 1});
fn["N"] = Object(Real{2048}); // 2^2048 overflows

Dictionary dict;
dict["ShadingType"] = Object(Integer{2});
dict["ColorSpace"] = Object(Name{"DeviceRGB"});
dict["Coords"] = reals({0, 0, 1, 0});
dict["Domain"] = reals({0, 2});
dict["Function"] = Object(fn);
EXPECT_EQ(parse_shading(Object(dict), context(), color_context()), nullptr);
}
Loading