mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
Clarify PDF import stages and module dependencies
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use super::writer::{Sheet, Writer, date_time};
|
||||
use super::*;
|
||||
use super::{Affine, Document, ExportReport, Exporter, HashSet, PageBoxes, PdfError, PdfOptions, Rect, write};
|
||||
|
||||
/// Export `doc` as PDF bytes: one page per artboard (or the artboards chosen in `opts`).
|
||||
pub fn export(doc: &Document, opts: &PdfOptions) -> Result<Vec<u8>, PdfError> {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::entry::RgbInCmyk;
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Arc, BezPath, BlendMode, Color, Compatibility, Document, Exporter, Fill, FillRule, HashMap, HashSet, Node, NormalizedF32, Path,
|
||||
PathBuilder, PathEl, Rect, Shape, Standard, Surface, Transform, cmyk, krilla, luma, rgb,
|
||||
};
|
||||
|
||||
/// How deep inline graphics nest before they are left out.
|
||||
pub(super) const MAX_INLINE_DEPTH: u32 = 4;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::exporter::{UNDECODED, writable, xf};
|
||||
use super::*;
|
||||
use super::{BlendMode, Exporter, Image, NodeKind, Rect, Size, Surface};
|
||||
|
||||
impl Exporter<'_> {
|
||||
/// Image `im` placed `size` points wide and high, resampled and compressed as the Compression
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::exporter::{address, blend, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, Paint, Path, Rect,
|
||||
Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke,
|
||||
};
|
||||
|
||||
impl Exporter<'_> {
|
||||
/// Opacity mask → luminosity soft mask. Outside the art the backdrop is black (clip) or white;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::exporter::{MAX_FIELD_PX, MAX_GRADIENT_PX, MAX_PATTERN_DEPTH, color, ctm, in_range, norm, q, to_path, writable, xf};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Color, Exporter, GradientKind, LinearGradient, Paint, RadialGradient, Rect, SepColor, SeparationColorant, SeparationSpace, Shape, Size,
|
||||
SpreadMethod, Stop, Surface, Vec2, cmyk, krilla, rgb,
|
||||
};
|
||||
|
||||
impl Exporter<'_> {
|
||||
/// A colour for the page, as the Output settings write it ([`ColorOut::color`]): without
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::exporter::{GlyphLine, MAX_INLINE_DEPTH, in_range, norm, to_path, writable, xf};
|
||||
use super::images::embed;
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, AppearanceItem, BezPath, BlendMode, Color, Cow, Embedding, Exporter, Fill, FillRule, Glyph, GlyphId, KrillaGlyph, ListMarker, Mark, Node,
|
||||
NormalizedF32, Paint, Path, Rect, Shape, Size, Surface, TextLayout, TextObject, krilla, write,
|
||||
};
|
||||
|
||||
impl Exporter<'_> {
|
||||
///
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::entry::RgbInCmyk;
|
||||
use super::exporter::{constant_mask, cover, to_path, writable, xf};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Arc, Archival, ColorOut, Compatibility, Document, Exporter, HashSet, Image, Metadata, Node, PageSettings, PdfError, PdfVersion, Rect,
|
||||
Shape, Size, Standard, find, krilla, rfind, write,
|
||||
};
|
||||
|
||||
/// A PDF being written, shared by export and print: the file's configuration and metadata, then
|
||||
/// its pages ([`Self::page`]), each drawn by an [`Exporter`].
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::cid::{CidText, FontInfo, comparable, font_chars, identify_glyph, matching_face, replacement, unify_radical};
|
||||
use super::entry::{MAX_NESTED, OVERPRINT_UNREAD, OVERPRINT_ZERO_MODE, PATTERN_REUSE, TEXT_OUTLINES, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE};
|
||||
use super::state::{Builder, Frame, FrameKind, Saved, bounds, grouped, line_width, mean_scale, paints, round3, stroke_layer};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, BfString, BlendMode, CacheKey, Color, Colors, Families, FillLayer, FillRule, FunctionBudget,
|
||||
FunctionPaint, HashMap, HashSet, ImageBlob, ImageObject, Knockout, Look, MaskSpec, MaskType, Native, Node, NodeId, NodeKind, OpacityMask, Paint,
|
||||
PathData, PathDrawMode, Pattern, PatternDef, Placement, Rect, Scan, Shape, SoftMask, StrokeProps, TextAs, TextLine, TextOverprint, TilingPattern,
|
||||
Upright, Walked, clipped, contains, fold_stop_opacity, function_paint, gradient_shape, is_rectangle, luminance, mask_spec, shading_gradient,
|
||||
};
|
||||
|
||||
impl<'p> Builder<'p> {
|
||||
pub(super) fn new(next: u64, colors: Colors<'p>, text_as: TextAs, route: bool) -> Self {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Arc, BezPath, ClipPath, Context, Device, Glyph, GlyphDrawMode, HashMap, Image, Page, PathDrawMode, PathEl, Rect, Shape, SoftMask,
|
||||
};
|
||||
|
||||
/// How many paths the interpreter draws on `page` (each fill and each stroke), leaving out those
|
||||
/// of soft masks, pattern cells and type 3 glyphs, which [`Builder`] reads nested. `None` when the
|
||||
|
||||
@@ -2,7 +2,10 @@ use super::entry::{INVISIBLE_TEXT_NOTE, MAX_NESTED, OUTLINED_TEXT_NOTE, UNSORTED
|
||||
use super::state::{
|
||||
Builder, Decoded, Frame, FrameKind, GlyphRun, blend, fill_rule, line_width, mean_scale, resize_alpha, rgba_png, same_outline, stroke_layer,
|
||||
};
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, BlendMode, ClipPath, Device, FillLayer, FillRule, Glyph, GlyphDrawMode, Image, ImageBlob,
|
||||
ImageData, Name, Node, NodeKind, PathData, PathDrawMode, Pattern, Shape, SoftMask, clipped, extend_clip, mesh_shading, tag_key,
|
||||
};
|
||||
|
||||
impl<'a> Device<'a> for Builder<'_> {
|
||||
fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
|
||||
|
||||
+122
-83
@@ -1,6 +1,10 @@
|
||||
use super::cid::drawn_paths;
|
||||
use super::state::Builder;
|
||||
use super::*;
|
||||
use super::{
|
||||
Affine, Arc, Artboard, AtomicBool, BlendMode, ColorMode, Colors, Context, CropTo, Document, FillRule, HashMap, HashSet, ImportOptions,
|
||||
ImportReport, InterpreterCache, InterpreterSettings, InterpreterWarning, LayerColor, MAX_NESTING, Mutex, Node, NodeKind, Ocgs, Ordering, Paint,
|
||||
PdfError, Rect, Shape, Swatch, all_on, contains, hides_forms, scan_page, white_cover,
|
||||
};
|
||||
|
||||
/// Mixed into an image's cache key when it is read white inside an alpha soft mask.
|
||||
pub(super) const WHITE_KEY: u128 = 0x7717_e000_a1fa;
|
||||
@@ -182,54 +186,12 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
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.skips_any();
|
||||
let mut hidden_form = false;
|
||||
if hide {
|
||||
for page in picked.iter().filter_map(|&n| original.pages().get(n)) {
|
||||
hidden_form |= hides_forms(page, &mut ocgs, original.decode_limits().max_decoded_stream())?;
|
||||
if hidden_form {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let turned_on = (hide && !hidden_form)
|
||||
.then(|| all_on(bytes, &original))
|
||||
.flatten()
|
||||
.and_then(|b| crate::pages::open_with_options(&b, opts.password.as_deref(), reader).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 (pdf, all_on, notes) = reopen_with_groups_on(bytes, opts, reader, original, &picked, &mut ocgs, route)?;
|
||||
let pages = pdf.pages();
|
||||
|
||||
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
|
||||
let sink2 = sink.clone();
|
||||
let incomplete = Arc::new(AtomicBool::new(false));
|
||||
let incomplete_sink = incomplete.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",
|
||||
InterpreterWarning::ContentTruncated => {
|
||||
incomplete_sink.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Ok(mut v) = sink2.lock()
|
||||
&& !v.iter().any(|x| x == msg)
|
||||
{
|
||||
v.push(msg.to_string());
|
||||
}
|
||||
}),
|
||||
render_annotations: false,
|
||||
..Default::default()
|
||||
};
|
||||
let settings = interpreter_settings(sink.clone(), incomplete.clone());
|
||||
|
||||
let mut doc = Document::new(1.0, 1.0);
|
||||
doc.title = "Imported PDF".into();
|
||||
@@ -316,17 +278,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
// crossing the artboard edge isn't clipped, so it opens whole (#646).
|
||||
let shown = xf.transform_rect_bbox(crate::pages::page_box(page, CropTo::Crop));
|
||||
let page_box = shown.inflate(0.01, 0.01);
|
||||
let mut clipped = false;
|
||||
for (_, art) in parts.iter_mut().filter(|_| !ai) {
|
||||
for n in art.iter_mut() {
|
||||
if n.visual_bounds().is_some_and(|r| !contains(page_box, r)) {
|
||||
let clip = Arc::new(b.clip_node(&shown.to_path(0.1), FillRule::NonZero));
|
||||
*n = Arc::new(Node::new(b.id(), NodeKind::Group { children: vec![clip, n.clone()], clip: true }));
|
||||
clipped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if clipped {
|
||||
if !ai && clip_past_page(&mut parts, &mut b, shown, page_box) {
|
||||
b.warn(PAST_PAGE_NOTE);
|
||||
}
|
||||
// A `.ai` page keeps its art past the artboard edge, unclipped (#646), so a page whose art
|
||||
@@ -336,24 +288,8 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
// art clears the art before it.
|
||||
let mut art_right = f64::NEG_INFINITY;
|
||||
if ai && let Some(art) = parts.iter().flat_map(|(_, v)| v.iter()).filter_map(|n| n.visual_bounds()).reduce(|a, b| a.union(b)) {
|
||||
// Art at a coordinate that isn't a number (a damaged file) moves nothing.
|
||||
let dx = Some((clear - art.x0).max(0.0)).filter(|dx| dx.is_finite()).unwrap_or(0.0);
|
||||
if dx > 0.0 {
|
||||
let shift = Affine::translate((dx, 0.0));
|
||||
for (_, v) in &mut parts {
|
||||
for n in v.iter_mut() {
|
||||
Arc::make_mut(n).transform(shift, false);
|
||||
}
|
||||
}
|
||||
// What the page's lines of type were is told where their glyphs are (the outlines
|
||||
// stroked on them are looked for there), so that moves with them.
|
||||
for facts in b.lines.iter_mut().filter(|(id, _)| id.0 >= first_id).map(|(_, facts)| facts) {
|
||||
for ink in &mut facts.ink {
|
||||
*ink = *ink + kurbo::Vec2::new(dx, 0.0);
|
||||
}
|
||||
}
|
||||
ab = Rect::new(ab.x0 + dx, ab.y0, ab.x1 + dx, ab.y1);
|
||||
}
|
||||
let dx = shift_ai_page(&mut parts, &mut b, first_id, clear, art);
|
||||
ab = Rect::new(ab.x0 + dx, ab.y0, ab.x1 + dx, ab.y1);
|
||||
art_right = art.x1 + dx;
|
||||
}
|
||||
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
|
||||
@@ -416,15 +352,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
}
|
||||
// The groups' layers stack as the pages mark them (the art outside them stays where it was
|
||||
// drawn): a group that first paints on a later page doesn't go on top of the others.
|
||||
let rank: HashMap<usize, usize> = order.iter().enumerate().map(|(i, &g)| (g, i)).collect();
|
||||
let at: Vec<usize> = slots.iter().enumerate().filter(|(_, s)| matches!(s, Slot::Group(_))).map(|(i, _)| i).collect();
|
||||
let mut stacked: Vec<usize> = slots.iter().filter_map(|s| if let Slot::Group(g) = s { Some(*g) } else { None }).collect();
|
||||
stacked.sort_by_key(|g| rank.get(g).copied().unwrap_or(usize::MAX));
|
||||
for (i, g) in at.into_iter().zip(stacked) {
|
||||
if let Some(s) = slots.get_mut(i) {
|
||||
*s = Slot::Group(g);
|
||||
}
|
||||
}
|
||||
stack_groups(&mut slots, &order);
|
||||
for (n, slot) in slots.into_iter().enumerate() {
|
||||
let mut layer = match slot {
|
||||
Slot::Page(l) => *l,
|
||||
@@ -528,3 +456,114 @@ fn is_only_text(nodes: &[Arc<Node>]) -> bool {
|
||||
let mut text = false;
|
||||
walk(nodes, &mut text) && text
|
||||
}
|
||||
|
||||
/// Opens the file again with every layer group turned on when the pages hide art in groups that are
|
||||
/// off, and tells whether it did. Gives the file to read, whether it is the turned-on copy, and the
|
||||
/// notes for the import's report.
|
||||
fn reopen_with_groups_on(
|
||||
bytes: &[u8],
|
||||
opts: &ImportOptions,
|
||||
reader: &craft_pdf::OpenOptions,
|
||||
original: craft_pdf::syntax::Pdf,
|
||||
picked: &[usize],
|
||||
ocgs: &mut Ocgs,
|
||||
route: bool,
|
||||
) -> Result<(craft_pdf::syntax::Pdf, bool, Vec<String>), PdfError> {
|
||||
// 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.skips_any();
|
||||
let mut hidden_form = false;
|
||||
if hide {
|
||||
for page in picked.iter().filter_map(|&n| original.pages().get(n)) {
|
||||
hidden_form |= hides_forms(page, ocgs, original.decode_limits().max_decoded_stream())?;
|
||||
if hidden_form {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let turned_on = (hide && !hidden_form)
|
||||
.then(|| all_on(bytes, &original))
|
||||
.flatten()
|
||||
.and_then(|b| crate::pages::open_with_options(&b, opts.password.as_deref(), reader).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();
|
||||
Ok((turned_on.unwrap_or(original), all_on, notes))
|
||||
}
|
||||
|
||||
/// The interpreter's settings, its warnings noted once each in `sink` (truncated content in `incomplete`).
|
||||
fn interpreter_settings(sink: Arc<Mutex<Vec<String>>>, incomplete: Arc<AtomicBool>) -> InterpreterSettings {
|
||||
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",
|
||||
InterpreterWarning::ContentTruncated => {
|
||||
incomplete.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Ok(mut v) = sink.lock()
|
||||
&& !v.iter().any(|x| x == msg)
|
||||
{
|
||||
v.push(msg.to_string());
|
||||
}
|
||||
}),
|
||||
render_annotations: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a `.ai` page's art (`art`, its bounds) right until it clears the art before it (at
|
||||
/// `clear`), and its lines of type with it. Gives how far.
|
||||
fn shift_ai_page(parts: &mut [(Option<usize>, Vec<Arc<Node>>)], b: &mut Builder<'_>, first_id: u64, clear: f64, art: Rect) -> f64 {
|
||||
// Art at a coordinate that isn't a number (a damaged file) moves nothing.
|
||||
let dx = Some((clear - art.x0).max(0.0)).filter(|dx| dx.is_finite()).unwrap_or(0.0);
|
||||
if dx > 0.0 {
|
||||
let shift = Affine::translate((dx, 0.0));
|
||||
for (_, v) in parts.iter_mut() {
|
||||
for n in v.iter_mut() {
|
||||
Arc::make_mut(n).transform(shift, false);
|
||||
}
|
||||
}
|
||||
// What the page's lines of type were is told where their glyphs are (the outlines
|
||||
// stroked on them are looked for there), so that moves with them.
|
||||
for facts in b.lines.iter_mut().filter(|(id, _)| id.0 >= first_id).map(|(_, facts)| facts) {
|
||||
for ink in &mut facts.ink {
|
||||
*ink = *ink + kurbo::Vec2::new(dx, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
dx
|
||||
}
|
||||
|
||||
/// Clips each object reaching past `page_box` to `shown`. Tells whether any did.
|
||||
fn clip_past_page(parts: &mut [(Option<usize>, Vec<Arc<Node>>)], b: &mut Builder<'_>, shown: Rect, page_box: Rect) -> bool {
|
||||
let mut clipped = false;
|
||||
for (_, art) in parts.iter_mut() {
|
||||
for n in art.iter_mut() {
|
||||
if n.visual_bounds().is_some_and(|r| !contains(page_box, r)) {
|
||||
let clip = Arc::new(b.clip_node(&shown.to_path(0.1), FillRule::NonZero));
|
||||
*n = Arc::new(Node::new(b.id(), NodeKind::Group { children: vec![clip, n.clone()], clip: true }));
|
||||
clipped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
clipped
|
||||
}
|
||||
|
||||
/// The groups' layers stack as the pages mark them (`order`, bottom first).
|
||||
fn stack_groups(slots: &mut [Slot], order: &[usize]) {
|
||||
let rank: HashMap<usize, usize> = order.iter().enumerate().map(|(i, &g)| (g, i)).collect();
|
||||
let at: Vec<usize> = slots.iter().enumerate().filter(|(_, s)| matches!(s, Slot::Group(_))).map(|(i, _)| i).collect();
|
||||
let mut stacked: Vec<usize> = slots.iter().filter_map(|s| if let Slot::Group(g) = s { Some(*g) } else { None }).collect();
|
||||
stacked.sort_by_key(|g| rank.get(g).copied().unwrap_or(usize::MAX));
|
||||
for (i, g) in at.into_iter().zip(stacked) {
|
||||
if let Some(s) = slots.get_mut(i) {
|
||||
*s = Slot::Group(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::cid::{CidText, FontInfo};
|
||||
use super::*;
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user