Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18e671817 |
No files matched your search
+1
-1
@@ -144,7 +144,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Fonts:** a font is found by any of its names (family and style in every language of its name table, legacy family/style pairs, PostScript name), so documents from other apps open with the font they name; Find Font and `text.fonts` say for each font whether it was found exactly, by another name, substituted or missing, and how many glyphs are missing. PDF and `.ai` import brings Japanese text back as live type: CID fonts without a ToUnicode map read through the standard CMaps, vertical columns (WMode 1) as vertical type, letter spacing as tracking; a glyph is live type only when the installed font (in the closest style of its family) draws the same glyph, and text in a font that isn't installed comes in as live type shown in the fallback font until the font is installed.
|
||||
- **Files (M4):**
|
||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export.
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3).
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text with type set along a curve coming back as type on a path, each artboard's art on its own page only, an `.ai`'s white page under its layers left out rather than coming in as art, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3).
|
||||
- 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 Illustrator's AGM/CoolType prologs (resource categories, `resourceforall`, `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters, `cshow`, metadata skipped with `flushfile`), so Illustrator 30 EPS files open as vectors, and falls back to its palette TIFF preview. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole).
|
||||
- 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.
|
||||
- **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.
|
||||
|
||||
@@ -20,7 +20,7 @@ use vectorcraft_doc::{
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
|
||||
use crate::import_color::{Colors, Native};
|
||||
use crate::import_mask::{MaskSpec, contains, is_rectangle, luminance, mask_spec};
|
||||
use crate::import_mask::{MaskSpec, contains, is_rectangle, luminance, mask_spec, white_cover};
|
||||
use crate::import_scan::{Ocgs, Scan, all_on, hides_forms, scan_page};
|
||||
use crate::import_shading::{clipped, extend_clip, fold_stop_opacity, mesh_shading, shading_gradient};
|
||||
use crate::import_text::{Families, Look, Placement, TextLine, Upright};
|
||||
@@ -112,6 +112,11 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
b.begin_page(scan);
|
||||
interpret_page(page, &mut ctx, &mut b);
|
||||
let mut parts = b.end_page();
|
||||
// A PDF-compatible `.ai` (the editor's private data next to the PDF art).
|
||||
let ai = crate::pages::has_private_data(page);
|
||||
if ai {
|
||||
drop_page_fill(&mut parts, xf.transform_rect_bbox(crate::pages::page_box(page, CropTo::Crop)));
|
||||
}
|
||||
// A file with several artboards writes, on each page, the art of its neighbours that
|
||||
// reaches into the page's box: art lying wholly outside this page is theirs (each page
|
||||
// draws it shifted by its own artboard spacing), so keep it only where it belongs.
|
||||
@@ -122,7 +127,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
parts.retain(|(_, art)| !art.is_empty());
|
||||
}
|
||||
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
|
||||
placeholder &= crate::pages::has_private_data(page) && only_text(&children);
|
||||
placeholder &= ai && only_text(&children);
|
||||
let mut right = ab.x1;
|
||||
if opts.crop == CropTo::Bounding
|
||||
&& let Some(art) = vectorcraft_doc::live::nodes_bounds(&children)
|
||||
@@ -347,6 +352,28 @@ struct FontInfo {
|
||||
face: Option<Arc<vectorcraft_text::FontFace>>,
|
||||
}
|
||||
|
||||
/// The page a PDF-compatible `.ai` paints under its layers is the editor's page, not art: an opaque
|
||||
/// white rectangle the size of the page (`page`, document space), outside every layer and before
|
||||
/// any of them. Drop it from the page's art (`parts`, as [`Builder::end_page`] gives them).
|
||||
fn drop_page_fill(parts: &mut Vec<(Option<usize>, Vec<Arc<Node>>)>, page: Rect) {
|
||||
let layered = parts.iter().any(|(g, _)| g.is_some());
|
||||
let Some((None, art)) = parts.first_mut() else { return };
|
||||
// Not a spot colour: a white ink is art.
|
||||
let fill = art.first().is_some_and(|n| {
|
||||
layered
|
||||
&& n.blend == BlendMode::Normal
|
||||
&& white_cover(n, page)
|
||||
&& n.appearance.fill().is_some_and(|f| matches!(f.paint, Paint::Solid { swatch: None, .. }))
|
||||
&& n.geometric_bounds().is_some_and(|b| contains(page.inflate(0.5, 0.5), b))
|
||||
});
|
||||
if fill {
|
||||
art.remove(0);
|
||||
if art.is_empty() {
|
||||
parts.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this art text and nothing else (in groups and clips), with some text?
|
||||
fn only_text(nodes: &[Arc<Node>]) -> bool {
|
||||
fn walk(nodes: &[Arc<Node>], text: &mut bool) -> bool {
|
||||
|
||||
@@ -42,6 +42,11 @@ fn cover(n: &Node, page: Rect) -> Option<(Color, f32)> {
|
||||
(covers && n.mask.is_none()).then_some((*color, f.opacity * n.opacity))
|
||||
}
|
||||
|
||||
/// An opaque white rectangle covering `page`, filled only.
|
||||
pub(crate) fn white_cover(n: &Node, page: Rect) -> bool {
|
||||
cover(n, page).is_some_and(|(c, a)| a >= 0.999 && luminance(&c) > 0.999)
|
||||
}
|
||||
|
||||
/// Does `outer` contain `inner`?
|
||||
pub(crate) fn contains(outer: Rect, inner: Rect) -> bool {
|
||||
outer.x0 <= inner.x0 && outer.y0 <= inner.y0 && outer.x1 >= inner.x1 && outer.y1 >= inner.y1
|
||||
@@ -105,7 +110,7 @@ pub(crate) fn mask_spec(
|
||||
if art.len() > 1
|
||||
&& let Some(last) = art.last()
|
||||
&& last.blend == BlendMode::Difference
|
||||
&& cover(last, page).is_some_and(|(c, a)| a >= 0.999 && luminance(&c) > 0.999)
|
||||
&& white_cover(last, page)
|
||||
{
|
||||
art.pop();
|
||||
invert = !invert;
|
||||
|
||||
@@ -134,3 +134,61 @@ fn an_encrypted_file_keeps_its_hidden_layers() {
|
||||
assert!(d.layers[0].visible && !d.layers[1].visible);
|
||||
assert_eq!(colors(&d.layers[1]), [Color::rgb(0.0, 0.0, 1.0)]);
|
||||
}
|
||||
|
||||
/// A page drawing `before`, white and red squares in "Shapes", then `after`; `ai`: with an
|
||||
/// editor's private data (a PDF-compatible `.ai`).
|
||||
fn page_fill(before: &str, after: &str, ai: bool) -> Document {
|
||||
let (objs, catalog, ids) = groups(1, "");
|
||||
let page = PdfPage {
|
||||
resources: format!(
|
||||
"/Properties << /MC0 {} 0 R >> /ExtGState << /GS0 << /ca 0.5 >> >> /ColorSpace << /W [/Separation /White /DeviceCMYK << /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 0 0 0] /N 1 >>] >>",
|
||||
ids[0]
|
||||
),
|
||||
entries: if ai { "/PieceInfo << /Illustrator << /Private << /AIPrivateData1 7 /NumBlock 1 >> >> >>".into() } else { String::new() },
|
||||
..PdfPage::new(200.0, 100.0, &format!("{before} /OC /MC0 BDC 1 1 1 rg 20 20 60 60 re f 1 0 0 rg 120 20 60 60 re f EMC {after}"))
|
||||
};
|
||||
import(&pdf_with_catalog(&[page], &[&objs[0], &objs[1]], &catalog, None)).unwrap()
|
||||
}
|
||||
|
||||
fn path_count(d: &Document) -> usize {
|
||||
let mut n = 0;
|
||||
d.walk(|c| n += usize::from(matches!(c.kind, NodeKind::Path { .. })));
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ai_files_white_page_outside_its_layers_is_not_art() {
|
||||
// RGB, grey and CMYK white, the rectangle drawn either way round.
|
||||
for fill in ["1 1 1 rg 0 0 200 100 re f", "1 g 0 100 200 -100 re f", "0 0 0 0 k 0 0 200 100 re f"] {
|
||||
let d = page_fill(fill, "", true);
|
||||
assert_eq!(names(&d), ["Shapes"], "{fill}");
|
||||
assert_eq!(path_count(&d), 2, "{fill}");
|
||||
assert_eq!(colors(&d.layers[0]), [Color::rgb(1.0, 1.0, 1.0), Color::rgb(1.0, 0.0, 0.0)], "{fill}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_fill_that_is_art_stays() {
|
||||
// A plain PDF's white background is art, as are an .ai's fills that aren't a plain white
|
||||
// page under its layers: grey, smaller or larger than the page, stroked, translucent or a
|
||||
// white ink.
|
||||
let page = "1 1 1 rg 0 0 200 100 re f";
|
||||
let kept = [
|
||||
(page, false),
|
||||
("0.9 g 0 0 200 100 re f", true),
|
||||
("1 g 10 10 180 80 re f", true),
|
||||
("1 g -20 -20 240 140 re f", true),
|
||||
("1 g 0 G 0 0 200 100 re B", true),
|
||||
("/GS0 gs 1 g 0 0 200 100 re f", true),
|
||||
("/W cs 1 scn 0 0 200 100 re f", true),
|
||||
];
|
||||
for (fill, ai) in kept {
|
||||
let d = page_fill(fill, "", ai);
|
||||
assert_eq!(names(&d), ["Page 1", "Shapes"], "{fill}");
|
||||
assert_eq!(path_count(&d), 3, "{fill}");
|
||||
}
|
||||
// Painted over the layers.
|
||||
let d = page_fill("", page, true);
|
||||
assert_eq!(names(&d), ["Shapes", "Page 1"]);
|
||||
assert_eq!(path_count(&d), 3);
|
||||
}
|
||||
+1
-1
@@ -366,7 +366,7 @@ their stop opacity and stop where the shading doesn't extend. Text becomes point
|
||||
the file's font (by name; fonts that aren't available are listed in `warnings` and show in the fallback font) —
|
||||
`textAs: "outlines"` keeps glyph outlines instead. Optional content groups (the layers of PDF and PDF-compatible `.ai`
|
||||
files) become layers with their name, visibility, print state and lock, art that is off coming in as a hidden layer;
|
||||
art outside them goes to a layer per page. `layers: false` gives one layer per page of only what shows:
|
||||
art outside them goes to a layer per page (except the opaque white page a `.ai` paints under its layers, which isn't art). `layers: false` gives one layer per page of only what shows:
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/map.pdf","textAs":"outlines","layers":false}}}
|
||||
|
||||
Reference in new issue
Block a user