mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Merge pull request #2199 from dotCooCoo/fix/ai-cmyk-images-keep-samples
.ai import: CMYK images in the editing data keep their ink amounts
This commit is contained in:
@@ -220,6 +220,36 @@ fn editing_data_false_opens_only_what_the_page_prints() {
|
||||
assert!(e.contains("placeholder"), "{e}");
|
||||
}
|
||||
|
||||
/// A CMYK image opens with the same ink amounts from a `.ai` file's editing data as from its PDF
|
||||
/// part.
|
||||
#[test]
|
||||
fn a_cmyk_image_keeps_its_inks_from_the_editing_data_as_from_the_pdf_part() {
|
||||
// 3 × 1: cyan, 100% black, then a mix, as hex samples in both parts.
|
||||
let inks = [255u8, 0, 0, 0, 0, 0, 0, 255, 12, 34, 56, 78];
|
||||
let hex: String = inks.iter().map(|b| format!("{b:02x}")).collect();
|
||||
let raster = format!(
|
||||
"%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceCMYK XN\n[ 10 0 0 10 10 60 ] 3 1 0 Xh\n\
|
||||
[ 10 0 0 10 10 60 ] 0 0 3 1 3 1 8 4 0 0 0 0 4 4 0 0\n%%BeginData: {}\rXI\n{hex}\n%%EndData\r\nXH\r\n%AI5_EndRaster\r\nN\r\n",
|
||||
hex.len() + 2
|
||||
);
|
||||
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", &raster));
|
||||
let page = format!("q 30 0 0 10 10 50 cm BI /W 3 /H 1 /CS /CMYK /BPC 8 /F /AHx ID {hex}> EI Q");
|
||||
let file = ai::ai(&compressed(data.as_bytes()), &page);
|
||||
let opened = |s: &Session| {
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let mut found = vec![];
|
||||
doc.visit_images(|_, im| found.push(doc.images[&im.key].cmyk().map(|i| i.data)));
|
||||
found
|
||||
};
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "cmyk.ai", &file, json!({})).unwrap();
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}");
|
||||
assert_eq!(opened(&s), [Some(inks.to_vec())], "from the editing data");
|
||||
let r = open(&mut s, "cmyk.ai", &file, json!({"editingData": false})).unwrap();
|
||||
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}");
|
||||
assert_eq!(opened(&s), [Some(inks.to_vec())], "from the PDF part");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_to_artwork_bounds_leaves_out_the_hidden_art_and_guides_the_editing_data_has() {
|
||||
// A shown square; a hidden one and a long guide far from it, which the page doesn't draw.
|
||||
|
||||
@@ -1860,6 +1860,10 @@ fn editing_text() -> String {
|
||||
"0 0 0 1 (Black global) 0.4 0 Xk".into(),
|
||||
"(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p".into(),
|
||||
square(60),
|
||||
// A 2 × 1 CMYK image with an alpha channel, its samples in hex.
|
||||
"%AI5_File:\n%AI5_BeginRaster\n() 1 XG\n/DeviceCMYK XN\n[ 4 0 0 4 10 30 ] 2 1 0 Xh\n[ 4 0 0 4 10 30 ] 0 0 2 1 2 1 8 4 1 0 0 0 4 4 0 0\n\
|
||||
%%BeginData: 18\rXI\nff000000000000ff80ff\n%%EndData\nXH\n%AI5_EndRaster\nN"
|
||||
.into(),
|
||||
]
|
||||
.join("\n");
|
||||
let layer = |name: &str, visible: u8, body: &str| {
|
||||
@@ -1886,6 +1890,17 @@ fn editing_eps(editing: &str) -> Vec<u8> {
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
/// The fuzzed editing data, as it is, reads its CMYK image with its ink amounts and its alpha
|
||||
/// channel as an opacity mask.
|
||||
#[test]
|
||||
fn the_editing_fixture_reads_its_cmyk_image_with_its_mask() {
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&editing_text())].concat();
|
||||
let (doc, warnings, layers) = vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, true);
|
||||
let mut found = vec![];
|
||||
doc.visit_images(|n, im| found.push((doc.images[&im.key].cmyk().map(|i| i.data), n.extras().mask.is_some())));
|
||||
assert!(layers && found.contains(&(Some(vec![255, 0, 0, 0, 0, 0, 0, 255]), true)), "{found:?} {warnings:?}");
|
||||
}
|
||||
|
||||
/// The text document of a story of centred type with two styles, as an editing copy keeps it.
|
||||
fn text_document_text() -> String {
|
||||
"/0 << /1 << /0 [ << /0 << /0 << /0 (Helvetica) >> >> >> ] >> /8 << /0 [ << /0 << /0 [ 0 0 ] /2 << /2 [ 1 0 0 1 3 4 ] >> >> >> ] >> >>\n\
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
//! the art size around the template box's centre, else (a file in the legacy format another
|
||||
//! app wrote) the bounding box.
|
||||
//! - Images: `%AI5_BeginRaster`, the colour space `XN`, `[matrix] bounds w h bits type alpha …`
|
||||
//! and the samples after `XI`.
|
||||
//! and the samples after `XI`. 8-bit CMYK samples keep their ink amounts in a CMYK TIFF, and
|
||||
//! an alpha channel that hides part of such an image becomes its opacity mask; other samples
|
||||
//! become an RGBA PNG.
|
||||
//! - Placed (linked) files, as written in files users sent: `%AI5_BeginPlace`, `[matrix] llx lly urx
|
||||
//! ury … (path)` and the operator `` ` ``, the file's preview between `%AI26_BeginPlacedObjectPreview`
|
||||
//! and its end (`w h rowBytes 32 n `, then rows of premultiplied ARGB, the top one first), `~`. The
|
||||
@@ -46,9 +48,10 @@ use kurbo::{Affine, BezPath, PathEl, Point, Rect};
|
||||
use vectorcraft_color::gradient::Gradient;
|
||||
use vectorcraft_color::swatch::Swatch;
|
||||
use vectorcraft_color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::cmyk::Inks;
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, Artboard, ColorMode, Dash, Document, FillLayer, ImageBlob, ImageObject, Knockout, LayerColor, LineCap, LineJoin,
|
||||
LinkInfo, Node, NodeId, NodeKind, PatternDef, StrokeLayer,
|
||||
LinkInfo, Node, NodeId, NodeKind, OpacityMask, PatternDef, StrokeLayer,
|
||||
};
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
|
||||
@@ -1826,6 +1829,14 @@ impl<'a> Reader<'a> {
|
||||
return Ok(());
|
||||
};
|
||||
let opacity = opacity.filter(|_| alpha_len > 0);
|
||||
// CMYK samples keep their ink amounts as a CMYK TIFF, as the PDF importer keeps a CMYK
|
||||
// image's, and an alpha channel that hides part of the image becomes its opacity mask.
|
||||
if channels == 4
|
||||
&& *bits == 8.0
|
||||
&& let Some(blob) = Inks::new(w as u32, h as u32, samples.to_vec()).and_then(|i| ImageBlob::cmyk_tiff(&i))
|
||||
{
|
||||
return self.add_raster(blob, opacity, &m, (w as u32, h as u32));
|
||||
}
|
||||
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
|
||||
for y in 0..h as usize {
|
||||
let line = samples.get(y * row..(y + 1) * row).unwrap_or_default();
|
||||
@@ -1858,25 +1869,56 @@ impl<'a> Reader<'a> {
|
||||
self.warn("an image VectorCraft can't read was left out");
|
||||
return Ok(());
|
||||
}
|
||||
self.add_raster(ImageBlob::new("image/png", png), None, &m, (w as u32, h as u32))
|
||||
}
|
||||
|
||||
/// Add the image `blob` (`w` × `h` pixels), which the matrix `m` maps into art space, with
|
||||
/// `alpha` (its alpha channel, one byte a pixel) as its opacity mask when it hides part of the
|
||||
/// image.
|
||||
fn add_raster(&mut self, blob: ImageBlob, alpha: Option<&[u8]>, m: &[f64], (w, h): (u32, u32)) -> Result<(), String> {
|
||||
// The matrix maps pixels (y down) into art space with its y flipped.
|
||||
let [a, b, c, d, tx, ty] = m.as_slice() else { return Ok(()) };
|
||||
let [a, b, c, d, tx, ty] = m else { return Ok(()) };
|
||||
let to_doc = self.space();
|
||||
let xf = to_doc * FLIP * Affine::new([*a, *b, *c, *d, *tx, -*ty]);
|
||||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||||
return Ok(());
|
||||
}
|
||||
let mask = alpha.and_then(|a| self.alpha_mask(a, w, h, xf));
|
||||
let key = blob.content_key();
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
self.add_image_masked(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }, mask, false)
|
||||
}
|
||||
|
||||
/// An image's alpha channel (`w` × `h` bytes) as a grayscale image over the image (`xf`): its
|
||||
/// opacity mask, as in PDF import. `None` when it hides nothing.
|
||||
fn alpha_mask(&mut self, alpha: &[u8], w: u32, h: u32, xf: Affine) -> Option<OpacityMask> {
|
||||
if alpha.iter().all(|v| *v == 255) {
|
||||
return None;
|
||||
}
|
||||
let mut png = Vec::new();
|
||||
image::GrayImage::from_raw(w, h, alpha.to_vec())?.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
|
||||
let blob = ImageBlob::new("image/png", png);
|
||||
let key = blob.content_key();
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
self.add_image(ImageObject { key, width: w as u32, height: h as u32, xf, link: None, placement: Default::default() }, false)
|
||||
let art = Node::new(
|
||||
self.doc.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
);
|
||||
Some(OpacityMask::new(art, true))
|
||||
}
|
||||
|
||||
/// Add an image object with the state's transparency.
|
||||
fn add_image(&mut self, im: ImageObject, hidden: bool) -> Result<(), String> {
|
||||
self.add_image_masked(im, None, hidden)
|
||||
}
|
||||
|
||||
/// Add an image object with the state's transparency and `mask` as its opacity mask.
|
||||
fn add_image_masked(&mut self, im: ImageObject, mask: Option<OpacityMask>, hidden: bool) -> Result<(), String> {
|
||||
let gs = self.gs.clone();
|
||||
let mut node = self.new_node(NodeKind::Image(Box::new(im)), &gs);
|
||||
node.opacity = gs.opacity;
|
||||
node.blend = gs.blend;
|
||||
node.extras_mut().mask = mask.map(Box::new);
|
||||
self.add(node, hidden)
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,44 @@ fn images_with_alpha_channels() {
|
||||
assert!(art(&r.doc).is_empty() && r.warnings.iter().any(|w| w.contains("cut short")));
|
||||
}
|
||||
|
||||
/// CMYK images keep their ink amounts, as the file's PDF part keeps them, and an alpha channel
|
||||
/// becomes their opacity mask: a grayscale image over the image.
|
||||
#[test]
|
||||
fn cmyk_images_keep_their_ink_amounts() {
|
||||
// 3 × 1: cyan, 100% black, then a mix.
|
||||
let inks = [255, 0, 0, 0, 0, 0, 0, 255, 12, 34, 56, 78];
|
||||
let doc = read(&image("[ 10 0 0 10 50 90 ]", (3, 1), ("DeviceCMYK", 4, 0), &inks, 14)).unwrap().doc;
|
||||
let a = art(&doc);
|
||||
let NodeKind::Image(im) = &a[0].kind else { panic!("{:?}", a[0].kind) };
|
||||
let blob = &doc.images[&im.key];
|
||||
assert!(blob.is_cmyk(), "{}", blob.mime);
|
||||
assert_eq!(blob.cmyk().map(|i| (i.width, i.height, i.data)), Some((3, 1, inks.to_vec())));
|
||||
assert!(a[0].extras().mask.is_none(), "no alpha channel, no mask");
|
||||
// The same samples with an alpha channel.
|
||||
let data = [&inks[..], &[255, 128, 0]].concat();
|
||||
let doc = read(&image("[ 10 0 0 10 50 90 ]", (3, 1), ("DeviceCMYK", 4, 1), &data, 17)).unwrap().doc;
|
||||
let a = art(&doc);
|
||||
let NodeKind::Image(im) = &a[0].kind else { panic!("{:?}", a[0].kind) };
|
||||
assert_eq!(doc.images[&im.key].cmyk().map(|i| i.data), Some(inks.to_vec()), "the inks are the same");
|
||||
let m = a[0].extras().mask.clone().expect("the alpha channel is an opacity mask");
|
||||
assert!(m.clip);
|
||||
let NodeKind::Image(mi) = &m.art.kind else { panic!("{:?}", m.art.kind) };
|
||||
assert_eq!((mi.width, mi.height, mi.xf), (3, 1, im.xf), "the mask covers the image");
|
||||
let gray = image::load_from_memory(&doc.images[&mi.key].bytes).unwrap().to_luma8();
|
||||
assert_eq!(gray.into_raw(), [255, 128, 0]);
|
||||
// An alpha channel that hides nothing adds no mask.
|
||||
let data = [&inks[..], &[255; 3]].concat();
|
||||
let doc = read(&image("[ 10 0 0 10 50 90 ]", (3, 1), ("DeviceCMYK", 4, 1), &data, 17)).unwrap().doc;
|
||||
let a = art(&doc);
|
||||
assert!(a[0].extras().mask.is_none());
|
||||
// Hex samples, and a four-channel image whose space the data doesn't name.
|
||||
let hex: String = inks.iter().map(|b| format!("{b:02x}")).collect();
|
||||
let text = String::from_utf8(image("[ 1 0 0 1 0 100 ]", (3, 1), ("Other", 4, 0), hex.as_bytes(), 26)).unwrap();
|
||||
let doc = read(text.replacen("8 4 0 0 1 0 4 4 0 0", "8 4 0 0 0 0 4 4 0 0", 1).as_bytes()).unwrap().doc;
|
||||
let NodeKind::Image(im) = &art(&doc)[0].kind else { panic!() };
|
||||
assert_eq!(doc.images[&im.key].cmyk().map(|i| i.data), Some(inks.to_vec()));
|
||||
}
|
||||
|
||||
/// Editing data with one layer (showing or not) holding a placed file: its matrix, the file at
|
||||
/// `path` in the box 10 20 14 22, and `preview` (what its preview section holds), then `after`.
|
||||
fn placed(visible: bool, matrix: &str, path: &str, preview: Option<&[u8]>, after: &str) -> Vec<u8> {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -538,7 +538,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Files (M4):**
|
||||
- PDF export and print composition use the optional shared guarded writer ([pdf-backend.md](pdf-backend.md)); app drawing and output policy and desktop printer submission remain in their existing layers. The shared reader keeps the app-owned import adapter; synthetic tests cover layer and overprint state across windows, long operands, inline images, failed decoding, document limits and incomplete tails. Empty and populated metadata key presence is regression-tested, and CI checks the immutable backend source.
|
||||
- `.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 (browser tabs hold their copies with Web Locks too, so a background tab's copies are never taken for a crash's, and a tab repairs copies another removed when it resumes: #367), background save and export. The desktop app writes its warnings and errors (graphics adapter, a lost graphics device, panics the guard recovered from) to standard error and to `logs/vectorcraft.log` next to the preferences, the two previous runs kept and each file capped at 16 MiB, so a launch without a terminal still leaves something to attach to a bug report (#465). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492). Saving to a network drive on macOS falls back to a plain fsync when the share refuses a full flush (#1625, part of #1620; not confirmed on a real share), and a save still succeeds when a folder refuses the temporary file, the rename over the document or copying permissions: the content is written elsewhere first and copied in, and a failed copy keeps the temporary file and names it (#1649, part of #1620). A document opened by a relative path finds its relatively linked images, and `vectorcraft-cli convert` lists what reading the input left out as `importWarnings` (#1606, fixes #1604).
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); 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). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
|
||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units (SVG type under a transform or `viewBox` scale opens at the size it draws at, the scale moved into its size, leading, baseline shift and strokes, a stretch, skew or rotation kept as its transform: #396; a top-level group with a clip path, such as a clipped Inkscape layer, opens as a layer with a clipping mask, its name kept: #420; hidden objects written hidden on Save and `convert`, and `data-*` attributes kept as the objects' own data: #522; top-level groups open as layers named from `data-name`, else the id, also when one more group (a transform on all the art, a `<switch>`) wraps them, hidden ones hidden, and a file whose art sits loose under a root `<svg>` with an id (or `data-name`), as one-layer exports write it, opens with its one layer named from it; nested groups stay groups, except the sublayers VectorCraft writes, marked `data-vc-layer`, and Inkscape sublayers, which open as sublayers in their layer's colour: #372); PDF and PDF-compatible `.ai` in and out (layers, with hidden ones kept hidden with their art and nested ones as sublayers, on pages of up to 64 million operators, art whose layer can't be told going to a hidden, non-printing layer rather than showing or printing: #419; strokes kept live, a fill and a stroke of one outline as one object; a type object's stroke written as each glyph's outline stroked back on the type, so it stays one editable object; lines wrapped in a frame (each wrapped line ending in a space, one left edge, leading and font) back as one area type object, left aligned or justified with its paragraph spacing, where laying it out breaks the lines where the file does; glyphs set apart by gaps wider than a space (a barcode's digit groups) keeping their places; layers stacked as the pages mark them even when one first paints on a later page, art ending a layer's marked content or a clip closing in a later layer kept in its own layer, and layers with no art kept empty: #508; art reaching past a plain PDF's page clipped to its crop box, as PDF viewers show it, each such object in a clip group of its own (a PDF-compatible `.ai` opens whole: #675); a `.ai` on its trim boxes with the bleed boxes round them as the document's bleed, as Crop To › Trim opens a PDF (#1266); 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). CMYK images keep their ink amounts from PDF import (and from placed CMYK TIFFs and the images in an `.ai` or EPS file's editing data, an alpha channel coming in as a grayscale opacity mask) to PDF export: a CMYK JPEG passes through unchanged, others stay CMYK (DeviceCMYK, or ICC-based CMYK in PDF/X-3/X-4) unless the Output settings convert them; the canvas shows them through the working CMYK profile (#320). Overprint is kept from PDF import (opened, the PDF part of an `.ai`, placed) to PDF export: the graphics state's `/OP` and `/op` become the overprint of fills, strokes and type, so a spot colour over a process colour still overprints; a page whose overprint can't be read for certain knocks out with a note, as does `/OPM 0` being read as nonzero overprint (#1388; hand-written fixtures, `pdf/src/tests_import_overprint.rs`). A masked CMYK image (`/SMask`, `/Mask` or a colour key) keeps its inks too, its alpha coming in as a greyscale opacity mask.
|
||||
SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `<style>` rules for its id, its classes and its element name, and the rules whose selector ends in none of these (`*`, `[x]`).
|
||||
A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304).
|
||||
Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests_links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).
|
||||
|
||||
Reference in New Issue
Block a user