mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
Merge pull request #5598 from dotCooCoo/fix/ai-open-skips-pdf-part
PDF/.ai import: a file whose type all comes from its text document opens without its PDF part
This commit is contained in:
@@ -165,18 +165,18 @@ pub fn load(name: &str, bytes: &[u8]) -> Result<Loaded> {
|
||||
/// of its art, which it carries: its layers and hidden objects as they were, and the art outside
|
||||
/// its artboard (its PDF part has only what is on the artboards), as an EPS of the editor is read.
|
||||
/// Anything else as it was. `unit`: the size in points of a unit of the PDF part's user space (its
|
||||
/// first page's `UserUnit`) → (document, warnings, whether its layers were read). `private`: the
|
||||
/// editor's copy ([`editing_copy`]).
|
||||
/// first page's `UserUnit`) → (document, warnings, whether its layers were read). `layers`: the
|
||||
/// editor's copy ([`editing_copy`]), as far as it was decoded and read.
|
||||
fn through_editing_data(
|
||||
private: Option<&[u8]>,
|
||||
layers: Option<vectorcraft_eps::AiLayers>,
|
||||
opts: &LoadOptions,
|
||||
doc: Document,
|
||||
warnings: Vec<String>,
|
||||
unit: f64,
|
||||
) -> (Document, Vec<String>, bool) {
|
||||
let Some(private) = private else { return (doc, warnings, false) };
|
||||
let Some(layers) = layers else { return (doc, warnings, false) };
|
||||
let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines;
|
||||
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai(private, doc, warnings, outlined, opts.trust_editing_data, unit);
|
||||
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai_from(layers, doc, warnings, outlined, opts.trust_editing_data, unit);
|
||||
// The layers weren't read (or couldn't be): the notes of reading the PDF part still hold.
|
||||
if !read {
|
||||
return (doc, warnings, false);
|
||||
@@ -216,6 +216,14 @@ fn reads_editing_copy(opts: &LoadOptions) -> bool {
|
||||
opts.pages.is_none() && opts.layers && opts.editing_data
|
||||
}
|
||||
|
||||
/// May a file opened with `opts` open from the editor's own copy of its art without its PDF part
|
||||
/// being imported ([`vectorcraft_eps::ai_layers_alone`]): it reads the editor's copy, on the pages'
|
||||
/// trim boxes (the crop box asked for, as a file with the editor's copy opens on them), with its
|
||||
/// type as text?
|
||||
fn reads_layers_alone(opts: &LoadOptions) -> bool {
|
||||
reads_editing_copy(opts) && opts.crop == vectorcraft_pdf::CropTo::Crop && opts.text_as != vectorcraft_pdf::TextAs::Outlines
|
||||
}
|
||||
|
||||
/// The editor's own copy of its art that `opened` carries (as [`vectorcraft_pdf::illustrator_data`]
|
||||
/// reads it), when `opts` read it ([`reads_editing_copy`]).
|
||||
fn editing_copy(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions) -> Option<Vec<u8>> {
|
||||
@@ -365,41 +373,68 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
Ok(o) => super::pdfimport::import(bytes, o, opts, format.id != "pdf"),
|
||||
Err(e) => Err(err(e)),
|
||||
};
|
||||
let read = restore_or_import(editing, || match imported() {
|
||||
Ok(r) => {
|
||||
// The editor's copy, read once the import is done with the pages.
|
||||
let private = opened.as_ref().ok().and_then(|o| editing_copy(o, opts));
|
||||
// Pages of text alone: the editing copy alone where it reads, as for a file
|
||||
// saved without its PDF part (#1387: else the page, which is the art). So too
|
||||
// pages that draw nothing at all: there is no page art to check the layers
|
||||
// against, and the empty page would lose all of it.
|
||||
// A page is empty only when nothing on it was skipped (an image that can't be
|
||||
// decoded draws nothing either): its notes are at most the page's own.
|
||||
let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w));
|
||||
let alone = (r.only_text || empty)
|
||||
.then(|| private.as_deref().map(editing_copy_alone))
|
||||
.flatten()
|
||||
.and_then(|read| read.ok())
|
||||
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts));
|
||||
let (doc, warnings, read) = match alone {
|
||||
Some((doc, warnings)) => (doc, warnings, true),
|
||||
None => through_editing_data(private.as_deref(), opts, r.document, r.warnings, r.user_unit),
|
||||
};
|
||||
editing_data = read;
|
||||
Ok((doc, warnings))
|
||||
let read = restore_or_import(editing, || {
|
||||
// Type all from the text document and nothing that needs the PDF part: the layers
|
||||
// alone, the PDF part not imported. Else what was read of the layers is kept for
|
||||
// reading them with the PDF part.
|
||||
let mut layers = None;
|
||||
if let Some((o, private)) = opened.as_ref().ok().filter(|_| reads_layers_alone(opts)).and_then(|o| Some((o, editing_copy(o, opts)?)))
|
||||
{
|
||||
let pages = o.trim_pages();
|
||||
match vectorcraft_eps::ai_layers_alone(&private, &pages.sizes, pages.user_unit) {
|
||||
Ok((mut doc, warnings)) => {
|
||||
doc.setup.bleed = pages.bleed;
|
||||
editing_data = true;
|
||||
return Ok((doc, warnings));
|
||||
}
|
||||
Err(read) => layers = Some(read),
|
||||
}
|
||||
}
|
||||
match imported() {
|
||||
Ok(r) => {
|
||||
// The editor's copy, read once the import is done with the pages, unless
|
||||
// its layers were read before it.
|
||||
let private = layers.is_none().then(|| opened.as_ref().ok().and_then(|o| editing_copy(o, opts))).flatten();
|
||||
// Pages of text alone: the editing copy alone where it reads, as for a file
|
||||
// saved without its PDF part (#1387: else the page, which is the art). So too
|
||||
// pages that draw nothing at all: there is no page art to check the layers
|
||||
// against, and the empty page would lose all of it.
|
||||
// A page is empty only when nothing on it was skipped (an image that can't be
|
||||
// decoded draws nothing either): its notes are at most the page's own.
|
||||
let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w));
|
||||
let copy_alone = || match &private {
|
||||
Some(private) => Some(editing_copy_alone(private)),
|
||||
None if layers.is_some() => opened.as_ref().ok().and_then(|o| editing_copy(o, opts)).map(|p| editing_copy_alone(&p)),
|
||||
None => None,
|
||||
};
|
||||
let alone = (r.only_text || empty)
|
||||
.then(copy_alone)
|
||||
.flatten()
|
||||
.and_then(|read| read.ok())
|
||||
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts));
|
||||
let (doc, warnings, read) = match alone {
|
||||
Some((doc, warnings)) => (doc, warnings, true),
|
||||
None => {
|
||||
let layers = layers.or_else(|| private.as_deref().map(vectorcraft_eps::AiLayers::decode));
|
||||
through_editing_data(layers, opts, r.document, r.warnings, r.user_unit)
|
||||
}
|
||||
};
|
||||
editing_data = read;
|
||||
Ok((doc, warnings))
|
||||
}
|
||||
// Saved without its PDF part: the editing copy is all the file has (`pages` that
|
||||
// name every page read it as the whole file).
|
||||
Err(e) if is_placeholder(&e) => opened
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|o| from_editing_data_alone(o, opts))
|
||||
.unwrap_or(Err(e))
|
||||
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts))
|
||||
.inspect(|_| editing_data = true),
|
||||
// A PDF part that can't be read (a stream the import refuses): the editing copy
|
||||
// alone where it reads, else the error.
|
||||
Err(e) => without_pdf_part(bytes, opts, &e).inspect(|_| editing_data = true).ok_or(e),
|
||||
}
|
||||
// Saved without its PDF part: the editing copy is all the file has (`pages` that
|
||||
// name every page read it as the whole file).
|
||||
Err(e) if is_placeholder(&e) => opened
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|o| from_editing_data_alone(o, opts))
|
||||
.unwrap_or(Err(e))
|
||||
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts))
|
||||
.inspect(|_| editing_data = true),
|
||||
// A PDF part that can't be read (a stream the import refuses): the editing copy
|
||||
// alone where it reads, else the error.
|
||||
Err(e) => without_pdf_part(bytes, opts, &e).inspect(|_| editing_data = true).ok_or(e),
|
||||
})?;
|
||||
// The File Info of a file read from its pages or its editing copy.
|
||||
if !read.2 {
|
||||
|
||||
@@ -1221,10 +1221,16 @@ fn half_outlined_line(y: f64, rest: &str, mode: u8) -> String {
|
||||
/// text document (a line starting at (14, 65)), then `more` of a story the text document doesn't
|
||||
/// have. Without `story` the file has no text document.
|
||||
fn half_outlined_ai(story: Option<&str>, more: usize, type_content: &str) -> Vec<u8> {
|
||||
half_outlined_ai_on(story, more, type_content, "", |page| page)
|
||||
}
|
||||
|
||||
/// [`half_outlined_ai`] with the editing data's comments `head` before its art, whose page is
|
||||
/// `page_of` the 200 × 100 pt page it draws on (other boxes, more entries).
|
||||
fn half_outlined_ai_on(story: Option<&str>, more: usize, type_content: &str, head: &str, page_of: impl FnOnce(PdfPage) -> PdfPage) -> Vec<u8> {
|
||||
let text = |story: u32| format!("/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n{story} /StoryIndex ,\n;\n");
|
||||
let texts: String = std::iter::once(text(0)).chain((0..more).map(|_| text(99))).collect();
|
||||
let mut body =
|
||||
format!("%AI3_TemplateBox: 100 50 100 50\n{}", ai::layer("Art", &format!("0.5 g\n{}f\n{texts}", ai::rect(120.0, 20.0, 160.0, 60.0))));
|
||||
format!("{head}%AI3_TemplateBox: 100 50 100 50\n{}", ai::layer("Art", &format!("0.5 g\n{}f\n{texts}", ai::rect(120.0, 20.0, 160.0, 60.0))));
|
||||
if let Some(story) = story {
|
||||
// The story's one line starts at the canvas center (8191.5, the template box's center) moved
|
||||
// to (14, 65): its y runs down the page.
|
||||
@@ -1255,7 +1261,7 @@ fn half_outlined_ai(story: Option<&str>, more: usize, type_content: &str) -> Vec
|
||||
};
|
||||
let cmap = "begincmap\n1 begincodespacerange\n<00> <FF>\nendcodespacerange\n1 beginbfchar\n<41> <FFFD>\nendbfchar\nendcmap\n";
|
||||
pdf_with(
|
||||
&[page],
|
||||
&[page_of(page)],
|
||||
&[&format!("<< /Length {} >>\nstream\n{data}\nendstream", data.len()), &format!("<< /Length {} >>\nstream\n{cmap}endstream", cmap.len())],
|
||||
None,
|
||||
)
|
||||
@@ -1287,6 +1293,12 @@ fn texts(s: &Session) -> Vec<String> {
|
||||
/// (art x 10 to 90 and 110 to 190, y 10 to 90), then a band across the first frame (y 30 to 70),
|
||||
/// made a wrap object with no offset when `wrap` is set.
|
||||
fn wrapped_thread_ai(wrap: bool) -> Vec<u8> {
|
||||
ai::ai(&wrapped_thread_data(wrap.then_some(0.0)), PLACEHOLDER)
|
||||
}
|
||||
|
||||
/// The editing data of [`wrapped_thread_ai`], compressed, with the band a wrap object of `offset`
|
||||
/// when one is given.
|
||||
fn wrapped_thread_data(offset: Option<f64>) -> Vec<u8> {
|
||||
// The template box is centred at (100, 50), the canvas centre 8191.5: y runs down the canvas.
|
||||
let canvas = |x: f64, y: f64| (x + 8091.5, 8241.5 - y);
|
||||
let rect = |x0: f64, y0: f64, x1: f64, y1: f64| {
|
||||
@@ -1306,14 +1318,47 @@ fn wrapped_thread_ai(wrap: bool) -> Vec<u8> {
|
||||
);
|
||||
let lines: Vec<String> = ascii85(document.as_bytes()).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
let frame = |f: u32| format!("/AI11Text :\n0 /FreeUndo ,\n{f} /FrameIndex ,\n0 /StoryIndex ,\n;\n");
|
||||
let dict = if wrap { "%_/ArtDictionary :\n%_0 /Real (AI11TextWrapOffset) ,\n%_0 /Bool (AI11TextWrapInvert) ,\n%_;\n%_\n" } else { "" };
|
||||
let dict = offset
|
||||
.map(|o| format!("%_/ArtDictionary :\n%_{o} /Real (AI11TextWrapOffset) ,\n%_0 /Bool (AI11TextWrapInvert) ,\n%_;\n%_\n"))
|
||||
.unwrap_or_default();
|
||||
let art = format!("{}{}0.5 g\n{}f\n{dict}", frame(0), frame(1), ai::rect(10.0, 30.0, 90.0, 70.0));
|
||||
let body = format!(
|
||||
"%AI3_TemplateBox: 100 50 100 50\n{}%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n{}\n%AI11_EndTextDocument\n",
|
||||
ai::layer("Art", &art),
|
||||
lines.join("\n")
|
||||
);
|
||||
ai::ai(&compressed(ai::editing_data(200.0, 100.0, &body).as_bytes()), PLACEHOLDER)
|
||||
compressed(ai::editing_data(200.0, 100.0, &body).as_bytes())
|
||||
}
|
||||
|
||||
/// The wrap offset of the first frame of `doc`'s threaded type, as its text resolves it.
|
||||
fn first_frame_wrap_offset(doc: &vectorcraft_doc::Document) -> f64 {
|
||||
match &doc.node(doc.text_threads[0][0]).unwrap().kind {
|
||||
NodeKind::Text(t) => t.wrap[0].wrap.offset,
|
||||
_ => panic!("area type"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `.ai` in units of 10 points whose threaded area type flows around a wrap object of a 2 unit
|
||||
/// offset opens with the type flowing round it 20 pt off, its PDF part read or not, and when its
|
||||
/// PDF part can't be read. The default graphic styles of a file without its own aren't scaled.
|
||||
#[test]
|
||||
fn wrapped_type_in_units_of_ten_points_flows_round_the_wrap_at_its_size_in_points() {
|
||||
let private = wrapped_thread_data(Some(2.0));
|
||||
let page = "0.5 g 10 30 80 40 re f";
|
||||
let refused = ai::ai_with_entries_and_content(&private, page, "/UserUnit 10", REFUSED_CONTENT);
|
||||
// On the trim boxes the PDF part is imported, and the refused stream stops it.
|
||||
let trim = LoadOptions { crop: vectorcraft_pdf::CropTo::Trim, ..Default::default() };
|
||||
let files = [(ai::ai_with(&private, page, "/UserUnit 10"), LoadOptions::default()), (refused, trim)];
|
||||
for (file, opts) in &files {
|
||||
let loaded = load_with("thread.ai", file, opts).unwrap();
|
||||
assert!(loaded.editing_data, "{:?}", loaded.warnings);
|
||||
assert!((first_frame_wrap_offset(&loaded.doc) - 20.0).abs() < 0.01, "{:?}", loaded.warnings);
|
||||
}
|
||||
let loaded = load_with("thread.ai", &files[1].0, &files[1].1).unwrap();
|
||||
assert!(loaded.warnings.iter().any(|w| w.starts_with("the PDF part couldn't be read")), "{:?}", loaded.warnings);
|
||||
// The file has no graphic styles of its own: the default ones aren't scaled either way.
|
||||
let read = load_with("thread.ai", &files[0].0, &files[0].1).unwrap();
|
||||
assert_eq!(loaded.doc.graphic_styles, read.doc.graphic_styles);
|
||||
}
|
||||
|
||||
/// Threaded area type below a wrap object flows around it when the file opens: the text the wrap
|
||||
@@ -1358,6 +1403,77 @@ fn type_from_the_text_document_opens_without_the_pdf_parts_note_on_outlines() {
|
||||
assert!(!r["warnings"].as_array().unwrap().contains(&without_outlines_note(1)), "{r}");
|
||||
}
|
||||
|
||||
/// A `.ai` whose text document makes all of its type, with nothing unread, opens from its layers
|
||||
/// without its PDF part being imported: a page that draws more than the layers isn't compared with
|
||||
/// them. On its trim box it takes the pages' bleed, and pages in units of 10 points draw it ten
|
||||
/// times as large, as when its PDF part is imported. With type the text document can't make,
|
||||
/// editing data on a large canvas, its type asked for as outlines, another box or pages picked, the
|
||||
/// PDF part is imported as before.
|
||||
#[test]
|
||||
fn type_all_from_the_text_document_opens_without_importing_the_pdf_part() {
|
||||
let line = half_outlined_line(65.0, "rst draft", 0);
|
||||
// The page draws a red square the layers don't have.
|
||||
let differs = half_outlined_ai(Some("first draft"), 0, &format!("{line} 1 0 0 rg 20 20 30 30 re f"));
|
||||
let mut s = Session::new();
|
||||
let before = vectorcraft_pdf::imports_of(&differs);
|
||||
let loaded = load_with("draft.ai", &differs, &LoadOptions::default()).unwrap();
|
||||
assert_eq!(vectorcraft_pdf::imports_of(&differs), before, "{:?}", loaded.warnings);
|
||||
assert!(loaded.editing_data, "{:?}", loaded.warnings);
|
||||
assert!(!loaded.warnings.iter().any(|w| w.contains("differently")), "{:?}", loaded.warnings);
|
||||
let r = open(&mut s, "draft.ai", &differs, json!({})).unwrap();
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}");
|
||||
assert_eq!(texts(&s), ["first draft"], "{r}");
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 2, "the square and the type, without the page's red square: {r}");
|
||||
// A 10 pt bleed round the trim box, the art drawn in the trim box.
|
||||
let bled = half_outlined_ai_on(Some("first draft"), 0, &line, "", |p| PdfPage {
|
||||
media: [0.0, 0.0, 220.0, 120.0],
|
||||
bleed: Some([0.0, 0.0, 220.0, 120.0]),
|
||||
trim: Some([10.0, 10.0, 210.0, 110.0]),
|
||||
content: format!("1 0 0 1 10 10 cm {}", p.content),
|
||||
..p
|
||||
});
|
||||
let before = vectorcraft_pdf::imports_of(&bled);
|
||||
let loaded = load_with("bled.ai", &bled, &LoadOptions::default()).unwrap();
|
||||
assert_eq!(vectorcraft_pdf::imports_of(&bled), before);
|
||||
assert_eq!(loaded.doc.setup.bleed, [10.0; 4]);
|
||||
// Pages of 10 pt units.
|
||||
let large = half_outlined_ai_on(Some("first draft"), 0, &line, "", |p| PdfPage { entries: format!("{} /UserUnit 10", p.entries), ..p });
|
||||
let before = vectorcraft_pdf::imports_of(&large);
|
||||
let loaded = load_with("large.ai", &large, &LoadOptions::default()).unwrap();
|
||||
assert_eq!(vectorcraft_pdf::imports_of(&large), before);
|
||||
let board = loaded.doc.artboards[0].rect;
|
||||
assert!((board.width() - 2000.0).abs() < 0.01 && (board.height() - 1000.0).abs() < 0.01, "{board:?}");
|
||||
// Opened on the trim boxes with the PDF part imported and compared, as before, both files have
|
||||
// the same artboards and bleed.
|
||||
for file in [&bled, &large] {
|
||||
let alone = load_with("draft.ai", file, &LoadOptions::default()).unwrap();
|
||||
let compared = load_with("draft.ai", file, &LoadOptions { crop: vectorcraft_pdf::CropTo::Trim, ..Default::default() }).unwrap();
|
||||
assert!(compared.editing_data, "{:?}", compared.warnings);
|
||||
let boards = |d: &vectorcraft_doc::Document| d.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
|
||||
assert_eq!(boards(&alone.doc), boards(&compared.doc));
|
||||
assert_eq!(alone.doc.setup.bleed, compared.doc.setup.bleed);
|
||||
assert_eq!(names(&alone.doc.layers), names(&compared.doc.layers));
|
||||
// The file has no graphic styles of its own: the default ones aren't scaled.
|
||||
assert_eq!(alone.doc.graphic_styles, compared.doc.graphic_styles);
|
||||
}
|
||||
// The PDF part is imported, once.
|
||||
let unmade = half_outlined_ai(Some("first draft"), 1, &line);
|
||||
// Editing data on a large canvas reads at its own scale when the file has no PDF part.
|
||||
let canvas = half_outlined_ai_on(Some("first draft"), 0, &line, "%AI24_LargeCanvasScale: 2\n", |p| p);
|
||||
let cases = [
|
||||
(&unmade, LoadOptions::default()),
|
||||
(&canvas, LoadOptions::default()),
|
||||
(&differs, LoadOptions { text_as: vectorcraft_pdf::TextAs::Outlines, ..Default::default() }),
|
||||
(&differs, LoadOptions { crop: vectorcraft_pdf::CropTo::Trim, ..Default::default() }),
|
||||
(&differs, LoadOptions { pages: Some("1".into()), ..Default::default() }),
|
||||
];
|
||||
for (file, opts) in cases {
|
||||
let before = vectorcraft_pdf::imports_of(file);
|
||||
load_with("draft.ai", file, &opts).unwrap();
|
||||
assert_eq!(vectorcraft_pdf::imports_of(file) - before, 1, "{opts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A text object that takes the page's type lacks the glyphs next to it that the page kept as
|
||||
/// outlines: a note counts such objects, in place of the PDF part's notes. Stroked glyphs that
|
||||
/// come in with the stroked type keep the PDF part's notes.
|
||||
|
||||
@@ -71,7 +71,7 @@ use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
pub(crate) use native::scale_document;
|
||||
pub use native::{ai_alone, ai_alone_on_page, is_loss, layered_ai};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, is_loss, layered_ai, layered_ai_from};
|
||||
pub use text::family_style;
|
||||
|
||||
/// The page of a PostScript file without a bounding box (US Letter).
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
//! before; on a layer that doesn't show it is left out of that layer, with a warning. The page is
|
||||
//! also the safety net: when the layers' art looks different from it (one draws an object the
|
||||
//! other doesn't, or much of it differs; an EPS whose page is its art's box has nothing printed
|
||||
//! outside it), the page is used.
|
||||
//! outside it), the page is used. A `.ai` whose text document makes all of its type, with no
|
||||
//! object unread and nothing else that the page would decide, opens from its layers without its
|
||||
//! PDF part being imported, so nothing is compared ([`ai_layers_alone`]).
|
||||
//!
|
||||
//! Where a file doesn't match what this module expects, it is left alone and its page is imported.
|
||||
|
||||
@@ -52,7 +54,7 @@ use build::{Frame, MADE, prune_unseen};
|
||||
use compare::{
|
||||
MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, PageView, Shares, compare, compared, comparison_render, comparison_samples, differ, differ_in_colour,
|
||||
};
|
||||
pub use layers::{ai_alone, ai_alone_on_page, layered_ai};
|
||||
pub use layers::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, layered_ai, layered_ai_from};
|
||||
pub(super) use layers::{layered, program_alone};
|
||||
pub use slots::is_loss;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -76,14 +76,98 @@ pub(super) fn build_on_page(data: &[u8], page: (f64, f64), unit: f64) -> Result<
|
||||
};
|
||||
let in_units = |r: Option<Rect>, k: f64| k.is_finite() && k > 1.0 && fits(r, k) && !fits(r, 1.0);
|
||||
let scale = [unit, structure.canvas_scale].into_iter().find(|&k| in_units(board, k) || (!fits(board, 1.0) && in_units(art, k)));
|
||||
let styles = structure.styles_read;
|
||||
let (mut doc, notes) = build_from(structure, data, None, Page::Artboard, false, false, 1.0)?;
|
||||
if let Some(k) = scale {
|
||||
// The graphic styles here are all the file's, drawn in its units too.
|
||||
scale_document(&mut doc, k, true);
|
||||
// The graphic styles read from the file are drawn in its units too.
|
||||
scale_document(&mut doc, k, styles);
|
||||
relayout_wrapped(&mut doc);
|
||||
}
|
||||
Ok((doc, notes))
|
||||
}
|
||||
|
||||
/// A document read from the editing copy and its notes, or why it can't be read.
|
||||
pub(super) type Built = Result<(Document, Vec<String>), String>;
|
||||
|
||||
/// [`build`] of a `.ai` without importing its PDF part, whose pages (each its trim box in points,
|
||||
/// width and height) are `pages` and are in units of `unit` points: the document read from
|
||||
/// `structure` (the editing copy `data` read) with nothing compared with the pages, when the file's
|
||||
/// text document makes the type of every text object (at least one shows, and none is type that a
|
||||
/// look draws), no object is unread or has another placement or drawn art that the page would
|
||||
/// choose, and its artboards are the pages, one for one and of their size, in points or in the pages'
|
||||
/// units ([`page_scale_alone`]). The document is drawn at that scale. Else `Err(structure)`, for
|
||||
/// [`build_from`] with the page.
|
||||
pub(super) fn build_without_page(structure: Structure, data: &[u8], pages: &[(f64, f64)], unit: f64) -> Result<Built, Box<Structure>> {
|
||||
let Some(scale) = page_scale_alone(&structure, pages, unit).filter(|_| nothing_waits_on_page(&structure) && type_all_made(&structure, data))
|
||||
else {
|
||||
return Err(Box::new(structure));
|
||||
};
|
||||
let styles = structure.styles_read;
|
||||
let built = build_from(structure, data, None, Page::Artboard, false, false, 1.0).map(|(mut doc, notes)| {
|
||||
if scale != 1.0 {
|
||||
// The graphic styles read from the file are drawn in its units too.
|
||||
scale_document(&mut doc, scale, styles);
|
||||
relayout_wrapped(&mut doc);
|
||||
}
|
||||
(doc, notes)
|
||||
});
|
||||
Ok(built)
|
||||
}
|
||||
|
||||
/// Is nothing in `structure` waiting on the page: no object unread (the page's art takes its
|
||||
/// place), no placed file with another placement, no live envelope with the drawn art it replaced
|
||||
/// (the comparison with the page chooses between them)?
|
||||
fn nothing_waits_on_page(structure: &Structure) -> bool {
|
||||
structure.unread.is_empty()
|
||||
&& structure.unread_elsewhere.is_empty()
|
||||
&& structure.placed_elsewhere.is_empty()
|
||||
&& structure.envelopes_drawn.is_empty()
|
||||
}
|
||||
|
||||
/// Does the text document of `data` (read as `structure`) make the type of every text object, those
|
||||
/// that don't show included, with at least one that shows, and none of them type that a look draws?
|
||||
fn type_all_made(structure: &Structure, data: &[u8]) -> bool {
|
||||
let mut infos = vec![];
|
||||
collect_slots(&structure.doc.layers, true, &mut infos);
|
||||
if !infos.iter().any(|i| i.shown) || infos.iter().any(|i| i.look.is_some()) {
|
||||
return false;
|
||||
}
|
||||
let frames = ai::text_frames(data);
|
||||
for info in &mut infos {
|
||||
info.art_path = info.story.and_then(|s| frames.get(&(s, info.frame))).map(|p| PathData::from_bezpath(&(structure.to_doc * p.clone())));
|
||||
}
|
||||
let Some(texts) = Texts::read(data) else { return false };
|
||||
let mut read: std::collections::BTreeMap<u32, Option<Arc<Story>>> = Default::default();
|
||||
let stories: Vec<Option<Arc<Story>>> = infos
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let n = i.story?;
|
||||
read.entry(n).or_insert_with(|| texts.story(n as usize).map(Arc::new)).clone()
|
||||
})
|
||||
.collect();
|
||||
// A slot without its story is made from nothing; past the text `build_from` reads, nothing is
|
||||
// made either: it refuses the file.
|
||||
if stories.iter().any(Option::is_none) || stories.iter().flatten().map(|s| s.len()).sum::<usize>() > MAX_TEXT {
|
||||
return false;
|
||||
}
|
||||
let template = structure.template.map(|t| ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0));
|
||||
make_slots(&infos, &stories, template, structure.to_doc, &mut vec![]).iter().all(Option::is_some)
|
||||
}
|
||||
|
||||
/// The scale at which the artboards of `structure` are the pages `pages` (each its width and height
|
||||
/// in points), one for one and in order, each within half a point: 1 when they are in points, else
|
||||
/// `unit` (the pages' `UserUnit`) when they are in those units, as [`page_scale`] finds it with the
|
||||
/// page. `None` when they aren't the pages, and for a file that declares a large canvas scale (a
|
||||
/// `.ai` saved without its PDF part reads one at that scale, [`super::ai_alone`]).
|
||||
fn page_scale_alone(structure: &Structure, pages: &[(f64, f64)], unit: f64) -> Option<f64> {
|
||||
let boards = &structure.doc.artboards;
|
||||
if boards.is_empty() || boards.len() != pages.len() || structure.canvas_scale != 1.0 {
|
||||
return None;
|
||||
}
|
||||
let fits = |k: f64| boards.iter().zip(pages).all(|(b, (w, h))| (b.rect.width() - w / k).abs() <= 0.5 && (b.rect.height() - h / k).abs() <= 0.5);
|
||||
[1.0, unit].into_iter().find(|&k| k.is_finite() && k >= 1.0 && fits(k))
|
||||
}
|
||||
|
||||
/// [`build`] from `structure`, the editing copy `data` already read.
|
||||
pub(super) fn build_from(
|
||||
structure: Structure,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::build::{PAGE_MOVE, Page, build, build_from, build_on_page};
|
||||
use super::build::{PAGE_MOVE, Page, build, build_from, build_on_page, build_without_page};
|
||||
use super::slots::LAYERS_UNREAD;
|
||||
use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai};
|
||||
|
||||
@@ -110,11 +110,24 @@ fn layer_tree(nodes: &[Arc<Node>]) -> Vec<Arc<Node>> {
|
||||
/// units comes in `unit` times as large, and so does the artboard layout `place_pages` takes from
|
||||
/// it.
|
||||
pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outlined: bool, trust: bool, unit: f64) -> (Document, Vec<String>, bool) {
|
||||
layered_ai_from(AiLayers::decode(private), visible, warnings, outlined, trust, unit)
|
||||
}
|
||||
|
||||
/// [`layered_ai`] of the editing copy `layers`, as far as it was already decoded and read.
|
||||
pub fn layered_ai_from(
|
||||
layers: AiLayers,
|
||||
visible: Document,
|
||||
warnings: Vec<String>,
|
||||
outlined: bool,
|
||||
trust: bool,
|
||||
unit: f64,
|
||||
) -> (Document, Vec<String>, bool) {
|
||||
let unread = |mut all: Vec<String>, why: &str| {
|
||||
all.push(format!("{LAYERS_UNREAD} from its editing data ({why}): they come from its PDF part"));
|
||||
all
|
||||
};
|
||||
let data = match ai::decode_private(private) {
|
||||
let AiLayers { data, structure } = layers;
|
||||
let data = match data {
|
||||
Ok(data) => data,
|
||||
Err(why) => return (visible, unread(warnings, &why), false),
|
||||
};
|
||||
@@ -137,7 +150,10 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outl
|
||||
use_variables(&data, &mut visible, &mut all);
|
||||
(visible, all, false)
|
||||
};
|
||||
match build(&data, Some(&visible), Page::Artboard, outlined, trust, unit) {
|
||||
let built = structure
|
||||
.unwrap_or_else(|| ai::read(&data, false).map(Box::new))
|
||||
.and_then(|structure| build_from(*structure, &data, Some(&visible), Page::Artboard, outlined, trust, unit));
|
||||
match built {
|
||||
// An image too large for the editing data's reader would be missing from the layers, while
|
||||
// the PDF part has it: the file comes in as its PDF part instead (#1479).
|
||||
Ok((_, notes)) if notes.iter().any(|n| n == ai::IMAGE_TOO_LARGE) => {
|
||||
@@ -153,6 +169,51 @@ pub fn layered_ai(private: &[u8], visible: Document, warnings: Vec<String>, outl
|
||||
}
|
||||
}
|
||||
|
||||
/// The `.ai` whose editing copy is `private` (as [`layered_ai`] takes it) from its layers alone,
|
||||
/// without its PDF part, whose pages (each its trim box in points, width and height) are `pages`
|
||||
/// and are in units of `unit` points (its first page's `UserUnit`): the document and the notes of
|
||||
/// reading it, when the file's text document makes all of its type and nothing in its layers needs
|
||||
/// the PDF part (see `build_without_page`). Nothing is compared with the PDF part. Else, as the
|
||||
/// error, what was decoded and read of the editing copy, for [`layered_ai_from`] once the PDF part
|
||||
/// is imported.
|
||||
pub fn ai_layers_alone(private: &[u8], pages: &[(f64, f64)], unit: f64) -> Result<(Document, Vec<String>), AiLayers> {
|
||||
let data = match ai::decode_private(private) {
|
||||
Ok(data) => data,
|
||||
Err(why) => return Err(AiLayers { data: Err(why), structure: None }),
|
||||
};
|
||||
if !has_layers(&data) {
|
||||
return Err(AiLayers { data: Ok(data), structure: None });
|
||||
}
|
||||
let structure = match ai::read(&data, false) {
|
||||
Ok(structure) => structure,
|
||||
Err(why) => return Err(AiLayers { data: Ok(data), structure: Some(Err(why)) }),
|
||||
};
|
||||
match build_without_page(structure, &data, pages, unit) {
|
||||
// An image too large for the editing data's reader is missing from the layers: the PDF
|
||||
// part has it (as in `layered_ai`).
|
||||
Ok(Ok((doc, notes))) if !notes.iter().any(|n| n == ai::IMAGE_TOO_LARGE) => Ok((doc, notes)),
|
||||
Ok(_) => Err(AiLayers { data: Ok(data), structure: None }),
|
||||
Err(structure) => Err(AiLayers { data: Ok(data), structure: Some(Ok(structure)) }),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `.ai`'s editing copy decoded, and its layers read when they were, for [`layered_ai_from`]:
|
||||
/// what [`ai_layers_alone`] read isn't decoded or read again.
|
||||
pub struct AiLayers {
|
||||
/// The editing copy decoded, or why it can't be.
|
||||
data: Result<Vec<u8>, String>,
|
||||
/// Its layers read with [`ai::read`] as [`layered_ai`] reads them, or why they can't be; `None`
|
||||
/// when they are still to be read.
|
||||
structure: Option<Result<Box<ai::Structure>, String>>,
|
||||
}
|
||||
|
||||
impl AiLayers {
|
||||
/// The editing copy `private` (as [`layered_ai`] takes it), decoded.
|
||||
pub fn decode(private: &[u8]) -> Self {
|
||||
Self { data: ai::decode_private(private), structure: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// `doc` (a file read as its page or PDF part, in place of its layers) with the variables, data
|
||||
/// sets, active data set and lock of the variable library of its editing data `data`, as [`build`]
|
||||
/// gives a document read from the layers, and the note about what of the library wasn't read
|
||||
|
||||
@@ -44,7 +44,8 @@ mod tiff;
|
||||
|
||||
pub use ai_editing::{AiEditingOptions, AiEditingOutput, CREATOR_VERSION, export_ai_editing, private_data as ai_private_data};
|
||||
pub use import::{
|
||||
Imported, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, family_style, import, import_trusting, import_with, is_loss, layered_ai, page,
|
||||
AiLayers, Imported, POSTSCRIPT_ERRORS, ai_alone, ai_alone_on_page, ai_layers_alone, family_style, import, import_trusting, import_with, is_loss,
|
||||
layered_ai, layered_ai_from, page,
|
||||
};
|
||||
pub use legacy_ai::{HiddenObjects, LegacyAiOptions, LegacyAiOutput, export_legacy_ai};
|
||||
pub use print::{PrintJob, PrintPage, print};
|
||||
|
||||
@@ -9,7 +9,7 @@ description = "VectorCraft PDF export and import"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# `vectorcraft_pdf::reads_of` for other crates' tests (dev-dependencies only).
|
||||
# `vectorcraft_pdf::reads_of` and `imports_of` for other crates' tests (dev-dependencies only).
|
||||
read-count = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -38,6 +38,8 @@ mod state;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use cid::{CidText, font_chars, identify_glyph, unify_radical};
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use entry::imports_of;
|
||||
pub use entry::{
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
|
||||
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
|
||||
@@ -221,6 +221,16 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
|
||||
read_opened(bytes, opts, reader, &original)
|
||||
}
|
||||
|
||||
/// How many times each buffer's pages have been imported, by its address (tests).
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
static IMPORTS: std::sync::Mutex<std::collections::BTreeMap<usize, usize>> = std::sync::Mutex::new(std::collections::BTreeMap::new());
|
||||
|
||||
/// How many times the pages of `bytes`, at its address, have been imported on any thread (tests).
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub fn imports_of(bytes: &[u8]) -> usize {
|
||||
IMPORTS.lock().ok().and_then(|i| i.get(&(bytes.as_ptr() as usize)).copied()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// [`read_with_reader_options`] of `original`, the PDF `bytes` read with `reader`.
|
||||
fn read_opened(
|
||||
bytes: &[u8],
|
||||
@@ -228,6 +238,11 @@ fn read_opened(
|
||||
reader: &craft_pdf::OpenOptions,
|
||||
original: &craft_pdf::syntax::Pdf,
|
||||
) -> Result<ImportReport, PdfError> {
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
if let Ok(mut imports) = IMPORTS.lock() {
|
||||
let n = imports.entry(bytes.as_ptr() as usize).or_default();
|
||||
*n = n.saturating_add(1);
|
||||
}
|
||||
let picked = crate::pages::picked(opts, original.pages().len())?;
|
||||
let mut ocgs = Ocgs::read(original);
|
||||
let route = opts.layers && !ocgs.list.is_empty();
|
||||
|
||||
@@ -76,6 +76,8 @@ pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_w
|
||||
pub use encrypt::Encryption;
|
||||
pub use export::{export, export_with_report, page_areas};
|
||||
pub use file_info::{FileInfo, file_info};
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use import::imports_of;
|
||||
pub use import::{
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
|
||||
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
@@ -83,8 +85,8 @@ pub use import::{
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use pages::reads_of;
|
||||
pub use pages::{
|
||||
IllustratorData, Opened, PageInfo, PdfInfo, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info, is_postscript,
|
||||
open_pdf,
|
||||
IllustratorData, Opened, PageInfo, PdfInfo, TrimPages, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info,
|
||||
is_postscript, open_pdf,
|
||||
};
|
||||
pub use piece_info::{PRIVATE_CHUNK, PrivateData};
|
||||
pub use post::{THUMBNAIL_SIZE, Thumbnail};
|
||||
|
||||
@@ -280,6 +280,36 @@ impl Opened {
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pdf.pages().len()
|
||||
}
|
||||
|
||||
/// The pages' trim boxes and bleed, as an import of every page on its trim box takes them,
|
||||
/// without importing any page's art.
|
||||
pub fn trim_pages(&self) -> TrimPages {
|
||||
let pages = self.pdf.pages();
|
||||
let mut bleed = [0.0; 4];
|
||||
for page in pages.iter() {
|
||||
for (side, b) in bleed.iter_mut().zip(self::bleed(page)) {
|
||||
*side = f64::max(*side, b);
|
||||
}
|
||||
}
|
||||
TrimPages {
|
||||
sizes: pages.iter().map(|page| frame(page, CropTo::Trim).1).map(|r| (r.width(), r.height())).collect(),
|
||||
user_unit: pages.first().map_or(1.0, |page| f64::from(page.user_unit())),
|
||||
bleed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PDF's pages as an import on their trim boxes sizes them ([`Opened::trim_pages`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TrimPages {
|
||||
/// Each page's trim box (its crop box when it has none) in points, rotated as the page shows:
|
||||
/// width and height.
|
||||
pub sizes: Vec<(f64, f64)>,
|
||||
/// The size in points of a unit of the first page's user space (its `UserUnit`).
|
||||
pub user_unit: f64,
|
||||
/// The bleed of the pages, `[top, bottom, left, right]` in points: on each side, the widest
|
||||
/// of the pages' bleed, as the import makes it the document's bleed.
|
||||
pub bleed: [f64; 4],
|
||||
}
|
||||
|
||||
/// Preserve the application's existing accepted file sizes. Ordinary stream decoding still
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user