Skip to content
Open
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
2 changes: 2 additions & 0 deletions app/pybind_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,7 @@ PYBIND11_MODULE(pdf_parsers, m) {
render_shapes (bool): Paint vector shape instructions [default=true].
render_shadings (bool): Paint shading instructions [default=true].
render_non_rect_clip_masks (bool): Apply non-rectangular clip masks [default=true].
render_soft_masks (bool): Composite paint through ExtGState soft masks (/SMask) [default=true].
min_stroke_width (float): Minimum stroke width in device pixels for hairlines and sub-pixel vector strokes [default=1.0].
draw_text_bbox (bool): Draw bounding quad for each text cell [default=false].
draw_text_basepoint (bool): Draw the text baseline origin as a small red dot [default=false].
Expand All @@ -1289,6 +1290,7 @@ PYBIND11_MODULE(pdf_parsers, m) {
.def_readwrite("render_shapes", &pdflib::render_config::render_shapes)
.def_readwrite("render_shadings", &pdflib::render_config::render_shadings)
.def_readwrite("render_non_rect_clip_masks", &pdflib::render_config::render_non_rect_clip_masks)
.def_readwrite("render_soft_masks", &pdflib::render_config::render_soft_masks)
.def_readwrite("min_stroke_width", &pdflib::render_config::min_stroke_width)
.def_readwrite("draw_text_bbox", &pdflib::render_config::draw_text_bbox)
.def_readwrite("draw_text_basepoint", &pdflib::render_config::draw_text_basepoint)
Expand Down
2 changes: 2 additions & 0 deletions docling_parse/pdf_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,8 @@ def _copy_render_config(src: RenderConfig) -> RenderConfig:
dst.render_shadings = src.render_shadings
if hasattr(src, "render_non_rect_clip_masks"):
dst.render_non_rect_clip_masks = src.render_non_rect_clip_masks
if hasattr(src, "render_soft_masks"):
dst.render_soft_masks = src.render_soft_masks
dst.min_stroke_width = src.min_stroke_width
dst.draw_text_bbox = src.draw_text_bbox
dst.resolve_fonts = src.resolve_fonts
Expand Down
10 changes: 6 additions & 4 deletions src/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
#include <parse/page_items/page_hyperlink.h>
#include <parse/page_items/page_hyperlinks.h>
#include <parse/page_items/render_instructions.h>
#include <parse/page_items/soft_mask_instruction.h>

// the outline resolves its destinations against the page geometry, so it comes
// after the page items rather than next to the other qpdf extractors
Expand All @@ -98,13 +99,14 @@
#include <parse/pdf_resources/page_font.h>
#include <parse/pdf_resources/page_fonts.h>

// pdf_function carries the colour ramp of a shading, the tint transform of a
// /Separation or /DeviceN colour space and the transfer function of a soft
// mask, so it comes before the resources that hold those
#include <parse/pdf_resources/pdf_function.h>

#include <parse/pdf_resources/page_grph.h>
#include <parse/pdf_resources/page_grphs.h>

// pdf_function carries both the colour ramp of a shading and the tint
// transform of a /Separation or /DeviceN colour space, so it comes first
#include <parse/pdf_resources/pdf_function.h>

#include <parse/pdf_resources/page_colorspace.h>
#include <parse/pdf_resources/page_colorspaces.h>

Expand Down
2 changes: 1 addition & 1 deletion src/parse/enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ namespace pdflib
enum soft_mask_state {
SOFT_MASK_ABSENT, // key not in the dictionary: inherit the current mask
SOFT_MASK_NONE, // /None: clear the current soft mask
SOFT_MASK_PRESENT, // a mask dictionary (not applied)
SOFT_MASK_PRESENT, // a mask dictionary (see soft_mask_spec)
};

std::string to_string(blend_mode_name name)
Expand Down
59 changes: 55 additions & 4 deletions src/parse/page_items/render_instructions.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ namespace pdflib
SHADING_RENDER_INSTRUCTION, // paint a shading (`sh`) over the clip region
};

// An ExtGState soft mask decoded for the renderer (soft_mask_instruction.h).
// It holds render instructions of its own, so it is defined after the
// container below; the paint instructions only ever hold it by pointer.
class soft_mask_instruction;

// One sample of a shading's colour ramp: `offset` runs from 0.0 at the
// start of the shading axis to 1.0 at its end (the shading's /Domain is
// already folded in), `rgb` is the colour there in 0-255 device RGB.
Expand Down Expand Up @@ -327,6 +332,14 @@ namespace pdflib
void set_blend_mode(blend_mode_name mode) { blend_mode_ = mode; }
blend_mode_name get_blend_mode() const { return blend_mode_; }

// ExtGState /SMask in force when this was painted (11.6.5), decoded to
// page-space instructions; null when none. Every instruction painted
// under the same mask shares one object, so the renderer rasterises the
// mask once.
void set_soft_mask(std::shared_ptr<soft_mask_instruction> mask) { soft_mask_ = std::move(mask); }
const std::shared_ptr<soft_mask_instruction>& get_soft_mask() const { return soft_mask_; }
bool has_soft_mask() const { return soft_mask_ != nullptr; }

void set_rendering_mode(int mode) { rendering_mode_ = mode; }
int get_rendering_mode() const { return rendering_mode_; }
bool is_invisible() const { return rendering_mode_ == 3 or rendering_mode_ == 7; }
Expand Down Expand Up @@ -401,6 +414,7 @@ namespace pdflib
double fill_alpha_ = 1.0;
clip_state_instruction clip_state_;
blend_mode_name blend_mode_ = BLEND_MODE_NORMAL;
std::shared_ptr<soft_mask_instruction> soft_mask_;
int rendering_mode_ = 0;
};

