mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Review fixes for #777: report sections per document, fmt, docs and ROADMAP
The report kept a placed document's LINKED FILES and FONTS inside its parent's list. Each document now gets its own sections, the packaged one first and unchanged (no Document: line), then each placed document's, headed by its file. Drops a debug_assert and an unneeded mut, runs rustfmt, and documents the nested packaging in docs/mcp.md and the ROADMAP. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
94f27299d4
commit
2762f61e83
5 files changed
+84
-50
No files matched your search
+2
-2
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -170,7 +170,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export. EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
|
||||
- Affinity documents in (AF.1–AF.4): `.af` (Affinity 3) and `.afdesign`/`.afphoto`/`.afpub` (Affinity 1 and 2, container versions 8–12) open and place as editable art: layers and sublayers, groups (pass-through or isolated), artboards (with their names and backgrounds) and Publisher pages side by side, curves with live corners, parametric shapes (rectangles with corner radii, ellipses, polygons, stars, pies, triangles, trapezoids), compound shapes, solid/linear/elliptical gradient fills and strokes (weight, dashes, alignment, scale-with-object), clipping by nesting, vector and pixel masks, artistic and frame text with fonts, sizes, tracking, leading and alignment, placed images (their original JPEG/PNG) and pixel layers (8/16-bit RGB, CMYK), opacity, visibility, locking and blend modes, at the document's resolution in points. Effects, adjustments, brushes and pressure profiles, transparency gradients, master pages, symbol instances (imported as their cached picture), special shapes (cloud, heart, cog…, imported as ellipses) and Affinity-only blend modes are named in the import warning. A file whose native data can't be read opens as its embedded preview with the reason, and that preview can't be placed. Save never writes back over the Affinity file and there is no Affinity export. AF.4 adds Affinity 3 artboard recognition and actual-outline clipping, source-backed JPEG/PNG raster tiles, Unicode text-run recovery, stored fallback/exact installed fonts, OpenType feature flags and compound-geometry limits. 51 public CC0/MIT/Apache-2.0 files (33 current `.af`), pinned and sha256-verified (`cargo xtask corpus --affinity`), test native retention and every-board native/SVG/PDF/PSD output for current files; selected files compare their saved thumbnails. No Affinity reopen or independent paired PSD oracle was available; [source audit and complex-font gaps](docs/affinity-validation.md); [scope, provenance and limits](crates/affinity/README.md).
|
||||
- Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it.
|
||||
- Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
|
||||
- Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457).
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
||||
- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland.
|
||||
- **Tests and robustness:** ~2,840 automated tests: model-based property tests, a junk-parameter sweep over every command, import fuzzing (SVG, PDF, libraries), golden renders, and MCP end-to-end tests over stdio. Shipped code never panics (workspace lints and a rollback safety net; see `docs/development.md`).
|
||||
|
||||
@@ -276,11 +276,8 @@ impl Document {
|
||||
walk(child, f);
|
||||
}
|
||||
}
|
||||
let roots = self
|
||||
.layers
|
||||
.iter_mut()
|
||||
.chain(self.symbols.iter_mut().map(|s| &mut s.art))
|
||||
.chain(self.patterns.iter_mut().flat_map(|p| &mut p.art));
|
||||
let roots =
|
||||
self.layers.iter_mut().chain(self.symbols.iter_mut().map(|s| &mut s.art)).chain(self.patterns.iter_mut().flat_map(|p| &mut p.art));
|
||||
for root in roots {
|
||||
walk(root, &mut f);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::collections::{BTreeMap, HashSet};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use vectorcraft_doc::{Document, LinkInfo};
|
||||
use vectorcraft_doc::links::hash_bytes;
|
||||
use vectorcraft_doc::{Document, LinkInfo};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -95,10 +95,8 @@ fn relative_in_package(document: &str, asset: &str) -> String {
|
||||
}
|
||||
|
||||
fn safe_asset_name(name: &str) -> String {
|
||||
let name: String = name
|
||||
.chars()
|
||||
.map(|c| if c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c })
|
||||
.collect();
|
||||
let name: String =
|
||||
name.chars().map(|c| if c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c }).collect();
|
||||
let name = name.trim().trim_matches('.');
|
||||
if name.is_empty() { "asset".into() } else { name.into() }
|
||||
}
|
||||
@@ -119,7 +117,9 @@ struct Collector<'a> {
|
||||
missing: Vec<String>,
|
||||
skipped: Vec<Value>,
|
||||
warnings: Vec<String>,
|
||||
lines: Vec<String>,
|
||||
/// The report's lines, one section per document: the packaged one first, then each placed
|
||||
/// document in the order it is reached.
|
||||
sections: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a> {
|
||||
@@ -139,37 +139,47 @@ impl<'a> Collector<'a> {
|
||||
missing: Vec::new(),
|
||||
skipped: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
sections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn warn(&mut self, message: String) {
|
||||
self.lines.push(format!("WARNING: {message}"));
|
||||
fn warn(&mut self, lines: &mut Vec<String>, message: String) {
|
||||
lines.push(format!("WARNING: {message}"));
|
||||
self.warnings.push(message);
|
||||
}
|
||||
|
||||
fn collect(&mut self, doc: &mut Document, source: &str, destination: &str, depth: usize) -> Result<()> {
|
||||
// The caller checks cycles and depth before descending; active documents have
|
||||
// a destination reserved so back-links can point at their single packaged copy.
|
||||
let previous = self.visiting.insert(source.to_string(), destination.to_string());
|
||||
debug_assert!(previous.is_none(), "collector visits each document once");
|
||||
self.visiting.insert(source.to_string(), destination.to_string());
|
||||
// This document's section comes before those of the documents it places.
|
||||
let section = self.sections.len();
|
||||
self.sections.push(vec![]);
|
||||
let mut lines = vec![];
|
||||
// The packaged document's sections read as before; a placed document's name its file.
|
||||
let heading = |lines: &mut Vec<String>, title: &str| {
|
||||
lines.push(title.into());
|
||||
if depth > 0 {
|
||||
lines.push(format!("Document: {source}"));
|
||||
}
|
||||
};
|
||||
// A file's saved path is only meaningful inside its own document.
|
||||
// Different linked documents can reuse the same stale absolute path yet
|
||||
// resolve it to distinct local assets. Keep a mapping per document.
|
||||
let mut local = BTreeMap::<String, Copied>::new();
|
||||
if self.opts.copy_links {
|
||||
let mut placed = HashSet::new();
|
||||
doc.visit_placed(|_, p| { placed.insert(p.link.path.clone()); });
|
||||
// Keep this exact header for programs parsing Package reports.
|
||||
self.lines.push("LINKED FILES".into());
|
||||
self.lines.push(format!("Document: {source}"));
|
||||
doc.visit_placed(|_, p| {
|
||||
placed.insert(p.link.path.clone());
|
||||
});
|
||||
heading(&mut lines, "LINKED FILES");
|
||||
for file in super::links::linked_files(doc, Some(source)) {
|
||||
let found_path = file.found_path.as_deref().unwrap_or(&file.path);
|
||||
if let Some(ancestor) = self.visiting.get(found_path).cloned() {
|
||||
// The ancestor is already included (possibly the root document). A
|
||||
// cyclic reference cannot have a final content hash until both files
|
||||
// have been written, so leave its size/hash unspecified, not stale.
|
||||
self.warn(format!("{}: circular placed-document link to {ancestor}; stopped following the cycle", file.path));
|
||||
self.warn(&mut lines, format!("{}: circular placed-document link to {ancestor}; stopped following the cycle", file.path));
|
||||
local.insert(file.path.clone(), Copied { name: ancestor, size: None, hash: None });
|
||||
continue;
|
||||
}
|
||||
@@ -178,7 +188,7 @@ impl<'a> Collector<'a> {
|
||||
continue;
|
||||
}
|
||||
let Some(mut bytes) = file.bytes else {
|
||||
self.lines.push(format!("{}: not found ({}), not copied", file.name, file.path));
|
||||
lines.push(format!("{}: not found ({}), not copied", file.name, file.path));
|
||||
self.missing.push(if depth == 0 { file.name.clone() } else { format!("{} ({source})", file.name) });
|
||||
continue;
|
||||
};
|
||||
@@ -191,18 +201,15 @@ impl<'a> Collector<'a> {
|
||||
let destination_file = format!("{dir}{unique}");
|
||||
if placed.contains(&file.path) {
|
||||
if depth >= vectorcraft_doc::placed_document::MAX_DEPTH {
|
||||
self.warn(format!("{}: nesting limit reached; copied unchanged without collecting its own links", file.path));
|
||||
self.warn(&mut lines, format!("{}: nesting limit reached; copied unchanged without collecting its own links", file.path));
|
||||
} else {
|
||||
match vectorcraft_format::load_file(&bytes) {
|
||||
Ok(saved) => {
|
||||
let preview = vectorcraft_format::preview(&bytes);
|
||||
let pdf = vectorcraft_format::pdf_content(&bytes);
|
||||
let compressed = bytes.starts_with(&[0x1f, 0x8b]);
|
||||
let include_linked = saved
|
||||
.doc
|
||||
.linked_only_images()
|
||||
.iter()
|
||||
.any(|key| saved.doc.images.get(key).is_some_and(|blob| !blob.is_proxy()));
|
||||
let include_linked =
|
||||
saved.doc.linked_only_images().iter().any(|key| saved.doc.images.get(key).is_some_and(|blob| !blob.is_proxy()));
|
||||
let mut nested = saved.doc;
|
||||
self.collect(&mut nested, found_path, &destination_file, depth + 1)?;
|
||||
if self.opts.relink {
|
||||
@@ -215,26 +222,34 @@ impl<'a> Collector<'a> {
|
||||
options.include_linked = include_linked;
|
||||
match vectorcraft_format::save_with(&nested, &options) {
|
||||
Ok(rewritten) => bytes = rewritten,
|
||||
Err(e) => self.warn(format!("{}: could not rewrite linked document ({e}); copied unchanged, its own links were not relinked", file.path)),
|
||||
Err(e) => self.warn(
|
||||
&mut lines,
|
||||
format!(
|
||||
"{}: could not rewrite linked document ({e}); copied unchanged, its own links were not relinked",
|
||||
file.path
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => self.warn(format!("{}: unreadable linked document ({e}); copied unchanged, its own links were not collected", file.path)),
|
||||
Err(e) => self.warn(
|
||||
&mut lines,
|
||||
format!("{}: unreadable linked document ({e}); copied unchanged, its own links were not collected", file.path),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
let copy = Copied { name: destination_file.clone(), size: Some(bytes.len() as u64), hash: Some(hash_bytes(&bytes)) };
|
||||
self.copies.insert(found_path.to_string(), copy.clone());
|
||||
local.insert(file.path.clone(), copy);
|
||||
self.lines.push(format!("{} → {destination_file}", file.path));
|
||||
lines.push(format!("{} → {destination_file}", file.path));
|
||||
self.entries.push((destination_file, bytes));
|
||||
}
|
||||
self.lines.push(String::new());
|
||||
lines.push(String::new());
|
||||
}
|
||||
if self.opts.copy_fonts {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
self.lines.push("FONTS".into());
|
||||
self.lines.push(format!("Document: {source}"));
|
||||
heading(&mut lines, "FONTS");
|
||||
for used in super::fonts::used_fonts(doc) {
|
||||
let font = super::fonts::font_label(&used);
|
||||
let (family, style, version) = used;
|
||||
@@ -257,16 +272,16 @@ impl<'a> Collector<'a> {
|
||||
self.entries.push((format!("Fonts/{file}"), data.to_vec()));
|
||||
self.fonts += 1;
|
||||
}
|
||||
self.lines.push(format!("{font}{shown} → Fonts/{file}"));
|
||||
lines.push(format!("{font}{shown} → Fonts/{file}"));
|
||||
continue;
|
||||
}
|
||||
Some(_) => "its licence doesn't allow embedding",
|
||||
None => "not available on this computer",
|
||||
};
|
||||
self.lines.push(format!("{font}{shown}: {reason}, not copied"));
|
||||
lines.push(format!("{font}{shown}: {reason}, not copied"));
|
||||
self.skipped.push(json!({ "font": font, "reason": reason }));
|
||||
}
|
||||
self.lines.push(String::new());
|
||||
lines.push(String::new());
|
||||
}
|
||||
if self.opts.relink {
|
||||
// The child files have already been rewritten; the hash and size of each
|
||||
@@ -282,7 +297,10 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
});
|
||||
}
|
||||
let _ = self.visiting.remove(source);
|
||||
if let Some(s) = self.sections.get_mut(section) {
|
||||
*s = lines;
|
||||
}
|
||||
self.visiting.remove(source);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -319,7 +337,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut entries = std::mem::take(&mut collector.entries);
|
||||
entries.insert(0, (doc_name.clone(), vectorcraft_format::save_file(&doc)));
|
||||
let mut taken = std::mem::take(&mut collector.taken);
|
||||
let mut lines = std::mem::take(&mut collector.lines);
|
||||
let lines = collector.sections.concat();
|
||||
let missing = std::mem::take(&mut collector.missing);
|
||||
let skipped = std::mem::take(&mut collector.skipped);
|
||||
let warnings = std::mem::take(&mut collector.warnings);
|
||||
|
||||
@@ -317,13 +317,8 @@ fn package_keeps_distinct_linked_documents_with_colliding_file_names() {
|
||||
assert_eq!(result["links"], 4, "{result}");
|
||||
assert_eq!(result["missingLinks"], json!([]));
|
||||
let folder = dir.0.join("delivery/poster Folder");
|
||||
let packaged: Vec<_> = result["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.filter(|name| name.ends_with(".vectorcraft"))
|
||||
.collect();
|
||||
let packaged: Vec<_> =
|
||||
result["files"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|name| name.ends_with(".vectorcraft")).collect();
|
||||
assert_eq!(packaged.len(), 3, "both nested documents must be retained: {packaged:?}");
|
||||
std::fs::remove_dir_all(dir.0.join("art1")).unwrap();
|
||||
std::fs::remove_dir_all(dir.0.join("art2")).unwrap();
|
||||
@@ -391,7 +386,13 @@ fn identical_stale_paths_in_different_nested_documents_keep_separate_assets() {
|
||||
}
|
||||
assert_eq!(actual.len(), 2, "stale source paths must not cause asset deduplication");
|
||||
assert_ne!(actual[0], actual[1], "different original illustrations must keep different pixels");
|
||||
for filename in result["files"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|name| name.starts_with("Links/") && name.ends_with(".vectorcraft")) {
|
||||
for filename in result["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.filter(|name| name.starts_with("Links/") && name.ends_with(".vectorcraft"))
|
||||
{
|
||||
let mut nested = session();
|
||||
let opened = open(&mut nested, &folder.join(filename).to_string_lossy());
|
||||
assert_eq!(opened["missingLinks"], json!([]), "{filename}: {opened}");
|
||||
@@ -426,6 +427,22 @@ fn packaging_nested_files_preserves_embedded_preview_pdf_and_compression() {
|
||||
assert_eq!(images[0].as_ref().unwrap().relative.as_deref(), Some("photo.png"));
|
||||
}
|
||||
|
||||
/// The report keeps the packaged document's sections as they were, then gives each placed
|
||||
/// document's own, headed by its file, instead of nesting them inside the parent's list.
|
||||
#[test]
|
||||
fn the_report_lists_a_placed_documents_links_in_a_section_of_its_own() {
|
||||
let dir = Folder::new("package-report-sections");
|
||||
let mut parent = poster_with_placed_document(&dir);
|
||||
run(&mut parent, "file.package", json!({ "folder": dir.file("delivery"), "copyFonts": false }));
|
||||
let report = std::fs::read_to_string(dir.0.join("delivery/poster Folder/poster Report.txt")).unwrap();
|
||||
let lines: Vec<&str> = report.lines().collect();
|
||||
let sections: Vec<usize> = lines.iter().enumerate().filter(|(_, l)| **l == "LINKED FILES").map(|(i, _)| i).collect();
|
||||
let [poster, part] = sections[..] else { panic!("two link sections: {report}") };
|
||||
assert!(lines[poster + 1].ends_with("→ Links/part.vectorcraft"), "the packaged document's section as before: {report}");
|
||||
assert!(lines[part + 1].starts_with("Document: ") && lines[part + 1].ends_with("part.vectorcraft"), "{report}");
|
||||
assert!(lines[part + 2].ends_with("→ Links/photo.png"), "{report}");
|
||||
}
|
||||
|
||||
/// A damaged or newer-format linked native file is still an asset worth delivering.
|
||||
/// Package must preserve the exact bytes instead of failing the entire handoff.
|
||||
#[test]
|
||||
|
||||
+3
-1
@@ -1844,7 +1844,9 @@ bounds; `clip` puts it in a clip group of the old bounds when it is larger). The
|
||||
unsaved one is an error) into `folder/name` (default name `<document> Folder`): `<document>.vectorcraft`, its linked
|
||||
files in `Links/` (relinked: the packaged document points at the copies; the open one doesn't change), the fonts its
|
||||
type uses in `Fonts/` (fonts whose licence doesn't allow embedding are listed in `skippedFonts` instead) and
|
||||
`<document> Report.txt`. Every option defaults to true. Without `folder` (the web, or an agent that wants the bytes) the
|
||||
`<document> Report.txt`. A placed `.vectorcraft` document is packaged with its own linked files and fonts, relinked to
|
||||
the copies (each file copied once); one that can't be read is copied as it is, and a cycle of placed documents or one
|
||||
past the nesting limit stops there, each with a line in `warnings` and the report. Every option defaults to true. Without `folder` (the web, or an agent that wants the bytes) the
|
||||
result carries the same files as a zip (`{name: "<name>.zip", dataBase64}`, entries under `<name>/`).
|
||||
|
||||
`document.info {selectionOnly?, category?, format?: "text"}` adds `sections` (`[{id, title, rows: [[label, value]]}]`:
|
||||
|
||||
Reference in new issue
Block a user