Skip to content
Closed
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
45 changes: 41 additions & 4 deletions src/rendering/ext_gstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ use crate::document::PdfDocument;
use crate::error::Result;
use crate::object::Object;

/// Parsed effects of a PDF `ExtGState` dictionary. Only the fields actually
/// applied during rendering are captured (fill/stroke alpha, blend mode, and
/// the overprint parameters from ISO 32000-1 §11.7.4). Anything else
/// (TK / SMask / AIS) is intentionally ignored so the cached entry stays tiny.
/// Parsed effects of a PDF `ExtGState` dictionary. Captures fill/stroke
/// alpha, blend mode, overprint parameters (§11.7.4), and the soft-mask
/// reference (§11.6.5.2). Anything else (TK / AIS) is intentionally ignored
/// so the cached entry stays tiny.
#[derive(Clone, Debug, Default)]
pub(crate) struct ParsedExtGState {
pub(crate) fill_alpha: Option<f32>,
Expand All @@ -25,6 +25,32 @@ pub(crate) struct ParsedExtGState {
pub(crate) fill_overprint: Option<bool>,
/// Overprint mode (ExtGState `/OPM`, §11.7.4). 0 = standard, 1 = nonzero.
pub(crate) overprint_mode: Option<u8>,
/// Soft-mask reference (§11.6.5.2). The renderer materialises this into
/// an alpha mask pixmap after parsing — the parser stays pure, just
/// captures the dict / `/None` sentinel for the caller to act on.
pub(crate) soft_mask: Option<SoftMaskSpec>,
/// Cached materialised soft-mask alpha buffer, populated by the renderer
/// on first use. The outer cache (the `ext_g_state_cache` HashMap in
/// `execute_operators`) is keyed by `dict_name`; this field is a
/// validity check against `cached_install_transform` so a repeat `gs`
/// call at a *different* CTM correctly re-rasterises the group instead
/// of reusing a stale buffer.
pub(crate) cached_soft_mask_alpha: Option<tiny_skia::Mask>,
/// CTM that produced `cached_soft_mask_alpha`. Compared bitwise to the
/// current install-time CTM to decide whether the cache is reusable.
pub(crate) cached_install_transform: Option<tiny_skia::Transform>,
}

/// What an ExtGState `/SMask` entry tells the renderer to do.
#[derive(Clone, Debug)]
pub(crate) enum SoftMaskSpec {
/// `/SMask /None` — clear any currently-active soft mask.
None,
/// `/SMask <dict>` — the dict is captured verbatim so the renderer can
/// read `/S` (subtype) and `/G` (the transparency-group Form XObject)
/// at render time, when it has the page pixmap context needed to
/// rasterise the group.
Dict(Object),
}

impl ParsedExtGState {
Expand Down Expand Up @@ -106,6 +132,17 @@ pub(crate) fn parse_ext_g_state_inner(
out.overprint_mode = Some(if opm == 1 { 1 } else { 0 });
}

// §11.6.5.2: `/SMask /None` clears any active soft mask; `/SMask <dict>`
// installs a new one. We capture the dict here and defer the actual
// group rasterisation to the renderer where the page pixmap exists.
if let Some(smask) = state_dict.get("SMask") {
let resolved = doc.resolve_object(smask)?;
out.soft_mask = Some(match &resolved {
Object::Name(n) if n == "None" => SoftMaskSpec::None,
_ => SoftMaskSpec::Dict(resolved),
});
}

Ok(out)
}

Expand Down
13 changes: 12 additions & 1 deletion src/rendering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ pub(crate) fn create_stroke_paint(gs: &GraphicsState, blend_mode: &str) -> Paint
paint
}

/// Convert PDF blend mode to tiny-skia.
/// Convert PDF blend mode (ISO 32000-1 §11.3.5) to tiny-skia. The four
/// non-separable modes (`Hue`, `Saturation`, `Color`, `Luminosity` —
/// §11.3.5.3) are dispatched to tiny-skia's native HSL-based blend
/// stages, which match the W3C / Skia / Acrobat formulas. The separable
/// modes are the §11.3.5.2 set.
pub(crate) fn pdf_blend_mode_to_skia(mode: &str) -> tiny_skia::BlendMode {
match mode {
"Normal" => tiny_skia::BlendMode::SourceOver,
// Separable (§11.3.5.2)
"Multiply" => tiny_skia::BlendMode::Multiply,
"Screen" => tiny_skia::BlendMode::Screen,
"Overlay" => tiny_skia::BlendMode::Overlay,
Expand All @@ -90,6 +95,12 @@ pub(crate) fn pdf_blend_mode_to_skia(mode: &str) -> tiny_skia::BlendMode {
"SoftLight" => tiny_skia::BlendMode::SoftLight,
"Difference" => tiny_skia::BlendMode::Difference,
"Exclusion" => tiny_skia::BlendMode::Exclusion,
// Non-separable (§11.3.5.3). These collapse to SourceOver if the
// tiny-skia version ever drops the HSL stages.
"Hue" => tiny_skia::BlendMode::Hue,
"Saturation" => tiny_skia::BlendMode::Saturation,
"Color" => tiny_skia::BlendMode::Color,
"Luminosity" => tiny_skia::BlendMode::Luminosity,
_ => tiny_skia::BlendMode::SourceOver,
}
}
Expand Down
Loading
Loading