diff --git a/CHANGELOG.md b/CHANGELOG.md index e26f3f515..9c91a748d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/odr/internal/pdf/pdf_color.cpp b/src/odr/internal/pdf/pdf_color.cpp index bb48b889e..db147c6f1 100644 --- a/src/odr/internal/pdf/pdf_color.cpp +++ b/src/odr/internal/pdf/pdf_color.cpp @@ -5,6 +5,7 @@ #include #include +#include namespace odr::internal::pdf { @@ -134,13 +135,22 @@ ColorSpaceDef::to_rgb(const std::span 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 { 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 diff --git a/src/odr/internal/pdf/pdf_function.cpp b/src/odr/internal/pdf/pdf_function.cpp index 2dc476b9d..29803b666 100644 --- a/src/odr/internal/pdf/pdf_function.cpp +++ b/src/odr/internal/pdf/pdf_function.cpp @@ -17,6 +17,11 @@ namespace odr::internal::pdf { namespace { +bool finite_values(const std::span 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)); } @@ -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) ---------------- @@ -49,7 +57,13 @@ class ExponentialFunction final : public Function { const std::size_t count = m_c0.size(); std::vector 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; } @@ -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; } @@ -582,6 +596,9 @@ std::vector Function::eval(std::vector 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]); } @@ -594,6 +611,9 @@ std::vector Function::eval(std::vector 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; } @@ -654,7 +674,12 @@ std::shared_ptr 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( @@ -686,8 +711,7 @@ std::shared_ptr 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( @@ -733,11 +757,8 @@ std::shared_ptr 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; diff --git a/src/odr/internal/pdf/pdf_function.hpp b/src/odr/internal/pdf/pdf_function.hpp index a28d8bb00..766a63bdd 100644 --- a/src/odr/internal/pdf/pdf_function.hpp +++ b/src/odr/internal/pdf/pdf_function.hpp @@ -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 eval(std::vector in) const; [[nodiscard]] std::size_t input_arity() const { return m_domain.size() / 2; } diff --git a/src/odr/internal/pdf/pdf_shading.cpp b/src/odr/internal/pdf/pdf_shading.cpp index b4958516b..fdd683576 100644 --- a/src/odr/internal/pdf/pdf_shading.cpp +++ b/src/odr/internal/pdf/pdf_shading.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace odr::internal::pdf { @@ -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(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(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 background = diff --git a/test/src/internal/pdf/pdf_color.cpp b/test/src/internal/pdf/pdf_color.cpp index 8e581c57e..4742b1332 100644 --- a/test/src/internal/pdf/pdf_color.cpp +++ b/test/src/internal/pdf/pdf_color.cpp @@ -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 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{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) { diff --git a/test/src/internal/pdf/pdf_function.cpp b/test/src/internal/pdf/pdf_function.cpp index e59bfe78e..63d5356a4 100644 --- a/test/src/internal/pdf/pdf_function.cpp +++ b/test/src/internal/pdf/pdf_function.cpp @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include #include @@ -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 [" @@ -331,3 +335,63 @@ TEST(PdfFunction, calculator_instruction_limit) { ASSERT_NE(fn, nullptr); EXPECT_EQ(fn->eval({0.5}), std::vector{0}); } + +TEST(PdfFunction, interpolation_avoids_intermediate_overflow) { + const double limit = std::numeric_limits::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::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::quiet_NaN(), + std::numeric_limits::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); + } +} diff --git a/test/src/internal/pdf/pdf_shading.cpp b/test/src/internal/pdf/pdf_shading.cpp index c9d61c75f..99abb7951 100644 --- a/test/src/internal/pdf/pdf_shading.cpp +++ b/test/src/internal/pdf/pdf_shading.cpp @@ -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); +}