Expand Down Expand Up @@ -573,6 +587,14 @@ namespace pdflib
void set_blend_mode(blend_mode_name mode) { blend_mode_ = mode; }
blend_mode_name get_blend_mode() const { return blend_mode_; }

// ExtGState /SMask in force when this was painted (11.6.5), decoded to
// page-space instructions; null when none. Every instruction painted
// under the same mask shares one object, so the renderer rasterises the
// mask once.
void set_soft_mask(std::shared_ptr<soft_mask_instruction> mask) { soft_mask_ = std::move(mask); }
const std::shared_ptr<soft_mask_instruction>& get_soft_mask() const { return soft_mask_; }
bool has_soft_mask() const { return soft_mask_ != nullptr; }

// The /Filter chain of the stream these samples came out of, in the order
// the PDF lists it (e.g. {"/FlateDecode", "/JPXDecode"}); empty for an
// uncompressed image and for a rasterised Type3 glyph, which has no stream
Expand All @@ -588,6 +610,7 @@ namespace pdflib
private:

blend_mode_name blend_mode_ = BLEND_MODE_NORMAL;
std::shared_ptr<soft_mask_instruction> soft_mask_;
std::vector<std::string> filters_;

const std::string xobject_key;
Expand Down Expand Up @@ -775,6 +798,8 @@ namespace pdflib
stroke_alpha, fill_alpha,
clip_state.translated(dx, dy));
copy.blend_mode_ = blend_mode_;
// the mask is anchored in page space, so a moved copy keeps it as is
copy.soft_mask_ = soft_mask_;
return copy;
}

Expand All @@ -784,9 +809,18 @@ namespace pdflib
void set_blend_mode(blend_mode_name mode) { blend_mode_ = mode; }
blend_mode_name get_blend_mode() const { return blend_mode_; }

// ExtGState /SMask in force when this was painted (11.6.5), decoded to
// page-space instructions; null when none. Every instruction painted
// under the same mask shares one object, so the renderer rasterises the
// mask once.
void set_soft_mask(std::shared_ptr<soft_mask_instruction> mask) { soft_mask_ = std::move(mask); }
const std::shared_ptr<soft_mask_instruction>& get_soft_mask() const { return soft_mask_; }
bool has_soft_mask() const { return soft_mask_ != nullptr; }

private:

blend_mode_name blend_mode_ = BLEND_MODE_NORMAL;
std::shared_ptr<soft_mask_instruction> soft_mask_;

const std::vector<shape_subpath> subpaths;

Expand Down Expand Up @@ -875,6 +909,8 @@ namespace pdflib
extend_start, extend_end, fill_alpha,
clip_state.translated(dx, dy));
copy.blend_mode_ = blend_mode_;
// the mask is anchored in page space, so a moved copy keeps it as is
copy.soft_mask_ = soft_mask_;
return copy;
}

Expand All @@ -884,9 +920,18 @@ namespace pdflib
void set_blend_mode(blend_mode_name mode) { blend_mode_ = mode; }
blend_mode_name get_blend_mode() const { return blend_mode_; }

// ExtGState /SMask in force when this was painted (11.6.5), decoded to
// page-space instructions; null when none. Every instruction painted
// under the same mask shares one object, so the renderer rasterises the
// mask once.
void set_soft_mask(std::shared_ptr<soft_mask_instruction> mask) { soft_mask_ = std::move(mask); }
const std::shared_ptr<soft_mask_instruction>& get_soft_mask() const { return soft_mask_; }
bool has_soft_mask() const { return soft_mask_ != nullptr; }

private:

