mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 21:17:55 -06:00
vendor/hayro-interpret/src/pattern.rs: ShadingPattern::new takes the default colour spaces (#3018) and no opacity (this PR); without_opacity (#2977) and set_alpha both kept
381 lines
12 KiB
Rust
381 lines
12 KiB
Rust
//! PDF patterns.
|
|
|
|
use crate::cache::Cache;
|
|
use crate::color::{Color, ColorSpace, DefaultColorSpaces};
|
|
use crate::context::{Context, InterpreterCache};
|
|
use crate::device::Device;
|
|
use crate::font::Glyph;
|
|
use crate::interpret::state::{ActiveTransferFunction, State};
|
|
use crate::shading::Shading;
|
|
use crate::soft_mask::SoftMask;
|
|
use crate::util::{Float32Ext, RectExt, hash128};
|
|
use crate::{BlendMode, CacheKey, ClipPath, GlyphDrawMode, Image, PathDrawMode};
|
|
use crate::{FillRule, InterpreterSettings, Paint, interpret};
|
|
use hayro_syntax::content::TypedIter;
|
|
use hayro_syntax::object::Dict;
|
|
use hayro_syntax::object::Stream;
|
|
use hayro_syntax::object::dict::keys::{
|
|
BBOX, EXT_G_STATE, MATRIX, PAINT_TYPE, RESOURCES, SHADING, X_STEP, Y_STEP,
|
|
};
|
|
use hayro_syntax::object::{Object, dict_or_stream};
|
|
use hayro_syntax::page::Resources;
|
|
use hayro_syntax::xref::XRef;
|
|
use kurbo::{Affine, BezPath, Rect, Shape};
|
|
use std::fmt::{Debug, Formatter};
|
|
use std::sync::Arc;
|
|
|
|
/// A PDF pattern.
|
|
#[derive(Debug, Clone)]
|
|
pub enum Pattern<'a> {
|
|
/// A shading pattern.
|
|
Shading(ShadingPattern),
|
|
/// A tiling pattern.
|
|
Tiling(Box<TilingPattern<'a>>),
|
|
}
|
|
|
|
impl<'a> Pattern<'a> {
|
|
/// PdfCraft patch (38): `defaults` are those of the resources the pattern is selected with,
|
|
/// for a shading pattern's shading in a device space.
|
|
pub(crate) fn new(
|
|
object: Object<'a>,
|
|
ctx: &Context<'a>,
|
|
resources: &Resources<'a>,
|
|
defaults: &DefaultColorSpaces<'_, '_>,
|
|
) -> Option<Self> {
|
|
match object {
|
|
Object::Dict(dict) => Some(Self::Shading(ShadingPattern::new(
|
|
&dict,
|
|
&ctx.interpreter_cache.object_cache,
|
|
defaults,
|
|
)?)),
|
|
Object::Stream(stream) => Some(Self::Tiling(Box::new(TilingPattern::new(
|
|
stream, ctx, resources,
|
|
)?))),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn pre_concat_transform(&mut self, transform: Affine) {
|
|
match self {
|
|
Self::Shading(p) => {
|
|
p.matrix = transform * p.matrix;
|
|
let transformed_clip_path = p.shading.clip_path.clone().map(|r| p.matrix * r);
|
|
Arc::make_mut(&mut p.shading).clip_path = transformed_clip_path;
|
|
}
|
|
Self::Tiling(p) => p.matrix = transform * p.matrix,
|
|
}
|
|
}
|
|
|
|
/// PdfCraft patch (46): the pattern without the fill or stroke opacity it captured when it was
|
|
/// set (`scn`). A stencil mask's transparency group applies the opacity in force when the mask
|
|
/// is drawn, so a pattern paint carrying it too drew the mask at the opacity squared (#2975).
|
|
pub(crate) fn without_opacity(mut self) -> Self {
|
|
match &mut self {
|
|
Self::Shading(p) => p.opacity = 1.0,
|
|
Self::Tiling(p) => {
|
|
p.non_stroking_paint = p.non_stroking_paint.clone().opaque();
|
|
p.stroke_paint = p.stroke_paint.clone().opaque();
|
|
}
|
|
}
|
|
self
|
|
}
|
|
|
|
/// PdfCraft patch: set the alpha of the graphics state in effect when the pattern is painted. The `gs` operator
|
|
/// can come after `scn`/`SCN`, so the alpha must not be captured when the pattern is selected.
|
|
pub(crate) fn set_alpha(&mut self, alpha: f32) {
|
|
if let Self::Shading(p) = self {
|
|
p.opacity = alpha;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_transfer_function(&mut self, tf: ActiveTransferFunction) {
|
|
if let Self::Shading(p) = self {
|
|
p.transfer_function = Some(tf);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CacheKey for Pattern<'_> {
|
|
fn cache_key(&self) -> u128 {
|
|
match self {
|
|
Self::Shading(p) => p.cache_key(),
|
|
Self::Tiling(p) => p.cache_key(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A shading pattern.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ShadingPattern {
|
|
/// The underlying shading of the pattern.
|
|
pub shading: Arc<Shading>,
|
|
/// A transformation matrix to apply prior to rendering.
|
|
pub matrix: Affine,
|
|
/// An additional opacity to apply to the shading pattern.
|
|
pub opacity: f32,
|
|
/// An optional transfer function to apply to the shading's output colors.
|
|
pub transfer_function: Option<ActiveTransferFunction>,
|
|
}
|
|
|
|
impl ShadingPattern {
|
|
pub(crate) fn new(
|
|
dict: &Dict<'_>,
|
|
cache: &Cache,
|
|
defaults: &DefaultColorSpaces<'_, '_>,
|
|
) -> Option<Self> {
|
|
let shading = dict.get::<Object<'_>>(SHADING).and_then(|o| {
|
|
let (dict, stream) = dict_or_stream(&o)?;
|
|
|
|
Shading::new(dict, stream, cache, defaults)
|
|
})?;
|
|
let matrix = dict
|
|
.get::<[f64; 6]>(MATRIX)
|
|
.map(Affine::new)
|
|
.unwrap_or_default();
|
|
|
|
if dict.contains_key(EXT_G_STATE) {
|
|
warn!("shading patterns with ext_g_state are not supported yet");
|
|
}
|
|
|
|
Some(Self {
|
|
shading: Arc::new(shading),
|
|
opacity: 1.0,
|
|
matrix,
|
|
transfer_function: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl CacheKey for ShadingPattern {
|
|
fn cache_key(&self) -> u128 {
|
|
hash128(&(self.shading.cache_key(), self.matrix.cache_key()))
|
|
}
|
|
}
|
|
|
|
/// A tiling pattern.
|
|
#[derive(Clone)]
|
|
pub struct TilingPattern<'a> {
|
|
cache_key: u128,
|
|
ctx_bbox: Rect,
|
|
/// The bbox of the tiling pattern.
|
|
pub bbox: Rect,
|
|
/// The step in the x direction.
|
|
pub x_step: f32,
|
|
/// The step in the y direction.
|
|
pub y_step: f32,
|
|
/// A transformation to apply prior to rendering.
|
|
pub matrix: Affine,
|
|
stream: Stream<'a>,
|
|
is_color: bool,
|
|
pub(crate) stroke_paint: Color,
|
|
pub(crate) non_stroking_paint: Color,
|
|
pub(crate) parent_resources: Resources<'a>,
|
|
pub(crate) cache: InterpreterCache<'a>,
|
|
pub(crate) settings: InterpreterSettings,
|
|
pub(crate) xref: &'a XRef,
|
|
nesting_depth: u32,
|
|
}
|
|
|
|
impl Debug for TilingPattern<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str("TilingPattern")
|
|
}
|
|
}
|
|
|
|
impl<'a> TilingPattern<'a> {
|
|
pub(crate) fn new(
|
|
stream: Stream<'a>,
|
|
ctx: &Context<'a>,
|
|
resources: &Resources<'a>,
|
|
) -> Option<Self> {
|
|
let cache_key = stream.cache_key();
|
|
let dict = stream.dict();
|
|
|
|
let bbox = dict.get::<hayro_syntax::object::Rect>(BBOX)?.to_kurbo();
|
|
let x_step = dict.get::<f32>(X_STEP)?;
|
|
let y_step = dict.get::<f32>(Y_STEP)?;
|
|
|
|
if x_step.is_nearly_zero() || y_step.is_nearly_zero() || bbox.is_zero_area() {
|
|
return None;
|
|
}
|
|
|
|
let is_color = dict.get::<u8>(PAINT_TYPE)? == 1;
|
|
let matrix = dict
|
|
.get::<[f64; 6]>(MATRIX)
|
|
.map(Affine::new)
|
|
.unwrap_or_default();
|
|
|
|
let state = ctx.get().clone();
|
|
let ctx_bbox = ctx.bbox();
|
|
|
|
let fill_cs = state
|
|
.graphics_state
|
|
.none_stroke_cs
|
|
.pattern_cs()
|
|
.unwrap_or(ColorSpace::device_gray());
|
|
let stroke_cs = state
|
|
.graphics_state
|
|
.stroke_cs
|
|
.pattern_cs()
|
|
.unwrap_or(ColorSpace::device_gray());
|
|
|
|
let non_stroking_paint = Color::new(
|
|
fill_cs,
|
|
state.graphics_state.non_stroke_color.clone(),
|
|
state.graphics_state.non_stroke_alpha,
|
|
);
|
|
let stroke_paint = Color::new(
|
|
stroke_cs,
|
|
state.graphics_state.stroke_color.clone(),
|
|
state.graphics_state.stroke_alpha,
|
|
);
|
|
let nesting_depth = ctx.nesting_depth() + 1;
|
|
|
|
Some(Self {
|
|
cache_key,
|
|
bbox,
|
|
x_step,
|
|
y_step,
|
|
matrix,
|
|
ctx_bbox,
|
|
is_color,
|
|
stream,
|
|
stroke_paint,
|
|
non_stroking_paint,
|
|
settings: ctx.settings.clone(),
|
|
parent_resources: resources.clone(),
|
|
cache: ctx.interpreter_cache.clone(),
|
|
xref: ctx.xref,
|
|
nesting_depth,
|
|
})
|
|
}
|
|
|
|
/// Interpret the contents of the pattern into the given device.
|
|
pub fn interpret(
|
|
&self,
|
|
device: &mut impl Device<'a>,
|
|
initial_transform: Affine,
|
|
is_stroke: bool,
|
|
) -> Option<()> {
|
|
// PdfCraft patch: a tiling pattern can paint itself (directly, or through resources it
|
|
// inherits when its own are missing), which recursed until the stack overflowed. Bound
|
|
// the nesting like XObjects do, tighter because every level rasterizes a tile.
|
|
if self.nesting_depth > crate::context::MAX_PAINT_NESTING || !crate::context::take_nested_paint(self.nesting_depth) {
|
|
warn!("tiling pattern nesting depth or paint budget exceeded");
|
|
return None;
|
|
}
|
|
let state = State::new(initial_transform);
|
|
|
|
let mut context = Context::new_with(
|
|
state.ctm,
|
|
// TODO: bbox?
|
|
(initial_transform * self.ctx_bbox.to_path(0.1)).bounding_box(),
|
|
&self.cache,
|
|
self.xref,
|
|
self.settings.clone(),
|
|
state,
|
|
self.nesting_depth,
|
|
);
|
|
|
|
// PdfCraft patch: see `context::MAX_PAGE_CONTENT`.
|
|
let decoded = crate::context::decode_content(&self.stream)?;
|
|
let resources = Resources::from_parent(
|
|
self.stream.dict().get(RESOURCES).unwrap_or_default(),
|
|
self.parent_resources.clone(),
|
|
);
|
|
let iter = TypedIter::new(decoded.as_ref());
|
|
|
|
let clip_path = ClipPath {
|
|
path: initial_transform * self.bbox.to_path(0.1),
|
|
fill: FillRule::NonZero,
|
|
};
|
|
device.push_clip_path(&clip_path);
|
|
|
|
if self.is_color {
|
|
interpret(iter, &resources, &mut context, device);
|
|
} else {
|
|
let paint = if !is_stroke {
|
|
Paint::Color(self.non_stroking_paint.clone())
|
|
} else {
|
|
Paint::Color(self.stroke_paint.clone())
|
|
};
|
|
|
|
let mut device = StencilPatternDevice::new(device, paint.clone());
|
|
interpret(iter, &resources, &mut context, &mut device);
|
|
}
|
|
|
|
device.pop_clip_path();
|
|
|
|
Some(())
|
|
}
|
|
}
|
|
|
|
impl CacheKey for TilingPattern<'_> {
|
|
fn cache_key(&self) -> u128 {
|
|
self.cache_key
|
|
}
|
|
}
|
|
|
|
struct StencilPatternDevice<'a, 'b, T: Device<'a>> {
|
|
inner: &'b mut T,
|
|
paint: Paint<'a>,
|
|
}
|
|
|
|
impl<'a, 'b, T: Device<'a>> StencilPatternDevice<'a, 'b, T> {
|
|
pub(crate) fn new(device: &'b mut T, paint: Paint<'a>) -> Self {
|
|
Self {
|
|
inner: device,
|
|
paint,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only filling, stroking of paths and stencil masks are allowed.
|
|
impl<'a, T: Device<'a>> Device<'a> for StencilPatternDevice<'a, '_, T> {
|
|
fn draw_path(
|
|
&mut self,
|
|
path: &BezPath,
|
|
transform: Affine,
|
|
_: &Paint<'_>,
|
|
draw_mode: &PathDrawMode,
|
|
) {
|
|
self.inner
|
|
.draw_path(path, transform, &self.paint, draw_mode);
|
|
}
|
|
|
|
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
|
|
|
|
fn push_clip_path(&mut self, clip_path: &ClipPath) {
|
|
self.inner.push_clip_path(clip_path);
|
|
}
|
|
|
|
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
|
|
|
|
fn draw_glyph(
|
|
&mut self,
|
|
g: &Glyph<'a>,
|
|
transform: Affine,
|
|
glyph_transform: Affine,
|
|
p: &Paint<'a>,
|
|
draw_mode: &GlyphDrawMode,
|
|
) {
|
|
self.inner
|
|
.draw_glyph(g, transform, glyph_transform, p, draw_mode);
|
|
}
|
|
|
|
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
|
if let Image::Stencil(mut s) = image {
|
|
s.paint = self.paint.clone();
|
|
self.inner.draw_image(Image::Stencil(s), transform);
|
|
}
|
|
}
|
|
|
|
fn pop_clip_path(&mut self) {
|
|
self.inner.pop_clip_path();
|
|
}
|
|
|
|
fn pop_transparency_group(&mut self) {}
|
|
|
|
fn set_blend_mode(&mut self, _: BlendMode) {}
|
|
}
|