Files
vectorcraft/crates/pdf/src/import.rs
Hiroshi InukaiandClaude Opus 5.5 d5e21cc735 PDF/.ai import: art of a neighbouring artboard stays on its own page
A file with several artboards writes, on each page, the art of its
neighbours that reaches the page's box, shifted by its own artboard
spacing. Kept, those copies drew over the art of the page they came from
(a hologram over its card's stripes and title). With several pages, art
wholly outside a page's box now stays with the page it belongs to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:20:07 +07:00

1230 lines
49 KiB
Rust

//! PDF → Document (hayro-interpret device that builds a VectorCraft node tree).
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use hayro_interpret::font::Glyph;
use hayro_interpret::hayro_cmap::BfString;
use hayro_interpret::pattern::{Pattern, TilingPattern};
use hayro_interpret::{
CacheKey, ClipPath, Context, Device, GlyphDrawMode, Image, ImageData, InterpreterCache, InterpreterSettings, InterpreterWarning, LumaData,
MaskType, PathDrawMode, SoftMask, StrokeProps, interpret_page,
};
use hayro_syntax::object::Name;
use kurbo::{Affine, BezPath, Rect, Shape};
use vectorcraft_color::{BlendMode, Color, Paint, Swatch};
use vectorcraft_doc::{
Appearance, AppearanceItem, Artboard, ColorMode, Dash, Document, FillLayer, ImageBlob, ImageObject, Knockout, LayerColor, LineCap, LineJoin,
Node, NodeId, NodeKind, OpacityMask, PatternDef, StrokeLayer,
};
use vectorcraft_geom::{FillRule, PathData};
use crate::import_color::{Colors, Native};
use crate::import_mask::{MaskSpec, contains, is_rectangle, luminance, mask_spec};
use crate::import_scan::{Ocgs, Scan, all_on, hides_forms, scan_page};
use crate::import_shading::{clipped, extend_clip, fold_stop_opacity, mesh_shading, shading_gradient};
use crate::import_text::{Families, Look, Placement, TextLine};
use crate::{CropTo, ImportOptions, ImportReport, PdfError, TextAs};
/// The name of the paths text imports as.
const TEXT_OUTLINES: &str = "<Text Outlines>";
/// The deepest soft masks, patterns and type 3 glyphs are read within each other.
const MAX_NESTED: u32 = 8;
/// A tiling pattern read this many times with the same art is taken to always draw it.
const PATTERN_REUSE: u32 = 16;
/// Import a PDF (or PDF-compatible `.ai`) with default options.
pub fn import(bytes: &[u8]) -> Result<Document, PdfError> {
import_with_report(bytes, &ImportOptions::default()).map(|r| r.document)
}
/// A layer of the imported document, in paint order.
enum Slot {
/// A page's art outside optional content groups.
Page(Box<Node>),
/// An optional content group (index into [`Ocgs::list`]).
Group(usize),
}
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open(bytes, opts.password.as_deref())?;
let picked = crate::pages::picked(opts, original.pages().len())?;
let mut ocgs = Ocgs::read(&original);
let route = opts.layers && !ocgs.list.is_empty();
// Content that is off imports as hidden layers: an update of the file turns every group on
// (unless a form is hidden by its own group: its art couldn't be told apart).
let hide = route && ocgs.any_off();
let turned_on = (hide && !picked.iter().filter_map(|&n| original.pages().get(n)).any(|p| hides_forms(p, &mut ocgs)))
.then(|| all_on(bytes, &original))
.flatten()
.and_then(|b| crate::pages::open(&b, opts.password.as_deref()).ok())
.filter(|p| Ocgs::read(p).list.is_empty());
let mut notes = vec![];
if hide && turned_on.is_none() {
notes.push("the art of hidden layers couldn't be read and was left out".to_string());
}
let all_on = turned_on.is_some();
let pdf = turned_on.unwrap_or(original);
let pages = pdf.pages();
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
let sink2 = sink.clone();
let settings = InterpreterSettings {
warning_sink: Arc::new(move |w| {
let msg = match w {
InterpreterWarning::UnsupportedFont => "a font could not be read; its text was skipped",
InterpreterWarning::ImageDecodeFailure => "an image could not be decoded and was skipped",
};
if let Ok(mut v) = sink2.lock()
&& !v.iter().any(|x| x == msg)
{
v.push(msg.to_string());
}
}),
render_annotations: false,
..Default::default()
};
let mut doc = Document::new(1.0, 1.0);
doc.title = "Imported PDF".into();
doc.artboards.clear();
doc.layers.clear();
let taken: Vec<String> = doc.swatches_iter().map(|s| s.name.clone()).chain(doc.swatch_groups.iter().map(|g| g.name.clone())).collect();
let mut b = Builder::new(doc.peek_next_id(), Colors::new(&pdf, taken.clone()), opts.text_as, route);
b.taken = taken;
b.warnings = notes;
let cache = InterpreterCache::new();
let mut x = 0.0;
// Every page only a placeholder (text) over private data: the file's art isn't in its PDF part.
let mut placeholder = true;
let mut slots: Vec<Slot> = vec![];
let mut group_art: HashMap<usize, Vec<Arc<Node>>> = HashMap::new();
for (i, &number) in picked.iter().enumerate() {
let Some(page) = pages.get(number) else { continue };
// The chosen box sits at (x, 0); the page draws round it.
let (init, frame) = crate::pages::frame(page, opts.crop);
let mut ab = Rect::new(x, 0.0, x + frame.width(), frame.height());
let xf = Affine::translate((x - frame.x0, -frame.y0)) * init;
let mut ctx = Context::new(xf, ab, &cache, pdf.xref(), settings.clone());
b.page = ab;
let scan = scan_page(page, &mut ocgs, all_on, &mut b.fonts);
b.begin_page(scan);
interpret_page(page, &mut ctx, &mut b);
let mut parts = b.end_page();
// A file with several artboards writes, on each page, the art of its neighbours that
// reaches into the page's box: art lying wholly outside this page is theirs (each page
// draws it shifted by its own artboard spacing), so keep it only where it belongs.
if picked.len() > 1 {
for (_, art) in &mut parts {
art.retain(|n| n.visual_bounds().is_none_or(|r| r.x0 <= ab.x1 && r.x1 >= ab.x0 && r.y0 <= ab.y1 && r.y1 >= ab.y0));
}
parts.retain(|(_, art)| !art.is_empty());
}
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
placeholder &= crate::pages::has_private_data(page) && only_text(&children);
let mut right = ab.x1;
if opts.crop == CropTo::Bounding
&& let Some(art) = vectorcraft_doc::live::nodes_bounds(&children)
{
right = right.max(art.x1);
ab = art;
}
x = right + opts.artboard_gap;
doc.artboards.push(Artboard {
id: i as u32 + 1,
name: format!("Artboard {}", i + 1),
rect: ab,
show_center_mark: false,
show_cross_hairs: false,
});
let page_layer = |b: &mut Builder<'_>, art: Vec<Arc<Node>>| {
let mut layer = Node::layer(b.id(), &format!("Page {}", number + 1), LayerColor::Preset((i % 27) as u8));
if let NodeKind::Layer { children: c, .. } = &mut layer.kind {
*c = art;
}
Slot::Page(Box::new(layer))
};
if parts.is_empty() {
slots.push(page_layer(&mut b, vec![]));
}
for (key, art) in parts {
match key {
None => slots.push(page_layer(&mut b, art)),
Some(g) => {
let into = group_art.entry(g).or_insert_with(|| {
slots.push(Slot::Group(g));
vec![]
});
into.extend(art);
}
}
}
}
if placeholder {
return Err(PdfError::PlaceholderOnly);
}
for (n, slot) in slots.into_iter().enumerate() {
let layer = match slot {
Slot::Page(l) => *l,
Slot::Group(g) => {
let Some(ocg) = ocgs.list.get(g) else { continue };
let mut l = Node::layer(b.id(), &ocg.name, LayerColor::Preset((n % 27) as u8));
(l.visible, l.locked) = (ocg.on, ocg.locked);
if let NodeKind::Layer { children, printable, .. } = &mut l.kind {
*children = group_art.remove(&g).unwrap_or_default();
*printable = ocg.print;
}
l
}
};
doc.layers.push(Arc::new(layer));
}
for (k, blob) in b.images.drain() {
doc.images.insert(k, blob);
}
// A file painted mostly in CMYK opens as a CMYK document (with CMYK default swatches).
if b.colors.cmyk_document() {
doc.color_mode = ColorMode::Cmyk;
(doc.swatches, doc.swatch_groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model());
}
doc.swatches.extend(b.colors.swatches());
// Tiling patterns are pattern swatches.
for p in b.patterns.drain(..) {
doc.swatches.push(Swatch {
name: p.name.clone(),
paint: Paint::Pattern { pattern: p.name.clone(), xf: Affine::IDENTITY },
global: false,
spot: false,
});
doc.patterns.push(p);
}
if b.colors.mixed {
b.warn("colours mixing several inks (DeviceN) were imported as RGB");
}
if !b.missing_fonts.is_empty() {
let list = b.missing_fonts.join(", ");
b.warn(&format!("fonts that aren't available show in the fallback font until they are: {list}"));
}
doc.fix_next_id();
doc.reserve_ids(b.next);
let mut warnings = b.warnings;
if let Ok(v) = sink.lock() {
for w in v.iter() {
if !warnings.contains(w) {
warnings.push(w.clone());
}
}
}
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf) })
}
/// Is this art text and nothing else (in groups and clips), with some text?
fn only_text(nodes: &[Arc<Node>]) -> bool {
fn walk(nodes: &[Arc<Node>], text: &mut bool) -> bool {
nodes.iter().all(|n| match &n.kind {
NodeKind::Path { clipping: true, .. } => true,
NodeKind::Path { .. } if n.name.as_deref() == Some(TEXT_OUTLINES) => {
*text = true;
true
}
NodeKind::Text(_) => {
*text = true;
true
}
NodeKind::Group { children, .. } => walk(children, text),
_ => false,
})
}
let mut text = false;
walk(nodes, &mut text) && text
}
/// Decoded RGBA pixels, width, height and hayro's scale factors.
type Decoded = (Vec<u8>, u32, u32, (f32, f32));
enum FrameKind {
Root,
/// A clip group whose clip path node is stored here.
Clip(Box<Node>),
/// A clip that was redundant (covers the page); children go straight to the parent.
Skip,
Group {
opacity: f32,
blend: BlendMode,
mask: Option<Arc<MaskSpec>>,
isolate: bool,
knockout: bool,
},
}
struct Frame {
kind: FrameKind,
children: Vec<Arc<Node>>,
/// The first optional content group entered inside (where the frame's art goes).
group: Option<usize>,
}
impl Frame {
fn new(kind: FrameKind) -> Self {
Self { kind, children: vec![], group: None }
}
}
/// Glyphs drawn consecutively with the same paint, merged into one path.
struct GlyphRun {
path: BezPath,
paint: Paint,
opacity: f32,
stroke: Option<StrokeProps>,
scale: f64,
}
/// How often a tiling pattern was read, and the art (as its paints) each reading drew with the
/// swatch made for it.
type PatternReadings = (u32, Vec<(Vec<Paint>, String)>);
/// Builder state a nested interpretation (soft mask, pattern cell) starts afresh and gives back.
struct Saved {
stack: Vec<Frame>,
root_groups: Vec<Option<usize>>,
blend: BlendMode,
mask: Option<Arc<MaskSpec>>,
page: Rect,
pending: bool,
}
struct Builder<'p> {
colors: Colors<'p>,
next: u64,
stack: Vec<Frame>,
/// The optional content group of each child of the root frame.
root_groups: Vec<Option<usize>>,
blend: BlendMode,
page: Rect,
glyphs: Option<GlyphRun>,
text: Option<TextLine>,
text_as: TextAs,
images: HashMap<String, ImageBlob>,
image_keys: HashMap<u128, (String, u32, u32)>,
warnings: Vec<String>,
/// Fonts by cache key → base font name (from [`scan_page`]).
fonts: HashMap<u128, String>,
font_names: HashMap<u128, (String, String)>,
families: Option<Families>,
missing_fonts: Vec<String>,
/// The soft mask of the graphics state, the art drawn through it so far, and the masks read.
mask: Option<Arc<MaskSpec>>,
masked: Vec<Arc<Node>>,
masks: HashMap<u128, Arc<MaskSpec>>,
/// Pattern swatches made, and per (pattern, stroke) the art signatures read and their names.
patterns: Vec<PatternDef>,
pattern_keys: HashMap<(u128, bool), PatternReadings>,
/// Swatch names in use.
taken: Vec<String>,
/// The page as walked by [`scan_page`], and how far the interpreter is through it.
scan: Scan,
tag_at: usize,
group_at: usize,
aligned: bool,
/// Whether optional content groups become layers.
route: bool,
/// The marked-content sequences open: the group each marks.
marked: Vec<Option<usize>>,
/// A transparency group was just pushed: a form's (whose flags come next) or an image's.
pending: bool,
nested: u32,
}
fn blend(b: hayro_interpret::BlendMode) -> BlendMode {
use hayro_interpret::BlendMode as H;
match b {
H::Normal => BlendMode::Normal,
H::Multiply => BlendMode::Multiply,
H::Screen => BlendMode::Screen,
H::Overlay => BlendMode::Overlay,
H::Darken => BlendMode::Darken,
H::Lighten => BlendMode::Lighten,
H::ColorDodge => BlendMode::ColorDodge,
H::ColorBurn => BlendMode::ColorBurn,
H::HardLight => BlendMode::HardLight,
H::SoftLight => BlendMode::SoftLight,
H::Difference => BlendMode::Difference,
H::Exclusion => BlendMode::Exclusion,
H::Hue => BlendMode::Hue,
H::Saturation => BlendMode::Saturation,
H::Color => BlendMode::Color,
H::Luminosity => BlendMode::Luminosity,
}
}
fn fill_rule(r: hayro_interpret::FillRule) -> FillRule {
match r {
hayro_interpret::FillRule::NonZero => FillRule::NonZero,
hayro_interpret::FillRule::EvenOdd => FillRule::EvenOdd,
}
}
fn mean_scale(a: Affine) -> f64 {
a.determinant().abs().sqrt()
}
fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f64) -> StrokeLayer {
let mut st = StrokeLayer::new(paint, p.line_width as f64 * scale);
st.opacity = opacity;
st.cap = match p.line_cap {
kurbo::Cap::Butt => LineCap::Butt,
kurbo::Cap::Round => LineCap::Round,
kurbo::Cap::Square => LineCap::Square,
};
st.join = match p.line_join {
kurbo::Join::Miter => LineJoin::Miter,
kurbo::Join::Round => LineJoin::Round,
kurbo::Join::Bevel => LineJoin::Bevel,
};
st.miter_limit = p.miter_limit as f64;
let dash = Dash { pattern: p.dash_array.iter().map(|v| *v as f64 * scale).collect(), offset: p.dash_offset as f64 * scale, align_corners: false };
// An invalid dash array (a negative value, or all zeros) strokes solid.
st.dash = dash.is_dashed().then_some(dash);
st
}
pub(crate) fn round3(v: f32) -> f32 {
(v * 1000.0).round() / 1000.0
}
/// The paints of `nodes`' leaves, in order: what tells two readings of a pattern cell apart.
fn paints(nodes: &[Arc<Node>]) -> Vec<Paint> {
let mut out = vec![];
for n in nodes {
n.walk(&mut |c| out.extend(c.appearance.items.iter().map(|i| i.paint().clone())));
}
out
}
/// Visual bounds of `nodes`; `None` when one has none.
fn bounds(nodes: &[Arc<Node>]) -> Option<Rect> {
nodes.iter().try_fold(None, |acc, n| n.visual_bounds().map(|b| Some(acc.map_or(b, |a: Rect| a.union(b)))))?
}
impl<'p> Builder<'p> {
fn new(next: u64, colors: Colors<'p>, text_as: TextAs, route: bool) -> Self {
Self {
colors,
next,
stack: vec![],
root_groups: vec![],
blend: BlendMode::Normal,
page: Rect::ZERO,
glyphs: None,
text: None,
text_as,
images: HashMap::new(),
image_keys: HashMap::new(),
warnings: vec![],
fonts: HashMap::new(),
font_names: HashMap::new(),
families: None,
missing_fonts: vec![],
mask: None,
masked: vec![],
masks: HashMap::new(),
patterns: vec![],
pattern_keys: HashMap::new(),
taken: vec![],
scan: Scan::default(),
tag_at: 0,
group_at: 0,
aligned: true,
route,
marked: vec![],
pending: false,
nested: 0,
}
}
fn id(&mut self) -> NodeId {
let id = NodeId(self.next);
self.next += 1;
id
}
/// A deep copy of `node` with fresh ids.
fn reid(&mut self, node: &Node) -> Node {
let mut n = node.clone();
n.id = self.id();
if let Some(ch) = n.children_mut() {
let old = std::mem::take(ch);
*ch = old.iter().map(|c| Arc::new(self.reid(c))).collect();
}
n
}
/// `m` with fresh ids for its art: a mask read once can mask several objects.
fn fresh_mask(&mut self, mut m: OpacityMask) -> OpacityMask {
m.art = Arc::new(self.reid(&m.art));
m
}
fn warn(&mut self, w: &str) {
if !self.warnings.iter().any(|x| x == w) {
self.warnings.push(w.to_string());
}
}
fn begin_page(&mut self, scan: Scan) {
self.stack = vec![Frame::new(FrameKind::Root)];
self.root_groups.clear();
self.blend = BlendMode::Normal;
self.glyphs = None;
self.text = None;
self.mask = None;
self.masked.clear();
self.marked.clear();
self.pending = false;
(self.scan, self.tag_at, self.group_at, self.aligned) = (scan, 0, 0, true);
}
/// The page's art, split by optional content group (in the order they first paint).
fn end_page(&mut self) -> Vec<(Option<usize>, Vec<Arc<Node>>)> {
let children = self.close_frames();
let groups = std::mem::take(&mut self.root_groups);
let mut parts: Vec<(Option<usize>, Vec<Arc<Node>>)> = vec![];
for (n, g) in children.into_iter().zip(groups.into_iter().chain(std::iter::repeat(None))) {
match parts.iter_mut().find(|(k, _)| *k == g) {
Some((_, v)) => v.push(n),
None => parts.push((g, vec![n])),
}
}
parts
}
/// Finish the open runs and frames → the root frame's children.
fn close_frames(&mut self) -> Vec<Arc<Node>> {
self.flush();
while self.stack.len() > 1 {
self.pop_frame();
}
self.stack.pop().map(|f| f.children).unwrap_or_default()
}
/// The group art drawn now belongs to (the innermost marked as one).
fn current_group(&self) -> Option<usize> {
self.marked.iter().rev().find_map(|g| *g)
}
/// Add `n` to the open frame; `group`: the optional content group it was drawn in, if it
/// knows one (else the current one).
fn emit(&mut self, n: Arc<Node>, group: Option<usize>) {
let root = self.stack.len() == 1;
let current = self.current_group();
let Some(f) = self.stack.last_mut() else { return };
f.children.push(n);
if root {
self.root_groups.push(group.or(current));
} else if f.group.is_none() {
f.group = group;
}
}
fn push_node(&mut self, mut n: Node) {
if self.blend != BlendMode::Normal && !n.is_container() {
n.blend = self.blend;
}
if self.mask.is_some() {
self.masked.push(Arc::new(n));
} else {
self.emit(Arc::new(n), None);
}
}
/// Finish the open glyph runs and the art drawn through the current soft mask.
fn flush(&mut self) {
self.flush_glyphs();
self.flush_text();
self.flush_masked();
}
/// The art drawn through the graphics state's soft mask so far, as one masked object.
fn flush_masked(&mut self) {
if self.masked.is_empty() {
return;
}
let art = std::mem::take(&mut self.masked);
let Some(spec) = self.mask.clone() else {
art.into_iter().for_each(|n| self.emit(n, None));
return;
};
match spec.as_ref() {
// Each object is drawn through the mask on its own.
MaskSpec::Opacity(a) => {
for mut n in art {
Arc::make_mut(&mut n).opacity *= a;
self.emit(n, None);
}
}
MaskSpec::Mask(m) => {
let m = self.fresh_mask(m.clone());
let node = match <[Arc<Node>; 1]>::try_from(art) {
Ok([one]) if one.mask.is_none() => {
fold_stop_opacity(&one, &m).unwrap_or_else(|| Node { mask: Some(Box::new(m)), ..Arc::unwrap_or_clone(one) })
}
Ok([one]) => {
let mut g = Node::group(self.id(), vec![one]);
g.mask = Some(Box::new(m));
g
}
Err(all) => {
let mut g = Node::group(self.id(), all);
g.mask = Some(Box::new(m));
g
}
};
self.emit(Arc::new(node), None);
}
}
}
/// A clip path node.
fn clip_node(&mut self, path: &BezPath, rule: FillRule) -> Node {
let mut clip = Node::new(self.id(), NodeKind::Path { path: PathData::from_bezpath(path), rule, live: None, clipping: true, guide: false });
clip.name = Some("<Clipping Path>".into());
clip
}
fn pop_frame(&mut self) {
if self.stack.len() <= 1 {
return;
}
let Some(f) = self.stack.pop() else { return };
let group = f.group;
let node = match f.kind {
FrameKind::Root => None,
FrameKind::Skip => {
f.children.into_iter().for_each(|c| self.emit(c, group));
None
}
FrameKind::Clip(clip) => {
let noop = clip.path_data().map(|p| p.to_bezpath()).filter(is_rectangle).map(|p| p.bounding_box().inflate(0.01, 0.01));
if f.children.is_empty() {
None
} else if noop.is_some_and(|r| bounds(&f.children).is_some_and(|b| contains(r, b))) {
// A rectangle around all of its art (a form's box) clips nothing.
f.children.into_iter().for_each(|c| self.emit(c, group));
None
} else {
let mut ch = vec![Arc::new(*clip)];
ch.extend(f.children);
Some(Node::new(self.id(), NodeKind::Group { children: ch, clip: true }))
}
}
FrameKind::Group { opacity, blend, mask, isolate, knockout } => {
let children = f.children;
// A constant alpha mask is opacity (of a group that isn't isolated).
let (opacity, mask) = match mask.as_deref() {
Some(MaskSpec::Opacity(a)) => (opacity * a, None),
Some(MaskSpec::Mask(m)) => (opacity, Some(m.clone())),
None => (opacity, None),
};
// Isolation only shows with blending inside; knockout with several objects.
let isolate = isolate && Node::children_blend(&children, &mut |c| c.blends_through());
let knockout = knockout && children.len() > 1;
let plain = opacity >= 0.999 && blend == BlendMode::Normal && mask.is_none() && !isolate && !knockout;
if children.is_empty() {
None
} else if plain {
children.into_iter().for_each(|c| self.emit(c, group));
None
} else if let [only] = children.as_slice()
&& !only.is_container()
&& !isolate
&& !knockout
&& (mask.is_none() || only.mask.is_none())
{
// A single object in a group: fold opacity, blend and mask into the object.
let mut only = (**only).clone();
only.opacity *= opacity;
if blend != BlendMode::Normal {
only.blend = blend;
}
if let Some(m) = mask {
let m = self.fresh_mask(m);
only = fold_stop_opacity(&only, &m).unwrap_or(Node { mask: Some(Box::new(m)), ..only });
}
Some(only)
} else {
let mut g = Node::group(self.id(), children);
(g.opacity, g.blend, g.isolate) = (opacity, blend, isolate);
if knockout {
g.knockout = Knockout::On;
}
g.mask = mask.map(|m| Box::new(self.fresh_mask(m)));
Some(g)
}
}
};
if let Some(n) = node {
self.emit(Arc::new(n), group);
}
}
fn flush_glyphs(&mut self) {
let Some(run) = self.glyphs.take() else { return };
if run.path.elements().is_empty() {
return;
}
let mut ap = Appearance::default();
match &run.stroke {
None => {
let mut f = FillLayer::new(run.paint);
f.opacity = run.opacity;
ap.items.push(AppearanceItem::Fill(f));
}
Some(p) => ap.items.push(AppearanceItem::Stroke(stroke_layer(run.paint, run.opacity, p, run.scale))),
}
let id = self.id();
let mut n = Node::path(id, PathData::from_bezpath(&run.path), ap);
n.name = Some(TEXT_OUTLINES.into());
self.push_node(n);
}
fn flush_text(&mut self) {
let Some((t, opacity)) = self.text.take().and_then(TextLine::finish) else { return };
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
n.opacity = opacity;
self.push_node(n);
}
/// Convert a hayro paint to a VectorCraft paint and opacity (`stroke`: for a stroke).
fn paint(&mut self, p: &hayro_interpret::Paint<'_>, stroke: bool) -> (Paint, f32) {
match p {
hayro_interpret::Paint::Color(c) => {
let [r, g, b, a] = c.to_rgba().components();
let paint = self.colors.solid(c).map_or_else(|| Paint::solid(Color::rgb(round3(r), round3(g), round3(b))), Native::paint);
(paint, a)
}
hayro_interpret::Paint::Pattern(pat) => match pat.as_ref() {
Pattern::Shading(sp) => match shading_gradient(sp, &mut self.colors) {
Some((g, _)) => (Paint::Gradient(Box::new(g)), sp.opacity),
None => {
self.warn("function-based shadings (and very large meshes) are imported as a flat colour");
(Paint::solid(Color::rgb(0.5, 0.5, 0.5)), sp.opacity)
}
},
Pattern::Tiling(t) => match self.tiling(t, stroke) {
Some(paint) => (paint, 1.0),
None => {
self.warn("tiling patterns nested too deeply are imported as a flat grey");
(Paint::solid(Color::rgb(0.5, 0.5, 0.5)), 1.0)
}
},
},
}
}
/// Read a soft mask, pattern cell or type 3 glyph (`read`) on its own: its art, in the
/// coordinates `page` has. `None` when nested too deeply.
fn nested(&mut self, page: Rect, read: impl FnOnce(&mut Self)) -> Option<Vec<Arc<Node>>> {
if self.nested >= MAX_NESTED {
return None;
}
self.flush();
let saved = Saved {
stack: std::mem::replace(&mut self.stack, vec![Frame::new(FrameKind::Root)]),
root_groups: std::mem::take(&mut self.root_groups),
blend: std::mem::replace(&mut self.blend, BlendMode::Normal),
mask: self.mask.take(),
page: std::mem::replace(&mut self.page, page),
pending: std::mem::take(&mut self.pending),
};
self.nested += 1;
read(self);
let art = self.close_frames();
self.nested -= 1;
(self.stack, self.root_groups, self.blend, self.mask, self.page, self.pending) =
(saved.stack, saved.root_groups, saved.blend, saved.mask, saved.page, saved.pending);
Some(art)
}
/// What soft mask `m` does (read once per mask and placement).
fn soft_mask(&mut self, m: &SoftMask<'_>) -> Arc<MaskSpec> {
let key = m.cache_key();
if let Some(s) = self.masks.get(&key) {
return s.clone();
}
let page = self.page;
let spec = match self.nested(page, |b| m.interpret(b)) {
Some(art) => {
let [r, g, b, _] = m.background_color().to_rgba().components();
let backdrop = luminance(&Color::rgb(r, g, b));
let inverted = m.transfer_function().is_some_and(|tf| tf.apply(0.0) > tf.apply(1.0));
let id = self.id();
mask_spec(art, m.mask_type() == MaskType::Alpha, backdrop, inverted, page, |all| Node::group(id, all))
}
None => {
self.warn("soft masks nested too deeply were left out (content drawn unmasked)");
MaskSpec::Opacity(1.0)
}
};
let spec = Arc::new(spec);
self.masks.insert(key, spec.clone());
spec
}
/// Tiling pattern `t` as a pattern swatch's paint (the swatch made the first time).
fn tiling(&mut self, t: &TilingPattern<'_>, stroke: bool) -> Option<Paint> {
let key = (t.cache_key(), stroke);
let tile = Rect::from_origin_size(t.bbox.origin(), (t.x_step.abs().max(1e-3) as f64, t.y_step.abs().max(1e-3) as f64));
let xf = t.matrix * Affine::translate(tile.origin().to_vec2());
// An uncoloured pattern draws in the current colour: read it again unless it always
// drew the same.
if let Some((n, seen)) = self.pattern_keys.get(&key)
&& *n >= PATTERN_REUSE
&& let [(_, name)] = seen.as_slice()
{
return Some(Paint::Pattern { pattern: name.clone(), xf });
}
let art = self.nested(Rect::ZERO, |b| {
// A cell whose content can't be decoded is empty.
let _ = t.interpret(b, Affine::IDENTITY, stroke);
})?;
let sig = paints(&art);
let entry = self.pattern_keys.entry(key).or_default();
entry.0 += 1;
let name = match entry.1.iter().find(|(s, _)| *s == sig) {
Some((_, name)) => name.clone(),
None => {
let name = (self.patterns.len() + 1..).map(|i| format!("Pattern {i}")).find(|n| !self.taken.contains(n)).unwrap_or_default();
self.taken.push(name.clone());
let mut def = PatternDef::new(&name, art);
def.tile = tile;
self.patterns.push(def);
if let Some(e) = self.pattern_keys.get_mut(&key) {
e.1.push((sig, name.clone()));
}
name
}
};
Some(Paint::Pattern { pattern: name, xf })
}
fn add_image(&mut self, key: u128, make: impl FnOnce() -> Option<(ImageBlob, u32, u32)>, xf: Affine) {
let (k, w, h) = if let Some(v) = self.image_keys.get(&key).cloned() {
v
} else {
let Some((blob, w, h)) = make() else {
self.warn("an image could not be decoded and was skipped");
return;
};
// Content keys: placing this page into another document merges its images safely.
let k = blob.content_key();
self.images.insert(k.clone(), blob);
self.image_keys.insert(key, (k.clone(), w, h));
(k, w, h)
};
let id = self.id();
self.push_node(Node::new(id, NodeKind::Image(ImageObject { key: k, width: w, height: h, xf, link: None, placement: Default::default() })));
}
/// Mesh shadings filling `region` (document space): gradient meshes, clipped to the region
/// when they reach past it.
fn draw_meshes(&mut self, meshes: Vec<vectorcraft_doc::live::GradientMesh>, region: &BezPath, opacity: f32) {
let nodes: Vec<Arc<Node>> = meshes.into_iter().map(|m| Arc::new(Node::new(self.id(), NodeKind::Mesh(m)))).collect();
let Some(art) = bounds(&nodes) else { return };
let mut content = match <[Arc<Node>; 1]>::try_from(nodes) {
Ok([one]) => Arc::unwrap_or_clone(one),
Err(all) => Node::group(self.id(), all),
};
content.opacity *= opacity;
if !contains(region.bounding_box().inflate(0.5, 0.5), art) {
let clip = self.clip_node(region, FillRule::NonZero);
content = clipped(clip, content, self.id());
}
self.push_node(content);
}
/// The family and style of font `key` (drawing glyph `o`).
fn font_name(&mut self, key: u128, o: &hayro_interpret::font::OutlineGlyph) -> (String, String) {
if let Some(n) = self.font_names.get(&key) {
return n.clone();
}
let data = o.font_data();
let name = self.fonts.get(&key).cloned().or_else(|| data.as_ref().and_then(|d| d.postscript_name.clone()));
let (weight, italic) = data.as_ref().map_or((None, false), |d| (d.weight, d.is_italic));
let families = self.families.get_or_insert_with(Families::available);
let n = match name {
Some(name) => {
let (family, style, found) = families.resolve(&name, weight, italic);
if !found && !family.is_empty() && !self.missing_fonts.contains(&family) {
self.missing_fonts.push(family.clone());
}
(family, style)
}
None => {
let d = vectorcraft_doc::CharStyle::default();
(d.font_family, d.font_style)
}
};
self.font_names.insert(key, n.clone());
n
}
/// Glyph `o` as type: added to the line being gathered (or starting one). `false`: it keeps
/// its outline (no Unicode text, mirrored, or not type mode).
fn type_glyph(
&mut self,
o: &hayro_interpret::font::OutlineGlyph,
m: Affine,
scale: f64,
paint: &hayro_interpret::Paint<'_>,
stroke: Option<&StrokeProps>,
) -> bool {
if self.text_as != TextAs::Text || self.nested > 0 {
return false;
}
let text = match o.as_unicode() {
Some(BfString::Char(c)) => c.to_string(),
Some(BfString::String(s)) => s,
None => return false,
};
let Some(at) = Placement::of(m) else { return false };
if text.is_empty() || text.chars().any(|c| c.is_control()) {
return false;
}
self.flush_glyphs();
let font = o.font_cache_key();
let (paint, opacity) = self.paint(paint, stroke.is_some());
let stroke = stroke.map(|p| (paint.clone(), p.line_width as f64 * scale));
// A stroke over the glyph just filled (fill and stroke rendering).
if let (Some(s), Some(line)) = (&stroke, &mut self.text)
&& line.stroke_last(font, at, s.clone())
{
return true;
}
let (family, style) = self.font_name(font, o);
let look = Look { font, family, style, size: at.size, h_scale: at.h_scale, fill: stroke.is_none().then_some(paint), stroke };
let advance = o.advance_width().filter(|w| w.is_finite()).map_or(at.size * 0.5, |w| w as f64 / 1000.0 * at.size * at.h_scale / 100.0);
if !self.text.as_mut().is_some_and(|l| l.push(&look, at, opacity, advance, &text)) {
self.flush_text();
let mut line = TextLine::new(at, opacity);
line.push(&look, at, opacity, advance, &text);
self.text = Some(line);
}
true
}
}
fn rgba_png(rgba: Vec<u8>, w: u32, h: u32) -> Option<Vec<u8>> {
let img = image::RgbaImage::from_raw(w, h, rgba)?;
let mut out = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;
Some(out)
}
fn resize_alpha(a: &LumaData, w: u32, h: u32) -> Vec<u8> {
if a.width == w && a.height == h {
return a.data.clone();
}
match image::GrayImage::from_raw(a.width, a.height, a.data.clone()) {
Some(g) => image::imageops::resize(&g, w, h, image::imageops::FilterType::Triangle).into_raw(),
None => vec![255; (w * h) as usize],
}
}
impl<'a> Device<'a> for Builder<'_> {
fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
let spec = mask.map(|m| self.soft_mask(&m));
let same = match (&spec, &self.mask) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
};
if !same {
self.flush();
self.mask = spec;
}
}
fn set_blend_mode(&mut self, blend_mode: hayro_interpret::BlendMode) {
let b = blend(blend_mode);
if b != self.blend {
self.flush_glyphs();
self.flush_text();
self.blend = b;
}
}
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) {
self.flush_glyphs();
self.flush_text();
self.pending = false;
let mut bp = path.clone();
bp.apply_affine(transform);
if bp.elements().is_empty() {
return;
}
let shading = match paint {
hayro_interpret::Paint::Pattern(p) => match p.as_ref() {
Pattern::Shading(sp) => Some(sp),
Pattern::Tiling(_) => None,
},
hayro_interpret::Paint::Color(_) => None,
};
// Patch and triangle meshes are gradient meshes.
if let (PathDrawMode::Fill(_), Some(sp)) = (draw_mode, shading)
&& let Some(meshes) = mesh_shading(sp, &mut self.colors)
{
self.draw_meshes(meshes, &bp, sp.opacity);
return;
}
let pd = PathData::from_bezpath(&bp);
let stroke = matches!(draw_mode, PathDrawMode::Stroke(_));
let (paint, opacity) = self.paint(paint, stroke);
// A gradient that doesn't extend past its ends paints only between them.
let width = match draw_mode {
PathDrawMode::Stroke(p) => p.line_width as f64 * mean_scale(transform),
PathDrawMode::Fill(_) => 0.0,
};
let band = shading.and_then(|sp| extend_clip(sp, bp.bounding_box().inflate(width, width)));
let wrap = |b: &mut Self, n: Node| match &band {
Some(band) => {
let clip = b.clip_node(band, FillRule::EvenOdd);
let id = b.id();
clipped(clip, n, id)
}
None => n,
};
match draw_mode {
PathDrawMode::Fill(rule) => {
let mut f = FillLayer::new(paint);
f.opacity = opacity;
let mut n = Node::path(self.id(), pd, Appearance { items: vec![AppearanceItem::Fill(f)], ..Default::default() });
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
*r = fill_rule(*rule);
}
let n = wrap(self, n);
self.push_node(n);
}
PathDrawMode::Stroke(props) => {
let st = stroke_layer(paint, opacity, props, mean_scale(transform));
// Fill-then-stroke of the same path (the `B` operator) becomes one object.
let blend = self.blend;
let last = match self.mask {
Some(_) => self.masked.last_mut(),
None => self.stack.last_mut().and_then(|f| f.children.last_mut()),
};
if band.is_none()
&& let Some(last) = last
&& last.blend == blend
&& last.path_data() == Some(&pd)
&& last.appearance.stroke().is_none()
{
Arc::make_mut(last).appearance.items.push(AppearanceItem::Stroke(st));
return;
}
let n = Node::path(self.id(), pd, Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
let n = wrap(self, n);
self.push_node(n);
}
}
}
fn push_clip_path(&mut self, clip_path: &ClipPath) {
self.flush();
// A form's box right after its transparency group: the group's isolate and knockout
// flags are the next ones read from the file.
if std::mem::take(&mut self.pending) {
let flags = self.scan.groups.get(self.group_at).copied();
self.group_at += 1;
if let Some((i, k)) = flags
&& let Some(Frame { kind: FrameKind::Group { isolate, knockout, .. }, .. }) = self.stack.last_mut()
{
(*isolate, *knockout) = (i, k);
}
}
let bb = clip_path.path.bounding_box();
// Clips that contain the whole page do nothing visible; don't create groups for them.
let redundant = clip_path.path.elements().len() <= 6
&& bb.x0 <= self.page.x0 + 0.01
&& bb.y0 <= self.page.y0 + 0.01
&& bb.x1 >= self.page.x1 - 0.01
&& bb.y1 >= self.page.y1 - 0.01
&& self.page.area() > 0.0;
let kind = if redundant { FrameKind::Skip } else { FrameKind::Clip(Box::new(self.clip_node(&clip_path.path, fill_rule(clip_path.fill)))) };
self.stack.push(Frame::new(kind));
}
fn push_transparency_group(&mut self, opacity: f32, mask: Option<SoftMask<'a>>, blend_mode: hayro_interpret::BlendMode) {
self.flush();
let mask = mask.map(|m| self.soft_mask(&m));
self.stack.push(Frame::new(FrameKind::Group { opacity, blend: blend(blend_mode), mask, isolate: false, knockout: false }));
self.pending = self.nested == 0;
// Blend mode applies to the group as a whole, not to its children.
self.blend = BlendMode::Normal;
}
fn draw_glyph(
&mut self,
glyph: &Glyph<'a>,
transform: Affine,
glyph_transform: Affine,
paint: &hayro_interpret::Paint<'a>,
draw_mode: &GlyphDrawMode,
) {
self.pending = false;
let stroke = match draw_mode {
GlyphDrawMode::Invisible => return,
GlyphDrawMode::Fill => None,
GlyphDrawMode::Stroke(p) => Some(p.clone()),
};
match glyph {
Glyph::Outline(o) => {
let scale = mean_scale(transform);
if self.type_glyph(o, transform * glyph_transform, scale, paint, stroke.as_ref()) {
return;
}
self.flush_text();
let mut bp = o.outline();
bp.apply_affine(transform * glyph_transform);
let (paint, opacity) = self.paint(paint, stroke.is_some());
let same = self.glyphs.as_ref().is_some_and(|r| {
r.paint == paint
&& r.opacity == opacity
&& r.stroke.as_ref().map(|s| s.line_width) == stroke.as_ref().map(|s| s.line_width)
&& (r.scale - scale).abs() < 1e-9
});
if !same {
self.flush_glyphs();
self.glyphs = Some(GlyphRun { path: BezPath::new(), paint, opacity, stroke, scale });
}
if let Some(r) = &mut self.glyphs {
r.path.extend(bp.iter());
}
self.warn("text was converted to outlines");
}
Glyph::Type3(t3) => {
if self.nested >= MAX_NESTED {
return;
}
self.flush();
self.nested += 1;
t3.interpret(self, transform, glyph_transform, paint);
self.nested -= 1;
}
}
}
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
self.flush_glyphs();
self.flush_text();
self.pending = false;
match image {
Image::Raster(r) => {
let key = hayro_interpret::CacheKey::cache_key(&r);
// JPEG passthrough for plain DeviceRGB/DeviceGray DCT images.
let st = r.stream();
let dict = st.dict();
let filters = st.filters();
let cs_ok = dict.get::<Name<'_>>(b"ColorSpace").is_some_and(|n| matches!(n.as_ref(), b"DeviceRGB" | b"DeviceGray"));
let jpeg = filters.len() == 1
&& matches!(filters[0], hayro_syntax::Filter::DctDecode)
&& cs_ok
&& !dict.contains_key(b"SMask")
&& !dict.contains_key(b"Mask")
&& !dict.contains_key(b"Decode");
if jpeg {
let (w, h) = (r.width(), r.height());
let bytes = st.raw_data().to_vec();
self.add_image(key, || Some((ImageBlob::new("image/jpeg", bytes), w, h)), transform);
return;
}
let mut decoded: Option<Decoded> = None;
r.with_rgba(
|img, alpha| {
let (w, h, sf) = (img.width(), img.height(), img.scale_factors());
let a = alpha.map(|a| resize_alpha(&a, w, h)).unwrap_or_else(|| vec![255; (w * h) as usize]);
let rgba: Vec<u8> = match img {
ImageData::Rgb(d) => d.data.as_chunks::<3>().0.iter().zip(a).flat_map(|(c, a)| [c[0], c[1], c[2], a]).collect(),
ImageData::Luma(d) => d.data.iter().zip(a).flat_map(|(g, a)| [*g, *g, *g, a]).collect(),
};
decoded = Some((rgba, w, h, sf));
},
None,
);
let Some((rgba, w, h, sf)) = decoded else {
self.warn("an image could not be decoded and was skipped");
return;
};
let xf = transform * Affine::scale_non_uniform(sf.0 as f64, sf.1 as f64);
self.add_image(key, || rgba_png(rgba, w, h).map(|png| (ImageBlob::new("image/png", png), w, h)), xf);
}
Image::Stencil(s) => {
let key = hayro_interpret::CacheKey::cache_key(&s);
let mut decoded: Option<Decoded> = None;
s.with_stencil(
|luma, paint| {
let rgb = match paint {
hayro_interpret::Paint::Color(c) => c.to_rgba().to_rgba8(),
_ => [0, 0, 0, 255],
};
let rgba = luma.data.iter().flat_map(|a| [rgb[0], rgb[1], rgb[2], *a]).collect();
decoded = Some((rgba, luma.width, luma.height, luma.scale_factors));
},
None,
);
let Some((rgba, w, h, sf)) = decoded else { return };
let xf = transform * Affine::scale_non_uniform(sf.0 as f64, sf.1 as f64);
self.add_image(key ^ 0x5_7e9c, || rgba_png(rgba, w, h).map(|png| (ImageBlob::new("image/png", png), w, h)), xf);
}
}
}
fn pop_clip_path(&mut self) {
self.flush();
self.pending = false;
self.pop_frame();
}
fn pop_transparency_group(&mut self) {
self.flush();
self.pending = false;
self.pop_frame();
}
fn begin_marked_content(&mut self, tag: &[u8], _mcid: Option<i32>) {
if self.nested > 0 {
return;
}
self.flush();
let at = self.tag_at;
self.tag_at += 1;
let group = match self.scan.tags.get(at) {
Some((t, g)) if self.aligned && t.as_slice() == tag => *g,
_ => {
if self.route && self.aligned {
self.aligned = false;
self.warn("some art couldn't be told apart by layer and went to its page's layer");
}
None
}
}
.filter(|_| self.route);
if let Some(g) = group
&& self.stack.len() > 1
&& let Some(f) = self.stack.last_mut()
{
f.group.get_or_insert(g);
}
self.marked.push(group);
}
fn end_marked_content(&mut self) {
if self.nested > 0 {
return;
}
self.flush();
self.marked.pop();
}
}