mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 02:41:57 -06:00
M13.2: PDF / Illustrator (.ai) / EPS vector footage (effectcraft-pdf): continuous rasterisation, layered composition import, Create Shapes from Vector Layer; SVG render tree clip groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
30a92ba82f
commit
da0ec526f3
@@ -0,0 +1,573 @@
|
||||
//! Pages and content streams (ISO 32000-1 §7.7.3, §8): the page tree, the graphics state,
|
||||
//! path construction and painting, clipping, colour, shading patterns and `sh`, form XObjects
|
||||
//! and optional-content marked sequences. Text, images and tiling patterns are skipped (listed
|
||||
//! in [`effectcraft_svg::Doc::skipped`]).
|
||||
|
||||
use effectcraft_svg::{Affine, BezPath, Cap, FillRule, Gradient, GradientKind, Join, Paint, Spread, Stroke};
|
||||
use kurbo::Point;
|
||||
|
||||
use crate::build::Builder;
|
||||
use crate::color::{Cs, Func, color_space, text_string};
|
||||
use crate::object::{Dict, File, Lexer, Obj, decode_stream};
|
||||
|
||||
/// A page: its dictionary with inherited attributes resolved.
|
||||
pub struct Page {
|
||||
pub dict: Dict,
|
||||
pub resources: Dict,
|
||||
/// CropBox (else MediaBox) `[x0, y0, x1, y1]`.
|
||||
pub bbox: [f64; 4],
|
||||
pub rotate: i32,
|
||||
}
|
||||
|
||||
/// The pages in order.
|
||||
pub fn pages(file: &File) -> Vec<Page> {
|
||||
let mut out = vec![];
|
||||
let Some(cat) = file.catalog() else { return out };
|
||||
let Some(root) = file.get_dict(cat, "Pages") else { return out };
|
||||
fn walk(file: &File, node: &Dict, inherited: (Option<Dict>, Option<Vec<f64>>, Option<Vec<f64>>, i32), out: &mut Vec<Page>, depth: usize) {
|
||||
if depth > 32 || out.len() > 10_000 {
|
||||
return;
|
||||
}
|
||||
let res = file.get_dict(node, "Resources").cloned().or(inherited.0);
|
||||
let media = file.get(node, "MediaBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.1);
|
||||
let crop = file.get(node, "CropBox").map(|m| file.nums(m)).filter(|v| v.len() == 4).or(inherited.2);
|
||||
let rot = file.get_num(node, "Rotate").map(|r| r as i32).unwrap_or(inherited.3);
|
||||
match file.get(node, "Kids").and_then(Obj::array) {
|
||||
Some(kids) if file.get(node, "Type").and_then(Obj::name) != Some("Page") => {
|
||||
for k in kids {
|
||||
if let Some(d) = file.resolve(k).dict() {
|
||||
walk(file, d, (res.clone(), media.clone(), crop.clone(), rot), out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let b = crop.or(media).unwrap_or_else(|| vec![0.0, 0.0, 612.0, 792.0]);
|
||||
let bbox = [b[0].min(b[2]), b[1].min(b[3]), b[0].max(b[2]), b[1].max(b[3])];
|
||||
out.push(Page { dict: node.clone(), resources: res.unwrap_or_default(), bbox, rotate: rot.rem_euclid(360) });
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(file, root, (None, None, None, 0), &mut out, 0);
|
||||
out
|
||||
}
|
||||
|
||||
/// The page's content streams, decoded and joined.
|
||||
pub fn page_content(file: &File, page: &Page) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
let parts: Vec<&Obj> = match page.dict.get("Contents").map(|c| file.resolve(c)) {
|
||||
Some(Obj::Array(a)) => a.iter().map(|x| file.resolve(x)).collect(),
|
||||
Some(o) => vec![o],
|
||||
None => vec![],
|
||||
};
|
||||
for p in parts {
|
||||
if let Obj::Stream(d, raw) = p
|
||||
&& let Some(data) = decode_stream(file, d, raw)
|
||||
{
|
||||
out.extend_from_slice(&data);
|
||||
out.push(b'\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Fill {
|
||||
Color([f64; 3]),
|
||||
Paint(Paint),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GState {
|
||||
ctm: Affine,
|
||||
fill_cs: Cs,
|
||||
stroke_cs: Cs,
|
||||
fill: Fill,
|
||||
stroke: Fill,
|
||||
lw: f64,
|
||||
cap: Cap,
|
||||
join: Join,
|
||||
miter: f64,
|
||||
dash: Option<(Vec<f64>, f64)>,
|
||||
fill_alpha: f64,
|
||||
stroke_alpha: f64,
|
||||
}
|
||||
|
||||
impl Default for GState {
|
||||
fn default() -> Self {
|
||||
GState {
|
||||
ctm: Affine::IDENTITY,
|
||||
fill_cs: Cs::Gray,
|
||||
stroke_cs: Cs::Gray,
|
||||
fill: Fill::Color([0.0; 3]),
|
||||
stroke: Fill::Color([0.0; 3]),
|
||||
lw: 1.0,
|
||||
cap: Cap::Butt,
|
||||
join: Join::Miter,
|
||||
miter: 10.0,
|
||||
dash: None,
|
||||
fill_alpha: 1.0,
|
||||
stroke_alpha: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Interp<'a> {
|
||||
file: &'a File,
|
||||
pub b: Builder,
|
||||
gs: GState,
|
||||
saved: Vec<(GState, usize)>,
|
||||
path: BezPath,
|
||||
cur: Option<Point>,
|
||||
start: Option<Point>,
|
||||
pending_clip: Option<FillRule>,
|
||||
marked: Vec<Option<usize>>,
|
||||
/// The page box in default user space (for `sh`).
|
||||
page_box: [f64; 4],
|
||||
budget: usize,
|
||||
}
|
||||
|
||||
fn affine(v: &[f64]) -> Affine {
|
||||
if v.len() < 6 {
|
||||
return Affine::IDENTITY;
|
||||
}
|
||||
Affine::new([v[0], v[1], v[2], v[3], v[4], v[5]])
|
||||
}
|
||||
|
||||
impl<'a> Interp<'a> {
|
||||
pub fn new(file: &'a File, page_box: [f64; 4]) -> Self {
|
||||
Interp {
|
||||
file,
|
||||
b: Builder::new(),
|
||||
gs: GState::default(),
|
||||
saved: vec![],
|
||||
path: BezPath::new(),
|
||||
cur: None,
|
||||
start: None,
|
||||
pending_clip: None,
|
||||
marked: vec![],
|
||||
page_box,
|
||||
budget: 20_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a content stream with resources `res`; `pattern_base` maps pattern space to the page
|
||||
/// (the CTM where the stream's default coordinate system starts).
|
||||
pub fn run(&mut self, content: &[u8], res: &Dict, pattern_base: Affine, depth: usize) {
|
||||
let mut lx = Lexer::new(content, 0);
|
||||
let mut args: Vec<Obj> = Vec::with_capacity(8);
|
||||
let mut in_text = false;
|
||||
while let Some(o) = lx.next() {
|
||||
let Obj::Op(op) = o else {
|
||||
if args.len() < 64 {
|
||||
args.push(o);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
if self.budget == 0 {
|
||||
self.b.skip("content (operation limit)");
|
||||
return;
|
||||
}
|
||||
self.budget -= 1;
|
||||
if in_text {
|
||||
if op == "ET" {
|
||||
in_text = false;
|
||||
}
|
||||
args.clear();
|
||||
continue;
|
||||
}
|
||||
let n = |i: usize| args.get(i).and_then(Obj::num).unwrap_or(0.0);
|
||||
let nums = || args.iter().filter_map(Obj::num).collect::<Vec<f64>>();
|
||||
match op.as_str() {
|
||||
"q" => self.saved.push((self.gs.clone(), self.b.depth())),
|
||||
"Q" => {
|
||||
if let Some((g, d)) = self.saved.pop() {
|
||||
self.gs = g;
|
||||
self.b.close_to(d);
|
||||
}
|
||||
}
|
||||
"cm" => self.gs.ctm *= affine(&nums()),
|
||||
"w" => self.gs.lw = n(0),
|
||||
"J" => self.gs.cap = [Cap::Butt, Cap::Round, Cap::Square][(n(0) as usize).min(2)],
|
||||
"j" => self.gs.join = [Join::Miter, Join::Round, Join::Bevel][(n(0) as usize).min(2)],
|
||||
"M" => self.gs.miter = n(0),
|
||||
"d" => {
|
||||
let arr = args.first().map(|a| self.file.nums(a)).unwrap_or_default();
|
||||
self.gs.dash = (!arr.is_empty() && arr.iter().any(|v| *v > 0.0)).then(|| (arr, n(1)));
|
||||
}
|
||||
"gs" => {
|
||||
if let Some(name) = args.first().and_then(Obj::name)
|
||||
&& let Some(g) = self.file.get_dict(res, "ExtGState").and_then(|e| self.file.get_dict(e, name))
|
||||
{
|
||||
self.ext_gstate(g);
|
||||
}
|
||||
}
|
||||
// Path construction.
|
||||
"m" => {
|
||||
let p = Point::new(n(0), n(1));
|
||||
self.path.move_to(p);
|
||||
self.cur = Some(p);
|
||||
self.start = Some(p);
|
||||
}
|
||||
"l" => {
|
||||
let p = Point::new(n(0), n(1));
|
||||
self.ensure_start();
|
||||
self.path.line_to(p);
|
||||
self.cur = Some(p);
|
||||
}
|
||||
"c" => {
|
||||
self.ensure_start();
|
||||
let p = Point::new(n(4), n(5));
|
||||
self.path.curve_to(Point::new(n(0), n(1)), Point::new(n(2), n(3)), p);
|
||||
self.cur = Some(p);
|
||||
}
|
||||
"v" => {
|
||||
self.ensure_start();
|
||||
let c0 = self.cur.unwrap_or_default();
|
||||
let p = Point::new(n(2), n(3));
|
||||
self.path.curve_to(c0, Point::new(n(0), n(1)), p);
|
||||
self.cur = Some(p);
|
||||
}
|
||||
"y" => {
|
||||
self.ensure_start();
|
||||
let p = Point::new(n(2), n(3));
|
||||
self.path.curve_to(Point::new(n(0), n(1)), p, p);
|
||||
self.cur = Some(p);
|
||||
}
|
||||
"h" => {
|
||||
if self.cur.is_some() {
|
||||
self.path.close_path();
|
||||
self.cur = self.start;
|
||||
}
|
||||
}
|
||||
"re" => {
|
||||
let (x, y, w, h) = (n(0), n(1), n(2), n(3));
|
||||
self.path.move_to((x, y));
|
||||
self.path.line_to((x + w, y));
|
||||
self.path.line_to((x + w, y + h));
|
||||
self.path.line_to((x, y + h));
|
||||
self.path.close_path();
|
||||
self.cur = Some(Point::new(x, y));
|
||||
self.start = self.cur;
|
||||
}
|
||||
// Painting.
|
||||
"S" => self.paint(false, None, true),
|
||||
"s" => {
|
||||
self.path.close_path();
|
||||
self.paint(false, None, true)
|
||||
}
|
||||
"f" | "F" => self.paint(true, Some(FillRule::NonZero), false),
|
||||
"f*" => self.paint(true, Some(FillRule::EvenOdd), false),
|
||||
"B" => self.paint(true, Some(FillRule::NonZero), true),
|
||||
"B*" => self.paint(true, Some(FillRule::EvenOdd), true),
|
||||
"b" => {
|
||||
self.path.close_path();
|
||||
self.paint(true, Some(FillRule::NonZero), true)
|
||||
}
|
||||
"b*" => {
|
||||
self.path.close_path();
|
||||
self.paint(true, Some(FillRule::EvenOdd), true)
|
||||
}
|
||||
"n" => self.paint(false, None, false),
|
||||
"W" => self.pending_clip = Some(FillRule::NonZero),
|
||||
"W*" => self.pending_clip = Some(FillRule::EvenOdd),
|
||||
// Colour.
|
||||
"g" => self.gs.fill_cs = Cs::Gray,
|
||||
"G" => self.gs.stroke_cs = Cs::Gray,
|
||||
"rg" => self.gs.fill_cs = Cs::Rgb,
|
||||
"RG" => self.gs.stroke_cs = Cs::Rgb,
|
||||
"k" => self.gs.fill_cs = Cs::Cmyk,
|
||||
"K" => self.gs.stroke_cs = Cs::Cmyk,
|
||||
"cs" | "CS" => {
|
||||
let cs = args.first().map(|a| color_space(self.file, a, Some(res))).unwrap_or(Cs::Gray);
|
||||
let init = Fill::Color(cs.to_rgb(&cs.initial()));
|
||||
if op == "cs" {
|
||||
self.gs.fill_cs = cs;
|
||||
self.gs.fill = init;
|
||||
} else {
|
||||
self.gs.stroke_cs = cs;
|
||||
self.gs.stroke = init;
|
||||
}
|
||||
}
|
||||
"sc" | "scn" | "SC" | "SCN" => {
|
||||
let fill = op.starts_with('s');
|
||||
let cs = if fill { self.gs.fill_cs.clone() } else { self.gs.stroke_cs.clone() };
|
||||
let v = if let (Cs::Pattern, Some(name)) = (&cs, args.last().and_then(Obj::name)) {
|
||||
self.pattern(res, name, pattern_base)
|
||||
} else {
|
||||
Fill::Color(cs.to_rgb(&nums()))
|
||||
};
|
||||
if fill {
|
||||
self.gs.fill = v;
|
||||
} else {
|
||||
self.gs.stroke = v;
|
||||
}
|
||||
}
|
||||
"sh" => {
|
||||
if let Some(name) = args.first().and_then(Obj::name) {
|
||||
self.shade(res, name);
|
||||
}
|
||||
}
|
||||
"Do" => {
|
||||
if let Some(name) = args.first().and_then(Obj::name) {
|
||||
self.xobject(res, name, depth);
|
||||
}
|
||||
}
|
||||
"BT" => {
|
||||
in_text = true;
|
||||
self.b.skip("text");
|
||||
}
|
||||
"BI" => {
|
||||
// Inline image: skip to EI.
|
||||
let d = lx.data;
|
||||
let mut p = lx.pos;
|
||||
while p + 2 < d.len() && !(d[p] == b'I' && d[p - 1] == b'D' && crate::object::is_white(d[p + 1])) {
|
||||
p += 1;
|
||||
}
|
||||
while p + 2 < d.len()
|
||||
&& !(crate::object::is_white(d[p - 1]) && d[p] == b'E' && d[p + 1] == b'I' && (p + 2 == d.len() || crate::object::is_white(d[p + 2])))
|
||||
{
|
||||
p += 1;
|
||||
}
|
||||
lx.pos = (p + 2).min(d.len());
|
||||
self.b.skip("image");
|
||||
}
|
||||
"BMC" => self.marked.push(None),
|
||||
"BDC" => {
|
||||
let layer = match (args.first().and_then(Obj::name), args.get(1)) {
|
||||
(Some("OC"), Some(Obj::Name(prop))) => self
|
||||
.file
|
||||
.get_dict(res, "Properties")
|
||||
.and_then(|p| self.file.get_dict(p, prop))
|
||||
.map(|ocg| self.ocg_name(ocg).unwrap_or_else(|| prop.clone())),
|
||||
(Some("OC"), Some(Obj::Dict(ocg))) => self.ocg_name(ocg),
|
||||
_ => None,
|
||||
};
|
||||
match layer {
|
||||
Some(name) => {
|
||||
self.marked.push(Some(self.b.depth()));
|
||||
self.b.open(&name, true);
|
||||
}
|
||||
None => self.marked.push(None),
|
||||
}
|
||||
}
|
||||
"EMC" => {
|
||||
if let Some(Some(d)) = self.marked.pop() {
|
||||
self.b.close_to(d);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Colour operands set by the device-space shorthands.
|
||||
match op.as_str() {
|
||||
"g" | "rg" | "k" => self.gs.fill = Fill::Color(self.gs.fill_cs.to_rgb(&nums())),
|
||||
"G" | "RG" | "K" => self.gs.stroke = Fill::Color(self.gs.stroke_cs.to_rgb(&nums())),
|
||||
_ => {}
|
||||
}
|
||||
args.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn ocg_name(&self, ocg: &Dict) -> Option<String> {
|
||||
// An optional-content membership dictionary names its groups in /OCGs.
|
||||
let d = match self.file.get(ocg, "OCGs") {
|
||||
Some(Obj::Dict(g)) => g,
|
||||
Some(Obj::Array(a)) => a.first().map(|x| self.file.resolve(x)).and_then(Obj::dict).unwrap_or(ocg),
|
||||
_ => ocg,
|
||||
};
|
||||
match self.file.get(d, "Name") {
|
||||
Some(Obj::Str(s)) => Some(text_string(s)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_start(&mut self) {
|
||||
if self.cur.is_none() {
|
||||
self.path.move_to((0.0, 0.0));
|
||||
self.cur = Some(Point::ZERO);
|
||||
self.start = self.cur;
|
||||
}
|
||||
}
|
||||
|
||||
fn ext_gstate(&mut self, g: &Dict) {
|
||||
let f = self.file;
|
||||
if let Some(v) = f.get_num(g, "LW") {
|
||||
self.gs.lw = v;
|
||||
}
|
||||
if let Some(v) = f.get_num(g, "LC") {
|
||||
self.gs.cap = [Cap::Butt, Cap::Round, Cap::Square][(v as usize).min(2)];
|
||||
}
|
||||
if let Some(v) = f.get_num(g, "LJ") {
|
||||
self.gs.join = [Join::Miter, Join::Round, Join::Bevel][(v as usize).min(2)];
|
||||
}
|
||||
if let Some(v) = f.get_num(g, "ML") {
|
||||
self.gs.miter = v;
|
||||
}
|
||||
if let Some(v) = f.get_num(g, "CA") {
|
||||
self.gs.stroke_alpha = v.clamp(0.0, 1.0);
|
||||
}
|
||||
if let Some(v) = f.get_num(g, "ca") {
|
||||
self.gs.fill_alpha = v.clamp(0.0, 1.0);
|
||||
}
|
||||
if let Some(Obj::Array(d)) = f.get(g, "D") {
|
||||
let arr = d.first().map(|a| f.nums(a)).unwrap_or_default();
|
||||
let ph = d.get(1).and_then(|p| f.resolve(p).num()).unwrap_or(0.0);
|
||||
self.gs.dash = (!arr.is_empty() && arr.iter().any(|v| *v > 0.0)).then_some((arr, ph));
|
||||
}
|
||||
if f.get(g, "SMask").is_some_and(|s| !matches!(s, Obj::Name(n) if n == "None")) {
|
||||
self.b.skip("soft mask");
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_of(f: &Fill) -> Option<Paint> {
|
||||
match f {
|
||||
Fill::Color(c) => Some(Paint::Color(*c)),
|
||||
Fill::Paint(p) => Some(p.clone()),
|
||||
Fill::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, fill: bool, rule: Option<FillRule>, stroke: bool) {
|
||||
let path = std::mem::take(&mut self.path);
|
||||
self.cur = None;
|
||||
self.start = None;
|
||||
let ctm = self.gs.ctm;
|
||||
let fill = if fill { Self::paint_of(&self.gs.fill).map(|p| (p, self.gs.fill_alpha, rule.unwrap_or(FillRule::NonZero))) } else { None };
|
||||
let stroke = if stroke {
|
||||
Self::paint_of(&self.gs.stroke).map(|p| {
|
||||
// Width 0: the thinnest line the device can draw (one pixel).
|
||||
let s = { (ctm.determinant().abs()).sqrt() };
|
||||
let width = if self.gs.lw <= 0.0 { if s > 0.0 { 1.0 / s } else { 1.0 } } else { self.gs.lw };
|
||||
Stroke {
|
||||
paint: p,
|
||||
opacity: self.gs.stroke_alpha,
|
||||
width,
|
||||
cap: self.gs.cap,
|
||||
join: self.gs.join,
|
||||
miter: self.gs.miter.max(1.0),
|
||||
dash: self.gs.dash.clone(),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if fill.is_some() || stroke.is_some() {
|
||||
self.b.fill_stroke(path.clone(), ctm, fill, stroke);
|
||||
}
|
||||
if let Some(r) = self.pending_clip.take() {
|
||||
self.b.clip(ctm * path, r);
|
||||
}
|
||||
}
|
||||
|
||||
/// A shading dictionary → gradient paint in shading space.
|
||||
fn shading_gradient(&mut self, sh: &Dict) -> Option<Gradient> {
|
||||
let f = self.file;
|
||||
let ty = f.get_num(sh, "ShadingType").unwrap_or(0.0) as i32;
|
||||
let cs = f.get(sh, "ColorSpace").map(|c| color_space(f, c, None)).unwrap_or(Cs::Rgb);
|
||||
let coords = f.get(sh, "Coords").map(|c| f.nums(c)).unwrap_or_default();
|
||||
let func = f.get(sh, "Function").map(|x| Func::read(f, x)).unwrap_or(Func::Unsupported);
|
||||
let dom = f.get(sh, "Domain").map(|d| f.nums(d)).filter(|d| d.len() == 2).unwrap_or_else(|| vec![0.0, 1.0]);
|
||||
let kind = match (ty, coords.len()) {
|
||||
(2, 4) => GradientKind::Linear { x1: coords[0], y1: coords[1], x2: coords[2], y2: coords[3] },
|
||||
(3, 6) => GradientKind::Radial { cx: coords[3], cy: coords[4], r: coords[5], fx: coords[0], fy: coords[1] },
|
||||
_ => {
|
||||
self.b.skip(&format!("shading type {ty}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if func == Func::Unsupported {
|
||||
self.b.skip("shading function");
|
||||
}
|
||||
// Sample the function: evenly, plus exactly at stitching bounds.
|
||||
let mut ts: Vec<f64> = (0..=24).map(|i| i as f64 / 24.0).collect();
|
||||
for b in func.breakpoints() {
|
||||
if dom[1] > dom[0] {
|
||||
ts.push(((b - dom[0]) / (dom[1] - dom[0])).clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
ts.sort_by(f64::total_cmp);
|
||||
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-9);
|
||||
let stops = ts
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let c = cs.to_rgb(&func.eval1(dom[0] + t * (dom[1] - dom[0])));
|
||||
(t, [c[0], c[1], c[2], 1.0])
|
||||
})
|
||||
.collect();
|
||||
Some(Gradient { kind, stops, bbox_units: false, transform: Affine::IDENTITY, spread: Spread::Pad })
|
||||
}
|
||||
|
||||
fn pattern(&mut self, res: &Dict, name: &str, base: Affine) -> Fill {
|
||||
let f = self.file;
|
||||
let Some(p) = f.get_dict(res, "Pattern").and_then(|pp| f.get_dict(pp, name)) else { return Fill::None };
|
||||
match f.get_num(p, "PatternType").unwrap_or(0.0) as i32 {
|
||||
2 => {
|
||||
let m = f.get(p, "Matrix").map(|m| affine(&f.nums(m))).unwrap_or(Affine::IDENTITY);
|
||||
let Some(sh) = f.get(p, "Shading").and_then(Obj::dict) else { return Fill::None };
|
||||
let sh = sh.clone();
|
||||
match self.shading_gradient(&sh) {
|
||||
// Gradient space → shape user space (shapes carry the CTM).
|
||||
Some(mut g) => {
|
||||
g.transform = self.gs.ctm.inverse() * base * m;
|
||||
Fill::Paint(Paint::Gradient(g))
|
||||
}
|
||||
None => Fill::None,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.b.skip("tiling pattern");
|
||||
Fill::Color([0.5; 3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shade(&mut self, res: &Dict, name: &str) {
|
||||
let f = self.file;
|
||||
let Some(sh) = f.get_dict(res, "Shading").and_then(|s| f.get_dict(s, name)).cloned() else { return };
|
||||
let Some(g) = self.shading_gradient(&sh) else { return };
|
||||
// Fill everything (the current clip limits it): the page box in user space.
|
||||
let inv = self.gs.ctm.inverse();
|
||||
let b = self.page_box;
|
||||
let mut path = BezPath::new();
|
||||
for (i, (x, y)) in [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])].into_iter().enumerate() {
|
||||
let p = inv * Point::new(x, y);
|
||||
if i == 0 {
|
||||
path.move_to(p);
|
||||
} else {
|
||||
path.line_to(p);
|
||||
}
|
||||
}
|
||||
path.close_path();
|
||||
self.b.fill_stroke(path, self.gs.ctm, Some((Paint::Gradient(g), self.gs.fill_alpha, FillRule::NonZero)), None);
|
||||
}
|
||||
|
||||
fn xobject(&mut self, res: &Dict, name: &str, depth: usize) {
|
||||
let f = self.file;
|
||||
let Some(x) = f.get_dict(res, "XObject").and_then(|xs| xs.get(name)).map(|x| f.resolve(x)) else { return };
|
||||
let Obj::Stream(d, raw) = x else { return };
|
||||
match f.get(d, "Subtype").and_then(Obj::name) {
|
||||
Some("Form") if depth < 12 => {
|
||||
let Some(content) = decode_stream(f, d, raw) else { return };
|
||||
let m = f.get(d, "Matrix").map(|m| affine(&f.nums(m))).unwrap_or(Affine::IDENTITY);
|
||||
let form_res = f.get_dict(d, "Resources").cloned().unwrap_or_else(|| res.clone());
|
||||
self.saved.push((self.gs.clone(), self.b.depth()));
|
||||
self.gs.ctm *= m;
|
||||
let bb = f.get(d, "BBox").map(|b| f.nums(b)).unwrap_or_default();
|
||||
if bb.len() == 4 {
|
||||
let r = kurbo::Rect::new(bb[0], bb[1], bb[2], bb[3]);
|
||||
self.b.clip(self.gs.ctm * kurbo::Shape::to_path(&r, 0.1), FillRule::NonZero);
|
||||
}
|
||||
let base = self.gs.ctm;
|
||||
let path = std::mem::take(&mut self.path);
|
||||
self.run(&content, &form_res, base, depth + 1);
|
||||
self.path = path;
|
||||
if let Some((g, dd)) = self.saved.pop() {
|
||||
self.gs = g;
|
||||
self.b.close_to(dd);
|
||||
}
|
||||
}
|
||||
Some("Image") => self.b.skip("image"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user