mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Merge pull request #4698 from mockydevs/fix/ai-empty-page-followups
.ai import: an empty page with skipped content isn't taken for empty; outlines asked for are given
This commit is contained in:
@@ -197,6 +197,34 @@ fn from_editing_data_alone(bytes: &[u8], opts: &LoadOptions) -> Option<Result<(D
|
||||
)
|
||||
}
|
||||
|
||||
/// Is `w` a note on the PDF part as a page, which says nothing was skipped: that the art off the
|
||||
/// artboards is in the editing data alone, or that art past the page was clipped to it?
|
||||
fn is_page_note(w: &str) -> bool {
|
||||
w == vectorcraft_pdf::OFF_ARTBOARD_NOTE || w == vectorcraft_pdf::PAST_PAGE_NOTE
|
||||
}
|
||||
|
||||
/// The document read from the editing data alone with its type as outlines when `opts` asks for
|
||||
/// them (`textAs: outlines`): the page that would have them has none to give.
|
||||
fn outlined_if_asked(mut doc: Document, warnings: Vec<String>, opts: &LoadOptions) -> (Document, Vec<String>) {
|
||||
if opts.text_as != vectorcraft_pdf::TextAs::Outlines {
|
||||
return (doc, warnings);
|
||||
}
|
||||
let mut texts = Vec::new();
|
||||
doc.walk(|n| {
|
||||
if matches!(n.kind, vectorcraft_doc::NodeKind::Text(_)) {
|
||||
texts.push(n.clone());
|
||||
}
|
||||
});
|
||||
for t in texts {
|
||||
let Some(o) = vectorcraft_render::effects::outline_text(&t) else { continue };
|
||||
let o = Node { visible: t.visible, locked: t.locked, name: t.name.clone(), ..doc.reid(&o) };
|
||||
if let Some(slot) = doc.node_mut(t.id) {
|
||||
*slot = o;
|
||||
}
|
||||
}
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
/// Does `doc` hold no art: only layers, with nothing in them?
|
||||
fn draws_nothing(doc: &Document) -> bool {
|
||||
let mut art = false;
|
||||
@@ -282,10 +310,14 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
// 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.
|
||||
let alone = (r.only_text || draws_nothing(&r.document))
|
||||
// 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(|| from_editing_data_alone(bytes, opts))
|
||||
.flatten()
|
||||
.and_then(|read| read.ok());
|
||||
.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(bytes, opts, r.document, r.warnings, r.user_unit),
|
||||
|
||||
@@ -576,6 +576,57 @@ fn an_ai_file_whose_pdf_part_draws_nothing_opens_from_its_editing_data() {
|
||||
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art", "Hidden"]);
|
||||
}
|
||||
|
||||
/// A page that draws nothing only because what it holds couldn't be read (an image that can't be
|
||||
/// decoded) isn't an empty page: its notes stay, and the editing data isn't taken in its place
|
||||
/// unchecked.
|
||||
#[test]
|
||||
fn a_page_whose_content_was_skipped_is_not_taken_for_an_empty_one() {
|
||||
let broken = "q 100 0 0 50 0 0 cm BI /W 2 /H 2 /CS /RGB /BPC 8 /F /DCT ID notajpeg EI Q";
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "broken.ai", &ai::ai(&compressed(&ai::sample_data()), broken), json!({})).unwrap();
|
||||
let warnings = r["warnings"].to_string();
|
||||
assert_ne!(r["warnings"], json!([]), "the skipped image is told: {r}");
|
||||
assert!(!warnings.contains("saved without PDF compatibility"), "{r}");
|
||||
}
|
||||
|
||||
/// Type asked for as outlines on a page that draws nothing: the editing data's type, outlined (the
|
||||
/// page has none to give).
|
||||
#[test]
|
||||
fn an_empty_page_with_type_as_outlines_outlines_the_editing_datas_type() {
|
||||
// One left-aligned story, "Hi", 14 pt Helvetica, anchored at the canvas centre (the template
|
||||
// box's centre), as the editing data's text document gives it.
|
||||
let story = "<< /0 << /0 (Hi\r) /5 << /0 [ << /0 << /0 << /0 () /5 << /0 0 >> /6 0 >> >> /1 3 >> ] >> \
|
||||
/6 << /0 [ << /0 << /0 << /0 () /5 0 /6 << /0 0 /1 14.0 /53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> >> >> >> /1 3 >> ] >> >> \
|
||||
/1 << /0 [ << /0 0 >> ] /2 [ << /99 /PC /6 [ << /99 /F /0 << /0 [ 8191.5 8191.5 ] >> /6 [ << /99 /R /6 [ << /99 /R /6 [ << /99 /L /6 [ \
|
||||
<< /99 /S /0 << /0 [ 0.0 0.0 ] >> /15 << /0 3 >> >> ] >> ] >> ] >> ] >> ] >> ] >> >>";
|
||||
let document = format!(
|
||||
"/0 << /1 << /0 [ << /0 << /0 << /0 (Helvetica) >> >> >> ] >> /8 << /0 [ << /0 << /0 [ 0 0 ] /2 << /2 [ 1 0 0 1 0 0 ] >> >> >> ] >> >>\n/1 << /1 [ {story} ] /2 << /1 12.0 >> >>\n"
|
||||
);
|
||||
let lines: Vec<String> = ascii85(document.as_bytes()).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
let text = "/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n2 /TextAntialiasing ,\n;\n";
|
||||
let data = format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n%AI3_TemplateBox: 50 50 50 50\n\
|
||||
%AI5_NumLayers: 1\n{}%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n{}\n%AI11_EndTextDocument\n%%Trailer\n",
|
||||
ai::layer("Art", text),
|
||||
lines.join("\n")
|
||||
);
|
||||
let file = ai::ai(&compressed(data.as_bytes()), "");
|
||||
let texts = |s: &Session| {
|
||||
let mut n = 0;
|
||||
s.doc().unwrap().doc.walk(|c| n += usize::from(matches!(c.kind, NodeKind::Text(_))));
|
||||
n
|
||||
};
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "typed.ai", &file, json!({})).unwrap();
|
||||
assert_eq!(texts(&s), 1, "by default the editing data's type is type: {r}");
|
||||
let r = open(&mut s, "typed.ai", &file, json!({"textAs": "outlines"})).unwrap();
|
||||
assert_eq!(texts(&s), 0, "no type left: {r}");
|
||||
let mut paths = 0;
|
||||
s.doc().unwrap().doc.walk(|c| paths += usize::from(matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })));
|
||||
assert!(paths > 0, "the outlines are there: {r}");
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "the editing data's layers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_data_false_opens_only_what_the_page_prints() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -137,6 +137,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||
| 2026-10-30 | minor | SVG export: axis-aligned rectangles are written as `<rect>`, circles and ellipses drawn with four standard arcs as `<circle>`/`<ellipse>`, straight lines as `<line>` and other straight-segment paths as `<polygon>`/`<polyline>`, at the written precision; a rectangle or ellipse with a dashed stroke, turned ellipses, curves and compound paths stay `<path>` (#4652); scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a placed (linked) file written as an object's copy on `%_` lines is read: its `%AI5_BeginPlace` and `%AI5_EndPlace` markers there were dropped, so the `~` that ends it was an unknown operator and the whole file came in as its PDF part; its preview's header, on a `%_` line too, is read. Own-fixture test in `import::ai::tests`; scores unchanged |
|
||||
| 2026-10-30 | minor | PDF export: an opaque gradient mesh is written as one PDF Type 6 (Coons patch) shading (ISO 32000-1, 8.7.4.5.7) filling its outline, so viewers blend its colours smoothly instead of showing the bands and seams of the 8 × 8 flat quads a patch it was cut into; a file of 65,558 small meshes exports in 14 s instead of 56 s, at half the size. A mesh with see-through points is still written as quads. Own-fixture tests in `tests::mesh_export`; scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a PDF part counts as an empty page only when nothing on it was skipped (an image that can't be decoded draws nothing too), so such a page keeps its notes and is checked against the editing data as before; with `textAs: "outlines"`, a file read from its editing data alone (an empty page, or the text-only placeholder of #1387) has its type outlined. Tests in `tests_aiimport.rs`; scores and hours unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a file whose PDF part is an empty page opens from its editing data where that reads, instead of opening blank; scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a file whose PDF part's pages are cropped to its art rather than its artboards no longer comes in as that PDF part (each page a copy of all the art) but from its layers, with its artboards, where they draw as the page does; own-fixture test in `tests_aiimport`; scores unchanged |
|
||||
| 2026-10-29 | minor | `.ai` and Illustrator EPS import: the editing data is read up to an eighth of installed memory, from 512 MiB to 2 GiB on 64-bit builds (it was 512 MiB on every machine), so a layered file whose embedded rasters unpack past 512 MiB keeps its layers, groups and text on a machine with 8 GiB or more instead of opening as its page or PDF part (#3510, #4628). Measured: 1.1 GB of editing data peaked 1.1 GB above the start; data past the limit is still refused (a 2 MB file unpacking to 2.2 GB stops in under a second). 32-bit and web builds keep 256 MiB. Own-fixture tests `import::ai::tests::editing_data_past_512_mib_is_read_where_memory_allows`, `the_editing_data_limit_follows_installed_memory`; scores and hours unchanged |
|
||||
|
||||
Reference in New Issue
Block a user