From bc4c04d4879e4c8ba7a3914acea0a24ab161562e Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 28 Jun 2026 17:24:01 +0200 Subject: [PATCH 1/4] PDF stage 4.9: axial & radial shadings (types 2/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render axial (type 2) and radial (type 3) shadings as SVG gradients, both via the `sh` operator and via `/PatternType 2` shading patterns selected by `scn`. - `pdf_shading.{hpp,cpp}`: `parse_shading` resolves a `/Shading` dictionary, pre-sampling its tint `/Function` across `/Domain` into 32 sRGB colour stops (no function evaluator needed at render time). Types other than 2/3 and malformed shadings return null; `/Extend`, `/Background` and `/BBox` are parsed. - Parser: `parse_resources` now builds the `/Shading` and `/Pattern` resource tables (after `/ColorSpace`, so named colour spaces resolve). A shading pattern resolves its `/Shading`; a tiling pattern is recognized (rendered in 4.10). `GraphicsState::Color` carries the selected `/Pattern` name. - Extractor: `scn` records the pattern name; `paint_path` resolves a shading pattern to `PathElement::fill_shading` + the pattern `/Matrix`; the `sh` operator emits a `ShadingElement` flooding the current clip. - HTML: a `GradientRegistry` emits ``/`` defs with `gradientUnits="userSpaceOnUse"`; a shading-pattern fill paints the path through `fill="url(#…)"`, and `sh` paints a clipped ``. `/Extend` is approximated by SVG's `pad` spread. Tests: shading parsing (axial/radial, domain/extend, background, unsupported type, short coords, bad function) and extractor wiring (shading-pattern fill, unknown pattern, `sh` element, unknown shading). Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 1 + src/odr/internal/html/pdf_file.cpp | 148 ++++++++++++++-- src/odr/internal/pdf/AGENTS.md | 9 + src/odr/internal/pdf/pdf_document_element.hpp | 30 ++++ src/odr/internal/pdf/pdf_document_parser.cpp | 96 +++++++++++ src/odr/internal/pdf/pdf_graphics_state.cpp | 6 + src/odr/internal/pdf/pdf_graphics_state.hpp | 4 + src/odr/internal/pdf/pdf_page_element.hpp | 34 +++- src/odr/internal/pdf/pdf_page_extractor.cpp | 79 +++++++-- src/odr/internal/pdf/pdf_shading.cpp | 153 +++++++++++++++++ src/odr/internal/pdf/pdf_shading.hpp | 68 ++++++++ test/CMakeLists.txt | 1 + test/src/internal/pdf/pdf_page_extractor.cpp | 69 ++++++++ test/src/internal/pdf/pdf_shading.cpp | 160 ++++++++++++++++++ 14 files changed, 825 insertions(+), 33 deletions(-) create mode 100644 src/odr/internal/pdf/pdf_shading.cpp create mode 100644 src/odr/internal/pdf/pdf_shading.hpp create mode 100644 test/src/internal/pdf/pdf_shading.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 807f259f..ba7369e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,7 @@ set(ODR_SOURCE_FILES "src/odr/internal/pdf/pdf_object.cpp" "src/odr/internal/pdf/pdf_object_parser.cpp" "src/odr/internal/pdf/pdf_page_extractor.cpp" + "src/odr/internal/pdf/pdf_shading.cpp" "src/odr/internal/font/cff_builder.cpp" "src/odr/internal/font/cff_font.cpp" diff --git a/src/odr/internal/html/pdf_file.cpp b/src/odr/internal/html/pdf_file.cpp index 10455d4c..b1e96174 100644 --- a/src/odr/internal/html/pdf_file.cpp +++ b/src/odr/internal/html/pdf_file.cpp @@ -79,6 +79,18 @@ std::string device_color_to_css(const pdf::GraphicsState::Color &color) { return std::move(s).str(); } +/// Convert an sRGB triple in [0, 1] (a shading colour stop) to a CSS +/// `rgb(...)`. +std::string rgb_to_css(const std::array &rgb) { + const auto to255 = [](const double v) { + return static_cast(std::lround(std::clamp(v, 0.0, 1.0) * 255.0)); + }; + std::ostringstream s; + s << "rgb(" << to255(rgb[0]) << ',' << to255(rgb[1]) << ',' << to255(rgb[2]) + << ')'; + return std::move(s).str(); +} + /// Build an SVG `d` attribute from a path's subpaths, each point mapped through /// `to_box` (PDF user space -> the page box, y-down). Lines become `L`, cubic /// Béziers `C`, and an explicitly closed subpath ends with `Z`. @@ -117,10 +129,12 @@ std::string svg_path_d(const std::vector &subpaths, /// stroke carries width (CTM-scaled in user space), caps, joins, miter limit /// and the dash pattern. A zero stroke width renders as a thin hairline. /// `clip_id`, when non-empty, references a `` installed via -/// `clip-path`. +/// `clip-path`. `gradient_id`, when non-empty, fills the path with that +/// gradient (a shading pattern) instead of `fill_color`. std::string svg_path_fragment(const pdf::PathElement &path, const util::math::Transform2D &to_box, - const std::string &clip_id) { + const std::string &clip_id, + const std::string &gradient_id) { if ((!path.fill && !path.stroke) || path.subpaths.empty()) { return {}; } @@ -131,7 +145,11 @@ std::string svg_path_fragment(const pdf::PathElement &path, } if (path.fill) { - f << " fill=\"" << device_color_to_css(path.fill_color) << '"'; + if (!gradient_id.empty()) { + f << " fill=\"url(#" << gradient_id << ")\""; + } else { + f << " fill=\"" << device_color_to_css(path.fill_color) << '"'; + } if (path.even_odd) { f << " fill-rule=\"evenodd\""; } @@ -263,6 +281,87 @@ class ClipRegistry { std::ostringstream m_defs; }; +/// Registers a page's shadings (axial/radial) as ``/ +/// `` defs, deduplicating by shading and placement. The +/// shading's pre-sampled colour stops become ``s; `gradientTransform` +/// (shading space -> page box) places the gradient in the page's user space, so +/// referencing elements use `gradientUnits="userSpaceOnUse"`. PDF `/Extend` is +/// approximated by SVG's default `pad` spread (the end stops extend outward). +/// Ids are namespaced per page (`g_`). +class GradientRegistry { +public: + explicit GradientRegistry(std::uint32_t page) : m_page{page} {} + + /// The gradient id to reference via `fill="url(#id)"` for `shading` placed by + /// `m` (shading space -> page box). Empty for an unrepresentable shading. + std::string register_gradient(const pdf::Shading &shading, + const util::math::Transform2D &m) { + if ((shading.type != 2 && shading.type != 3) || shading.stops.empty()) { + return {}; + } + std::ostringstream sig; + sig << shading.type << ':' << static_cast(&shading) << ':' + << m.a << ',' << m.b << ',' << m.c << ',' << m.d << ',' << m.e << ',' + << m.f; + const auto [it, inserted] = m_id_by_signature.try_emplace(sig.str()); + if (!inserted) { + return it->second; + } + it->second = "g" + std::to_string(m_page) + "_" + std::to_string(++m_count); + const std::string &id = it->second; + + const std::array &c = shading.coords; + if (shading.type == 2) { + m_defs << ""; + for (const pdf::GradientStop &stop : shading.stops) { + m_defs << ""; + } + m_defs << (shading.type == 2 ? "" : ""); + return id; + } + + [[nodiscard]] std::string defs() const { return m_defs.str(); } + +private: + std::uint32_t m_page; + std::uint32_t m_count{0}; + std::unordered_map m_id_by_signature; + std::ostringstream m_defs; +}; + +/// Serialize an `sh` shading flood to an SVG `` covering the page box, +/// filled with `gradient_id` and bounded by `clip_id` (the clip in force at +/// `sh` time). Returns "" when the shading produced no gradient. The rect spans +/// the whole page; the clip (and the gradient's own extent) bound the paint. +std::string svg_shading_fragment(const std::string &gradient_id, + const std::string &clip_id, double width, + double height) { + if (gradient_id.empty()) { + return {}; + } + std::ostringstream f; + f << ""; + return std::move(f).str(); +} + /// Deduplicates CSS declarations into atomic, single-property classes. PDF text /// emits one absolutely-positioned span per glyph run, and the same font sizes, /// offsets and spacings recur across the (potentially millions of) spans. @@ -585,14 +684,41 @@ class HtmlServiceImpl final : public HtmlService { util::math::Transform2D::scaling_translation(1, -1, 0, height); ClipRegistry clips(static_cast(pages_out.size())); + GradientRegistry gradients(static_cast(pages_out.size())); for (const pdf::PageElement &element : pdf::extract_page(stream, *page->resources, *m_logger)) { // A painted path: serialize its subpaths to an SVG `` fragment in - // the page viewBox (fill and/or stroke), under any active clip. + // the page viewBox (fill and/or stroke), under any active clip. A + // shading-pattern fill is painted through a gradient instead of a + // colour. if (const auto *path = std::get_if(&element)) { const std::string clip_id = clips.register_clip(path->clip, to_box); - std::string fragment = svg_path_fragment(*path, to_box, clip_id); + std::string gradient_id; + if (path->fill_shading != nullptr) { + gradient_id = gradients.register_gradient( + *path->fill_shading, path->shading_transform * to_box); + } + std::string fragment = + svg_path_fragment(*path, to_box, clip_id, gradient_id); + if (!fragment.empty()) { + page_out.items.push_back(PathOut{std::move(fragment)}); + } + continue; + } + + // An `sh` shading flood: a `` over the page box filled with the + // shading's gradient, bounded by the clip in force at `sh` time. + if (const auto *shading = std::get_if(&element)) { + if (shading->shading == nullptr) { + continue; + } + const std::string clip_id = + clips.register_clip(shading->clip, to_box); + const std::string gradient_id = gradients.register_gradient( + *shading->shading, shading->transform * to_box); + std::string fragment = + svg_shading_fragment(gradient_id, clip_id, width, height); if (!fragment.empty()) { page_out.items.push_back(PathOut{std::move(fragment)}); } @@ -823,7 +949,8 @@ class HtmlServiceImpl final : public HtmlService { } } - page_out.clip_defs = clips.defs(); + // Clip-path and gradient defs share the page's hidden ``. + page_out.clip_defs = clips.defs() + gradients.defs(); } // Post-pass: every page has been scanned, so the per-font used-scalar sets @@ -969,10 +1096,11 @@ class HtmlServiceImpl final : public HtmlService { for (const PageOut &page : pages_out) { out.write_element_begin("div", HtmlElementOptions().set_class(page.classes)); - // Clip-path defs for this page, in a hidden zero-size ``. They are - // referenced by id from the page's path fragments; `clipPathUnits` - // defaults to `userSpaceOnUse`, so the geometry is read in the user space - // of the referencing element (the page viewBox), not this ``. + // Clip-path and gradient defs for this page, in a hidden zero-size + // ``. They are referenced by id from the page's fragments; + // `clipPathUnits`/`gradientUnits` are `userSpaceOnUse`, so the geometry + // is read in the user space of the referencing element (the page + // viewBox), not this ``. if (!page.clip_defs.empty()) { out.write_raw( "" diff --git a/src/odr/internal/pdf/AGENTS.md b/src/odr/internal/pdf/AGENTS.md index ccf3aa8f..a7a723e7 100644 --- a/src/odr/internal/pdf/AGENTS.md +++ b/src/odr/internal/pdf/AGENTS.md @@ -574,6 +574,15 @@ stage exists to avoid. current fill colour; `/SMask` and `/Mask` (stencil + colour-key) composited into RGBA on the raster path (a mask on a JPEG base is ignored — decoding the JPEG to composite is out of scope). +- **Shadings & shading patterns** (axial type 2, radial type 3): `parse_shading` + pre-samples the tint `/Function` across `/Domain` into 32 sRGB colour stops, so + the renderer needs no function evaluator. The `sh` operator floods the current + clip (a `ShadingElement` → `` filled with the gradient); a `/PatternType + 2` shading pattern selected by `scn` fills a path (`PathElement::fill_shading` + + the pattern `/Matrix`). Both emit SVG ``/`` + with `gradientUnits="userSpaceOnUse"`; `/Extend` is approximated by SVG's `pad` + spread. Mesh/function shadings (types 1, 4–7) and tiling patterns + (`/PatternType 1`) are still future stages. - **SVG residue** — where no 1:1 primitive exists; all at generation time, never rasterization: mesh/function shadings (types 1, 4–7) → tessellate into small flat polygons (pdf.js's approach); color spaces diff --git a/src/odr/internal/pdf/pdf_document_element.hpp b/src/odr/internal/pdf/pdf_document_element.hpp index 2c7bf58f..47bc73a8 100644 --- a/src/odr/internal/pdf/pdf_document_element.hpp +++ b/src/odr/internal/pdf/pdf_document_element.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ struct Annotation; struct Resources; struct Font; struct XObject; +struct Pattern; struct ColorSpaceDef; struct Element { @@ -95,6 +97,13 @@ struct Resources final : Element { /// referenced by `BDC`. Each value is the resolved property-list dictionary /// `Object`; used to recover `/ActualText` for a `BDC /Tag /Name` sequence. std::unordered_map properties; + /// The `/Shading` subdictionary (ISO 32000-1 8.7.4.3): named shadings painted + /// by the `sh` operator. Resolved eagerly (the tint function sampled into + /// colour stops) so extraction needs no parser handle. + std::unordered_map> shading; + /// The `/Pattern` subdictionary (ISO 32000-1 8.7.3.3): named tiling/shading + /// patterns selected as a colour by `scn`/`SCN` in a `/Pattern` colour space. + std::unordered_map pattern; }; /// An external object referenced by `Do` and listed in a resource dictionary's @@ -145,6 +154,27 @@ struct XObject final : Element { std::vector stencil_decode; ///< `/Decode`, empty = default `[0 1]` }; +/// A pattern listed in a resource dictionary's `/Pattern` subdictionary +/// (ISO 32000-1 8.7.3), selected as a colour by `scn`/`SCN` in a `/Pattern` +/// colour space. Shading patterns (`/PatternType 2`) paint a gradient through +/// the path; tiling patterns (`/PatternType 1`) repeat a content-stream cell. +struct Pattern final : Element { + enum class Type { + unknown, + tiling, ///< `/PatternType 1` + shading, ///< `/PatternType 2` + }; + Type type{Type::unknown}; + + /// `/Matrix` mapping pattern space to the default coordinate system of the + /// pattern's parent content stream (8.7.3.1); default identity. + util::math::Transform2D matrix; + + /// Shading pattern (`/PatternType 2`): the shading painted through the path, + /// pre-resolved (its tint function sampled into stops). Null otherwise. + std::shared_ptr shading; +}; + /// A non-owning view over a string of PDF character codes, splitting it into /// fixed-width (`Font::code_byte_width()`) big-endian codes on iteration. Holds /// only a `string_view`, so it must not outlive the underlying bytes; iterate diff --git a/src/odr/internal/pdf/pdf_document_parser.cpp b/src/odr/internal/pdf/pdf_document_parser.cpp index da84bbca..66199482 100644 --- a/src/odr/internal/pdf/pdf_document_parser.cpp +++ b/src/odr/internal/pdf/pdf_document_parser.cpp @@ -759,6 +759,79 @@ XObject *parse_x_object(State &state, const ObjectReference &reference) { return x_object; } +/// A `ColorSpaceContext` over the parser, resolving a base/alternate space +/// named by name against the (being-built) `/ColorSpace` table of `resources`. +ColorSpaceContext make_color_space_context(DocumentParser &parser, + const Resources *resources) { + ColorSpaceContext context; + context.resolve = [&parser](const Object &object) { + return parser.resolve_object_copy(object); + }; + context.load_stream = [&parser](const Object &object) { + return object.is_reference() + ? parser.read_decoded_stream(object.as_reference()) + : std::string{}; + }; + context.named = + [resources](const std::string &name) -> std::shared_ptr { + const auto it = resources->color_space.find(name); + return it != resources->color_space.end() ? it->second : nullptr; + }; + return context; +} + +/// Parse a `/Shading` dictionary into a resolved `Shading` (its tint function +/// sampled into colour stops). `resources` supplies named colour spaces. +std::shared_ptr parse_shading_resource(State &state, + const Object &object, + const Resources *resources) { + DocumentParser &parser = state.parser(); + ShadingContext context; + context.resolve = [&parser](const Object &o) { + return parser.resolve_object_copy(o); + }; + context.load_stream = [&parser](const Object &o) { + return o.is_reference() ? parser.read_decoded_stream(o.as_reference()) + : std::string{}; + }; + return parse_shading(object, context, + make_color_space_context(parser, resources)); +} + +/// Parse a `/Pattern` entry. A shading pattern (`/PatternType 2`) resolves its +/// `/Shading`; a tiling pattern (`/PatternType 1`) is recognized here and its +/// content rendered in a later stage. `/Matrix` is taken either way. +Pattern *parse_pattern(State &state, const ObjectReference &reference, + const Resources *resources) { + DocumentParser &parser = state.parser(); + Document &document = state.document(); + + auto *pattern = document.create_element(); + IndirectObject object = parser.read_object(reference); + if (!object.object.is_dictionary()) { + return pattern; + } + const Dictionary &dictionary = object.object.as_dictionary(); + pattern->object_reference = reference; + pattern->object = Object(dictionary); + + if (dictionary.has_value("Matrix")) { + pattern->matrix = parse_matrix(parser, dictionary["Matrix"]); + } + const auto pattern_type = static_cast( + parser.resolve_object_copy(dictionary.get("PatternType")) + .as_integer_opt() + .value_or(0)); + if (pattern_type == 2) { + pattern->type = Pattern::Type::shading; + pattern->shading = + parse_shading_resource(state, dictionary.get("Shading"), resources); + } else if (pattern_type == 1) { + pattern->type = Pattern::Type::tiling; + } + return pattern; +} + Resources *parse_resources(State &state, const Object &object) { DocumentParser &parser = state.parser(); Document &document = state.document(); @@ -820,6 +893,29 @@ Resources *parse_resources(State &state, const Object &object) { } } + // Shadings and patterns are parsed after `/ColorSpace` so a named colour + // space they reference is already in `resources->color_space`. + if (dictionary.has_value("Shading")) { + const Dictionary shading_table = + parser.resolve_object_copy(dictionary["Shading"]).as_dictionary(); + for (const auto &[key, value] : shading_table) { + if (auto shading = parse_shading_resource(state, value, resources)) { + resources->shading[key] = std::move(shading); + } + } + } + + if (dictionary.has_value("Pattern")) { + const Dictionary pattern_table = + parser.resolve_object_copy(dictionary["Pattern"]).as_dictionary(); + for (const auto &[key, value] : pattern_table) { + if (value.is_reference()) { + resources->pattern[key] = + parse_pattern(state, value.as_reference(), resources); + } + } + } + if (dictionary.has_key("Properties") && !dictionary["Properties"].is_null()) { // Named property lists for `BDC`; resolved eagerly so text extraction can // recover `/ActualText` without a parser handle (cf. form XObjects). diff --git a/src/odr/internal/pdf/pdf_graphics_state.cpp b/src/odr/internal/pdf/pdf_graphics_state.cpp index 8e12074c..d1527e09 100644 --- a/src/odr/internal/pdf/pdf_graphics_state.cpp +++ b/src/odr/internal/pdf/pdf_graphics_state.cpp @@ -293,6 +293,7 @@ void GraphicsState::execute(const GraphicsOperator &op) { current().stroke_color.space = ColorSpace::device_grey; current().stroke_color.grey = op.arguments.at(0).as_real(); current().stroke_color.def = nullptr; + current().stroke_color.pattern.clear(); break; case GraphicsOperatorType::set_stroke_rgb_color: current().stroke_color.space = ColorSpace::device_rgb; @@ -300,6 +301,7 @@ void GraphicsState::execute(const GraphicsOperator &op) { current().stroke_color.rgb.at(i) = op.arguments.at(i).as_real(); } current().stroke_color.def = nullptr; + current().stroke_color.pattern.clear(); break; case GraphicsOperatorType::set_stroke_cmyk_color: current().stroke_color.space = ColorSpace::device_cmyk; @@ -307,12 +309,14 @@ void GraphicsState::execute(const GraphicsOperator &op) { current().stroke_color.cmyk.at(i) = op.arguments.at(i).as_real(); } current().stroke_color.def = nullptr; + current().stroke_color.pattern.clear(); break; case GraphicsOperatorType::set_other_grey_color: current().other_color.space = ColorSpace::device_grey; current().other_color.grey = op.arguments.at(0).as_real(); current().other_color.def = nullptr; + current().other_color.pattern.clear(); break; case GraphicsOperatorType::set_other_rgb_color: current().other_color.space = ColorSpace::device_rgb; @@ -320,6 +324,7 @@ void GraphicsState::execute(const GraphicsOperator &op) { current().other_color.rgb.at(i) = op.arguments.at(i).as_real(); } current().other_color.def = nullptr; + current().other_color.pattern.clear(); break; case GraphicsOperatorType::set_other_cmyk_color: current().other_color.space = ColorSpace::device_cmyk; @@ -327,6 +332,7 @@ void GraphicsState::execute(const GraphicsOperator &op) { current().other_color.cmyk.at(i) = op.arguments.at(i).as_real(); } current().other_color.def = nullptr; + current().other_color.pattern.clear(); break; case GraphicsOperatorType::set_glyph_width: diff --git a/src/odr/internal/pdf/pdf_graphics_state.hpp b/src/odr/internal/pdf/pdf_graphics_state.hpp index 322cb7f0..276fdc10 100644 --- a/src/odr/internal/pdf/pdf_graphics_state.hpp +++ b/src/odr/internal/pdf/pdf_graphics_state.hpp @@ -113,6 +113,10 @@ struct GraphicsState { /// at the time the operator runs; null for a device colour space. Cleared /// by the device colour operators (`g`/`rg`/`k`). const ColorSpaceDef *def{nullptr}; + /// The resource name of the `/Pattern` selected by `scn`/`SCN` (a shading + /// or tiling pattern), resolved against `Resources::pattern` at paint time. + /// Empty for a plain colour; cleared by the device colour operators. + std::string pattern; }; struct State { diff --git a/src/odr/internal/pdf/pdf_page_element.hpp b/src/odr/internal/pdf/pdf_page_element.hpp index 1212fcef..538c5067 100644 --- a/src/odr/internal/pdf/pdf_page_element.hpp +++ b/src/odr/internal/pdf/pdf_page_element.hpp @@ -10,6 +10,7 @@ namespace odr::internal::pdf { struct Font; +struct Shading; /// One show-text operation laid out in user space. The transform places the /// text origin and orientation; the font size is kept separate so the renderer @@ -65,8 +66,9 @@ struct TextElement { /// space. The geometry is fully resolved (the CTM applied at construction), so /// a renderer maps it through the page transform alone. The paint intent and /// the paint-relevant graphics state are snapshotted; colors are kept as PDF -/// device colors and converted to RGB by the renderer. `/Pattern` color is -/// stage 4.9+ and not yet represented. +/// device colors and converted to RGB by the renderer. A `/PatternType 2` +/// shading pattern fill is carried by `fill_shading`; tiling patterns +/// (`/PatternType 1`) are a later stage. struct PathElement { /// The subpaths to paint, in user space. std::vector subpaths; @@ -81,6 +83,13 @@ struct PathElement { /// Non-stroking (fill) color and stroking color, as device colors. GraphicsState::Color fill_color; GraphicsState::Color stroke_color; + /// When the fill is a shading pattern (`scn` naming a `/PatternType 2` + /// pattern), the resolved shading to paint through the path instead of + /// `fill_color`, with `shading_transform` mapping shading space to user space + /// (the pattern `/Matrix`). Null for a plain colour fill. Owned by + /// `Resources`, which outlives the element. + const Shading *fill_shading{nullptr}; + util::math::Transform2D shading_transform; /// Stroke parameters. `line_width` and the dash lengths are in the path's /// user space (the CTM scale is already folded in, so they live in the same /// space as the geometry). A `line_width` of 0 means a device-thin line. @@ -105,9 +114,24 @@ struct ImageElement { std::string mime; // e.g. "image/jpeg" }; +/// One area painted by the `sh` operator (ISO 32000-1 8.7.4.2): a shading +/// flooded over the current clip region (no path geometry of its own). The +/// transform maps shading space to user space (the CTM at `sh` time); the clip +/// is snapshotted as for a path. The renderer paints the shading's gradient +/// across the clipped area. +struct ShadingElement { + /// The shading to paint. Owned by `Resources`, which outlives the element. + const Shading *shading{nullptr}; + /// Shading space -> user space (the CTM in force at `sh` time). + util::math::Transform2D transform; + /// The clip in force, snapshotted so the renderer bounds the flood. + std::vector clip; +}; + /// A single page-content element in paint (z) order: a shown text segment, a -/// painted path or an image. Shadings and patterns join this variant in later -/// stage-4 PRs. -using PageElement = std::variant; +/// painted path, an image, or a shading flood (`sh`). Shading *patterns* ride +/// on `PathElement::fill_shading`; tiling patterns join in a later stage. +using PageElement = + std::variant; } // namespace odr::internal::pdf diff --git a/src/odr/internal/pdf/pdf_page_extractor.cpp b/src/odr/internal/pdf/pdf_page_extractor.cpp index fbc4e6d4..0c8db7bf 100644 --- a/src/odr/internal/pdf/pdf_page_extractor.cpp +++ b/src/odr/internal/pdf/pdf_page_extractor.cpp @@ -340,26 +340,30 @@ void set_color_space(GraphicsState::Color &color, const std::string &name, /// Resolve a general colour operator (`sc`/`scn`/`SC`/`SCN`): convert the /// operand components through the active colour space to RGB. With no resource /// colour space, interpret the components as a device colour by their count -/// (ISO 32000-1 8.6.8). A trailing name operand (a `/Pattern`) carries no -/// convertible components — left as-is (stage 4.9/4.10). +/// (ISO 32000-1 8.6.8). A trailing name operand selects a `/Pattern`: its name +/// is recorded on `color.pattern` and resolved against `Resources::pattern` at +/// paint time (a shading pattern then fills the path through its gradient). void set_color(GraphicsState::Color &color, const GraphicsOperator &op) { std::vector components; - bool has_pattern_name = false; + std::string pattern_name; for (const Object &argument : op.arguments) { if (argument.is_name()) { - has_pattern_name = true; + pattern_name = argument.as_name(); } else if (argument.is_real()) { components.push_back(argument.as_real()); } } + color.pattern = pattern_name; + if (!pattern_name.empty()) { + // A pattern colour carries no device components to convert; the pattern is + // resolved at paint time. Leave any underlying colour as-is. + return; + } if (color.def != nullptr) { color.space = ColorSpace::device_rgb; color.rgb = color.def->to_rgb(components); return; } - if (has_pattern_name) { - return; - } switch (components.size()) { case 1: color.space = ColorSpace::device_grey; @@ -404,9 +408,9 @@ std::array color_to_rgb(const GraphicsState::Color &color) { /// Emit a path-painting element from the path accumulated in `state` and the /// current paint state, then clear the path (as every painting operator does). /// `close` first closes the current subpath (the `s`/`b`/`b*` variants). -void paint_path(std::vector &out, GraphicsState &state, - const bool fill, const bool stroke, const bool even_odd, - const bool close) { +void paint_path(std::vector &out, const Resources &resources, + GraphicsState &state, const bool fill, const bool stroke, + const bool even_odd, const bool close) { if (close) { state.path_close(); } @@ -421,6 +425,22 @@ void paint_path(std::vector &out, GraphicsState &state, element.even_odd = even_odd; element.fill_color = s.other_color; element.stroke_color = s.stroke_color; + // A `/Pattern`-coloured fill: resolve the pattern selected by `scn`. A + // shading pattern (`/PatternType 2`) paints its gradient through the path; + // its + // `/Matrix` maps shading space to the page's default user space (ISO 32000-1 + // 8.7.3.1). Other pattern types fall through to the plain fill colour. + if (fill && !s.other_color.pattern.empty()) { + if (const auto it = resources.pattern.find(s.other_color.pattern); + it != resources.pattern.end() && it->second != nullptr) { + const Pattern *pattern = it->second; + if (pattern->type == Pattern::Type::shading && + pattern->shading != nullptr) { + element.fill_shading = pattern->shading.get(); + element.shading_transform = pattern->matrix; + } + } + } // The stroke width and dash lengths are given in the CTM's space; fold the // CTM scale in so they live in the same user space as the geometry. Use the // area-preserving factor sqrt|det| (an anisotropic CTM can't be expressed as @@ -561,6 +581,26 @@ void emit_inline_image(const GraphicsOperator &op, const Resources &resources, out.push_back(std::move(image)); } +/// Emit a `ShadingElement` for the `sh` operator (ISO 32000-1 8.7.4.2): flood +/// the named `/Shading` over the current clip region. The shading is mapped to +/// user space by the CTM in force; unsupported shadings (parsed to nothing) +/// produce no element. +void emit_shading(const GraphicsOperator &op, const Resources &resources, + GraphicsState &state, std::vector &out) { + if (op.arguments.empty() || !op.arguments.at(0).is_name()) { + return; + } + const auto it = resources.shading.find(op.arguments.at(0).as_name()); + if (it == resources.shading.end() || it->second == nullptr) { + return; + } + ShadingElement element; + element.shading = it->second.get(); + element.transform = state.current().general.transform_matrix; + element.clip = state.current().clip; + out.push_back(std::move(element)); +} + /// Form XObjects currently being rendered, by element identity. The parser /// represents the file faithfully, so the XObject graph may contain cycles /// (the spec forbids them — ISO 32000-1 8.10.1 — but real files err); this set @@ -722,28 +762,31 @@ void run_content(const std::string &content, const Resources &resources, break; case GraphicsOperatorType::stroke: // S - paint_path(out, state, false, true, false, false); + paint_path(out, resources, state, false, true, false, false); break; case GraphicsOperatorType::close_stroke: // s - paint_path(out, state, false, true, false, true); + paint_path(out, resources, state, false, true, false, true); break; case GraphicsOperatorType::fill_nonzero: // f, F - paint_path(out, state, true, false, false, false); + paint_path(out, resources, state, true, false, false, false); break; case GraphicsOperatorType::fill_evenodd: // f* - paint_path(out, state, true, false, true, false); + paint_path(out, resources, state, true, false, true, false); break; case GraphicsOperatorType::fill_nonzero_stroke: // B - paint_path(out, state, true, true, false, false); + paint_path(out, resources, state, true, true, false, false); break; case GraphicsOperatorType::fill_evenodd_stroke: // B* - paint_path(out, state, true, true, true, false); + paint_path(out, resources, state, true, true, true, false); break; case GraphicsOperatorType::close_fill_nonzero_stroke: // b - paint_path(out, state, true, true, false, true); + paint_path(out, resources, state, true, true, false, true); break; case GraphicsOperatorType::close_fill_evenodd_stroke: // b* - paint_path(out, state, true, true, true, true); + paint_path(out, resources, state, true, true, true, true); + break; + case GraphicsOperatorType::set_clipping_path_shading: // sh + emit_shading(op, resources, state, out); break; case GraphicsOperatorType::end_path: // n // Path painted with no marks — its only role is to install a pending diff --git a/src/odr/internal/pdf/pdf_shading.cpp b/src/odr/internal/pdf/pdf_shading.cpp new file mode 100644 index 00000000..d8f856a5 --- /dev/null +++ b/src/odr/internal/pdf/pdf_shading.cpp @@ -0,0 +1,153 @@ +#include + +#include + +#include + +namespace odr::internal::pdf { + +namespace { + +/// Read a numeric array entry as doubles ([] when absent or not an array). +std::vector read_numbers(const Dictionary &dict, const std::string &key, + const ShadingContext &context) { + std::vector result; + const Object value = context.resolve(dict.get(key)); + if (value.is_array()) { + for (const Object &item : value.as_array()) { + result.push_back(item.as_real()); + } + } + return result; +} + +/// Parse the `/Function` of a shading: either one function or an array of +/// single-output functions (one per colour component, ISO 32000-1 8.7.4.5.2). +std::vector> +parse_shading_functions(const Object &function, const ShadingContext &context) { + FunctionContext function_context; + function_context.resolve = context.resolve; + function_context.load_stream = context.load_stream; + + std::vector> functions; + const Object resolved = context.resolve(function); + if (resolved.is_array()) { + for (const Object &item : resolved.as_array()) { + functions.push_back(parse_function(item, function_context)); + } + } else { + functions.push_back(parse_function(resolved, function_context)); + } + // A null entry (an unsupported function type) makes the whole shading + // unusable: sampling would yield wrong colours silently. + for (const auto &f : functions) { + if (f == nullptr) { + return {}; + } + } + return functions; +} + +/// Evaluate the shading's tint functions at `t`, concatenating their outputs +/// into the colour-component vector the colour space expects. +std::vector +eval_components(const std::vector> &functions, + double t) { + std::vector components; + for (const auto &function : functions) { + std::vector out = function->eval({t}); + components.insert(components.end(), out.begin(), out.end()); + } + return components; +} + +} // namespace + +} // namespace odr::internal::pdf + +namespace odr::internal { + +std::shared_ptr +pdf::parse_shading(const Object &object, const ShadingContext &context, + const ColorSpaceContext &color_context) { + const Object resolved = context.resolve(object); + if (!resolved.is_dictionary()) { + return nullptr; + } + const Dictionary &dict = resolved.as_dictionary(); + + if (!dict.get("ShadingType").is_integer()) { + return nullptr; + } + const auto type = + static_cast(dict.get("ShadingType").as_integer()); + if (type != 2 && type != 3) { + return nullptr; // function-based / mesh shadings are a later stage + } + + const std::shared_ptr color_space = + parse_color_space(dict.get("ColorSpace"), color_context); + if (color_space == nullptr) { + return nullptr; + } + + const std::vector coords = read_numbers(dict, "Coords", context); + const std::size_t need = type == 2 ? 4 : 6; + if (coords.size() < need) { + return nullptr; + } + + const std::vector> functions = + parse_shading_functions(dict.get("Function"), context); + if (functions.empty()) { + return nullptr; + } + + auto shading = std::make_shared(); + shading->type = type; + for (std::size_t i = 0; i < need; ++i) { + shading->coords[i] = coords[i]; + } + + const std::vector domain = read_numbers(dict, "Domain", context); + if (domain.size() >= 2) { + shading->domain = {domain[0], domain[1]}; + } + + const Object extend = context.resolve(dict.get("Extend")); + if (extend.is_array() && extend.as_array().size() >= 2) { + shading->extend = {extend.as_array()[0].as_bool_opt().value_or(false), + extend.as_array()[1].as_bool_opt().value_or(false)}; + } + + // Sample the tint function(s) across the domain into colour stops. A fixed + // count captures non-linear (stitching/sampled/PostScript) functions well + // enough for an SVG gradient; an exponential one is reproduced near-exactly. + constexpr std::size_t sample_count = 32; + 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))}); + } + + const std::vector background = + read_numbers(dict, "Background", context); + if (!background.empty()) { + shading->has_background = true; + shading->background = color_space->to_rgb(background); + } + + const std::vector bbox = read_numbers(dict, "BBox", context); + if (bbox.size() >= 4) { + shading->has_bbox = true; + shading->bbox = {bbox[0], bbox[1], bbox[2], bbox[3]}; + } + + return shading; +} + +} // namespace odr::internal diff --git a/src/odr/internal/pdf/pdf_shading.hpp b/src/odr/internal/pdf/pdf_shading.hpp new file mode 100644 index 00000000..9c478bd5 --- /dev/null +++ b/src/odr/internal/pdf/pdf_shading.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace odr::internal::pdf { + +class Object; + +/// One colour stop of a gradient: an `offset` in [0, 1] along the gradient axis +/// and the sRGB colour there. Sampled from a shading's `/Function` across its +/// `/Domain` at parse time, so the renderer needs no function evaluator. +struct GradientStop { + double offset{0}; + std::array rgb{}; +}; + +/// A resolved axial (type 2) or radial (type 3) shading (ISO 32000-1 8.7.4.5). +/// The tint `/Function` is pre-sampled into `stops`, so a renderer maps this to +/// an SVG ``/`` directly. Other shading types +/// (1, 4–7) are not modelled here (a later stage tessellates them). +struct Shading { + /// `/ShadingType`: 2 (axial) or 3 (radial). + std::int32_t type{0}; + /// Axial: `[x0 y0 x1 y1]` (the axis). Radial: `[x0 y0 r0 x1 y1 r1]` (the two + /// circles). In shading space (mapped to user space by the caller's CTM or a + /// pattern matrix). + std::array coords{}; + /// `/Domain` `[t0 t1]` of the parametric variable (default `[0 1]`). + std::array domain{0, 1}; + /// `/Extend`: whether the shading continues beyond the axis ends. + std::array extend{false, false}; + /// Colour stops sampled across `domain` (offsets in [0, 1], `stops.front()` + /// at `t0`), in source order — at least two. + std::vector stops; + /// `/Background` colour (sRGB), painted outside the shading where `/Extend` + /// does not reach; absent when the shading declares none. + bool has_background{false}; + std::array background{}; + /// `/BBox` `[x0 y0 x1 y1]` in shading space, clipping the shading; absent + /// when none is declared. + bool has_bbox{false}; + std::array bbox{}; +}; + +/// How `parse_shading` reaches indirect data: `resolve` dereferences an object, +/// `load_stream` decodes a stream's bytes (a type-0 sampled `/Function`). +struct ShadingContext { + std::function resolve; + std::function load_stream; +}; + +/// Build a shading from its `/Shading` dictionary, sampling its tint function +/// into `stops` through the shading's colour space. `color_context` resolves +/// the +/// `/ColorSpace`. Returns `nullptr` for a malformed or unsupported shading +/// (types other than 2/3). +std::shared_ptr parse_shading(const Object &object, + const ShadingContext &context, + const ColorSpaceContext &color_context); + +} // namespace odr::internal::pdf diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 58d64837..b202b672 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -55,6 +55,7 @@ add_executable(odr_test "src/internal/pdf/pdf_object.cpp" "src/internal/pdf/pdf_object_parser.cpp" "src/internal/pdf/pdf_page_extractor.cpp" + "src/internal/pdf/pdf_shading.cpp" "src/internal/pdf/pdf_test_file_builder.cpp" "src/internal/font/cff_font.cpp" diff --git a/test/src/internal/pdf/pdf_page_extractor.cpp b/test/src/internal/pdf/pdf_page_extractor.cpp index b72bd528..5ee99aed 100644 --- a/test/src/internal/pdf/pdf_page_extractor.cpp +++ b/test/src/internal/pdf/pdf_page_extractor.cpp @@ -876,6 +876,75 @@ TEST(PdfPageExtractor, device_color_clears_color_space) { EXPECT_DOUBLE_EQ(p.fill_color.rgb[1], 0.0); } +// --- shadings & shading patterns ------------------------------- + +namespace { + +// A minimal axial shading with two stops (black to white). +std::shared_ptr axial_shading() { + auto shading = std::make_shared(); + shading->type = 2; + shading->coords = {0, 0, 1, 0, 0, 0}; + shading->stops = {GradientStop{0.0, {0, 0, 0}}, GradientStop{1.0, {1, 1, 1}}}; + return shading; +} + +} // namespace + +// `scn` naming a `/PatternType 2` pattern fills the path through the pattern's +// shading; `fill_shading` is resolved and the pattern `/Matrix` carried. +TEST(PdfPageExtractor, scn_shading_pattern_fills_path) { + Pattern pattern; + pattern.type = Pattern::Type::shading; + pattern.shading = axial_shading(); + pattern.matrix = Transform2D::translation(5, 7); + Resources res; + res.pattern["P0"] = &pattern; + + const auto page = + extract_page("/Pattern cs /P0 scn 0 0 10 10 re f", res, Logger::null()); + ASSERT_EQ(page.size(), 1); + const PathElement &p = std::get(page[0]); + ASSERT_NE(p.fill_shading, nullptr); + EXPECT_EQ(p.fill_shading->type, 2); + EXPECT_TRUE(p.fill); + EXPECT_DOUBLE_EQ(p.shading_transform.e, 5); + EXPECT_DOUBLE_EQ(p.shading_transform.f, 7); +} + +// `scn` naming an unknown pattern leaves the fill with no shading (a plain +// colour fill); the path still paints. +TEST(PdfPageExtractor, scn_unknown_pattern_has_no_shading) { + Resources res; + const auto page = extract_page("/Pattern cs /Missing scn 0 0 10 10 re f", res, + Logger::null()); + ASSERT_EQ(page.size(), 1); + EXPECT_EQ(std::get(page[0]).fill_shading, nullptr); +} + +// The `sh` operator floods the current clip with a named `/Shading`, emitting a +// `ShadingElement` placed by the CTM. +TEST(PdfPageExtractor, sh_emits_shading_element) { + Resources res; + res.shading["Sh0"] = axial_shading(); + + const auto page = + extract_page("q 2 0 0 2 10 20 cm /Sh0 sh Q", res, Logger::null()); + ASSERT_EQ(page.size(), 1); + const ShadingElement &s = std::get(page[0]); + ASSERT_NE(s.shading, nullptr); + EXPECT_EQ(s.shading->type, 2); + EXPECT_DOUBLE_EQ(s.transform.a, 2); + EXPECT_DOUBLE_EQ(s.transform.e, 10); + EXPECT_DOUBLE_EQ(s.transform.f, 20); +} + +// `sh` naming an unknown shading emits nothing. +TEST(PdfPageExtractor, sh_unknown_shading_emits_nothing) { + Resources res; + EXPECT_TRUE(extract_page("/Missing sh", res, Logger::null()).empty()); +} + // --- image XObjects (JPEG pass-through) ------------------------ namespace { diff --git a/test/src/internal/pdf/pdf_shading.cpp b/test/src/internal/pdf/pdf_shading.cpp new file mode 100644 index 00000000..c9d61c75 --- /dev/null +++ b/test/src/internal/pdf/pdf_shading.cpp @@ -0,0 +1,160 @@ +#include + +#include +#include + +#include +#include + +#include + +using namespace odr::internal::pdf; + +namespace { + +// A context with no indirection: objects resolve to themselves and no stream is +// needed (the shadings under test use exponential tint functions). +ShadingContext context() { + ShadingContext ctx; + ctx.resolve = [](const Object &object) { return object; }; + ctx.load_stream = [](const Object &) { return std::string{}; }; + return ctx; +} + +// DeviceRGB resolves without a named lookup; the colour-space resolver is +// inert. +ColorSpaceContext color_context() { + ColorSpaceContext ctx; + ctx.resolve = [](const Object &object) { return object; }; + ctx.load_stream = [](const Object &) { return std::string{}; }; + ctx.named = nullptr; + return ctx; +} + +Object reals(std::initializer_list values) { + std::vector holder; + for (const double value : values) { + holder.emplace_back(Real{value}); + } + return Object(Array(std::move(holder))); +} + +// A linear DeviceRGB tint from C0 to C1 (type 2, N = 1). +Object linear_function(std::initializer_list c0, + std::initializer_list c1) { + Dictionary fn; + fn["FunctionType"] = Object(Integer{2}); + fn["Domain"] = reals({0, 1}); + fn["C0"] = reals(c0); + fn["C1"] = reals(c1); + fn["N"] = Object(Real{1}); + return Object(fn); +} + +} // namespace + +// Type 2 (axial): the tint function is sampled into stops spanning black to +// white, with the axis coordinates carried verbatim. +TEST(PdfShading, axial_basic) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{2}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({10, 20, 110, 20}); + dict["Function"] = linear_function({0, 0, 0}, {1, 1, 1}); + + const auto shading = parse_shading(Object(dict), context(), color_context()); + ASSERT_NE(shading, nullptr); + EXPECT_EQ(shading->type, 2); + EXPECT_DOUBLE_EQ(shading->coords[0], 10); + EXPECT_DOUBLE_EQ(shading->coords[1], 20); + EXPECT_DOUBLE_EQ(shading->coords[2], 110); + EXPECT_DOUBLE_EQ(shading->coords[3], 20); + ASSERT_GE(shading->stops.size(), 2); + EXPECT_DOUBLE_EQ(shading->stops.front().offset, 0.0); + EXPECT_DOUBLE_EQ(shading->stops.back().offset, 1.0); + EXPECT_NEAR(shading->stops.front().rgb[0], 0.0, 1e-6); + EXPECT_NEAR(shading->stops.back().rgb[0], 1.0, 1e-6); +} + +// Type 3 (radial): both circles' six coordinates are taken. +TEST(PdfShading, radial_basic) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{3}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({0, 0, 0, 0, 0, 50}); + dict["Function"] = linear_function({1, 0, 0}, {0, 0, 1}); + + const auto shading = parse_shading(Object(dict), context(), color_context()); + ASSERT_NE(shading, nullptr); + EXPECT_EQ(shading->type, 3); + EXPECT_DOUBLE_EQ(shading->coords[2], 0); // inner radius + EXPECT_DOUBLE_EQ(shading->coords[5], 50); // outer radius +} + +// /Domain and /Extend are parsed; defaults apply when absent. +TEST(PdfShading, domain_and_extend) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{2}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({0, 0, 1, 0}); + dict["Domain"] = reals({0.25, 0.75}); + std::vector extend{Object(Boolean{true}), Object(Boolean{false})}; + dict["Extend"] = Object(Array(std::move(extend))); + dict["Function"] = linear_function({0}, {1}); + + const auto shading = parse_shading(Object(dict), context(), color_context()); + ASSERT_NE(shading, nullptr); + EXPECT_DOUBLE_EQ(shading->domain[0], 0.25); + EXPECT_DOUBLE_EQ(shading->domain[1], 0.75); + EXPECT_TRUE(shading->extend[0]); + EXPECT_FALSE(shading->extend[1]); +} + +// /Background is converted through the colour space. +TEST(PdfShading, background) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{2}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({0, 0, 1, 0}); + dict["Background"] = reals({1, 0, 0}); + dict["Function"] = linear_function({0}, {1}); + + const auto shading = parse_shading(Object(dict), context(), color_context()); + ASSERT_NE(shading, nullptr); + ASSERT_TRUE(shading->has_background); + EXPECT_NEAR(shading->background[0], 1.0, 1e-6); + EXPECT_NEAR(shading->background[1], 0.0, 1e-6); +} + +// Shading types other than 2/3 are not modelled and yield null. +TEST(PdfShading, unsupported_type_is_null) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{1}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Function"] = linear_function({0}, {1}); + EXPECT_EQ(parse_shading(Object(dict), context(), color_context()), nullptr); +} + +// Too few /Coords for the type makes the shading unusable. +TEST(PdfShading, short_coords_is_null) { + Dictionary dict; + dict["ShadingType"] = Object(Integer{2}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({0, 0}); // axial needs four + dict["Function"] = linear_function({0}, {1}); + EXPECT_EQ(parse_shading(Object(dict), context(), color_context()), nullptr); +} + +// An unsupported tint function makes the whole shading null (no silent wrong +// colours). +TEST(PdfShading, bad_function_is_null) { + Dictionary fn; + fn["FunctionType"] = Object(Integer{9}); // unsupported + + Dictionary dict; + dict["ShadingType"] = Object(Integer{2}); + dict["ColorSpace"] = Object(Name{"DeviceRGB"}); + dict["Coords"] = reals({0, 0, 1, 0}); + dict["Function"] = Object(fn); + EXPECT_EQ(parse_shading(Object(dict), context(), color_context()), nullptr); +} From 8384b7b8b77af4bf8309d9e60e436a7b61760742 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 28 Jun 2026 15:59:48 +0200 Subject: [PATCH 2/4] =?UTF-8?q?PDF=20stage=204.9:=20review=20cleanups=20?= =?UTF-8?q?=E2=80=94=20dedup=20context=20I/O,=20as=5Freals,=20defer=20note?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tightening from PR review, no behaviour change (197 PDF tests still green): - Add `as_reals(const Object&)` to `pdf_object`, collapsing the repeated "array `Object` -> vector" loops in `image_decode`, the colour-key `/Mask` branch, the inline-image `/Decode`, and the shading `read_numbers`. - Add a file-local `bind_parser_io` template in the parser to bind the shared `resolve`/`load_stream` hooks of every typed context (colour space, function, shading), replacing four identical lambda pairs. - Make the deferred shading features visible: `Shading::{extend,background,bbox}` are parsed but not yet honoured by the renderer (it always uses SVG `pad` spread). Documented on the `Shading` struct, the `GradientRegistry`, and `pdf/AGENTS.md`. - Note the `Resources::shading` (shared_ptr) vs `Resources::pattern` (Element*) ownership asymmetry, and why only the gradient transform's translation is rounded. Reflow two clang-format comment artifacts. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Uqx1dUpDUykRxui8FYwxvM --- src/odr/internal/html/pdf_file.cpp | 14 ++++- src/odr/internal/pdf/AGENTS.md | 9 ++- src/odr/internal/pdf/pdf_document_element.hpp | 6 +- src/odr/internal/pdf/pdf_document_parser.cpp | 63 +++++++------------ src/odr/internal/pdf/pdf_object.cpp | 12 ++++ src/odr/internal/pdf/pdf_object.hpp | 6 ++ src/odr/internal/pdf/pdf_page_extractor.cpp | 10 +-- src/odr/internal/pdf/pdf_shading.cpp | 12 +--- src/odr/internal/pdf/pdf_shading.hpp | 21 ++++--- 9 files changed, 80 insertions(+), 73 deletions(-) diff --git a/src/odr/internal/html/pdf_file.cpp b/src/odr/internal/html/pdf_file.cpp index b1e96174..11ae9e72 100644 --- a/src/odr/internal/html/pdf_file.cpp +++ b/src/odr/internal/html/pdf_file.cpp @@ -285,9 +285,14 @@ class ClipRegistry { /// `` defs, deduplicating by shading and placement. The /// shading's pre-sampled colour stops become ``s; `gradientTransform` /// (shading space -> page box) places the gradient in the page's user space, so -/// referencing elements use `gradientUnits="userSpaceOnUse"`. PDF `/Extend` is -/// approximated by SVG's default `pad` spread (the end stops extend outward). -/// Ids are namespaced per page (`g_`). +/// referencing elements use `gradientUnits="userSpaceOnUse"`. Ids are +/// namespaced per page (`g_`). +/// +/// DEFERRED (out of scope for this stage): PDF `/Extend` is approximated by +/// SVG's default `pad` spread (the end stops extend outward), so a non-extended +/// shading is over-painted beyond its interval instead of being masked to it; +/// `Shading::background` and `Shading::bbox` are likewise not yet honoured. +/// Honouring them needs the fill clipped to the gradient band/annulus. class GradientRegistry { public: explicit GradientRegistry(std::uint32_t page) : m_page{page} {} @@ -322,6 +327,9 @@ class GradientRegistry { << "\" cy=\"" << c[4] << "\" r=\"" << c[5] << "\" fx=\"" << c[0] << "\" fy=\"" << c[1] << "\" fr=\"" << c[2] << '"'; } + // Only the translation (e, f) is rounded — it lives in page-box units where + // 1/100 px is plenty; the linear part (a..d) keeps full precision so small + // scale/skew factors aren't quantized to zero. m_defs << " gradientUnits=\"userSpaceOnUse\" gradientTransform=\"matrix(" << m.a << ',' << m.b << ',' << m.c << ',' << m.d << ',' << round2(m.e) << ',' << round2(m.f) << ")\">"; diff --git a/src/odr/internal/pdf/AGENTS.md b/src/odr/internal/pdf/AGENTS.md index a7a723e7..8994e6c7 100644 --- a/src/odr/internal/pdf/AGENTS.md +++ b/src/odr/internal/pdf/AGENTS.md @@ -580,9 +580,12 @@ stage exists to avoid. clip (a `ShadingElement` → `` filled with the gradient); a `/PatternType 2` shading pattern selected by `scn` fills a path (`PathElement::fill_shading` + the pattern `/Matrix`). Both emit SVG ``/`` - with `gradientUnits="userSpaceOnUse"`; `/Extend` is approximated by SVG's `pad` - spread. Mesh/function shadings (types 1, 4–7) and tiling patterns - (`/PatternType 1`) are still future stages. + with `gradientUnits="userSpaceOnUse"`. `/Extend`, `/Background` and `/BBox` are + parsed onto `Shading` but **not yet honoured** by the renderer (deferred): it + always uses SVG's `pad` spread, so a non-extended shading is over-painted past + its interval rather than masked to it (honouring it needs the fill clipped to + the gradient band/annulus). Mesh/function shadings (types 1, 4–7) and tiling + patterns (`/PatternType 1`) are still future stages. - **SVG residue** — where no 1:1 primitive exists; all at generation time, never rasterization: mesh/function shadings (types 1, 4–7) → tessellate into small flat polygons (pdf.js's approach); color spaces diff --git a/src/odr/internal/pdf/pdf_document_element.hpp b/src/odr/internal/pdf/pdf_document_element.hpp index 47bc73a8..c0c6f85f 100644 --- a/src/odr/internal/pdf/pdf_document_element.hpp +++ b/src/odr/internal/pdf/pdf_document_element.hpp @@ -99,10 +99,14 @@ struct Resources final : Element { std::unordered_map properties; /// The `/Shading` subdictionary (ISO 32000-1 8.7.4.3): named shadings painted /// by the `sh` operator. Resolved eagerly (the tint function sampled into - /// colour stops) so extraction needs no parser handle. + /// colour stops) so extraction needs no parser handle. Held by `shared_ptr` + /// because `Shading` is a plain value type, not a document `Element` (a + /// shading pattern shares ownership of the same `Shading`). std::unordered_map> shading; /// The `/Pattern` subdictionary (ISO 32000-1 8.7.3.3): named tiling/shading /// patterns selected as a colour by `scn`/`SCN` in a `/Pattern` colour space. + /// A non-owning pointer: `Pattern` is a document `Element`, owned by the + /// `Document` graph like the other resource elements (`Font`, `XObject`). std::unordered_map pattern; }; diff --git a/src/odr/internal/pdf/pdf_document_parser.cpp b/src/odr/internal/pdf/pdf_document_parser.cpp index 66199482..107fa5b3 100644 --- a/src/odr/internal/pdf/pdf_document_parser.cpp +++ b/src/odr/internal/pdf/pdf_document_parser.cpp @@ -538,15 +538,22 @@ std::int32_t image_int(DocumentParser &parser, const Dictionary &dictionary, /// The `/Decode` array of an image dictionary as doubles ([] when absent). std::vector image_decode(DocumentParser &parser, const Dictionary &dictionary) { - std::vector decode; - const Object decode_object = - parser.resolve_object_copy(dictionary.get("Decode")); - if (decode_object.is_array()) { - for (const Object &item : decode_object.as_array()) { - decode.push_back(item.as_real()); - } - } - return decode; + return as_reals(parser.resolve_object_copy(dictionary.get("Decode"))); +} + +/// Bind the parser-backed `resolve`/`load_stream` hooks shared by every typed +/// context (`ColorSpaceContext`, `FunctionContext`, `ShadingContext`): each +/// dereferences an object and decodes a stream's bytes through `parser`. +template +void bind_parser_io(Context &context, DocumentParser &parser) { + context.resolve = [&parser](const Object &object) { + return parser.resolve_object_copy(object); + }; + context.load_stream = [&parser](const Object &object) { + return object.is_reference() + ? parser.read_decoded_stream(object.as_reference()) + : std::string{}; + }; } /// Resolve a `/SMask` (soft mask) or stencil `/Mask` sub-image referenced by @@ -647,13 +654,7 @@ void parse_image_data(DocumentParser &parser, const Dictionary &dictionary, std::shared_ptr color_space; if (dictionary.has_value("ColorSpace")) { ColorSpaceContext context; - context.resolve = [&parser](const Object &o) { - return parser.resolve_object_copy(o); - }; - context.load_stream = [&parser](const Object &o) { - return o.is_reference() ? parser.read_decoded_stream(o.as_reference()) - : std::string{}; - }; + bind_parser_io(context, parser); color_space = parse_color_space(dictionary.get("ColorSpace"), context); } const std::int32_t width = image_int(parser, dictionary, "Width", 0); @@ -675,9 +676,7 @@ void parse_image_data(DocumentParser &parser, const Dictionary &dictionary, if (alpha.empty() && dictionary.has_value("Mask")) { const Object mask = parser.resolve_object_copy(dictionary["Mask"]); if (mask.is_array()) { - for (const Object &item : mask.as_array()) { - color_key.push_back(item.as_real()); - } + color_key = as_reals(mask); } else if (dictionary["Mask"].is_reference()) { alpha = resolve_mask_alpha(parser, dictionary["Mask"], width, height, /*stencil=*/true); @@ -764,14 +763,7 @@ XObject *parse_x_object(State &state, const ObjectReference &reference) { ColorSpaceContext make_color_space_context(DocumentParser &parser, const Resources *resources) { ColorSpaceContext context; - context.resolve = [&parser](const Object &object) { - return parser.resolve_object_copy(object); - }; - context.load_stream = [&parser](const Object &object) { - return object.is_reference() - ? parser.read_decoded_stream(object.as_reference()) - : std::string{}; - }; + bind_parser_io(context, parser); context.named = [resources](const std::string &name) -> std::shared_ptr { const auto it = resources->color_space.find(name); @@ -787,13 +779,7 @@ std::shared_ptr parse_shading_resource(State &state, const Resources *resources) { DocumentParser &parser = state.parser(); ShadingContext context; - context.resolve = [&parser](const Object &o) { - return parser.resolve_object_copy(o); - }; - context.load_stream = [&parser](const Object &o) { - return o.is_reference() ? parser.read_decoded_stream(o.as_reference()) - : std::string{}; - }; + bind_parser_io(context, parser); return parse_shading(object, context, make_color_space_context(parser, resources)); } @@ -863,14 +849,7 @@ Resources *parse_resources(State &state, const Object &object) { const Dictionary color_space_table = parser.resolve_object_copy(dictionary["ColorSpace"]).as_dictionary(); ColorSpaceContext context; - context.resolve = [&parser](const Object &object) { - return parser.resolve_object_copy(object); - }; - context.load_stream = [&parser](const Object &object) { - return object.is_reference() - ? parser.read_decoded_stream(object.as_reference()) - : std::string{}; - }; + bind_parser_io(context, parser); // A base/alternate space may be named (referencing another `/ColorSpace` // entry); resolve it lazily from the same table, caching the result. context.named = diff --git a/src/odr/internal/pdf/pdf_object.cpp b/src/odr/internal/pdf/pdf_object.cpp index 0331474a..ce45ad2b 100644 --- a/src/odr/internal/pdf/pdf_object.cpp +++ b/src/odr/internal/pdf/pdf_object.cpp @@ -11,6 +11,18 @@ namespace odr::internal::pdf { +std::vector as_reals(const Object &object) { + std::vector result; + if (object.is_array()) { + const Array &array = object.as_array(); + result.reserve(array.size()); + for (const Object &item : array) { + result.push_back(item.as_real()); + } + } + return result; +} + void StandardString::to_stream(std::ostream &out) const { // TODO escape out << "(" << string << ")"; diff --git a/src/odr/internal/pdf/pdf_object.hpp b/src/odr/internal/pdf/pdf_object.hpp index b15d48cb..b740534e 100644 --- a/src/odr/internal/pdf/pdf_object.hpp +++ b/src/odr/internal/pdf/pdf_object.hpp @@ -343,6 +343,12 @@ class Dictionary final { Holder m_holder; }; +/// The elements of an array `Object` as doubles, in order (each via +/// `Object::as_real`). Returns an empty vector when `object` is not an array. +/// Convenience for the many PDF arrays that are plain number lists (`/Decode`, +/// `/Coords`, `/Domain`, `/Background`, a colour-key `/Mask`, ...). +std::vector as_reals(const Object &object); + std::ostream &operator<<(std::ostream &, const StandardString &); std::ostream &operator<<(std::ostream &, const HexString &); std::ostream &operator<<(std::ostream &, const Name &); diff --git a/src/odr/internal/pdf/pdf_page_extractor.cpp b/src/odr/internal/pdf/pdf_page_extractor.cpp index 0c8db7bf..d75d3ae8 100644 --- a/src/odr/internal/pdf/pdf_page_extractor.cpp +++ b/src/odr/internal/pdf/pdf_page_extractor.cpp @@ -427,8 +427,7 @@ void paint_path(std::vector &out, const Resources &resources, element.stroke_color = s.stroke_color; // A `/Pattern`-coloured fill: resolve the pattern selected by `scn`. A // shading pattern (`/PatternType 2`) paints its gradient through the path; - // its - // `/Matrix` maps shading space to the page's default user space (ISO 32000-1 + // its matrix maps shading space to the page's default user space (ISO 32000-1 // 8.7.3.1). Other pattern types fall through to the plain fill colour. if (fill && !s.other_color.pattern.empty()) { if (const auto it = resources.pattern.find(s.other_color.pattern); @@ -528,12 +527,7 @@ void emit_inline_image(const GraphicsOperator &op, const Resources &resources, dictionary.get("Width").as_integer_opt().value_or(0)); const auto height = static_cast( dictionary.get("Height").as_integer_opt().value_or(0)); - std::vector decode_array; - if (const Object &d = dictionary.get("Decode"); d.is_array()) { - for (const Object &item : d.as_array()) { - decode_array.push_back(item.as_real()); - } - } + const std::vector decode_array = as_reals(dictionary.get("Decode")); // An inline `/ImageMask true` stencil: decode the 1-bpc bitmap and paint it // in the current fill colour, as for a stencil image XObject (ISO diff --git a/src/odr/internal/pdf/pdf_shading.cpp b/src/odr/internal/pdf/pdf_shading.cpp index d8f856a5..7c0dc66f 100644 --- a/src/odr/internal/pdf/pdf_shading.cpp +++ b/src/odr/internal/pdf/pdf_shading.cpp @@ -8,17 +8,11 @@ namespace odr::internal::pdf { namespace { -/// Read a numeric array entry as doubles ([] when absent or not an array). +/// Read a numeric array entry as doubles ([] when absent or not an array), +/// resolving an indirect reference first. std::vector read_numbers(const Dictionary &dict, const std::string &key, const ShadingContext &context) { - std::vector result; - const Object value = context.resolve(dict.get(key)); - if (value.is_array()) { - for (const Object &item : value.as_array()) { - result.push_back(item.as_real()); - } - } - return result; + return as_reals(context.resolve(dict.get(key))); } /// Parse the `/Function` of a shading: either one function or an array of diff --git a/src/odr/internal/pdf/pdf_shading.hpp b/src/odr/internal/pdf/pdf_shading.hpp index 9c478bd5..6ea18af9 100644 --- a/src/odr/internal/pdf/pdf_shading.hpp +++ b/src/odr/internal/pdf/pdf_shading.hpp @@ -25,6 +25,12 @@ struct GradientStop { /// The tint `/Function` is pre-sampled into `stops`, so a renderer maps this to /// an SVG ``/`` directly. Other shading types /// (1, 4–7) are not modelled here (a later stage tessellates them). +/// +/// NOTE: `extend`, `background` and `bbox` are parsed but not yet honoured by +/// the renderer — it always emits the gradient with SVG's `pad` spread, which +/// over-paints a non-extended shading beyond its interval (see +/// `GradientRegistry` in `html/pdf_file.cpp`). They are carried here so the +/// deferred bounds and background handling needs no re-parse. struct Shading { /// `/ShadingType`: 2 (axial) or 3 (radial). std::int32_t type{0}; @@ -34,17 +40,19 @@ struct Shading { std::array coords{}; /// `/Domain` `[t0 t1]` of the parametric variable (default `[0 1]`). std::array domain{0, 1}; - /// `/Extend`: whether the shading continues beyond the axis ends. - std::array extend{false, false}; /// Colour stops sampled across `domain` (offsets in [0, 1], `stops.front()` /// at `t0`), in source order — at least two. std::vector stops; + /// `/Extend`: whether the shading continues beyond the axis ends. Parsed but + /// not yet honoured (see the struct note). + std::array extend{false, false}; /// `/Background` colour (sRGB), painted outside the shading where `/Extend` - /// does not reach; absent when the shading declares none. + /// does not reach; absent when the shading declares none. Parsed but not yet + /// honoured (see the struct note). bool has_background{false}; std::array background{}; /// `/BBox` `[x0 y0 x1 y1]` in shading space, clipping the shading; absent - /// when none is declared. + /// when none is declared. Parsed but not yet honoured (see the struct note). bool has_bbox{false}; std::array bbox{}; }; @@ -57,9 +65,8 @@ struct ShadingContext { }; /// Build a shading from its `/Shading` dictionary, sampling its tint function -/// into `stops` through the shading's colour space. `color_context` resolves -/// the -/// `/ColorSpace`. Returns `nullptr` for a malformed or unsupported shading +/// into `stops` through the shading's colour space (resolved via +/// `color_context`). Returns `nullptr` for a malformed or unsupported shading /// (types other than 2/3). std::shared_ptr parse_shading(const Object &object, const ShadingContext &context, From 5a775f4722b7abd85d63cf5b070c6510410c2804 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 28 Jun 2026 17:33:40 +0200 Subject: [PATCH 3/4] cleanup --- src/odr/internal/pdf/pdf_document_parser.cpp | 4 ++-- src/odr/internal/pdf/pdf_object.cpp | 24 ++++++++++---------- src/odr/internal/pdf/pdf_object.hpp | 13 ++++++----- src/odr/internal/pdf/pdf_page_extractor.cpp | 2 +- src/odr/internal/pdf/pdf_shading.cpp | 2 +- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/odr/internal/pdf/pdf_document_parser.cpp b/src/odr/internal/pdf/pdf_document_parser.cpp index 107fa5b3..897cb430 100644 --- a/src/odr/internal/pdf/pdf_document_parser.cpp +++ b/src/odr/internal/pdf/pdf_document_parser.cpp @@ -538,7 +538,7 @@ std::int32_t image_int(DocumentParser &parser, const Dictionary &dictionary, /// The `/Decode` array of an image dictionary as doubles ([] when absent). std::vector image_decode(DocumentParser &parser, const Dictionary &dictionary) { - return as_reals(parser.resolve_object_copy(dictionary.get("Decode"))); + return parser.resolve_object_copy(dictionary.get("Decode")).as_reals(); } /// Bind the parser-backed `resolve`/`load_stream` hooks shared by every typed @@ -676,7 +676,7 @@ void parse_image_data(DocumentParser &parser, const Dictionary &dictionary, if (alpha.empty() && dictionary.has_value("Mask")) { const Object mask = parser.resolve_object_copy(dictionary["Mask"]); if (mask.is_array()) { - color_key = as_reals(mask); + color_key = mask.as_reals(); } else if (dictionary["Mask"].is_reference()) { alpha = resolve_mask_alpha(parser, dictionary["Mask"], width, height, /*stencil=*/true); diff --git a/src/odr/internal/pdf/pdf_object.cpp b/src/odr/internal/pdf/pdf_object.cpp index ce45ad2b..356ce0e1 100644 --- a/src/odr/internal/pdf/pdf_object.cpp +++ b/src/odr/internal/pdf/pdf_object.cpp @@ -11,18 +11,6 @@ namespace odr::internal::pdf { -std::vector as_reals(const Object &object) { - std::vector result; - if (object.is_array()) { - const Array &array = object.as_array(); - result.reserve(array.size()); - for (const Object &item : array) { - result.push_back(item.as_real()); - } - } - return result; -} - void StandardString::to_stream(std::ostream &out) const { // TODO escape out << "(" << string << ")"; @@ -123,6 +111,18 @@ std::optional Object::as_string_opt() && { return std::nullopt; } +std::vector Object::as_reals() const { + std::vector result; + if (is_array()) { + const Array &array = as_array(); + result.reserve(array.size()); + for (const Object &item : array) { + result.push_back(item.as_real()); + } + } + return result; +} + void Object::to_stream(std::ostream &out) const { if (is_null()) { out << "null"; diff --git a/src/odr/internal/pdf/pdf_object.hpp b/src/odr/internal/pdf/pdf_object.hpp index b740534e..4e8f70bc 100644 --- a/src/odr/internal/pdf/pdf_object.hpp +++ b/src/odr/internal/pdf/pdf_object.hpp @@ -223,6 +223,13 @@ class Object final { Array *as_array_ptr() & { return as_ptr(); } Dictionary *as_dictionary_ptr() & { return as_ptr(); } + /// The elements of an array `Object` as doubles, in order (each via + /// `Object::as_real`). Returns an empty vector when `object` is not an array. + /// Convenience for the many PDF arrays that are plain number lists + /// (`/Decode`, + /// `/Coords`, `/Domain`, `/Background`, a colour-key `/Mask`, ...). + std::vector as_reals() const; + void to_stream(std::ostream &) const; [[nodiscard]] std::string to_string() const; @@ -343,12 +350,6 @@ class Dictionary final { Holder m_holder; }; -/// The elements of an array `Object` as doubles, in order (each via -/// `Object::as_real`). Returns an empty vector when `object` is not an array. -/// Convenience for the many PDF arrays that are plain number lists (`/Decode`, -/// `/Coords`, `/Domain`, `/Background`, a colour-key `/Mask`, ...). -std::vector as_reals(const Object &object); - std::ostream &operator<<(std::ostream &, const StandardString &); std::ostream &operator<<(std::ostream &, const HexString &); std::ostream &operator<<(std::ostream &, const Name &); diff --git a/src/odr/internal/pdf/pdf_page_extractor.cpp b/src/odr/internal/pdf/pdf_page_extractor.cpp index d75d3ae8..74f09df3 100644 --- a/src/odr/internal/pdf/pdf_page_extractor.cpp +++ b/src/odr/internal/pdf/pdf_page_extractor.cpp @@ -527,7 +527,7 @@ void emit_inline_image(const GraphicsOperator &op, const Resources &resources, dictionary.get("Width").as_integer_opt().value_or(0)); const auto height = static_cast( dictionary.get("Height").as_integer_opt().value_or(0)); - const std::vector decode_array = as_reals(dictionary.get("Decode")); + const std::vector decode_array = dictionary.get("Decode").as_reals(); // An inline `/ImageMask true` stencil: decode the 1-bpc bitmap and paint it // in the current fill colour, as for a stencil image XObject (ISO diff --git a/src/odr/internal/pdf/pdf_shading.cpp b/src/odr/internal/pdf/pdf_shading.cpp index 7c0dc66f..6aa80f6b 100644 --- a/src/odr/internal/pdf/pdf_shading.cpp +++ b/src/odr/internal/pdf/pdf_shading.cpp @@ -12,7 +12,7 @@ namespace { /// resolving an indirect reference first. std::vector read_numbers(const Dictionary &dict, const std::string &key, const ShadingContext &context) { - return as_reals(context.resolve(dict.get(key))); + return context.resolve(dict.get(key)).as_reals(); } /// Parse the `/Function` of a shading: either one function or an array of From 752e40378fc300a78b5c36e6190148b5ec021e12 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 28 Jun 2026 17:39:35 +0200 Subject: [PATCH 4/4] cleanup --- src/odr/internal/html/pdf_file.cpp | 14 ++++++++------ src/odr/internal/pdf/pdf_shading.cpp | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/odr/internal/html/pdf_file.cpp b/src/odr/internal/html/pdf_file.cpp index 11ae9e72..b2eda69e 100644 --- a/src/odr/internal/html/pdf_file.cpp +++ b/src/odr/internal/html/pdf_file.cpp @@ -295,7 +295,7 @@ class ClipRegistry { /// Honouring them needs the fill clipped to the gradient band/annulus. class GradientRegistry { public: - explicit GradientRegistry(std::uint32_t page) : m_page{page} {} + explicit GradientRegistry(const std::uint32_t page) : m_page{page} {} /// The gradient id to reference via `fill="url(#id)"` for `shading` placed by /// `m` (shading space -> page box). Empty for an unrepresentable shading. @@ -344,7 +344,7 @@ class GradientRegistry { [[nodiscard]] std::string defs() const { return m_defs.str(); } private: - std::uint32_t m_page; + std::uint32_t m_page{}; std::uint32_t m_count{0}; std::unordered_map m_id_by_signature; std::ostringstream m_defs; @@ -355,8 +355,8 @@ class GradientRegistry { /// `sh` time). Returns "" when the shading produced no gradient. The rect spans /// the whole page; the clip (and the gradient's own extent) bound the paint. std::string svg_shading_fragment(const std::string &gradient_id, - const std::string &clip_id, double width, - double height) { + const std::string &clip_id, const double width, + const double height) { if (gradient_id.empty()) { return {}; } @@ -700,7 +700,8 @@ class HtmlServiceImpl final : public HtmlService { // the page viewBox (fill and/or stroke), under any active clip. A // shading-pattern fill is painted through a gradient instead of a // colour. - if (const auto *path = std::get_if(&element)) { + if (const auto *path = std::get_if(&element); + path != nullptr) { const std::string clip_id = clips.register_clip(path->clip, to_box); std::string gradient_id; if (path->fill_shading != nullptr) { @@ -717,7 +718,8 @@ class HtmlServiceImpl final : public HtmlService { // An `sh` shading flood: a `` over the page box filled with the // shading's gradient, bounded by the clip in force at `sh` time. - if (const auto *shading = std::get_if(&element)) { + if (const auto *shading = std::get_if(&element); + shading != nullptr) { if (shading->shading == nullptr) { continue; } diff --git a/src/odr/internal/pdf/pdf_shading.cpp b/src/odr/internal/pdf/pdf_shading.cpp index 6aa80f6b..5ab6e8ff 100644 --- a/src/odr/internal/pdf/pdf_shading.cpp +++ b/src/odr/internal/pdf/pdf_shading.cpp @@ -46,7 +46,7 @@ parse_shading_functions(const Object &function, const ShadingContext &context) { /// into the colour-component vector the colour space expects. std::vector eval_components(const std::vector> &functions, - double t) { + const double t) { std::vector components; for (const auto &function : functions) { std::vector out = function->eval({t});