blend_mode_name blend_mode_ = BLEND_MODE_NORMAL;
std::shared_ptr<soft_mask_instruction> soft_mask_;

const std::string shading_key;
const shading_geometry geometry;
Expand Down Expand Up @@ -941,9 +986,11 @@ namespace pdflib
return shading_instructions;
}

// render method
// render method. `set_size` sizes the renderer's canvas from the size
// instruction first; a soft-mask group is painted onto a canvas the
// renderer already holds, so it passes false.
template<typename renderer_type>
void iterate_over_instructions(renderer_type& renderer);
void iterate_over_instructions(renderer_type& renderer, bool set_size = true);

private:

Expand Down Expand Up @@ -1005,9 +1052,13 @@ namespace pdflib
}

template<typename renderer_type>
void pdf_render_instructions::iterate_over_instructions(renderer_type& renderer)
void pdf_render_instructions::iterate_over_instructions(renderer_type& renderer,
bool set_size)
{
renderer.set_size(size_instr);
if(set_size)
{
renderer.set_size(size_instr);
}

for(const auto& instr : instructions)
{
Expand Down
93 changes: 93 additions & 0 deletions src/parse/page_items/soft_mask_instruction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//-*-C++-*-

#ifndef PDF_SOFT_MASK_INSTRUCTION_H
#define PDF_SOFT_MASK_INSTRUCTION_H

namespace pdflib
{
// An ExtGState soft mask (ISO 32000-1, 11.6.5), decoded for the renderer.
//
// The mask is a transparency group whose luminosity (or alpha) becomes the
// coverage of everything painted while the mask is in force. The group's
// content has been sub-decoded into `group`: ordinary render instructions
// in page space, placed by the CTM in force at the `gs` that installed the
// mask -- which is what fixes the mask's coordinate system (11.6.5.1), not
// the CTM at the paint -- composed with the group's /Matrix.
//
// One instance is shared by every instruction painted under the same mask,
// so a renderer rasterises the group once and reuses the coverage.
class soft_mask_instruction
{
public:
soft_mask_instruction(std::string key,
bool luminosity,
std::array<int, 3> backdrop_rgb,
std::vector<uint8_t> transfer,
std::array<double, 8> bbox_quad):
key_(std::move(key)),
luminosity_(luminosity),
backdrop_rgb_(backdrop_rgb),
transfer_(std::move(transfer)),
bbox_quad_(bbox_quad)
{}

// Two masks in force at once, the coverage being the product of both.
// A transparency group painted under a mask is masked as a whole, and a
// mask its content installs applies inside the group (11.6.6); with the
// group's parameters pushed down onto its content (see
// pdf_state<GRPH>::enter_transparency_group), each paint inside carries
// both masks.
soft_mask_instruction(std::shared_ptr<soft_mask_instruction> outer,
std::shared_ptr<soft_mask_instruction> inner):
key_(outer->get_key() + "*" + inner->get_key()),
luminosity_(true),
backdrop_rgb_({0, 0, 0}),
bbox_quad_({}),
outer_(std::move(outer)),
inner_(std::move(inner))
{}

// The ExtGState resource name the mask came from, for diagnostics.
const std::string& get_key() const { return key_; }

// /S /Luminosity reads the coverage off the group's colour, /S /Alpha
// off its alpha.
bool is_luminosity() const { return luminosity_; }

// /BC, the colour the group is composited against, in device RGB. It is
// also what shows outside the group's /BBox. Black when absent.
const std::array<int, 3>& get_backdrop_rgb() const { return backdrop_rgb_; }

// /TR sampled at 256 points, mapping luminosity (or alpha) to coverage;
// empty for the identity.
const std::vector<uint8_t>& get_transfer() const { return transfer_; }

// The group's /BBox as four page-space corners (x0, y0, ..., x3, y3).
const std::array<double, 8>& get_bbox_quad() const { return bbox_quad_; }

// The group's content, as instructions in page space.
pdf_render_instructions& get_group() { return group_; }
const pdf_render_instructions& get_group() const { return group_; }

// True for the product of two masks, which has no group of its own.
bool is_intersection() const { return outer_ != nullptr; }
const std::shared_ptr<soft_mask_instruction>& get_outer() const { return outer_; }
const std::shared_ptr<soft_mask_instruction>& get_inner() const { return inner_; }

private:

std::string key_;
bool luminosity_;
std::array<int, 3> backdrop_rgb_;
std::vector<uint8_t> transfer_;
std::array<double, 8> bbox_quad_;

pdf_render_instructions group_;

std::shared_ptr<soft_mask_instruction> outer_;
std::shared_ptr<soft_mask_instruction> inner_;
};

}

#endif
Loading
Loading