mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
328 lines
14 KiB
Rust
328 lines
14 KiB
Rust
use super::cid::{CidText, FontInfo};
|
|
use super::{
|
|
Affine, Arc, BezPath, BlendMode, Colors, Dash, Families, FillRule, FontNames, FunctionBudget, HashMap, HashSet, ImageBlob, LineCap, LineFacts,
|
|
LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine,
|
|
};
|
|
|
|
/// Decoded RGBA pixels, width, height and hayro's scale factors.
|
|
pub(super) type Decoded = (Vec<u8>, u32, u32, (f32, f32));
|
|
|
|
pub(super) 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,
|
|
},
|
|
}
|
|
|
|
pub(super) struct Frame {
|
|
pub(super) kind: FrameKind,
|
|
pub(super) children: Vec<Arc<Node>>,
|
|
/// The first optional content group entered inside (where the frame's art goes).
|
|
pub(super) group: Option<usize>,
|
|
/// The group each child was drawn in, where the frame's art goes when the frame dissolves
|
|
/// into its parent (a layer's form drawing its sublayers' forms).
|
|
pub(super) child_groups: Vec<Option<usize>>,
|
|
}
|
|
|
|
impl Frame {
|
|
pub(super) fn new(kind: FrameKind) -> Self {
|
|
Self { kind, children: vec![], group: None, child_groups: vec![] }
|
|
}
|
|
}
|
|
|
|
/// A dissolving frame's `children`, each with the group it was drawn in (`groups`, else the
|
|
/// frame's `group`).
|
|
pub(super) fn grouped(
|
|
children: Vec<Arc<Node>>,
|
|
groups: Vec<Option<usize>>,
|
|
group: Option<usize>,
|
|
) -> impl Iterator<Item = (Arc<Node>, Option<usize>)> {
|
|
children.into_iter().zip(groups.into_iter().map(move |g| g.or(group)).chain(std::iter::repeat(group)))
|
|
}
|
|
|
|
/// Glyphs drawn consecutively with the same paint, merged into one path.
|
|
pub(super) struct GlyphRun {
|
|
pub(super) path: BezPath,
|
|
pub(super) paint: Paint,
|
|
pub(super) opacity: f32,
|
|
pub(super) stroke: Option<StrokeProps>,
|
|
pub(super) scale: f64,
|
|
pub(super) overprint: bool,
|
|
}
|
|
|
|
/// How often a tiling pattern was read, and the art (as its paints) each reading drew with the
|
|
/// swatch made for it.
|
|
pub(super) type PatternReadings = (u32, Vec<(Vec<Paint>, String)>);
|
|
|
|
/// Builder state a nested interpretation (soft mask, pattern cell) starts afresh and gives back.
|
|
pub(super) struct Saved {
|
|
pub(super) stack: Vec<Frame>,
|
|
pub(super) root_groups: Vec<Option<usize>>,
|
|
pub(super) blend: BlendMode,
|
|
pub(super) mask: Option<Arc<MaskSpec>>,
|
|
pub(super) page: Rect,
|
|
pub(super) pending: bool,
|
|
}
|
|
|
|
pub(super) struct Builder<'p> {
|
|
pub(super) colors: Colors<'p>,
|
|
pub(super) next: u64,
|
|
pub(super) stack: Vec<Frame>,
|
|
/// The optional content group of each child of the root frame.
|
|
pub(super) root_groups: Vec<Option<usize>>,
|
|
pub(super) blend: BlendMode,
|
|
pub(super) page: Rect,
|
|
pub(super) glyphs: Option<GlyphRun>,
|
|
pub(super) text: Option<TextLine>,
|
|
pub(super) text_as: TextAs,
|
|
pub(super) images: HashMap<String, ImageBlob>,
|
|
pub(super) image_keys: HashMap<u128, (String, u32, u32)>,
|
|
/// The images kept with their CMYK samples ([`crate::import_image`]).
|
|
pub(super) cmyk_keys: HashSet<u128>,
|
|
/// Mask keys of CMYK images whose mask turned out to hide nothing.
|
|
pub(super) opaque: HashSet<u128>,
|
|
pub(super) warnings: Vec<String>,
|
|
/// Fonts by cache key → base font name and whether the file carries its glyphs (from
|
|
/// [`scan_page`]).
|
|
pub(super) fonts: FontNames,
|
|
pub(super) font_names: HashMap<u128, FontInfo>,
|
|
pub(super) families: Option<Families>,
|
|
pub(super) missing_fonts: Vec<String>,
|
|
/// Font (cache key) → its characters by glyph, for CID-keyed fonts embedded without a
|
|
/// ToUnicode map.
|
|
pub(super) cid_text: HashMap<u128, Option<Arc<CidText>>>,
|
|
/// Font (cache key) → the character of each glyph its embedded font program maps
|
|
/// ([`font_chars`]), for glyphs the file's ToUnicode map doesn't name (#708).
|
|
pub(super) font_chars: HashMap<u128, Option<Arc<HashMap<u32, char>>>>,
|
|
/// Font (cache key) → the installed face that draws its glyphs, decided from its first glyph
|
|
/// that can be compared ([`matching_face`]); `None`: its glyphs differ, so it stays outlines.
|
|
pub(super) matched: HashMap<u128, Option<Arc<vectorcraft_text::FontFace>>>,
|
|
/// (Font, glyph) → the character its outline matches in the installed face, for glyphs no
|
|
/// mapping names ([`Builder::identified_char`]).
|
|
pub(super) identified: HashMap<(u128, u32), Option<char>>,
|
|
/// Per font: the installed version its type names, when it isn't the one its family and style
|
|
/// resolve to (see `CharStyle::font_version`).
|
|
pub(super) versions: HashMap<u128, Option<String>>,
|
|
/// The soft mask of the graphics state, the art drawn through it so far, and the masks read.
|
|
pub(super) mask: Option<Arc<MaskSpec>>,
|
|
pub(super) masked: Vec<Arc<Node>>,
|
|
pub(super) masks: HashMap<u128, Arc<MaskSpec>>,
|
|
/// Pattern swatches made, and per (pattern, stroke) the art signatures read and their names.
|
|
pub(super) patterns: Vec<PatternDef>,
|
|
pub(super) pattern_keys: HashMap<(u128, bool), PatternReadings>,
|
|
/// Per function-based shading (and the inks it paints with): what it comes in as, read once.
|
|
pub(super) function_paints: HashMap<(u128, Option<Vec<String>>), Paint>,
|
|
/// What is left of the import's function evaluations for those shadings.
|
|
pub(super) function_budget: FunctionBudget,
|
|
/// What is left of the import's objects ([`super::entry::max_objects`]).
|
|
pub(super) objects_left: usize,
|
|
/// Swatch names in use.
|
|
pub(super) taken: Vec<String>,
|
|
/// The page as walked by [`scan_page`], and how far the interpreter is through it.
|
|
pub(super) scan: Scan,
|
|
pub(super) tag_at: usize,
|
|
pub(super) group_at: usize,
|
|
/// The fills and strokes of paths drawn so far ([`Scan::paints`]).
|
|
pub(super) paint_at: usize,
|
|
pub(super) aligned: bool,
|
|
/// Whether optional content groups become layers.
|
|
pub(super) route: bool,
|
|
/// Some groups are hidden or don't print: art in marked content whose group can't be told
|
|
/// (once the scan and the interpreter part) is [`UNSORTED`] rather than the page's.
|
|
pub(super) unsure: bool,
|
|
/// The marked-content sequences open: the group each marks.
|
|
pub(super) marked: Vec<Option<usize>>,
|
|
/// A transparency group was just pushed: a form's (whose flags come next) or an image's.
|
|
pub(super) pending: bool,
|
|
pub(super) nested: u32,
|
|
/// Reading an alpha soft mask's art: images are read white with their own alpha, so the
|
|
/// mask's luminance is their alpha, as whitened vector art's is (cairo, and so Inkscape,
|
|
/// rasterizes masks into such images).
|
|
pub(super) white_images: bool,
|
|
/// The last filled path: its node and its outline (a stroke of the same outline joins it).
|
|
pub(super) last_fill: Option<(NodeId, BezPath)>,
|
|
/// What each line of type made tells beyond its text object ([`crate::import_lines`]).
|
|
pub(super) lines: HashMap<NodeId, LineFacts>,
|
|
}
|
|
|
|
pub(super) fn blend(b: craft_pdf::interpret::BlendMode) -> BlendMode {
|
|
use craft_pdf::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,
|
|
}
|
|
}
|
|
|
|
pub(super) fn fill_rule(r: craft_pdf::interpret::FillRule) -> FillRule {
|
|
match r {
|
|
craft_pdf::interpret::FillRule::NonZero => FillRule::NonZero,
|
|
craft_pdf::interpret::FillRule::EvenOdd => FillRule::EvenOdd,
|
|
}
|
|
}
|
|
|
|
/// `p` with every subpath closed: how a fill reads it.
|
|
pub(crate) fn closed_subpaths(p: &BezPath) -> BezPath {
|
|
let mut out = BezPath::new();
|
|
let mut open = false;
|
|
for el in p.elements() {
|
|
if open && matches!(el, PathEl::MoveTo(_)) {
|
|
out.push(PathEl::ClosePath);
|
|
}
|
|
out.push(*el);
|
|
open = !matches!(el, PathEl::ClosePath);
|
|
}
|
|
if open {
|
|
out.push(PathEl::ClosePath);
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Do a fill of `fill` and a stroke of `stroke` paint one object's outline? Applications write the
|
|
/// fill without closing the path (filling closes it) and the stroke closed.
|
|
pub(super) fn same_outline(fill: &BezPath, stroke: &BezPath) -> bool {
|
|
fill.elements() == stroke.elements() || closed_subpaths(fill).elements() == closed_subpaths(stroke).elements()
|
|
}
|
|
|
|
pub(super) fn mean_scale(a: Affine) -> f64 {
|
|
a.determinant().abs().sqrt()
|
|
}
|
|
|
|
/// Width a PDF line width of 0 imports as, points: a print hairline. In PDF `0 w` is the thinnest
|
|
/// line the device can draw (ISO 32000-1, 8.4.3.2), not no line.
|
|
const HAIRLINE: f64 = 0.25;
|
|
|
|
/// Document width of the stroke `p` drawn at `scale`: `0 w` becomes a [`HAIRLINE`].
|
|
pub(super) fn line_width(p: &StrokeProps, scale: f64) -> f64 {
|
|
if p.line_width == 0.0 { HAIRLINE } else { p.line_width as f64 * scale }
|
|
}
|
|
|
|
pub(super) fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f64) -> StrokeLayer {
|
|
let mut st = StrokeLayer::new(paint, line_width(p, 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;
|
|
// (Every stroke keeps its own copy: a dash array has a few entries, and one with thousands is cut.)
|
|
let dash = Dash {
|
|
pattern: p.dash_array.iter().take(MAX_DASH_ENTRIES).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
|
|
}
|
|
|
|
/// The most entries of a dash array kept (an even count keeps it a pattern of dashes and gaps).
|
|
const MAX_DASH_ENTRIES: usize = 64;
|
|
/// Most dash periods [`stroke_outline`] cuts a stroke into.
|
|
const MAX_OUTLINE_DASHES: f64 = 1e5;
|
|
|
|
/// The area stroke `p` paints along `path` (in its own space, before `transform`), in the document:
|
|
/// where a shading painting the stroke shows. Width, caps, joins and dashes as [`stroke_layer`]
|
|
/// takes them. `None` when it paints nothing.
|
|
pub(super) fn stroke_outline(path: &BezPath, transform: Affine, p: &StrokeProps) -> Option<BezPath> {
|
|
let scale = mean_scale(transform);
|
|
if !(scale.is_finite() && scale > 1e-9) {
|
|
return None;
|
|
}
|
|
let mut style =
|
|
kurbo::Stroke::new(line_width(p, scale) / scale).with_join(p.line_join).with_caps(p.line_cap).with_miter_limit(p.miter_limit as f64);
|
|
let dashes: Vec<f64> = p.dash_array.iter().take(MAX_DASH_ENTRIES).map(|v| *v as f64).collect();
|
|
let dash = Dash { pattern: dashes.clone(), offset: p.dash_offset as f64, align_corners: false };
|
|
// A dash pattern far finer than the path would cut it into countless pieces: it strokes solid.
|
|
let period: f64 = dashes.iter().sum();
|
|
if dash.is_dashed() && period > 0.0 && kurbo::Shape::perimeter(path, 1e-3) / period < MAX_OUTLINE_DASHES {
|
|
style = style.with_dashes(dash.offset, dashes);
|
|
}
|
|
// To a hundredth of a point in the document, within reason at extreme scales.
|
|
let tolerance = (0.01 / scale).clamp(1e-4, 1.0);
|
|
let mut out = kurbo::stroke(path.iter(), &style, &kurbo::StrokeOpts::default(), tolerance);
|
|
out.apply_affine(transform);
|
|
(!out.elements().is_empty()).then_some(out)
|
|
}
|
|
|
|
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.
|
|
pub(super) 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.
|
|
pub(super) 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)))))?
|
|
}
|
|
|
|
pub(super) 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)
|
|
}
|
|
|
|
pub(super) 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],
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod stroke_outline_tests {
|
|
use super::*;
|
|
|
|
/// A dash pattern far finer than the path strokes it solid instead of cutting it into
|
|
/// millions of pieces; one in reason dashes it.
|
|
#[test]
|
|
fn a_dash_far_finer_than_the_path_strokes_solid() {
|
|
let mut path = BezPath::new();
|
|
path.move_to((0.0, 0.0));
|
|
path.line_to((1000.0, 0.0));
|
|
let props = |dash: f32| StrokeProps { line_width: 2.0, dash_array: [dash; 4].into(), ..StrokeProps::default() };
|
|
let solid = stroke_outline(&path, Affine::IDENTITY, &props(1e-6)).unwrap();
|
|
assert!(solid.elements().len() < 20, "{} elements", solid.elements().len());
|
|
let dashed = stroke_outline(&path, Affine::IDENTITY, &props(10.0)).unwrap();
|
|
assert!(dashed.elements().len() > 100, "dashed: {} elements", dashed.elements().len());
|
|
}
|
|
}
|