1221 lines
48 KiB
Rust
1221 lines
48 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 parts = b.end_page();
|
|
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();
|
|
}
|
|
}
|